@rowsncolumns/toolkit 24.4.5 → 24.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/autofilter-condition.d.ts +45 -1
- package/dist/cjs/autofilter-condition.d.ts.map +1 -1
- package/dist/cjs/autofilter-condition.js +135 -12
- package/dist/cjs/excel-parser/helpers.d.ts +15 -1
- package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
- package/dist/cjs/excel-parser/helpers.js +119 -5
- package/dist/cjs/excel-parser/interface.d.ts +3 -1
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.js +186 -97
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/styles.js +80 -6
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/index.js +107 -103
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.js +920 -122
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +24 -9
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +25 -3
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +561 -394
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +312 -21
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/cjs/excel-parser/types.d.ts +9 -1
- package/dist/cjs/excel-parser/types.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.d.ts +1 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.js +5 -0
- package/dist/cjs/excel-parser/worker.js +2 -0
- package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/cjs/exporter/ods/ods-export.js +255 -80
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/chart-builder.js +902 -119
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/form-controls-builder.js +30 -8
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/pivot-builder.js +206 -162
- package/dist/cjs/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/style-builder.js +25 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/xlsx-export.js +728 -452
- package/dist/esm/autofilter-condition.d.ts +45 -1
- package/dist/esm/autofilter-condition.d.ts.map +1 -1
- package/dist/esm/autofilter-condition.js +131 -11
- package/dist/esm/excel-parser/helpers.d.ts +15 -1
- package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
- package/dist/esm/excel-parser/helpers.js +94 -4
- package/dist/esm/excel-parser/interface.d.ts +3 -1
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.js +105 -22
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/styles.js +79 -5
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/index.js +15 -7
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.js +708 -16
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.js +23 -0
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +21 -3
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +192 -54
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/pivots.js +220 -4
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/esm/excel-parser/types.d.ts +9 -1
- package/dist/esm/excel-parser/types.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.d.ts +1 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.js +4 -0
- package/dist/esm/excel-parser/worker.js +2 -0
- package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/esm/exporter/ods/ods-export.js +238 -72
- package/dist/esm/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/chart-builder.js +674 -58
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/form-controls-builder.js +29 -8
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/pivot-builder.js +187 -139
- package/dist/esm/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/style-builder.js +28 -0
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/xlsx-export.js +451 -197
- package/package.json +10 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { cellRangeToAddress, cellToAddress, escapeCharacters, getCellEffectiveFormat, getCellEffectiveValue, getCellFormattedValue, getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, isNil, sortSheetsByIndex, } from "@rowsncolumns/utils";
|
|
2
2
|
import { zipSync } from "fflate";
|
|
3
3
|
import { omitInvalidDataValidationError } from "../invalid-data-validation";
|
|
4
|
-
import {
|
|
5
|
-
import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
|
|
4
|
+
import { buildAutoFilterCriteriaXml } from "../../autofilter-condition";
|
|
5
|
+
import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, controlAnchorFlags, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
|
|
6
6
|
import { buildChartExGraphicFrameXml, buildChartExXml, CHART_EX_CONTENT_TYPE, CHART_EX_REL_TYPE, isChartExType, } from "./chartex-builder";
|
|
7
7
|
import { applyPowerQueryParts } from "./power-query-builder";
|
|
8
8
|
import { encodeImplicitIntersection } from "../../helpers/implicit-intersection";
|
|
@@ -19,7 +19,7 @@ import { buildChartXml } from "./chart-builder";
|
|
|
19
19
|
import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, calculatedFieldReferences, } from "./pivot-builder";
|
|
20
20
|
import { buildSlicerCacheXml, buildSlicerXml } from "./slicer-builder";
|
|
21
21
|
import { buildSparklineGroupsXml } from "./sparkline-builder";
|
|
22
|
-
import { StyleBuilder } from "./style-builder";
|
|
22
|
+
import { StyleBuilder, tableStyleElementsFromBanding, } from "./style-builder";
|
|
23
23
|
const BLANK_LABEL = "(Blanks)";
|
|
24
24
|
const isStyleReference = (value) => !!value && typeof value === "object" && "sid" in value;
|
|
25
25
|
const DEFAULT_COLUMN_WIDTH_PX = 100;
|
|
@@ -38,9 +38,15 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
38
38
|
if (namedCellStyles && namedCellStyles.length > 0) {
|
|
39
39
|
styleBuilder.setNamedCellStyles(namedCellStyles);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// The document facet carries `{ name, bandedRange }` (#668 T-103); a host may still pass the
|
|
42
|
+
// parser's element records. Both become `<tableStyle>` elements.
|
|
43
|
+
const customTableStyleRecords = (customTableStyles ?? []).map((style) => "bandedRange" in style
|
|
44
|
+
? { name: style.name, elements: tableStyleElementsFromBanding(style.bandedRange) }
|
|
45
|
+
: style);
|
|
46
|
+
if (customTableStyleRecords.length > 0) {
|
|
47
|
+
styleBuilder.setCustomTableStyles(customTableStyleRecords);
|
|
43
48
|
}
|
|
49
|
+
const customTableStyleNames = new Set(customTableStyleRecords.map((s) => s.name));
|
|
44
50
|
// Drop tables with invalid coords or overlapping ranges — Excel rejects them
|
|
45
51
|
// and would trigger a recovery prompt. Filter once so the id remap and the
|
|
46
52
|
// prepared output stay in lockstep.
|
|
@@ -52,10 +58,13 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
52
58
|
tableIdMap.set(String(table.id), index + 1);
|
|
53
59
|
});
|
|
54
60
|
const preparedTables = prepareTables(exportableTables);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
61
|
+
// Charts and embeds share one z-order per sheet (`position.zOrder`, else
|
|
62
|
+
// creation order: charts below embeds) — the drawing anchors follow it.
|
|
63
|
+
const chartCount = charts?.length ?? 0;
|
|
64
|
+
const embedsBySheet = groupEmbedsBySheet(embeds ?? [], chartCount);
|
|
65
|
+
const textBoxesBySheet = groupTextBoxesBySheet(embeds ?? [], chartCount);
|
|
66
|
+
const shapesBySheet = groupShapesBySheet(embeds ?? [], chartCount);
|
|
67
|
+
const formControlsBySheet = groupFormControlsBySheet(embeds ?? [], chartCount);
|
|
59
68
|
const ctrlProps = { parts: [] };
|
|
60
69
|
const chartsBySheet = groupChartsBySheet(charts ?? []);
|
|
61
70
|
// Remap slicer tableIds to match prepared table IDs
|
|
@@ -100,6 +109,22 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
100
109
|
const pivotCacheRelsEntries = [];
|
|
101
110
|
const pivotTableEntries = [];
|
|
102
111
|
const pivotTableRelsEntries = [];
|
|
112
|
+
// `<c:pivotSource><c:name>` for PivotCharts: the exported pivot's name on
|
|
113
|
+
// its target sheet (`pivot-builder` names an unnamed pivot the same way).
|
|
114
|
+
// Only pivots the pivot writer emits (source + pivotRange) are named, so
|
|
115
|
+
// a chart never points Excel at a pivot part that is not in the file.
|
|
116
|
+
const pivotSourceNameById = new Map();
|
|
117
|
+
for (const p of pivotTables) {
|
|
118
|
+
const targetSheet = sortedSheets.find((s) => s.sheetId === p.targetPosition?.sheetId);
|
|
119
|
+
if (!targetSheet || !p.source || !p.pivotRange)
|
|
120
|
+
continue;
|
|
121
|
+
const sheetTitle = targetSheet.title ?? `Sheet${targetSheet.sheetId}`;
|
|
122
|
+
const quoted = /^[A-Za-z_][A-Za-z0-9_.]*$/.test(sheetTitle)
|
|
123
|
+
? sheetTitle
|
|
124
|
+
: `'${sheetTitle.replace(/'/g, "''")}'`;
|
|
125
|
+
pivotSourceNameById.set(String(p.pivotId), `[Workbook]${quoted}!${p.name ?? `PivotTable${p.pivotId}`}`);
|
|
126
|
+
}
|
|
127
|
+
const resolvePivotSourceName = (pivotTableId) => pivotSourceNameById.get(String(pivotTableId));
|
|
103
128
|
// Group pivots by the sheet they render on (targetPosition.sheetId).
|
|
104
129
|
const pivotsByTargetSheet = new Map();
|
|
105
130
|
for (const p of pivotTables) {
|
|
@@ -115,7 +140,15 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
115
140
|
// Records pivot caches that need a workbook-level entry (added
|
|
116
141
|
// after the sheet loop closes). Each entry: { cacheIndex, rIdOnWorkbook }
|
|
117
142
|
const workbookPivotCacheRefs = [];
|
|
143
|
+
// Worksheet and chartsheet parts are numbered 1..N within their own folder
|
|
144
|
+
// (`xl/worksheets/sheetN.xml` / `xl/chartsheets/sheetN.xml`), as Excel does;
|
|
145
|
+
// drawings stay keyed by the sheet's position in `sortedSheets`.
|
|
146
|
+
let worksheetCount = 0;
|
|
147
|
+
let chartsheetCount = 0;
|
|
118
148
|
const worksheetEntries = sortedSheets.map((sheet, index) => {
|
|
149
|
+
const isChartsheet = sheet.type === "chartsheet";
|
|
150
|
+
const partIndex = isChartsheet ? ++chartsheetCount : ++worksheetCount;
|
|
151
|
+
const partFolder = isChartsheet ? "chartsheets" : "worksheets";
|
|
119
152
|
const rows = getRowsForSheet(sheetData, sheet.sheetId);
|
|
120
153
|
const sheetRelationships = new RelationshipsBuilder();
|
|
121
154
|
const sheetTables = preparedTables.filter((table) => table.sheetId === sheet.sheetId);
|
|
@@ -165,6 +198,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
165
198
|
offsetY: placement.offsetY,
|
|
166
199
|
width: placement.width,
|
|
167
200
|
height: placement.height,
|
|
201
|
+
overlay: slicer.position?.overlayPosition,
|
|
168
202
|
});
|
|
169
203
|
globalSlicerIndex += 1;
|
|
170
204
|
}
|
|
@@ -233,35 +267,42 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
233
267
|
workbookPivotCacheRefs.push({ cacheIndex });
|
|
234
268
|
globalPivotIndex += 1;
|
|
235
269
|
}
|
|
236
|
-
const result =
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
const result = isChartsheet
|
|
271
|
+
? buildChartsheetXml({
|
|
272
|
+
sheet,
|
|
273
|
+
relationships: sheetRelationships,
|
|
274
|
+
drawingContext,
|
|
275
|
+
})
|
|
276
|
+
: buildWorksheetXml({
|
|
277
|
+
sheet,
|
|
278
|
+
rows,
|
|
279
|
+
sharedStrings,
|
|
280
|
+
sourceSharedStrings,
|
|
281
|
+
cellXfs,
|
|
282
|
+
styleBuilder,
|
|
283
|
+
tables: sheetTables,
|
|
284
|
+
relationships: sheetRelationships,
|
|
285
|
+
drawingContext,
|
|
286
|
+
conditionalFormats: sheetConditionalFormats,
|
|
287
|
+
dataValidationsById,
|
|
288
|
+
dataValidations: _dataValidations,
|
|
289
|
+
ignoreFormulaPatterns,
|
|
290
|
+
citations: sheetCitations,
|
|
291
|
+
comments,
|
|
292
|
+
personRegistry,
|
|
293
|
+
slicerRelIds: worksheetSlicerRelIds,
|
|
294
|
+
sheetData,
|
|
295
|
+
sheets: sortedSheets,
|
|
296
|
+
locale,
|
|
297
|
+
protectedRanges,
|
|
298
|
+
formControls: sheetFormControls,
|
|
299
|
+
ctrlProps,
|
|
300
|
+
});
|
|
261
301
|
return {
|
|
262
302
|
index,
|
|
263
|
-
|
|
264
|
-
|
|
303
|
+
isChartsheet,
|
|
304
|
+
path: `xl/${partFolder}/sheet${partIndex}.xml`,
|
|
305
|
+
relationshipsPath: `xl/${partFolder}/_rels/sheet${partIndex}.xml.rels`,
|
|
265
306
|
content: result.xml,
|
|
266
307
|
relationshipsXml: result.relationshipsXml,
|
|
267
308
|
drawingPath: `xl/drawings/drawing${index + 1}.xml`,
|
|
@@ -278,12 +319,25 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
278
319
|
threadedCommentsXml: result.threadedCommentsXml,
|
|
279
320
|
};
|
|
280
321
|
});
|
|
281
|
-
const { xml: workbookRelationshipsXml, sheetRelationshipIds, slicerCacheRelationshipIds, pivotCacheRelationshipIds, externalLinkRelationshipIds, } = buildWorkbookRelationships(worksheetEntries.
|
|
322
|
+
const { xml: workbookRelationshipsXml, sheetRelationshipIds, slicerCacheRelationshipIds, pivotCacheRelationshipIds, externalLinkRelationshipIds, } = buildWorkbookRelationships(worksheetEntries.map((entry) => ({
|
|
323
|
+
chartsheet: entry.isChartsheet,
|
|
324
|
+
target: entry.path.replace(/^xl\//, ""),
|
|
325
|
+
})), slicerCacheEntries.length, externalLinks.length, pivotCacheEntries.length, !personRegistry.isEmpty());
|
|
282
326
|
const workbookXml = buildWorkbookXml(sortedSheets, sheetRelationshipIds, workbookSheetNames, namedRanges, iterativeCalculationOptions, slicerCacheRelationshipIds, use1904, externalLinkRelationshipIds, pivotCacheRelationshipIds);
|
|
283
327
|
const sharedStringsXml = buildSharedStringsXml(sharedStrings);
|
|
284
328
|
const rootRelationshipsXml = buildRootRelationshipsXml();
|
|
285
|
-
const appXml = buildAppXml(workbookSheetNames, properties);
|
|
329
|
+
const appXml = buildAppXml(workbookSheetNames, sortedSheets.map((sheet) => sheet.type === "chartsheet"), properties);
|
|
286
330
|
const coreXml = buildCoreXml(properties);
|
|
331
|
+
// Table parts register dxfs (colour filters, colour sort levels), so they
|
|
332
|
+
// are built before styles.xml is serialized.
|
|
333
|
+
const tablePartFiles = {};
|
|
334
|
+
for (const table of preparedTables) {
|
|
335
|
+
const rows = getRowsForSheet(sheetData, table.sheetId);
|
|
336
|
+
const tableXml = buildTableXml(table, rows, sourceSharedStrings, styleBuilder, customTableStyleNames);
|
|
337
|
+
if (tableXml) {
|
|
338
|
+
tablePartFiles[table.path] = encodeXml(tableXml);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
287
341
|
const stylesXml = styleBuilder.toXml();
|
|
288
342
|
const themeXml = theme ? generateThemeXml(theme) : theme1Xml;
|
|
289
343
|
const files = {
|
|
@@ -317,13 +371,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
317
371
|
if (personsXml) {
|
|
318
372
|
files["xl/persons/person.xml"] = encodeXml(personsXml);
|
|
319
373
|
}
|
|
320
|
-
|
|
321
|
-
const rows = getRowsForSheet(sheetData, table.sheetId);
|
|
322
|
-
const tableXml = buildTableXml(table, rows, sourceSharedStrings, styleBuilder);
|
|
323
|
-
if (tableXml) {
|
|
324
|
-
files[table.path] = encodeXml(tableXml);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
374
|
+
Object.assign(files, tablePartFiles);
|
|
327
375
|
let globalImageIndex = 1;
|
|
328
376
|
let globalChartIndex = 1;
|
|
329
377
|
const drawingSheets = [];
|
|
@@ -356,6 +404,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
356
404
|
charts: chartsForSheet,
|
|
357
405
|
slicers: slicersForSheet,
|
|
358
406
|
sheetNameById,
|
|
407
|
+
resolvePivotSourceName,
|
|
359
408
|
});
|
|
360
409
|
if (!drawingParts)
|
|
361
410
|
continue;
|
|
@@ -425,7 +474,8 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
|
|
|
425
474
|
.filter((entry) => entry.threadedCommentsXml)
|
|
426
475
|
.map((entry) => entry.sheet.sheetId ?? entry.index + 1);
|
|
427
476
|
const contentTypesXml = buildContentTypesXml({
|
|
428
|
-
|
|
477
|
+
worksheetCount,
|
|
478
|
+
chartsheetCount,
|
|
429
479
|
tableCount: preparedTables.length,
|
|
430
480
|
drawingSheets,
|
|
431
481
|
mediaExtensions: Array.from(imageExtensions),
|
|
@@ -929,6 +979,27 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
|
|
|
929
979
|
threadedCommentsXml,
|
|
930
980
|
};
|
|
931
981
|
};
|
|
982
|
+
/**
|
|
983
|
+
* `CT_Chartsheet` — a sheet tab whose whole body is one chart and that has no
|
|
984
|
+
* cells (#668 C-17). Children in schema order: `sheetPr` (tab colour only),
|
|
985
|
+
* `sheetViews`, `pageMargins`, `drawing`. The chart itself rides the sheet's
|
|
986
|
+
* drawing part as an `absoluteAnchor`, like every other chart.
|
|
987
|
+
*/
|
|
988
|
+
const buildChartsheetXml = ({ sheet, relationships, drawingContext, }) => {
|
|
989
|
+
const tabColor = serializeColorXml(sheet.tabColor ?? undefined);
|
|
990
|
+
const sheetPrXml = tabColor ? `<sheetPr><tabColor ${tabColor}/></sheetPr>` : "";
|
|
991
|
+
const drawingXml = drawingContext.hasDrawing()
|
|
992
|
+
? `<drawing r:id="${drawingContext.relId}"/>`
|
|
993
|
+
: "";
|
|
994
|
+
return {
|
|
995
|
+
xml: `${XML_DECLARATION}<chartsheet xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${sheetPrXml}<sheetViews><sheetView workbookViewId="0" zoomToFit="1"/></sheetViews>${buildPageMarginsXml(sheet.printSettings)}${drawingXml}</chartsheet>`,
|
|
996
|
+
relationshipsXml: relationships.toXml(),
|
|
997
|
+
images: [],
|
|
998
|
+
commentsXml: undefined,
|
|
999
|
+
vmlDrawingXml: undefined,
|
|
1000
|
+
threadedCommentsXml: undefined,
|
|
1001
|
+
};
|
|
1002
|
+
};
|
|
932
1003
|
/**
|
|
933
1004
|
* Build combined extLst XML for worksheet (sparklines + slicers)
|
|
934
1005
|
*/
|
|
@@ -2360,7 +2431,7 @@ const resolveTableColumnTotals = (column, totalsCell) => {
|
|
|
2360
2431
|
const label = getExtendedValueString(getCellUserEnteredValue(totalsCell));
|
|
2361
2432
|
return label && label.trim() !== "" ? { totalsRowLabel: label } : undefined;
|
|
2362
2433
|
};
|
|
2363
|
-
const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
2434
|
+
const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder, customTableStyleNames) => {
|
|
2364
2435
|
const rangeRef = table.ref;
|
|
2365
2436
|
const columnCount = Math.max(table.columns?.length ?? 0, (table.range.endColumnIndex ?? table.range.startColumnIndex) -
|
|
2366
2437
|
table.range.startColumnIndex +
|
|
@@ -2434,17 +2505,71 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
|
|
|
2434
2505
|
})
|
|
2435
2506
|
.join("")}</tableColumns>`;
|
|
2436
2507
|
const isBuiltInStyle = table.theme && /^TableStyle(Light|Medium|Dark)\d+$/.test(table.theme);
|
|
2437
|
-
|
|
2508
|
+
// A custom style written to `<tableStyles>` keeps its name here (#668 T-103).
|
|
2509
|
+
const isCustomStyle = !!table.theme && (customTableStyleNames?.has(table.theme) ?? false);
|
|
2510
|
+
const tableStyleName = isBuiltInStyle || isCustomStyle ? table.theme : "TableStyleMedium2";
|
|
2438
2511
|
const tableStyleInfo = `<tableStyleInfo name="${tableStyleName}" showFirstColumn="${table.showFirstColumn ? 1 : 0}" showLastColumn="${table.showLastColumn ? 1 : 0}" showRowStripes="${table.showRowStripes ? 1 : 0}" showColumnStripes="${table.showColumnStripes ? 1 : 0}"/>`;
|
|
2512
|
+
// Excel's per-column "filter button" toggle: `<filterColumn hiddenButton="1">`
|
|
2513
|
+
// (the table-level toggle is the presence of `<autoFilter>` itself).
|
|
2514
|
+
const hiddenButtonColumns = new Set(columnNames
|
|
2515
|
+
.map((_, idx) => idx)
|
|
2516
|
+
.filter((idx) => table.columns?.[idx]?.filterButton === false));
|
|
2439
2517
|
const autoFilter = headerRowCount > 0 && table.filterButton !== false
|
|
2440
2518
|
? (() => {
|
|
2441
|
-
const filterColumns = buildFilterColumnsXml(table.filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false, styleBuilder);
|
|
2519
|
+
const filterColumns = buildFilterColumnsXml(table.filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false, styleBuilder, hiddenButtonColumns);
|
|
2442
2520
|
return filterColumns
|
|
2443
2521
|
? `<autoFilter ref="${rangeRef}">${filterColumns}</autoFilter>`
|
|
2444
2522
|
: `<autoFilter ref="${rangeRef}"/>`;
|
|
2445
2523
|
})()
|
|
2446
2524
|
: "";
|
|
2447
|
-
|
|
2525
|
+
const sortState = buildTableSortStateXml(table, headerRowCount, totalsRowCount, styleBuilder);
|
|
2526
|
+
return `${XML_DECLARATION}<table xmlns="${WORKSHEET_NS}" id="${table.id}" name="${escapeXmlAttr(table.name)}" displayName="${escapeXmlAttr(table.displayName)}" ref="${rangeRef}" totalsRowCount="${totalsRowCount}" headerRowCount="${headerRowCount}">${autoFilter}${sortState}${tableColumns}${tableStyleInfo}</table>`;
|
|
2527
|
+
};
|
|
2528
|
+
/**
|
|
2529
|
+
* A table's persisted sort levels as `<sortState ref="<body>">` with one
|
|
2530
|
+
* `<sortCondition>` per level (`CT_SortCondition`: `ref` is the level's
|
|
2531
|
+
* column over the body rows, `descending`, `sortBy` + `dxfId` for a colour
|
|
2532
|
+
* level, `sortBy="icon"` + `iconSet`/`iconId`, and Excel's comma-joined
|
|
2533
|
+
* `customList`). Sits between `<autoFilter>` and `<tableColumns>` per
|
|
2534
|
+
* `CT_Table`. The inverse of the parser's `createSortSpecsFromSortState`.
|
|
2535
|
+
*/
|
|
2536
|
+
const buildTableSortStateXml = (table, headerRowCount, totalsRowCount, styleBuilder) => {
|
|
2537
|
+
const sortSpecs = table.sortSpecs ?? [];
|
|
2538
|
+
const bodyStartRow = table.range.startRowIndex + headerRowCount;
|
|
2539
|
+
const bodyEndRow = table.range.endRowIndex - totalsRowCount;
|
|
2540
|
+
if (!sortSpecs.length || bodyEndRow < bodyStartRow)
|
|
2541
|
+
return "";
|
|
2542
|
+
const bodyRef = (startColumnIndex, endColumnIndex) => cellRangeToAddress(undefined, {
|
|
2543
|
+
startRowIndex: bodyStartRow,
|
|
2544
|
+
endRowIndex: bodyEndRow,
|
|
2545
|
+
startColumnIndex,
|
|
2546
|
+
endColumnIndex,
|
|
2547
|
+
});
|
|
2548
|
+
const conditions = sortSpecs.map((spec) => {
|
|
2549
|
+
const columnIndex = table.range.startColumnIndex + spec.dimensionIndex;
|
|
2550
|
+
const attrs = [`ref="${bodyRef(columnIndex, columnIndex)}"`];
|
|
2551
|
+
if (spec.sortOrder === "DESCENDING")
|
|
2552
|
+
attrs.push(`descending="1"`);
|
|
2553
|
+
const sortBy = spec.sortBy ?? "value";
|
|
2554
|
+
if (sortBy === "cellColor" || sortBy === "fontColor") {
|
|
2555
|
+
attrs.push(`sortBy="${sortBy}"`);
|
|
2556
|
+
const dxfId = spec.sortColor && styleBuilder
|
|
2557
|
+
? styleBuilder.registerDifferentialFormat(sortBy === "cellColor"
|
|
2558
|
+
? { backgroundColor: spec.sortColor }
|
|
2559
|
+
: { textFormat: { color: spec.sortColor } })
|
|
2560
|
+
: undefined;
|
|
2561
|
+
if (typeof dxfId === "number")
|
|
2562
|
+
attrs.push(`dxfId="${dxfId}"`);
|
|
2563
|
+
}
|
|
2564
|
+
else if (sortBy === "icon" && spec.sortIcon) {
|
|
2565
|
+
attrs.push(`sortBy="icon"`, `iconSet="${escapeXmlAttr(spec.sortIcon.iconSet)}"`, `iconId="${spec.sortIcon.iconId ?? 0}"`);
|
|
2566
|
+
}
|
|
2567
|
+
if (spec.customList?.length) {
|
|
2568
|
+
attrs.push(`customList="${escapeXmlAttr(spec.customList.join(","))}"`);
|
|
2569
|
+
}
|
|
2570
|
+
return `<sortCondition ${attrs.join(" ")}/>`;
|
|
2571
|
+
});
|
|
2572
|
+
return `<sortState ref="${bodyRef(table.range.startColumnIndex, table.range.endColumnIndex)}">${conditions.join("")}</sortState>`;
|
|
2448
2573
|
};
|
|
2449
2574
|
const getFilterVisibleValues = (spec, range, rows, headerRow = true, totalRow = false) => {
|
|
2450
2575
|
const values = new Set();
|
|
@@ -2615,6 +2740,12 @@ const xmlNumAttr = (name, value) => typeof value === "number" && Number.isFinite
|
|
|
2615
2740
|
* mandatory in practice (Excel always writes it), so the default margins go
|
|
2616
2741
|
* out when the sheet has none of its own.
|
|
2617
2742
|
*/
|
|
2743
|
+
const buildPageMarginsXml = (print) => {
|
|
2744
|
+
const margins = print?.pageMargins;
|
|
2745
|
+
return margins && Object.values(margins).some((v) => typeof v === "number")
|
|
2746
|
+
? `<pageMargins left="${margins.left ?? 0.7}" right="${margins.right ?? 0.7}" top="${margins.top ?? 0.75}" bottom="${margins.bottom ?? 0.75}" header="${margins.header ?? 0.3}" footer="${margins.footer ?? 0.3}"/>`
|
|
2747
|
+
: DEFAULT_PAGE_MARGINS;
|
|
2748
|
+
};
|
|
2618
2749
|
const buildPrintSettingsXml = (print) => {
|
|
2619
2750
|
const parts = [];
|
|
2620
2751
|
const options = print?.printOptions;
|
|
@@ -2624,11 +2755,7 @@ const buildPrintSettingsXml = (print) => {
|
|
|
2624
2755
|
xmlBoolAttr("gridLines", options?.gridLines);
|
|
2625
2756
|
if (optionAttrs)
|
|
2626
2757
|
parts.push(`<printOptions${optionAttrs}/>`);
|
|
2627
|
-
|
|
2628
|
-
parts.push(margins &&
|
|
2629
|
-
Object.values(margins).some((v) => typeof v === "number")
|
|
2630
|
-
? `<pageMargins left="${margins.left ?? 0.7}" right="${margins.right ?? 0.7}" top="${margins.top ?? 0.75}" bottom="${margins.bottom ?? 0.75}" header="${margins.header ?? 0.3}" footer="${margins.footer ?? 0.3}"/>`
|
|
2631
|
-
: DEFAULT_PAGE_MARGINS);
|
|
2758
|
+
parts.push(buildPageMarginsXml(print));
|
|
2632
2759
|
const setup = print?.pageSetup;
|
|
2633
2760
|
if (setup) {
|
|
2634
2761
|
// Excel keeps `firstPageNumber` as a dormant value until the user picks
|
|
@@ -2693,13 +2820,27 @@ const buildPrintSettingsXml = (print) => {
|
|
|
2693
2820
|
}
|
|
2694
2821
|
return parts.join("");
|
|
2695
2822
|
};
|
|
2696
|
-
const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
const entries =
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2823
|
+
const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder,
|
|
2824
|
+
/** Table-relative column ids whose header hides its filter button. */
|
|
2825
|
+
hiddenButtonColumns) => {
|
|
2826
|
+
const entries = new Map();
|
|
2827
|
+
for (const spec of filterSpecs ?? []) {
|
|
2828
|
+
const xml = buildFilterColumnXml(spec, range, rows, headerRow, totalRow, styleBuilder);
|
|
2829
|
+
if (xml)
|
|
2830
|
+
entries.set(spec.columnIndex, xml);
|
|
2831
|
+
}
|
|
2832
|
+
// `hiddenButton="1"` rides the column's criteria element when it has one,
|
|
2833
|
+
// else a bare `<filterColumn/>` carries it.
|
|
2834
|
+
hiddenButtonColumns?.forEach((colId) => {
|
|
2835
|
+
const existing = entries.get(colId);
|
|
2836
|
+
entries.set(colId, existing
|
|
2837
|
+
? existing.replace(`<filterColumn colId="${colId}">`, `<filterColumn colId="${colId}" hiddenButton="1">`)
|
|
2838
|
+
: `<filterColumn colId="${colId}" hiddenButton="1"/>`);
|
|
2839
|
+
});
|
|
2840
|
+
return [...entries.entries()]
|
|
2841
|
+
.sort(([a], [b]) => a - b)
|
|
2842
|
+
.map(([, xml]) => xml)
|
|
2843
|
+
.join("");
|
|
2703
2844
|
};
|
|
2704
2845
|
const buildFilterColumnXml = (spec, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
|
|
2705
2846
|
const parts = [];
|
|
@@ -2728,7 +2869,9 @@ const buildFilterColumnXml = (spec, range, rows, headerRow = true, totalRow = fa
|
|
|
2728
2869
|
// above-average …) takes precedence over a value-list and is emitted as
|
|
2729
2870
|
// <customFilters> or <dynamicFilter>. Falls back to the value list when the
|
|
2730
2871
|
// condition has no AutoFilter representation.
|
|
2731
|
-
const conditionXml =
|
|
2872
|
+
const conditionXml = spec.filterCriteria
|
|
2873
|
+
? buildAutoFilterCriteriaXml(spec.filterCriteria)
|
|
2874
|
+
: null;
|
|
2732
2875
|
if (conditionXml) {
|
|
2733
2876
|
parts.push(conditionXml);
|
|
2734
2877
|
}
|
|
@@ -2758,9 +2901,9 @@ const gridRangeToRef = (range) => {
|
|
|
2758
2901
|
return undefined;
|
|
2759
2902
|
}
|
|
2760
2903
|
};
|
|
2761
|
-
const groupEmbedsBySheet = (embeds) => {
|
|
2904
|
+
const groupEmbedsBySheet = (embeds, chartCount) => {
|
|
2762
2905
|
const result = new Map();
|
|
2763
|
-
(embeds ?? []).forEach((embed,
|
|
2906
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2764
2907
|
if (!embed.position?.overlayPosition)
|
|
2765
2908
|
return;
|
|
2766
2909
|
if (embed.spec?.type !== "image")
|
|
@@ -2779,13 +2922,14 @@ const groupEmbedsBySheet = (embeds) => {
|
|
|
2779
2922
|
adjustments: Object.values(adjustments).some((v) => v !== undefined)
|
|
2780
2923
|
? adjustments
|
|
2781
2924
|
: undefined,
|
|
2782
|
-
order,
|
|
2925
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2783
2926
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2784
2927
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2785
2928
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
2786
2929
|
offsetY: overlay.offsetYPixels ?? 0,
|
|
2787
2930
|
width: overlay.widthPixels ?? 120,
|
|
2788
2931
|
height: overlay.heightPixels ?? 120,
|
|
2932
|
+
overlay,
|
|
2789
2933
|
};
|
|
2790
2934
|
const list = result.get(embed.position.sheetId) ?? [];
|
|
2791
2935
|
list.push(placement);
|
|
@@ -2793,9 +2937,9 @@ const groupEmbedsBySheet = (embeds) => {
|
|
|
2793
2937
|
});
|
|
2794
2938
|
return result;
|
|
2795
2939
|
};
|
|
2796
|
-
const groupTextBoxesBySheet = (embeds) => {
|
|
2940
|
+
const groupTextBoxesBySheet = (embeds, chartCount) => {
|
|
2797
2941
|
const result = new Map();
|
|
2798
|
-
(embeds ?? []).forEach((embed,
|
|
2942
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2799
2943
|
if (!embed.position?.overlayPosition)
|
|
2800
2944
|
return;
|
|
2801
2945
|
if (embed.spec?.type !== "textbox")
|
|
@@ -2803,7 +2947,7 @@ const groupTextBoxesBySheet = (embeds) => {
|
|
|
2803
2947
|
const overlay = embed.position.overlayPosition;
|
|
2804
2948
|
const placement = {
|
|
2805
2949
|
textBox: embed,
|
|
2806
|
-
order,
|
|
2950
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2807
2951
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2808
2952
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2809
2953
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2818,9 +2962,9 @@ const groupTextBoxesBySheet = (embeds) => {
|
|
|
2818
2962
|
return result;
|
|
2819
2963
|
};
|
|
2820
2964
|
/** Form-control embeds (`spec.type === "formControl"`) by sheet, in host order. */
|
|
2821
|
-
const groupFormControlsBySheet = (embeds) => {
|
|
2965
|
+
const groupFormControlsBySheet = (embeds, chartCount) => {
|
|
2822
2966
|
const result = new Map();
|
|
2823
|
-
(embeds ?? []).forEach((embed,
|
|
2967
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2824
2968
|
if (!embed.position?.overlayPosition)
|
|
2825
2969
|
return;
|
|
2826
2970
|
if (embed.spec?.type !== "formControl")
|
|
@@ -2828,7 +2972,7 @@ const groupFormControlsBySheet = (embeds) => {
|
|
|
2828
2972
|
const overlay = embed.position.overlayPosition;
|
|
2829
2973
|
const placement = {
|
|
2830
2974
|
shape: embed,
|
|
2831
|
-
order,
|
|
2975
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2832
2976
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2833
2977
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2834
2978
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2883,18 +3027,33 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
|
|
|
2883
3027
|
const heightPx = Math.max(5, placement.height ?? 20);
|
|
2884
3028
|
const columnSize = (index) => getColumnWidthPx(sheet, index, true);
|
|
2885
3029
|
const rowSize = (index) => getRowHeightPx(sheet, index, true);
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3030
|
+
// A "Move and size" control keeps its persisted second anchor (#668 C-46)
|
|
3031
|
+
const overlay = placement.shape.position?.overlayPosition;
|
|
3032
|
+
const persistedTo = controlAnchorFlags(spec, overlay).sizeWithCells && overlay?.toCell
|
|
3033
|
+
? overlay.toCell
|
|
3034
|
+
: undefined;
|
|
3035
|
+
const toColumn = persistedTo
|
|
3036
|
+
? {
|
|
3037
|
+
index: Math.max(0, persistedTo.columnIndex - 1),
|
|
3038
|
+
offset: Math.max(0, overlay?.toOffsetXPixels ?? 0),
|
|
3039
|
+
}
|
|
3040
|
+
: resolveAnchorEndpoint({
|
|
3041
|
+
startIndex: fromColumn,
|
|
3042
|
+
startOffset: offsetX,
|
|
3043
|
+
span: widthPx,
|
|
3044
|
+
getSize: columnSize,
|
|
3045
|
+
});
|
|
3046
|
+
const toRow = persistedTo
|
|
3047
|
+
? {
|
|
3048
|
+
index: Math.max(0, persistedTo.rowIndex - 1),
|
|
3049
|
+
offset: Math.max(0, overlay?.toOffsetYPixels ?? 0),
|
|
3050
|
+
}
|
|
3051
|
+
: resolveAnchorEndpoint({
|
|
3052
|
+
startIndex: fromRow,
|
|
3053
|
+
startOffset: offsetY,
|
|
3054
|
+
span: heightPx,
|
|
3055
|
+
getSize: rowSize,
|
|
3056
|
+
});
|
|
2898
3057
|
const partIndex = ctrlProps.parts.length + 1;
|
|
2899
3058
|
const path = `xl/ctrlProps/ctrlProp${partIndex}.xml`;
|
|
2900
3059
|
ctrlProps.parts.push({ path, xml: buildCtrlPropXml(spec) });
|
|
@@ -2916,11 +3075,13 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
|
|
|
2916
3075
|
row: toRow.index,
|
|
2917
3076
|
rowOff: toRow.offset * EMUS_PER_PIXEL,
|
|
2918
3077
|
},
|
|
3078
|
+
overlay,
|
|
2919
3079
|
}));
|
|
2920
3080
|
vmlShapes.push(buildFormControlVmlShape({
|
|
2921
3081
|
spec,
|
|
2922
3082
|
shapeId,
|
|
2923
3083
|
zIndex: zIndexStart + position + 1,
|
|
3084
|
+
overlay,
|
|
2924
3085
|
geometry: {
|
|
2925
3086
|
leftPx: sumSizes(fromColumn, columnSize) + offsetX,
|
|
2926
3087
|
topPx: sumSizes(fromRow, rowSize) + offsetY,
|
|
@@ -2941,9 +3102,9 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
|
|
|
2941
3102
|
});
|
|
2942
3103
|
return { controlsXml: buildControlsXml(controlElements), vmlShapes };
|
|
2943
3104
|
};
|
|
2944
|
-
const groupShapesBySheet = (embeds) => {
|
|
3105
|
+
const groupShapesBySheet = (embeds, chartCount) => {
|
|
2945
3106
|
const result = new Map();
|
|
2946
|
-
(embeds ?? []).forEach((embed,
|
|
3107
|
+
(embeds ?? []).forEach((embed, index) => {
|
|
2947
3108
|
if (!embed.position?.overlayPosition)
|
|
2948
3109
|
return;
|
|
2949
3110
|
if (embed.spec?.type !== "shape")
|
|
@@ -2951,7 +3112,7 @@ const groupShapesBySheet = (embeds) => {
|
|
|
2951
3112
|
const overlay = embed.position.overlayPosition;
|
|
2952
3113
|
const placement = {
|
|
2953
3114
|
shape: embed,
|
|
2954
|
-
order,
|
|
3115
|
+
order: embed.position.zOrder ?? chartCount + index,
|
|
2955
3116
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2956
3117
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2957
3118
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2967,12 +3128,13 @@ const groupShapesBySheet = (embeds) => {
|
|
|
2967
3128
|
};
|
|
2968
3129
|
const groupChartsBySheet = (charts) => {
|
|
2969
3130
|
const result = new Map();
|
|
2970
|
-
|
|
3131
|
+
(charts ?? []).forEach((chart, index) => {
|
|
2971
3132
|
const overlay = chart.position?.overlayPosition;
|
|
2972
3133
|
if (!overlay)
|
|
2973
|
-
|
|
3134
|
+
return;
|
|
2974
3135
|
const placement = {
|
|
2975
3136
|
chart,
|
|
3137
|
+
order: chart.position.zOrder ?? index,
|
|
2976
3138
|
rowIndex: overlay.anchorCell.rowIndex,
|
|
2977
3139
|
columnIndex: overlay.anchorCell.columnIndex,
|
|
2978
3140
|
offsetX: overlay.offsetXPixels ?? 0,
|
|
@@ -2983,7 +3145,7 @@ const groupChartsBySheet = (charts) => {
|
|
|
2983
3145
|
const list = result.get(chart.position.sheetId) ?? [];
|
|
2984
3146
|
list.push(placement);
|
|
2985
3147
|
result.set(chart.position.sheetId, list);
|
|
2986
|
-
}
|
|
3148
|
+
});
|
|
2987
3149
|
return result;
|
|
2988
3150
|
};
|
|
2989
3151
|
const groupSlicersBySheet = (slicers) => {
|
|
@@ -3554,7 +3716,7 @@ const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseC
|
|
|
3554
3716
|
rules.push(dataBarXml);
|
|
3555
3717
|
return rules.length ? rules : undefined;
|
|
3556
3718
|
};
|
|
3557
|
-
const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, }) => {
|
|
3719
|
+
const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, resolvePivotSourceName, }) => {
|
|
3558
3720
|
if (!placements.length &&
|
|
3559
3721
|
!textBoxes.length &&
|
|
3560
3722
|
!shapes.length &&
|
|
@@ -3571,6 +3733,12 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3571
3733
|
let frameId = 1;
|
|
3572
3734
|
const chartEntries = [];
|
|
3573
3735
|
const chartExEntries = [];
|
|
3736
|
+
// Charts and embeds (text boxes, shapes, images) are written back-to-front
|
|
3737
|
+
// by their shared z-order — anchor order IS Excel's z-order, so a picture
|
|
3738
|
+
// sent behind a chart must be emitted before it regardless of kind.
|
|
3739
|
+
// Placements without an order (in-cell images) go last, in their own order.
|
|
3740
|
+
const orderedAnchors = [];
|
|
3741
|
+
const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
|
|
3574
3742
|
for (const placement of charts) {
|
|
3575
3743
|
if (isChartExType(placement.chart.spec.chartType)) {
|
|
3576
3744
|
// Treemap / sunburst live in a chartex part with an mc:AlternateContent anchor.
|
|
@@ -3578,7 +3746,16 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3578
3746
|
if (!chartExXml)
|
|
3579
3747
|
continue;
|
|
3580
3748
|
const relId = relationships.add(CHART_EX_REL_TYPE, `../charts/chartEx${chartExIndex}.xml`);
|
|
3581
|
-
|
|
3749
|
+
orderedAnchors.push({
|
|
3750
|
+
order: placement.order,
|
|
3751
|
+
xml: createChartAnchor({
|
|
3752
|
+
sheet,
|
|
3753
|
+
placement,
|
|
3754
|
+
relId,
|
|
3755
|
+
frameId,
|
|
3756
|
+
chartEx: true,
|
|
3757
|
+
}),
|
|
3758
|
+
});
|
|
3582
3759
|
chartExEntries.push({
|
|
3583
3760
|
path: `xl/charts/chartEx${chartExIndex}.xml`,
|
|
3584
3761
|
xml: chartExXml,
|
|
@@ -3587,16 +3764,19 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3587
3764
|
frameId += 1;
|
|
3588
3765
|
continue;
|
|
3589
3766
|
}
|
|
3590
|
-
const chartXml = buildChartXml(placement.chart, sheetNameById);
|
|
3767
|
+
const chartXml = buildChartXml(placement.chart, sheetNameById, resolvePivotSourceName);
|
|
3591
3768
|
if (!chartXml)
|
|
3592
3769
|
continue;
|
|
3593
3770
|
const relId = relationships.add(CHART_REL_TYPE, `../charts/chart${chartIndex}.xml`);
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3771
|
+
orderedAnchors.push({
|
|
3772
|
+
order: placement.order,
|
|
3773
|
+
xml: createChartAnchor({
|
|
3774
|
+
sheet,
|
|
3775
|
+
placement,
|
|
3776
|
+
relId,
|
|
3777
|
+
frameId,
|
|
3778
|
+
}),
|
|
3779
|
+
});
|
|
3600
3780
|
chartEntries.push({
|
|
3601
3781
|
path: `xl/charts/chart${chartIndex}.xml`,
|
|
3602
3782
|
xml: chartXml,
|
|
@@ -3612,14 +3792,8 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3612
3792
|
}));
|
|
3613
3793
|
frameId += 1;
|
|
3614
3794
|
}
|
|
3615
|
-
// Embeds (text boxes, shapes, images) keep the host's `embeds` order in the
|
|
3616
|
-
// drawing part — anchor order IS Excel's z-order, so an image drawn over a
|
|
3617
|
-
// shape must be emitted after it regardless of kind. Placements without an
|
|
3618
|
-
// order (in-cell images) go last, in their own order.
|
|
3619
|
-
const embedAnchors = [];
|
|
3620
|
-
const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
|
|
3621
3795
|
for (const placement of textBoxes) {
|
|
3622
|
-
|
|
3796
|
+
orderedAnchors.push({
|
|
3623
3797
|
order: orderOf(placement.order),
|
|
3624
3798
|
xml: createTextBoxAnchor({
|
|
3625
3799
|
sheet,
|
|
@@ -3630,7 +3804,7 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3630
3804
|
frameId += 1;
|
|
3631
3805
|
}
|
|
3632
3806
|
for (const placement of shapes) {
|
|
3633
|
-
|
|
3807
|
+
orderedAnchors.push({
|
|
3634
3808
|
order: orderOf(placement.order),
|
|
3635
3809
|
xml: createShapeAnchor({
|
|
3636
3810
|
sheet,
|
|
@@ -3654,7 +3828,7 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3654
3828
|
extension: buffer.extension,
|
|
3655
3829
|
});
|
|
3656
3830
|
const relId = relationships.add(IMAGE_REL_TYPE, `../media/image${imageIndex}.${buffer.extension}`);
|
|
3657
|
-
|
|
3831
|
+
orderedAnchors.push({
|
|
3658
3832
|
order: orderOf(placement.order),
|
|
3659
3833
|
xml: createTwoCellAnchor({
|
|
3660
3834
|
sheet,
|
|
@@ -3666,8 +3840,8 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
|
|
|
3666
3840
|
imageIndex += 1;
|
|
3667
3841
|
pictureId += 1;
|
|
3668
3842
|
}
|
|
3669
|
-
|
|
3670
|
-
for (const entry of
|
|
3843
|
+
orderedAnchors.sort((a, b) => a.order - b.order);
|
|
3844
|
+
for (const entry of orderedAnchors)
|
|
3671
3845
|
anchors.push(entry.xml);
|
|
3672
3846
|
const drawingXml = `${XML_DECLARATION}<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${REL_NS}">${anchors.join("")}</xdr:wsDr>`;
|
|
3673
3847
|
return {
|
|
@@ -3690,25 +3864,21 @@ const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
|
|
|
3690
3864
|
const defaultHeight = placement.kind === "cell" ? getRowHeightPx(sheet, fromRow) : 120;
|
|
3691
3865
|
const widthPx = Math.max(5, placement.width ?? defaultWidth);
|
|
3692
3866
|
const heightPx = Math.max(5, placement.height ?? defaultHeight);
|
|
3693
|
-
const toColumn = resolveAnchorEndpoint({
|
|
3694
|
-
startIndex: fromColumn,
|
|
3695
|
-
startOffset: offsetX,
|
|
3696
|
-
span: widthPx,
|
|
3697
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3698
|
-
});
|
|
3699
|
-
const toRow = resolveAnchorEndpoint({
|
|
3700
|
-
startIndex: fromRow,
|
|
3701
|
-
startOffset: offsetY,
|
|
3702
|
-
span: heightPx,
|
|
3703
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3704
|
-
});
|
|
3705
|
-
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
|
|
3706
|
-
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
|
|
3707
3867
|
const { blipChildren, srcRectXml } = buildPictureAdjustmentsXml(placement.adjustments);
|
|
3708
3868
|
const blipXml = blipChildren
|
|
3709
3869
|
? `<a:blip r:embed="${relId}">${blipChildren}</a:blip>`
|
|
3710
3870
|
: `<a:blip r:embed="${relId}"/>`;
|
|
3711
|
-
return
|
|
3871
|
+
return buildDrawingAnchorXml({
|
|
3872
|
+
sheet,
|
|
3873
|
+
overlay: placement.overlay,
|
|
3874
|
+
fromRow,
|
|
3875
|
+
fromColumn,
|
|
3876
|
+
offsetX,
|
|
3877
|
+
offsetY,
|
|
3878
|
+
widthPx,
|
|
3879
|
+
heightPx,
|
|
3880
|
+
body: `<xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${pictureId}" name="Picture ${pictureId}"/><xdr:cNvPicPr/></xdr:nvPicPr><xdr:blipFill>${blipXml}${srcRectXml}<a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic>`,
|
|
3881
|
+
});
|
|
3712
3882
|
};
|
|
3713
3883
|
/** Fraction (0…1) → DrawingML per-mille percent (100000 = 100%). */
|
|
3714
3884
|
const fractionToPct = (value) => Math.round(Math.max(-1, Math.min(1, value)) * 100000);
|
|
@@ -3759,29 +3929,30 @@ const createChartAnchor = ({ placement, relId, frameId, sheet, chartEx = false,
|
|
|
3759
3929
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3760
3930
|
const widthPx = Math.max(5, placement.width ?? 300);
|
|
3761
3931
|
const heightPx = Math.max(5, placement.height ?? 200);
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
startOffset: offsetX,
|
|
3765
|
-
span: widthPx,
|
|
3766
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3767
|
-
});
|
|
3768
|
-
const toRow = resolveAnchorEndpoint({
|
|
3769
|
-
startIndex: fromRow,
|
|
3770
|
-
startOffset: offsetY,
|
|
3771
|
-
span: heightPx,
|
|
3772
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3773
|
-
});
|
|
3774
|
-
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
|
|
3775
|
-
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
|
|
3776
|
-
if (chartEx) {
|
|
3777
|
-
return `<xdr:twoCellAnchor>${fromXml}${toXml}${buildChartExGraphicFrameXml({
|
|
3932
|
+
const body = chartEx
|
|
3933
|
+
? buildChartExGraphicFrameXml({
|
|
3778
3934
|
relId,
|
|
3779
3935
|
frameId,
|
|
3780
3936
|
widthEmu: Math.round(widthPx * EMUS_PER_PIXEL),
|
|
3781
3937
|
heightEmu: Math.round(heightPx * EMUS_PER_PIXEL),
|
|
3782
|
-
})
|
|
3783
|
-
|
|
3784
|
-
|
|
3938
|
+
})
|
|
3939
|
+
: `<xdr:graphicFrame><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="Chart ${frameId}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="${REL_NS}" r:id="${relId}"/></a:graphicData></a:graphic></xdr:graphicFrame>`;
|
|
3940
|
+
const overlay = placement.chart.position?.overlayPosition;
|
|
3941
|
+
return buildDrawingAnchorXml({
|
|
3942
|
+
sheet,
|
|
3943
|
+
// A chartsheet has no cells to anchor to: Excel only reads an
|
|
3944
|
+
// `absoluteAnchor` there, whatever placement the chart carries.
|
|
3945
|
+
overlay: sheet.type === "chartsheet" && overlay
|
|
3946
|
+
? { ...overlay, placement: "absolute" }
|
|
3947
|
+
: overlay,
|
|
3948
|
+
fromRow,
|
|
3949
|
+
fromColumn,
|
|
3950
|
+
offsetX,
|
|
3951
|
+
offsetY,
|
|
3952
|
+
widthPx,
|
|
3953
|
+
heightPx,
|
|
3954
|
+
body,
|
|
3955
|
+
});
|
|
3785
3956
|
};
|
|
3786
3957
|
const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
|
|
3787
3958
|
const fromRow = Math.max(0, placement.rowIndex - 1);
|
|
@@ -3790,21 +3961,42 @@ const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3790
3961
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3791
3962
|
const widthPx = Math.max(50, placement.width ?? 200);
|
|
3792
3963
|
const heightPx = Math.max(50, placement.height ?? 200);
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
const
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3964
|
+
// Excel keeps a slicer in a `twoCellAnchor` whatever its placement and
|
|
3965
|
+
// spells the other two modes as `editAs` (#668 C-46); a "Move and size"
|
|
3966
|
+
// slicer keeps its persisted second anchor.
|
|
3967
|
+
const overlay = placement.overlay;
|
|
3968
|
+
const mode = overlay?.placement ?? "moveAndSize";
|
|
3969
|
+
const persistedTo = mode === "moveAndSize" ? overlay?.toCell : undefined;
|
|
3970
|
+
const editAs = mode === "moveOnly"
|
|
3971
|
+
? ' editAs="oneCell"'
|
|
3972
|
+
: mode === "absolute"
|
|
3973
|
+
? ' editAs="absolute"'
|
|
3974
|
+
: "";
|
|
3975
|
+
const toColumn = persistedTo
|
|
3976
|
+
? {
|
|
3977
|
+
index: Math.max(0, persistedTo.columnIndex - 1),
|
|
3978
|
+
offset: Math.max(0, overlay?.toOffsetXPixels ?? 0),
|
|
3979
|
+
}
|
|
3980
|
+
: resolveAnchorEndpoint({
|
|
3981
|
+
startIndex: fromColumn,
|
|
3982
|
+
startOffset: offsetX,
|
|
3983
|
+
span: widthPx,
|
|
3984
|
+
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3985
|
+
});
|
|
3986
|
+
const toRow = persistedTo
|
|
3987
|
+
? {
|
|
3988
|
+
index: Math.max(0, persistedTo.rowIndex - 1),
|
|
3989
|
+
offset: Math.max(0, overlay?.toOffsetYPixels ?? 0),
|
|
3990
|
+
}
|
|
3991
|
+
: resolveAnchorEndpoint({
|
|
3992
|
+
startIndex: fromRow,
|
|
3993
|
+
startOffset: offsetY,
|
|
3994
|
+
span: heightPx,
|
|
3995
|
+
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3996
|
+
});
|
|
3805
3997
|
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
|
|
3806
3998
|
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
|
|
3807
|
-
return `<xdr:twoCellAnchor
|
|
3999
|
+
return `<xdr:twoCellAnchor${editAs}>${fromXml}${toXml}<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:sle15="http://schemas.microsoft.com/office/drawing/2012/slicer" Requires="sle15"><xdr:graphicFrame macro=""><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="${escapeXmlAttr(placement.title)}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/drawing/2010/slicer"><sle:slicer xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" name="${escapeXmlAttr(placement.title)}"/></a:graphicData></a:graphic></xdr:graphicFrame></mc:Choice></mc:AlternateContent><xdr:clientData/></xdr:twoCellAnchor>`;
|
|
3808
4000
|
};
|
|
3809
4001
|
const createTextBoxAnchor = ({ placement, frameId, sheet, }) => {
|
|
3810
4002
|
const fromRow = Math.max(0, placement.rowIndex - 1);
|
|
@@ -3813,24 +4005,20 @@ const createTextBoxAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3813
4005
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3814
4006
|
const widthPx = Math.max(50, placement.width ?? 200);
|
|
3815
4007
|
const heightPx = Math.max(50, placement.height ?? 60);
|
|
3816
|
-
const toColumn = resolveAnchorEndpoint({
|
|
3817
|
-
startIndex: fromColumn,
|
|
3818
|
-
startOffset: offsetX,
|
|
3819
|
-
span: widthPx,
|
|
3820
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3821
|
-
});
|
|
3822
|
-
const toRow = resolveAnchorEndpoint({
|
|
3823
|
-
startIndex: fromRow,
|
|
3824
|
-
startOffset: offsetY,
|
|
3825
|
-
span: heightPx,
|
|
3826
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3827
|
-
});
|
|
3828
|
-
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
|
|
3829
|
-
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
|
|
3830
4008
|
const spec = placement.textBox.spec;
|
|
3831
4009
|
const textBodyXml = buildTextBodyXml(spec);
|
|
3832
4010
|
const shapePropertiesXml = buildShapePropertiesXml(spec, widthPx, heightPx);
|
|
3833
|
-
return
|
|
4011
|
+
return buildDrawingAnchorXml({
|
|
4012
|
+
sheet,
|
|
4013
|
+
overlay: placement.textBox.position?.overlayPosition,
|
|
4014
|
+
fromRow,
|
|
4015
|
+
fromColumn,
|
|
4016
|
+
offsetX,
|
|
4017
|
+
offsetY,
|
|
4018
|
+
widthPx,
|
|
4019
|
+
heightPx,
|
|
4020
|
+
body: `<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="TextBox ${frameId}"/><xdr:cNvSpPr txBox="1"/></xdr:nvSpPr>${shapePropertiesXml}${textBodyXml}</xdr:sp>`,
|
|
4021
|
+
});
|
|
3834
4022
|
};
|
|
3835
4023
|
/** ColorIndex slots → canonical DrawingML scheme names (inverse of the importer's map). */
|
|
3836
4024
|
const THEME_INDEX_TO_SCHEME = [
|
|
@@ -3948,20 +4136,6 @@ const createShapeAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3948
4136
|
const offsetY = Math.max(0, placement.offsetY ?? 0);
|
|
3949
4137
|
const widthPx = Math.max(10, placement.width ?? 120);
|
|
3950
4138
|
const heightPx = Math.max(10, placement.height ?? 60);
|
|
3951
|
-
const toColumn = resolveAnchorEndpoint({
|
|
3952
|
-
startIndex: fromColumn,
|
|
3953
|
-
startOffset: offsetX,
|
|
3954
|
-
span: widthPx,
|
|
3955
|
-
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
3956
|
-
});
|
|
3957
|
-
const toRow = resolveAnchorEndpoint({
|
|
3958
|
-
startIndex: fromRow,
|
|
3959
|
-
startOffset: offsetY,
|
|
3960
|
-
span: heightPx,
|
|
3961
|
-
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
3962
|
-
});
|
|
3963
|
-
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
|
|
3964
|
-
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
|
|
3965
4139
|
const xfrmAttrs = [
|
|
3966
4140
|
placement.shape.rotation
|
|
3967
4141
|
? ` rot="${Math.round(placement.shape.rotation * 60000)}"`
|
|
@@ -3988,7 +4162,17 @@ const createShapeAnchor = ({ placement, frameId, sheet, }) => {
|
|
|
3988
4162
|
? buildTextBodyXml(spec.textBody)
|
|
3989
4163
|
: `<xdr:txBody><a:bodyPr vertOverflow="clip" horzOverflow="clip" rtlCol="0" anchor="t"/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></xdr:txBody>`;
|
|
3990
4164
|
const name = escapeXmlAttr(spec.name ?? `Shape ${frameId}`);
|
|
3991
|
-
return
|
|
4165
|
+
return buildDrawingAnchorXml({
|
|
4166
|
+
sheet,
|
|
4167
|
+
overlay: placement.shape.position?.overlayPosition,
|
|
4168
|
+
fromRow,
|
|
4169
|
+
fromColumn,
|
|
4170
|
+
offsetX,
|
|
4171
|
+
offsetY,
|
|
4172
|
+
widthPx,
|
|
4173
|
+
heightPx,
|
|
4174
|
+
body: `<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="${name}"/><xdr:cNvSpPr/></xdr:nvSpPr>${spPr}${styleXml}${textBodyXml}</xdr:sp>`,
|
|
4175
|
+
});
|
|
3992
4176
|
};
|
|
3993
4177
|
const buildTextWithTabs = (text, rPrXml) => {
|
|
3994
4178
|
const parts = text.split("\t");
|
|
@@ -4239,6 +4423,60 @@ const buildShapePropertiesXml = (spec, widthPx, heightPx) => {
|
|
|
4239
4423
|
}
|
|
4240
4424
|
return `<xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom>${fillXml}${outlineXml}</xdr:spPr>`;
|
|
4241
4425
|
};
|
|
4426
|
+
/**
|
|
4427
|
+
* Wrap a drawing object's body in the anchor its placement mode calls for
|
|
4428
|
+
* (#668 C-06). "Move and size with cells" (Excel's default) is a
|
|
4429
|
+
* `twoCellAnchor` whose `to` is the object's persisted second anchor — or,
|
|
4430
|
+
* for an object that has none yet, the cell its pixel extent reaches over
|
|
4431
|
+
* this sheet's row / column sizes. "Move but don't size" is a
|
|
4432
|
+
* `oneCellAnchor` (`from` + `ext`); "Don't move or size" is an
|
|
4433
|
+
* `absoluteAnchor` (`pos` + `ext`). The importers read all three back into
|
|
4434
|
+
* the same placement.
|
|
4435
|
+
*/
|
|
4436
|
+
const buildDrawingAnchorXml = ({ sheet, overlay, fromRow, fromColumn, offsetX, offsetY, widthPx, heightPx, body, }) => {
|
|
4437
|
+
const emu = (px) => Math.round(px * EMUS_PER_PIXEL);
|
|
4438
|
+
const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${emu(offsetX)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${emu(offsetY)}</xdr:rowOff></xdr:from>`;
|
|
4439
|
+
const extXml = `<xdr:ext cx="${emu(widthPx)}" cy="${emu(heightPx)}"/>`;
|
|
4440
|
+
const placement = overlay?.placement ?? "moveAndSize";
|
|
4441
|
+
if (placement === "moveOnly") {
|
|
4442
|
+
return `<xdr:oneCellAnchor>${fromXml}${extXml}${body}<xdr:clientData/></xdr:oneCellAnchor>`;
|
|
4443
|
+
}
|
|
4444
|
+
if (placement === "absolute") {
|
|
4445
|
+
let x = offsetX;
|
|
4446
|
+
for (let index = 0; index < fromColumn; index++) {
|
|
4447
|
+
x += getColumnWidthPx(sheet, index, true);
|
|
4448
|
+
}
|
|
4449
|
+
let y = offsetY;
|
|
4450
|
+
for (let index = 0; index < fromRow; index++) {
|
|
4451
|
+
y += getRowHeightPx(sheet, index, true);
|
|
4452
|
+
}
|
|
4453
|
+
return `<xdr:absoluteAnchor><xdr:pos x="${emu(x)}" y="${emu(y)}"/>${extXml}${body}<xdr:clientData/></xdr:absoluteAnchor>`;
|
|
4454
|
+
}
|
|
4455
|
+
const toColumn = overlay?.toCell
|
|
4456
|
+
? {
|
|
4457
|
+
index: Math.max(0, overlay.toCell.columnIndex - 1),
|
|
4458
|
+
offset: Math.max(0, overlay.toOffsetXPixels ?? 0),
|
|
4459
|
+
}
|
|
4460
|
+
: resolveAnchorEndpoint({
|
|
4461
|
+
startIndex: fromColumn,
|
|
4462
|
+
startOffset: offsetX,
|
|
4463
|
+
span: widthPx,
|
|
4464
|
+
getSize: (index) => getColumnWidthPx(sheet, index, true),
|
|
4465
|
+
});
|
|
4466
|
+
const toRow = overlay?.toCell
|
|
4467
|
+
? {
|
|
4468
|
+
index: Math.max(0, overlay.toCell.rowIndex - 1),
|
|
4469
|
+
offset: Math.max(0, overlay.toOffsetYPixels ?? 0),
|
|
4470
|
+
}
|
|
4471
|
+
: resolveAnchorEndpoint({
|
|
4472
|
+
startIndex: fromRow,
|
|
4473
|
+
startOffset: offsetY,
|
|
4474
|
+
span: heightPx,
|
|
4475
|
+
getSize: (index) => getRowHeightPx(sheet, index, true),
|
|
4476
|
+
});
|
|
4477
|
+
const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${emu(toColumn.offset)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${emu(toRow.offset)}</xdr:rowOff></xdr:to>`;
|
|
4478
|
+
return `<xdr:twoCellAnchor>${fromXml}${toXml}${body}<xdr:clientData/></xdr:twoCellAnchor>`;
|
|
4479
|
+
};
|
|
4242
4480
|
const resolveAnchorEndpoint = ({ startIndex, startOffset, span, getSize, }) => {
|
|
4243
4481
|
let index = startIndex;
|
|
4244
4482
|
let offset = Math.max(0, startOffset);
|
|
@@ -4548,15 +4786,18 @@ const buildWorkbookSlicerCachesXml = (slicerCacheRelIds) => {
|
|
|
4548
4786
|
.join("");
|
|
4549
4787
|
return `<extLst><ext uri="{46BE6895-7355-4a93-B00E-2C351335B9C9}" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"><x15:slicerCaches xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">${cacheEntries}</x15:slicerCaches></ext></extLst>`;
|
|
4550
4788
|
};
|
|
4551
|
-
const buildWorkbookRelationships = (
|
|
4789
|
+
const buildWorkbookRelationships = (
|
|
4790
|
+
/** One entry per sheet in `<sheets>` order; `target` is relative to `xl/`. */
|
|
4791
|
+
sheetParts, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, hasPersons = false) => {
|
|
4552
4792
|
const relationships = [];
|
|
4553
4793
|
const sheetRelIds = [];
|
|
4554
4794
|
const slicerCacheRelIds = [];
|
|
4555
|
-
|
|
4795
|
+
const sheetCount = sheetParts.length;
|
|
4796
|
+
sheetParts.forEach((part, i) => {
|
|
4556
4797
|
const relId = `rId${i + 1}`;
|
|
4557
4798
|
sheetRelIds.push(relId);
|
|
4558
|
-
relationships.push(`<Relationship Id="${relId}" Type="${REL_NS}
|
|
4559
|
-
}
|
|
4799
|
+
relationships.push(`<Relationship Id="${relId}" Type="${REL_NS}/${part.chartsheet ? "chartsheet" : "worksheet"}" Target="${part.target}"/>`);
|
|
4800
|
+
});
|
|
4560
4801
|
const stylesId = `rId${sheetCount + 1}`;
|
|
4561
4802
|
const themeId = `rId${sheetCount + 2}`;
|
|
4562
4803
|
const sharedStringsId = `rId${sheetCount + 3}`;
|
|
@@ -4600,10 +4841,11 @@ const buildWorkbookRelationships = (sheetCount, slicerCacheCount = 0, externalLi
|
|
|
4600
4841
|
slicerCacheRelationshipIds: slicerCacheRelIds,
|
|
4601
4842
|
};
|
|
4602
4843
|
};
|
|
4603
|
-
const buildContentTypesXml = ({
|
|
4604
|
-
const sheetOverrides = Array.from({ length:
|
|
4844
|
+
const buildContentTypesXml = ({ worksheetCount, chartsheetCount = 0, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, }) => {
|
|
4845
|
+
const sheetOverrides = Array.from({ length: worksheetCount }, (_, index) => {
|
|
4605
4846
|
return `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
|
4606
4847
|
}).join("");
|
|
4848
|
+
const chartsheetOverrides = Array.from({ length: chartsheetCount }, (_, index) => `<Override PartName="/xl/chartsheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"/>`).join("");
|
|
4607
4849
|
const tableOverrides = Array.from({ length: tableCount }, (_, index) => {
|
|
4608
4850
|
return `<Override PartName="/xl/tables/table${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"/>`;
|
|
4609
4851
|
}).join("");
|
|
@@ -4644,13 +4886,25 @@ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExte
|
|
|
4644
4886
|
? `<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>`
|
|
4645
4887
|
: "";
|
|
4646
4888
|
const ctrlPropOverrides = Array.from({ length: ctrlPropCount }, (_, index) => `<Override PartName="/xl/ctrlProps/ctrlProp${index + 1}.xml" ContentType="${CTRL_PROP_CONTENT_TYPE}"/>`).join("");
|
|
4647
|
-
return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
|
|
4889
|
+
return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${chartsheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
|
|
4648
4890
|
};
|
|
4649
4891
|
const buildRootRelationshipsXml = () => {
|
|
4650
4892
|
return `${XML_DECLARATION}<Relationships xmlns="${PACKAGE_REL_NS}"><Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="${REL_NS}/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId3" Type="${REL_NS}/core-properties" Target="docProps/core.xml"/></Relationships>`;
|
|
4651
4893
|
};
|
|
4652
|
-
const buildAppXml = (sheetNames, properties) => {
|
|
4653
|
-
|
|
4894
|
+
const buildAppXml = (sheetNames, isChartsheet, properties) => {
|
|
4895
|
+
// Excel groups the tabs by kind: `HeadingPairs` counts `Worksheets` then
|
|
4896
|
+
// `Charts` (a pair is only written for a non-empty kind) and `TitlesOfParts`
|
|
4897
|
+
// lists the worksheet names followed by the chartsheet names.
|
|
4898
|
+
const worksheetNames = sheetNames.filter((_, index) => !isChartsheet[index]);
|
|
4899
|
+
const chartsheetNames = sheetNames.filter((_, index) => isChartsheet[index]);
|
|
4900
|
+
const headingPairs = [
|
|
4901
|
+
["Worksheets", worksheetNames.length],
|
|
4902
|
+
["Charts", chartsheetNames.length],
|
|
4903
|
+
].filter(([, count]) => count > 0);
|
|
4904
|
+
const headingPairsXml = headingPairs
|
|
4905
|
+
.map(([kind, count]) => `<vt:variant><vt:lpstr>${kind}</vt:lpstr></vt:variant><vt:variant><vt:i4>${count}</vt:i4></vt:variant>`)
|
|
4906
|
+
.join("");
|
|
4907
|
+
const titles = [...worksheetNames, ...chartsheetNames]
|
|
4654
4908
|
.map((name) => `<vt:lpstr>${escapeXmlText(name)}</vt:lpstr>`)
|
|
4655
4909
|
.join("");
|
|
4656
4910
|
const application = properties?.application ?? CREATOR;
|
|
@@ -4660,7 +4914,7 @@ const buildAppXml = (sheetNames, properties) => {
|
|
|
4660
4914
|
const managerTag = properties?.manager
|
|
4661
4915
|
? `<Manager>${escapeXmlText(properties.manager)}</Manager>`
|
|
4662
4916
|
: "";
|
|
4663
|
-
return `${XML_DECLARATION}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>${escapeXmlText(application)}</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="2" baseType="variant"
|
|
4917
|
+
return `${XML_DECLARATION}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>${escapeXmlText(application)}</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="${headingPairs.length * 2}" baseType="variant">${headingPairsXml}</vt:vector></HeadingPairs><TitlesOfParts><vt:vector size="${sheetNames.length}" baseType="lpstr">${titles}</vt:vector></TitlesOfParts>${companyTag}${managerTag}</Properties>`;
|
|
4664
4918
|
};
|
|
4665
4919
|
const buildCoreXml = (properties) => {
|
|
4666
4920
|
const now = new Date().toISOString();
|