@niicojs/excel 0.3.4 → 0.3.5
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/LICENSE +20 -20
- package/README.md +8 -2
- package/dist/index.cjs +1187 -1265
- package/dist/index.d.cts +171 -324
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +171 -324
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1187 -1266
- package/package.json +4 -4
- package/src/index.ts +8 -10
- package/src/pivot-table.ts +619 -524
- package/src/shared-strings.ts +33 -9
- package/src/styles.ts +38 -9
- package/src/types.ts +295 -323
- package/src/utils/address.ts +48 -0
- package/src/utils/format.ts +8 -7
- package/src/utils/xml.ts +1 -1
- package/src/utils/zip.ts +153 -11
- package/src/workbook.ts +330 -350
- package/src/worksheet.ts +1003 -935
- package/src/pivot-cache.ts +0 -449
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
|
3
|
-
import { unzipSync, unzip,
|
|
3
|
+
import { unzipSync, unzip, zipSync, zip, strFromU8, strToU8 } from 'fflate';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Converts a column index (0-based) to Excel column letters (A, B, ..., Z, AA, AB, ...)
|
|
@@ -89,6 +89,45 @@ import { unzipSync, unzip, strFromU8, zipSync, zip, strToU8 } from 'fflate';
|
|
|
89
89
|
}
|
|
90
90
|
return `${start}:${end}`;
|
|
91
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* Parses a qualified sheet + address reference.
|
|
94
|
+
* Supports Sheet!A1 and 'Sheet Name'!A1.
|
|
95
|
+
*/ const parseSheetAddress = (reference)=>{
|
|
96
|
+
const exclamationIndex = reference.lastIndexOf('!');
|
|
97
|
+
if (exclamationIndex <= 0 || exclamationIndex >= reference.length - 1) {
|
|
98
|
+
throw new Error(`Invalid sheet address reference: ${reference}`);
|
|
99
|
+
}
|
|
100
|
+
const rawSheet = reference.slice(0, exclamationIndex);
|
|
101
|
+
const addressPart = reference.slice(exclamationIndex + 1);
|
|
102
|
+
const sheet = unquoteSheetName(rawSheet);
|
|
103
|
+
return {
|
|
104
|
+
sheet,
|
|
105
|
+
address: parseAddress(addressPart)
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Parses a qualified sheet + range reference.
|
|
110
|
+
* Supports Sheet!A1:B10 and 'Sheet Name'!A1:B10.
|
|
111
|
+
*/ const parseSheetRange = (reference)=>{
|
|
112
|
+
const exclamationIndex = reference.lastIndexOf('!');
|
|
113
|
+
if (exclamationIndex <= 0 || exclamationIndex >= reference.length - 1) {
|
|
114
|
+
throw new Error(`Invalid sheet range reference: ${reference}`);
|
|
115
|
+
}
|
|
116
|
+
const rawSheet = reference.slice(0, exclamationIndex);
|
|
117
|
+
const rangePart = reference.slice(exclamationIndex + 1);
|
|
118
|
+
const sheet = unquoteSheetName(rawSheet);
|
|
119
|
+
return {
|
|
120
|
+
sheet,
|
|
121
|
+
range: parseRange(rangePart)
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
const unquoteSheetName = (sheet)=>{
|
|
125
|
+
const trimmed = sheet.trim();
|
|
126
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
|
|
127
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
128
|
+
}
|
|
129
|
+
return trimmed;
|
|
130
|
+
};
|
|
92
131
|
/**
|
|
93
132
|
* Normalizes a range so start is always top-left and end is bottom-right
|
|
94
133
|
*/ const normalizeRange = (range)=>{
|
|
@@ -312,6 +351,10 @@ const formatDatePart = (value, token, locale)=>{
|
|
|
312
351
|
return padNumber(value.getMonth() + 1, 2);
|
|
313
352
|
case 'm':
|
|
314
353
|
return String(value.getMonth() + 1);
|
|
354
|
+
case 'dddd':
|
|
355
|
+
return value.toLocaleString(locale, {
|
|
356
|
+
weekday: 'long'
|
|
357
|
+
});
|
|
315
358
|
case 'dd':
|
|
316
359
|
return padNumber(value.getDate(), 2);
|
|
317
360
|
case 'd':
|
|
@@ -371,6 +414,7 @@ const tokenizeDateFormat = (format)=>{
|
|
|
371
414
|
'mmm',
|
|
372
415
|
'mm',
|
|
373
416
|
'm',
|
|
417
|
+
'dddd',
|
|
374
418
|
'dd',
|
|
375
419
|
'd',
|
|
376
420
|
'hh',
|
|
@@ -410,6 +454,9 @@ const isDateFormat = (format)=>{
|
|
|
410
454
|
return /[ymdhss]/.test(lowered);
|
|
411
455
|
};
|
|
412
456
|
const formatDate = (value, format, locale)=>{
|
|
457
|
+
if (locale === 'fr-FR' && format === '[$-F800]dddd\\,\\ mmmm\\ dd\\,\\ yyyy') {
|
|
458
|
+
format = 'dddd, dd mmmm yyyy';
|
|
459
|
+
}
|
|
413
460
|
const tokens = tokenizeDateFormat(format);
|
|
414
461
|
return tokens.map((token)=>formatDatePart(value, token, locale)).join('');
|
|
415
462
|
};
|
|
@@ -888,7 +935,7 @@ const builderOptions = {
|
|
|
888
935
|
commentPropName: '#comment',
|
|
889
936
|
cdataPropName: '#cdata',
|
|
890
937
|
format: false,
|
|
891
|
-
suppressEmptyNode:
|
|
938
|
+
suppressEmptyNode: true,
|
|
892
939
|
suppressBooleanAttributes: false
|
|
893
940
|
};
|
|
894
941
|
const parser = new XMLParser(parserOptions);
|
|
@@ -1274,15 +1321,14 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1274
1321
|
}
|
|
1275
1322
|
}
|
|
1276
1323
|
|
|
1277
|
-
/**
|
|
1278
|
-
* Represents a worksheet in a workbook
|
|
1324
|
+
/**
|
|
1325
|
+
* Represents a worksheet in a workbook
|
|
1279
1326
|
*/ class Worksheet {
|
|
1280
1327
|
constructor(workbook, name){
|
|
1281
1328
|
this._cells = new Map();
|
|
1282
1329
|
this._xmlNodes = null;
|
|
1283
1330
|
this._dirty = false;
|
|
1284
1331
|
this._mergedCells = new Set();
|
|
1285
|
-
this._sheetData = [];
|
|
1286
1332
|
this._columnWidths = new Map();
|
|
1287
1333
|
this._rowHeights = new Map();
|
|
1288
1334
|
this._frozenPane = null;
|
|
@@ -1290,36 +1336,57 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1290
1336
|
this._boundsDirty = true;
|
|
1291
1337
|
this._tables = [];
|
|
1292
1338
|
this._preserveXml = false;
|
|
1339
|
+
this._rawXml = null;
|
|
1340
|
+
this._lazyParse = false;
|
|
1293
1341
|
this._tableRelIds = null;
|
|
1342
|
+
this._pivotTableRelIds = null;
|
|
1294
1343
|
this._sheetViewsDirty = false;
|
|
1295
1344
|
this._colsDirty = false;
|
|
1296
1345
|
this._tablePartsDirty = false;
|
|
1346
|
+
this._pivotTablePartsDirty = false;
|
|
1297
1347
|
this._workbook = workbook;
|
|
1298
1348
|
this._name = name;
|
|
1299
1349
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Get the workbook this sheet belongs to
|
|
1350
|
+
/**
|
|
1351
|
+
* Get the workbook this sheet belongs to
|
|
1302
1352
|
*/ get workbook() {
|
|
1303
1353
|
return this._workbook;
|
|
1304
1354
|
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Get the sheet name
|
|
1355
|
+
/**
|
|
1356
|
+
* Get the sheet name
|
|
1307
1357
|
*/ get name() {
|
|
1308
1358
|
return this._name;
|
|
1309
1359
|
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Set the sheet name
|
|
1360
|
+
/**
|
|
1361
|
+
* Set the sheet name
|
|
1312
1362
|
*/ set name(value) {
|
|
1313
1363
|
this._name = value;
|
|
1314
1364
|
this._dirty = true;
|
|
1315
1365
|
}
|
|
1316
|
-
/**
|
|
1317
|
-
* Parse worksheet XML content
|
|
1318
|
-
*/ parse(xml) {
|
|
1319
|
-
this.
|
|
1366
|
+
/**
|
|
1367
|
+
* Parse worksheet XML content
|
|
1368
|
+
*/ parse(xml, options = {}) {
|
|
1369
|
+
this._rawXml = xml;
|
|
1370
|
+
this._xmlNodes = null;
|
|
1371
|
+
this._preserveXml = true;
|
|
1372
|
+
this._lazyParse = options.lazy ?? true;
|
|
1373
|
+
if (!this._lazyParse) {
|
|
1374
|
+
this._ensureParsed();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
_ensureParsed() {
|
|
1378
|
+
if (!this._lazyParse) return;
|
|
1379
|
+
if (!this._rawXml) {
|
|
1380
|
+
this._lazyParse = false;
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
this._xmlNodes = parseXml(this._rawXml);
|
|
1320
1384
|
this._preserveXml = true;
|
|
1321
1385
|
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
1322
|
-
if (!worksheet)
|
|
1386
|
+
if (!worksheet) {
|
|
1387
|
+
this._lazyParse = false;
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1323
1390
|
const worksheetChildren = getChildren(worksheet, 'worksheet');
|
|
1324
1391
|
// Parse sheet views (freeze panes)
|
|
1325
1392
|
const sheetViews = findElement(worksheetChildren, 'sheetViews');
|
|
@@ -1344,8 +1411,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1344
1411
|
// Parse sheet data (cells)
|
|
1345
1412
|
const sheetData = findElement(worksheetChildren, 'sheetData');
|
|
1346
1413
|
if (sheetData) {
|
|
1347
|
-
|
|
1348
|
-
this._parseSheetData(
|
|
1414
|
+
const rows = getChildren(sheetData, 'sheetData');
|
|
1415
|
+
this._parseSheetData(rows);
|
|
1349
1416
|
}
|
|
1350
1417
|
// Parse column widths
|
|
1351
1418
|
const cols = findElement(worksheetChildren, 'cols');
|
|
@@ -1377,9 +1444,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1377
1444
|
}
|
|
1378
1445
|
}
|
|
1379
1446
|
}
|
|
1447
|
+
this._lazyParse = false;
|
|
1380
1448
|
}
|
|
1381
|
-
/**
|
|
1382
|
-
* Parse the sheetData element to extract cells
|
|
1449
|
+
/**
|
|
1450
|
+
* Parse the sheetData element to extract cells
|
|
1383
1451
|
*/ _parseSheetData(rows) {
|
|
1384
1452
|
for (const rowNode of rows){
|
|
1385
1453
|
if (!('row' in rowNode)) continue;
|
|
@@ -1401,8 +1469,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1401
1469
|
}
|
|
1402
1470
|
this._boundsDirty = true;
|
|
1403
1471
|
}
|
|
1404
|
-
/**
|
|
1405
|
-
* Parse a cell XML node to CellData
|
|
1472
|
+
/**
|
|
1473
|
+
* Parse a cell XML node to CellData
|
|
1406
1474
|
*/ _parseCellNode(node) {
|
|
1407
1475
|
const data = {};
|
|
1408
1476
|
// Type attribute
|
|
@@ -1477,9 +1545,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1477
1545
|
}
|
|
1478
1546
|
return data;
|
|
1479
1547
|
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Get a cell by address or row/col
|
|
1548
|
+
/**
|
|
1549
|
+
* Get a cell by address or row/col
|
|
1482
1550
|
*/ cell(rowOrAddress, col) {
|
|
1551
|
+
this._ensureParsed();
|
|
1483
1552
|
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
1484
1553
|
const address = toAddress(row, c);
|
|
1485
1554
|
let cell = this._cells.get(address);
|
|
@@ -1490,14 +1559,16 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1490
1559
|
}
|
|
1491
1560
|
return cell;
|
|
1492
1561
|
}
|
|
1493
|
-
/**
|
|
1494
|
-
* Get an existing cell without creating it.
|
|
1562
|
+
/**
|
|
1563
|
+
* Get an existing cell without creating it.
|
|
1495
1564
|
*/ getCellIfExists(rowOrAddress, col) {
|
|
1565
|
+
this._ensureParsed();
|
|
1496
1566
|
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
1497
1567
|
const address = toAddress(row, c);
|
|
1498
1568
|
return this._cells.get(address);
|
|
1499
1569
|
}
|
|
1500
1570
|
range(startRowOrRange, startCol, endRow, endCol) {
|
|
1571
|
+
this._ensureParsed();
|
|
1501
1572
|
let rangeAddr;
|
|
1502
1573
|
if (typeof startRowOrRange === 'string') {
|
|
1503
1574
|
rangeAddr = parseRange(startRowOrRange);
|
|
@@ -1518,9 +1589,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1518
1589
|
}
|
|
1519
1590
|
return new Range(this, rangeAddr);
|
|
1520
1591
|
}
|
|
1521
|
-
/**
|
|
1522
|
-
* Merge cells in the given range
|
|
1592
|
+
/**
|
|
1593
|
+
* Merge cells in the given range
|
|
1523
1594
|
*/ mergeCells(rangeOrStart, end) {
|
|
1595
|
+
this._ensureParsed();
|
|
1524
1596
|
let rangeStr;
|
|
1525
1597
|
if (end) {
|
|
1526
1598
|
rangeStr = `${rangeOrStart}:${end}`;
|
|
@@ -1530,34 +1602,39 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1530
1602
|
this._mergedCells.add(rangeStr);
|
|
1531
1603
|
this._dirty = true;
|
|
1532
1604
|
}
|
|
1533
|
-
/**
|
|
1534
|
-
* Unmerge cells in the given range
|
|
1605
|
+
/**
|
|
1606
|
+
* Unmerge cells in the given range
|
|
1535
1607
|
*/ unmergeCells(rangeStr) {
|
|
1608
|
+
this._ensureParsed();
|
|
1536
1609
|
this._mergedCells.delete(rangeStr);
|
|
1537
1610
|
this._dirty = true;
|
|
1538
1611
|
}
|
|
1539
|
-
/**
|
|
1540
|
-
* Get all merged cell ranges
|
|
1612
|
+
/**
|
|
1613
|
+
* Get all merged cell ranges
|
|
1541
1614
|
*/ get mergedCells() {
|
|
1615
|
+
this._ensureParsed();
|
|
1542
1616
|
return Array.from(this._mergedCells);
|
|
1543
1617
|
}
|
|
1544
|
-
/**
|
|
1545
|
-
* Check if the worksheet has been modified
|
|
1618
|
+
/**
|
|
1619
|
+
* Check if the worksheet has been modified
|
|
1546
1620
|
*/ get dirty() {
|
|
1621
|
+
this._ensureParsed();
|
|
1547
1622
|
if (this._dirty) return true;
|
|
1548
1623
|
for (const cell of this._cells.values()){
|
|
1549
1624
|
if (cell.dirty) return true;
|
|
1550
1625
|
}
|
|
1551
1626
|
return false;
|
|
1552
1627
|
}
|
|
1553
|
-
/**
|
|
1554
|
-
* Get all cells in the worksheet
|
|
1628
|
+
/**
|
|
1629
|
+
* Get all cells in the worksheet
|
|
1555
1630
|
*/ get cells() {
|
|
1631
|
+
this._ensureParsed();
|
|
1556
1632
|
return this._cells;
|
|
1557
1633
|
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Set a column width (0-based index or column letter)
|
|
1634
|
+
/**
|
|
1635
|
+
* Set a column width (0-based index or column letter)
|
|
1560
1636
|
*/ setColumnWidth(col, width) {
|
|
1637
|
+
this._ensureParsed();
|
|
1561
1638
|
if (!Number.isFinite(width) || width <= 0) {
|
|
1562
1639
|
throw new Error('Column width must be a positive number');
|
|
1563
1640
|
}
|
|
@@ -1569,15 +1646,17 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1569
1646
|
this._colsDirty = true;
|
|
1570
1647
|
this._dirty = true;
|
|
1571
1648
|
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Get a column width if set
|
|
1649
|
+
/**
|
|
1650
|
+
* Get a column width if set
|
|
1574
1651
|
*/ getColumnWidth(col) {
|
|
1652
|
+
this._ensureParsed();
|
|
1575
1653
|
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
1576
1654
|
return this._columnWidths.get(colIndex);
|
|
1577
1655
|
}
|
|
1578
|
-
/**
|
|
1579
|
-
* Set a row height (0-based index)
|
|
1656
|
+
/**
|
|
1657
|
+
* Set a row height (0-based index)
|
|
1580
1658
|
*/ setRowHeight(row, height) {
|
|
1659
|
+
this._ensureParsed();
|
|
1581
1660
|
if (!Number.isFinite(height) || height <= 0) {
|
|
1582
1661
|
throw new Error('Row height must be a positive number');
|
|
1583
1662
|
}
|
|
@@ -1588,14 +1667,16 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1588
1667
|
this._colsDirty = true;
|
|
1589
1668
|
this._dirty = true;
|
|
1590
1669
|
}
|
|
1591
|
-
/**
|
|
1592
|
-
* Get a row height if set
|
|
1670
|
+
/**
|
|
1671
|
+
* Get a row height if set
|
|
1593
1672
|
*/ getRowHeight(row) {
|
|
1673
|
+
this._ensureParsed();
|
|
1594
1674
|
return this._rowHeights.get(row);
|
|
1595
1675
|
}
|
|
1596
|
-
/**
|
|
1597
|
-
* Freeze panes at a given row/column split (counts from top-left)
|
|
1676
|
+
/**
|
|
1677
|
+
* Freeze panes at a given row/column split (counts from top-left)
|
|
1598
1678
|
*/ freezePane(rowSplit, colSplit) {
|
|
1679
|
+
this._ensureParsed();
|
|
1599
1680
|
if (rowSplit < 0 || colSplit < 0) {
|
|
1600
1681
|
throw new Error('Freeze pane splits must be >= 0');
|
|
1601
1682
|
}
|
|
@@ -1610,69 +1691,85 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1610
1691
|
this._sheetViewsDirty = true;
|
|
1611
1692
|
this._dirty = true;
|
|
1612
1693
|
}
|
|
1613
|
-
/**
|
|
1614
|
-
* Get current frozen pane configuration
|
|
1694
|
+
/**
|
|
1695
|
+
* Get current frozen pane configuration
|
|
1615
1696
|
*/ getFrozenPane() {
|
|
1697
|
+
this._ensureParsed();
|
|
1616
1698
|
return this._frozenPane ? {
|
|
1617
1699
|
...this._frozenPane
|
|
1618
1700
|
} : null;
|
|
1619
1701
|
}
|
|
1620
|
-
/**
|
|
1621
|
-
* Get all tables in the worksheet
|
|
1702
|
+
/**
|
|
1703
|
+
* Get all tables in the worksheet
|
|
1622
1704
|
*/ get tables() {
|
|
1705
|
+
this._ensureParsed();
|
|
1623
1706
|
return [
|
|
1624
1707
|
...this._tables
|
|
1625
1708
|
];
|
|
1626
1709
|
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Get column width entries
|
|
1629
|
-
* @internal
|
|
1710
|
+
/**
|
|
1711
|
+
* Get column width entries
|
|
1712
|
+
* @internal
|
|
1630
1713
|
*/ getColumnWidths() {
|
|
1714
|
+
this._ensureParsed();
|
|
1631
1715
|
return new Map(this._columnWidths);
|
|
1632
1716
|
}
|
|
1633
|
-
/**
|
|
1634
|
-
* Get row height entries
|
|
1635
|
-
* @internal
|
|
1717
|
+
/**
|
|
1718
|
+
* Get row height entries
|
|
1719
|
+
* @internal
|
|
1636
1720
|
*/ getRowHeights() {
|
|
1721
|
+
this._ensureParsed();
|
|
1637
1722
|
return new Map(this._rowHeights);
|
|
1638
1723
|
}
|
|
1639
|
-
/**
|
|
1640
|
-
* Set table relationship IDs for tableParts generation.
|
|
1641
|
-
* @internal
|
|
1724
|
+
/**
|
|
1725
|
+
* Set table relationship IDs for tableParts generation.
|
|
1726
|
+
* @internal
|
|
1642
1727
|
*/ setTableRelIds(ids) {
|
|
1728
|
+
this._ensureParsed();
|
|
1643
1729
|
this._tableRelIds = ids ? [
|
|
1644
1730
|
...ids
|
|
1645
1731
|
] : null;
|
|
1646
1732
|
this._tablePartsDirty = true;
|
|
1647
1733
|
}
|
|
1648
|
-
/**
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
*
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1670
|
-
*
|
|
1671
|
-
*
|
|
1672
|
-
*
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1734
|
+
/**
|
|
1735
|
+
* Set pivot table relationship IDs for pivotTableParts generation.
|
|
1736
|
+
* @internal
|
|
1737
|
+
*/ setPivotTableRelIds(ids) {
|
|
1738
|
+
this._ensureParsed();
|
|
1739
|
+
this._pivotTableRelIds = ids ? [
|
|
1740
|
+
...ids
|
|
1741
|
+
] : null;
|
|
1742
|
+
this._pivotTablePartsDirty = true;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Create an Excel Table (ListObject) from a data range.
|
|
1746
|
+
*
|
|
1747
|
+
* Tables provide structured data features like auto-filter, banded styling,
|
|
1748
|
+
* and total row with aggregation functions.
|
|
1749
|
+
*
|
|
1750
|
+
* @param config - Table configuration
|
|
1751
|
+
* @returns Table instance for method chaining
|
|
1752
|
+
*
|
|
1753
|
+
* @example
|
|
1754
|
+
* ```typescript
|
|
1755
|
+
* // Create a table with default styling
|
|
1756
|
+
* const table = sheet.createTable({
|
|
1757
|
+
* name: 'SalesData',
|
|
1758
|
+
* range: 'A1:D10',
|
|
1759
|
+
* });
|
|
1760
|
+
*
|
|
1761
|
+
* // Create a table with total row
|
|
1762
|
+
* const table = sheet.createTable({
|
|
1763
|
+
* name: 'SalesData',
|
|
1764
|
+
* range: 'A1:D10',
|
|
1765
|
+
* totalRow: true,
|
|
1766
|
+
* style: { name: 'TableStyleMedium2' }
|
|
1767
|
+
* });
|
|
1768
|
+
*
|
|
1769
|
+
* table.setTotalFunction('Sales', 'sum');
|
|
1770
|
+
* ```
|
|
1675
1771
|
*/ createTable(config) {
|
|
1772
|
+
this._ensureParsed();
|
|
1676
1773
|
// Validate table name is unique within the workbook
|
|
1677
1774
|
for (const sheet of this._workbook.sheetNames){
|
|
1678
1775
|
const ws = this._workbook.sheet(sheet);
|
|
@@ -1694,24 +1791,25 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1694
1791
|
this._dirty = true;
|
|
1695
1792
|
return table;
|
|
1696
1793
|
}
|
|
1697
|
-
/**
|
|
1794
|
+
/**
|
|
1698
1795
|
* Convert sheet data to an array of JSON objects.
|
|
1699
|
-
*
|
|
1796
|
+
*
|
|
1700
1797
|
* @param config - Configuration options
|
|
1701
1798
|
* @returns Array of objects where keys are field names and values are cell values
|
|
1702
1799
|
*
|
|
1703
1800
|
* @example
|
|
1704
|
-
* ```typescript
|
|
1705
|
-
* // Using first row as headers
|
|
1706
|
-
* const data = sheet.toJson();
|
|
1707
|
-
*
|
|
1708
|
-
* // Using custom field names
|
|
1709
|
-
* const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
|
|
1710
|
-
*
|
|
1711
|
-
* // Starting from a specific row/column
|
|
1712
|
-
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
1713
|
-
* ```
|
|
1801
|
+
* ```typescript
|
|
1802
|
+
* // Using first row as headers
|
|
1803
|
+
* const data = sheet.toJson();
|
|
1804
|
+
*
|
|
1805
|
+
* // Using custom field names
|
|
1806
|
+
* const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
|
|
1807
|
+
*
|
|
1808
|
+
* // Starting from a specific row/column
|
|
1809
|
+
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
1810
|
+
* ```
|
|
1714
1811
|
*/ toJson(config = {}) {
|
|
1812
|
+
this._ensureParsed();
|
|
1715
1813
|
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling, asText = false, locale } = config;
|
|
1716
1814
|
// Get the bounds of data in the sheet
|
|
1717
1815
|
const bounds = this._getDataBounds();
|
|
@@ -1783,8 +1881,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1783
1881
|
}
|
|
1784
1882
|
return value;
|
|
1785
1883
|
}
|
|
1786
|
-
/**
|
|
1787
|
-
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
1884
|
+
/**
|
|
1885
|
+
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
1788
1886
|
*/ _getDataBounds() {
|
|
1789
1887
|
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
1790
1888
|
return this._dataBoundsCache;
|
|
@@ -1820,9 +1918,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1820
1918
|
this._boundsDirty = false;
|
|
1821
1919
|
return this._dataBoundsCache;
|
|
1822
1920
|
}
|
|
1823
|
-
/**
|
|
1824
|
-
* Generate XML for this worksheet
|
|
1921
|
+
/**
|
|
1922
|
+
* Generate XML for this worksheet
|
|
1825
1923
|
*/ toXml() {
|
|
1924
|
+
if (this._lazyParse && !this._dirty && this._rawXml) {
|
|
1925
|
+
return this._rawXml;
|
|
1926
|
+
}
|
|
1927
|
+
this._ensureParsed();
|
|
1826
1928
|
const preserved = this._preserveXml && this._xmlNodes ? this._buildPreservedWorksheet() : null;
|
|
1827
1929
|
// Build sheetData from cells
|
|
1828
1930
|
const sheetDataNode = this._buildSheetDataNode();
|
|
@@ -1897,6 +1999,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
1897
1999
|
if (tablePartsNode) {
|
|
1898
2000
|
worksheetChildren.push(tablePartsNode);
|
|
1899
2001
|
}
|
|
2002
|
+
const pivotTablePartsNode = this._buildPivotTablePartsNode();
|
|
2003
|
+
if (pivotTablePartsNode) {
|
|
2004
|
+
worksheetChildren.push(pivotTablePartsNode);
|
|
2005
|
+
}
|
|
1900
2006
|
const worksheetNode = createElement('worksheet', {
|
|
1901
2007
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
1902
2008
|
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
|
|
@@ -2021,6 +2127,15 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2021
2127
|
count: String(this._tables.length)
|
|
2022
2128
|
}, tablePartNodes);
|
|
2023
2129
|
}
|
|
2130
|
+
_buildPivotTablePartsNode() {
|
|
2131
|
+
if (!this._pivotTableRelIds || this._pivotTableRelIds.length === 0) return null;
|
|
2132
|
+
const pivotPartNodes = this._pivotTableRelIds.map((relId)=>createElement('pivotTablePart', {
|
|
2133
|
+
'r:id': relId
|
|
2134
|
+
}, []));
|
|
2135
|
+
return createElement('pivotTableParts', {
|
|
2136
|
+
count: String(pivotPartNodes.length)
|
|
2137
|
+
}, pivotPartNodes);
|
|
2138
|
+
}
|
|
2024
2139
|
_buildPreservedWorksheet() {
|
|
2025
2140
|
if (!this._xmlNodes) return null;
|
|
2026
2141
|
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
@@ -2054,10 +2169,14 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2054
2169
|
const tablePartsNode = this._buildTablePartsNode();
|
|
2055
2170
|
upsertChild('tableParts', tablePartsNode);
|
|
2056
2171
|
}
|
|
2172
|
+
if (this._pivotTablePartsDirty) {
|
|
2173
|
+
const pivotTablePartsNode = this._buildPivotTablePartsNode();
|
|
2174
|
+
upsertChild('pivotTableParts', pivotTablePartsNode);
|
|
2175
|
+
}
|
|
2057
2176
|
return worksheet;
|
|
2058
2177
|
}
|
|
2059
|
-
/**
|
|
2060
|
-
* Build a cell XML node from a Cell object
|
|
2178
|
+
/**
|
|
2179
|
+
* Build a cell XML node from a Cell object
|
|
2061
2180
|
*/ _buildCellNode(cell) {
|
|
2062
2181
|
const data = cell.data;
|
|
2063
2182
|
const attrs = {
|
|
@@ -2097,32 +2216,45 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2097
2216
|
* Parse shared strings from XML content
|
|
2098
2217
|
*/ static parse(xml) {
|
|
2099
2218
|
const ss = new SharedStrings();
|
|
2100
|
-
|
|
2219
|
+
ss._rawXml = xml;
|
|
2220
|
+
ss._parse();
|
|
2221
|
+
return ss;
|
|
2222
|
+
}
|
|
2223
|
+
_parse() {
|
|
2224
|
+
if (this._parsed) return;
|
|
2225
|
+
if (!this._rawXml) {
|
|
2226
|
+
this._parsed = true;
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const parsed = parseXml(this._rawXml);
|
|
2101
2230
|
const sst = findElement(parsed, 'sst');
|
|
2102
|
-
if (!sst)
|
|
2231
|
+
if (!sst) {
|
|
2232
|
+
this._parsed = true;
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2103
2235
|
const countAttr = getAttr(sst, 'count');
|
|
2104
2236
|
if (countAttr) {
|
|
2105
2237
|
const total = parseInt(countAttr, 10);
|
|
2106
2238
|
if (Number.isFinite(total) && total >= 0) {
|
|
2107
|
-
|
|
2239
|
+
this._totalCount = total;
|
|
2108
2240
|
}
|
|
2109
2241
|
}
|
|
2110
2242
|
const children = getChildren(sst, 'sst');
|
|
2111
2243
|
for (const child of children){
|
|
2112
2244
|
if ('si' in child) {
|
|
2113
2245
|
const siChildren = getChildren(child, 'si');
|
|
2114
|
-
const text =
|
|
2115
|
-
|
|
2246
|
+
const text = this.extractText(siChildren);
|
|
2247
|
+
this.entries.push({
|
|
2116
2248
|
text,
|
|
2117
2249
|
node: child
|
|
2118
2250
|
});
|
|
2119
|
-
|
|
2251
|
+
this.stringToIndex.set(text, this.entries.length - 1);
|
|
2120
2252
|
}
|
|
2121
2253
|
}
|
|
2122
|
-
if (
|
|
2123
|
-
|
|
2254
|
+
if (this._totalCount === 0 && this.entries.length > 0) {
|
|
2255
|
+
this._totalCount = this.entries.length;
|
|
2124
2256
|
}
|
|
2125
|
-
|
|
2257
|
+
this._parsed = true;
|
|
2126
2258
|
}
|
|
2127
2259
|
/**
|
|
2128
2260
|
* Extract text from a string item (si element)
|
|
@@ -2158,12 +2290,14 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2158
2290
|
/**
|
|
2159
2291
|
* Get a string by index
|
|
2160
2292
|
*/ getString(index) {
|
|
2293
|
+
this._parse();
|
|
2161
2294
|
return this.entries[index]?.text;
|
|
2162
2295
|
}
|
|
2163
2296
|
/**
|
|
2164
2297
|
* Add a string and return its index
|
|
2165
2298
|
* If the string already exists, returns the existing index
|
|
2166
2299
|
*/ addString(str) {
|
|
2300
|
+
this._parse();
|
|
2167
2301
|
const existing = this.stringToIndex.get(str);
|
|
2168
2302
|
if (existing !== undefined) {
|
|
2169
2303
|
this._totalCount++;
|
|
@@ -2191,26 +2325,31 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2191
2325
|
/**
|
|
2192
2326
|
* Check if the shared strings table has been modified
|
|
2193
2327
|
*/ get dirty() {
|
|
2328
|
+
this._parse();
|
|
2194
2329
|
return this._dirty;
|
|
2195
2330
|
}
|
|
2196
2331
|
/**
|
|
2197
2332
|
* Get the count of strings
|
|
2198
2333
|
*/ get count() {
|
|
2334
|
+
this._parse();
|
|
2199
2335
|
return this.entries.length;
|
|
2200
2336
|
}
|
|
2201
2337
|
/**
|
|
2202
2338
|
* Get total usage count of shared strings
|
|
2203
2339
|
*/ get totalCount() {
|
|
2340
|
+
this._parse();
|
|
2204
2341
|
return Math.max(this._totalCount, this.entries.length);
|
|
2205
2342
|
}
|
|
2206
2343
|
/**
|
|
2207
2344
|
* Get all unique shared strings in insertion order.
|
|
2208
2345
|
*/ getAllStrings() {
|
|
2346
|
+
this._parse();
|
|
2209
2347
|
return this.entries.map((entry)=>entry.text);
|
|
2210
2348
|
}
|
|
2211
2349
|
/**
|
|
2212
2350
|
* Generate XML for the shared strings table
|
|
2213
2351
|
*/ toXml() {
|
|
2352
|
+
this._parse();
|
|
2214
2353
|
const siElements = [];
|
|
2215
2354
|
for (const entry of this.entries){
|
|
2216
2355
|
if (entry.node) {
|
|
@@ -2243,6 +2382,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2243
2382
|
this.stringToIndex = new Map();
|
|
2244
2383
|
this._dirty = false;
|
|
2245
2384
|
this._totalCount = 0;
|
|
2385
|
+
this._rawXml = null;
|
|
2386
|
+
this._parsed = false;
|
|
2246
2387
|
}
|
|
2247
2388
|
}
|
|
2248
2389
|
|
|
@@ -2435,9 +2576,26 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2435
2576
|
* Parse styles from XML content
|
|
2436
2577
|
*/ static parse(xml) {
|
|
2437
2578
|
const styles = new Styles();
|
|
2438
|
-
styles.
|
|
2439
|
-
|
|
2440
|
-
|
|
2579
|
+
styles._rawXml = xml;
|
|
2580
|
+
styles._parse();
|
|
2581
|
+
return styles;
|
|
2582
|
+
}
|
|
2583
|
+
_parse() {
|
|
2584
|
+
if (this._parsed) return;
|
|
2585
|
+
if (!this._rawXml) {
|
|
2586
|
+
this._parsed = true;
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
this._xmlNodes = parseXml(this._rawXml);
|
|
2590
|
+
if (!this._xmlNodes) {
|
|
2591
|
+
this._parsed = true;
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
const styleSheet = findElement(this._xmlNodes, 'styleSheet');
|
|
2595
|
+
if (!styleSheet) {
|
|
2596
|
+
this._parsed = true;
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2441
2599
|
const children = getChildren(styleSheet, 'styleSheet');
|
|
2442
2600
|
// Parse number formats
|
|
2443
2601
|
const numFmts = findElement(children, 'numFmts');
|
|
@@ -2446,7 +2604,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2446
2604
|
if ('numFmt' in child) {
|
|
2447
2605
|
const id = parseInt(getAttr(child, 'numFmtId') || '0', 10);
|
|
2448
2606
|
const code = getAttr(child, 'formatCode') || '';
|
|
2449
|
-
|
|
2607
|
+
this._numFmts.set(id, code);
|
|
2450
2608
|
}
|
|
2451
2609
|
}
|
|
2452
2610
|
}
|
|
@@ -2455,7 +2613,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2455
2613
|
if (fonts) {
|
|
2456
2614
|
for (const child of getChildren(fonts, 'fonts')){
|
|
2457
2615
|
if ('font' in child) {
|
|
2458
|
-
|
|
2616
|
+
this._fonts.push(this._parseFont(child));
|
|
2459
2617
|
}
|
|
2460
2618
|
}
|
|
2461
2619
|
}
|
|
@@ -2464,7 +2622,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2464
2622
|
if (fills) {
|
|
2465
2623
|
for (const child of getChildren(fills, 'fills')){
|
|
2466
2624
|
if ('fill' in child) {
|
|
2467
|
-
|
|
2625
|
+
this._fills.push(this._parseFill(child));
|
|
2468
2626
|
}
|
|
2469
2627
|
}
|
|
2470
2628
|
}
|
|
@@ -2473,7 +2631,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2473
2631
|
if (borders) {
|
|
2474
2632
|
for (const child of getChildren(borders, 'borders')){
|
|
2475
2633
|
if ('border' in child) {
|
|
2476
|
-
|
|
2634
|
+
this._borders.push(this._parseBorder(child));
|
|
2477
2635
|
}
|
|
2478
2636
|
}
|
|
2479
2637
|
}
|
|
@@ -2482,16 +2640,17 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2482
2640
|
if (cellXfs) {
|
|
2483
2641
|
for (const child of getChildren(cellXfs, 'cellXfs')){
|
|
2484
2642
|
if ('xf' in child) {
|
|
2485
|
-
|
|
2643
|
+
this._cellXfs.push(this._parseCellXf(child));
|
|
2486
2644
|
}
|
|
2487
2645
|
}
|
|
2488
2646
|
}
|
|
2489
|
-
|
|
2647
|
+
this._parsed = true;
|
|
2490
2648
|
}
|
|
2491
2649
|
/**
|
|
2492
2650
|
* Create an empty styles object with defaults
|
|
2493
2651
|
*/ static createDefault() {
|
|
2494
2652
|
const styles = new Styles();
|
|
2653
|
+
styles._parsed = true;
|
|
2495
2654
|
// Default font (Calibri 11)
|
|
2496
2655
|
styles._fonts.push({
|
|
2497
2656
|
bold: false,
|
|
@@ -2631,6 +2790,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2631
2790
|
/**
|
|
2632
2791
|
* Get a style by index
|
|
2633
2792
|
*/ getStyle(index) {
|
|
2793
|
+
this._parse();
|
|
2634
2794
|
const cached = this._styleObjectCache.get(index);
|
|
2635
2795
|
if (cached) return {
|
|
2636
2796
|
...cached
|
|
@@ -2697,6 +2857,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2697
2857
|
* Create a style and return its index
|
|
2698
2858
|
* Uses caching to deduplicate identical styles
|
|
2699
2859
|
*/ createStyle(style) {
|
|
2860
|
+
this._parse();
|
|
2700
2861
|
const key = this._getStyleKey(style);
|
|
2701
2862
|
const cached = this._styleCache.get(key);
|
|
2702
2863
|
if (cached !== undefined) {
|
|
@@ -2737,6 +2898,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2737
2898
|
/**
|
|
2738
2899
|
* Clone an existing style by index, optionally overriding fields.
|
|
2739
2900
|
*/ cloneStyle(index, overrides = {}) {
|
|
2901
|
+
this._parse();
|
|
2740
2902
|
const baseStyle = this.getStyle(index);
|
|
2741
2903
|
return this.createStyle({
|
|
2742
2904
|
...baseStyle,
|
|
@@ -2824,17 +2986,20 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
2824
2986
|
* Returns built-in IDs (0-163) for standard formats, or creates custom IDs (164+).
|
|
2825
2987
|
* @param format - The number format string (e.g., '0.00', '#,##0', '$#,##0.00')
|
|
2826
2988
|
*/ getOrCreateNumFmtId(format) {
|
|
2989
|
+
this._parse();
|
|
2827
2990
|
this._dirty = true;
|
|
2828
2991
|
return this._findOrCreateNumFmt(format);
|
|
2829
2992
|
}
|
|
2830
2993
|
/**
|
|
2831
2994
|
* Check if styles have been modified
|
|
2832
2995
|
*/ get dirty() {
|
|
2996
|
+
this._parse();
|
|
2833
2997
|
return this._dirty;
|
|
2834
2998
|
}
|
|
2835
2999
|
/**
|
|
2836
3000
|
* Generate XML for styles
|
|
2837
3001
|
*/ toXml() {
|
|
3002
|
+
this._parse();
|
|
2838
3003
|
const children = [];
|
|
2839
3004
|
// Number formats
|
|
2840
3005
|
if (this._numFmts.size > 0) {
|
|
@@ -3026,6 +3191,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
3026
3191
|
this._borders = [];
|
|
3027
3192
|
this._cellXfs = []; // Cell formats (combined style index)
|
|
3028
3193
|
this._xmlNodes = null;
|
|
3194
|
+
this._rawXml = null;
|
|
3195
|
+
this._parsed = false;
|
|
3029
3196
|
this._dirty = false;
|
|
3030
3197
|
// Cache for style deduplication
|
|
3031
3198
|
this._styleCache = new Map();
|
|
@@ -3033,313 +3200,361 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
3033
3200
|
}
|
|
3034
3201
|
}
|
|
3035
3202
|
|
|
3203
|
+
const AGGREGATION_TO_XML = {
|
|
3204
|
+
sum: 'sum',
|
|
3205
|
+
count: 'count',
|
|
3206
|
+
average: 'average',
|
|
3207
|
+
min: 'min',
|
|
3208
|
+
max: 'max'
|
|
3209
|
+
};
|
|
3210
|
+
const SORT_TO_XML = {
|
|
3211
|
+
asc: 'ascending',
|
|
3212
|
+
desc: 'descending'
|
|
3213
|
+
};
|
|
3036
3214
|
/**
|
|
3037
|
-
* Represents an Excel
|
|
3215
|
+
* Represents an Excel PivotTable with a fluent configuration API.
|
|
3038
3216
|
*/ class PivotTable {
|
|
3039
|
-
constructor(
|
|
3217
|
+
constructor(workbook, config, sourceSheetName, sourceSheet, sourceRange, targetSheetName, targetCell, cacheId, pivotId, cachePartIndex, fields){
|
|
3040
3218
|
this._rowFields = [];
|
|
3041
3219
|
this._columnFields = [];
|
|
3042
|
-
this._valueFields = [];
|
|
3043
3220
|
this._filterFields = [];
|
|
3044
|
-
this.
|
|
3045
|
-
this.
|
|
3046
|
-
this.
|
|
3047
|
-
this.
|
|
3048
|
-
this.
|
|
3221
|
+
this._valueFields = [];
|
|
3222
|
+
this._sortOrders = new Map();
|
|
3223
|
+
this._filters = new Map();
|
|
3224
|
+
this._workbook = workbook;
|
|
3225
|
+
this._name = config.name;
|
|
3226
|
+
this._sourceSheetName = sourceSheetName;
|
|
3227
|
+
this._sourceSheet = sourceSheet;
|
|
3228
|
+
this._sourceRange = sourceRange;
|
|
3229
|
+
this._targetSheetName = targetSheetName;
|
|
3049
3230
|
this._targetCell = targetCell;
|
|
3050
|
-
this.
|
|
3051
|
-
this.
|
|
3052
|
-
this.
|
|
3053
|
-
this.
|
|
3231
|
+
this._refreshOnLoad = config.refreshOnLoad !== false;
|
|
3232
|
+
this._cacheId = cacheId;
|
|
3233
|
+
this._pivotId = pivotId;
|
|
3234
|
+
this._cachePartIndex = cachePartIndex;
|
|
3235
|
+
this._fields = fields;
|
|
3054
3236
|
}
|
|
3055
|
-
|
|
3056
|
-
* Get the pivot table name
|
|
3057
|
-
*/ get name() {
|
|
3237
|
+
get name() {
|
|
3058
3238
|
return this._name;
|
|
3059
3239
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
*/ get targetSheet() {
|
|
3063
|
-
return this._targetSheet;
|
|
3240
|
+
get sourceSheetName() {
|
|
3241
|
+
return this._sourceSheetName;
|
|
3064
3242
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3243
|
+
get sourceRange() {
|
|
3244
|
+
return {
|
|
3245
|
+
start: {
|
|
3246
|
+
...this._sourceRange.start
|
|
3247
|
+
},
|
|
3248
|
+
end: {
|
|
3249
|
+
...this._sourceRange.end
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3069
3252
|
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
*/ get cache() {
|
|
3073
|
-
return this._cache;
|
|
3253
|
+
get targetSheetName() {
|
|
3254
|
+
return this._targetSheetName;
|
|
3074
3255
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3256
|
+
get targetCell() {
|
|
3257
|
+
return {
|
|
3258
|
+
...this._targetCell
|
|
3259
|
+
};
|
|
3079
3260
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
* @internal
|
|
3083
|
-
*/ get cacheFileIndex() {
|
|
3084
|
-
return this._cacheFileIndex;
|
|
3261
|
+
get refreshOnLoad() {
|
|
3262
|
+
return this._refreshOnLoad;
|
|
3085
3263
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
this.
|
|
3264
|
+
get cacheId() {
|
|
3265
|
+
return this._cacheId;
|
|
3266
|
+
}
|
|
3267
|
+
get pivotId() {
|
|
3268
|
+
return this._pivotId;
|
|
3269
|
+
}
|
|
3270
|
+
get cachePartIndex() {
|
|
3271
|
+
return this._cachePartIndex;
|
|
3272
|
+
}
|
|
3273
|
+
addRowField(fieldName) {
|
|
3274
|
+
this._assertFieldExists(fieldName);
|
|
3275
|
+
if (!this._rowFields.includes(fieldName)) {
|
|
3276
|
+
this._rowFields.push(fieldName);
|
|
3277
|
+
}
|
|
3091
3278
|
return this;
|
|
3092
3279
|
}
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
3098
|
-
if (fieldIndex < 0) {
|
|
3099
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
3280
|
+
addColumnField(fieldName) {
|
|
3281
|
+
this._assertFieldExists(fieldName);
|
|
3282
|
+
if (!this._columnFields.includes(fieldName)) {
|
|
3283
|
+
this._columnFields.push(fieldName);
|
|
3100
3284
|
}
|
|
3101
|
-
const assignment = {
|
|
3102
|
-
fieldName,
|
|
3103
|
-
fieldIndex,
|
|
3104
|
-
axis: 'row'
|
|
3105
|
-
};
|
|
3106
|
-
this._rowFields.push(assignment);
|
|
3107
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
3108
3285
|
return this;
|
|
3109
3286
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
3115
|
-
if (fieldIndex < 0) {
|
|
3116
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
3287
|
+
addFilterField(fieldName) {
|
|
3288
|
+
this._assertFieldExists(fieldName);
|
|
3289
|
+
if (!this._filterFields.includes(fieldName)) {
|
|
3290
|
+
this._filterFields.push(fieldName);
|
|
3117
3291
|
}
|
|
3118
|
-
const assignment = {
|
|
3119
|
-
fieldName,
|
|
3120
|
-
fieldIndex,
|
|
3121
|
-
axis: 'column'
|
|
3122
|
-
};
|
|
3123
|
-
this._columnFields.push(assignment);
|
|
3124
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
3125
3292
|
return this;
|
|
3126
3293
|
}
|
|
3127
3294
|
addValueField(fieldNameOrConfig, aggregation = 'sum', displayName, numberFormat) {
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
name = fieldNameOrConfig.name;
|
|
3137
|
-
format = fieldNameOrConfig.numberFormat;
|
|
3295
|
+
let config;
|
|
3296
|
+
if (typeof fieldNameOrConfig === 'string') {
|
|
3297
|
+
config = {
|
|
3298
|
+
field: fieldNameOrConfig,
|
|
3299
|
+
aggregation,
|
|
3300
|
+
name: displayName,
|
|
3301
|
+
numberFormat
|
|
3302
|
+
};
|
|
3138
3303
|
} else {
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
let numFmtId;
|
|
3151
|
-
if (format && this._styles) {
|
|
3152
|
-
numFmtId = this._styles.getOrCreateNumFmtId(format);
|
|
3153
|
-
}
|
|
3154
|
-
const assignment = {
|
|
3155
|
-
fieldName,
|
|
3156
|
-
fieldIndex,
|
|
3157
|
-
axis: 'value',
|
|
3158
|
-
aggregation: agg,
|
|
3159
|
-
displayName: name || defaultName,
|
|
3160
|
-
numFmtId
|
|
3161
|
-
};
|
|
3162
|
-
this._valueFields.push(assignment);
|
|
3163
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
3304
|
+
config = fieldNameOrConfig;
|
|
3305
|
+
}
|
|
3306
|
+
this._assertFieldExists(config.field);
|
|
3307
|
+
const resolvedAggregation = config.aggregation ?? 'sum';
|
|
3308
|
+
const resolvedName = config.name ?? `${this._aggregationLabel(resolvedAggregation)} of ${config.field}`;
|
|
3309
|
+
this._valueFields.push({
|
|
3310
|
+
field: config.field,
|
|
3311
|
+
aggregation: resolvedAggregation,
|
|
3312
|
+
name: resolvedName,
|
|
3313
|
+
numberFormat: config.numberFormat
|
|
3314
|
+
});
|
|
3164
3315
|
return this;
|
|
3165
3316
|
}
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
3171
|
-
if (fieldIndex < 0) {
|
|
3172
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
3317
|
+
sortField(fieldName, order) {
|
|
3318
|
+
this._assertFieldExists(fieldName);
|
|
3319
|
+
if (!this._rowFields.includes(fieldName) && !this._columnFields.includes(fieldName)) {
|
|
3320
|
+
throw new Error(`Cannot sort field "${fieldName}": only row or column fields can be sorted`);
|
|
3173
3321
|
}
|
|
3174
|
-
|
|
3175
|
-
fieldName,
|
|
3176
|
-
fieldIndex,
|
|
3177
|
-
axis: 'filter'
|
|
3178
|
-
};
|
|
3179
|
-
this._filterFields.push(assignment);
|
|
3180
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
3322
|
+
this._sortOrders.set(fieldName, order);
|
|
3181
3323
|
return this;
|
|
3182
3324
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
if (fieldIndex < 0) {
|
|
3190
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
3325
|
+
filterField(fieldName, filter) {
|
|
3326
|
+
this._assertFieldExists(fieldName);
|
|
3327
|
+
const hasInclude = 'include' in filter;
|
|
3328
|
+
const hasExclude = 'exclude' in filter;
|
|
3329
|
+
if (hasInclude && hasExclude || !hasInclude && !hasExclude) {
|
|
3330
|
+
throw new Error('Pivot filter must contain either include or exclude');
|
|
3191
3331
|
}
|
|
3192
|
-
const
|
|
3193
|
-
if (!
|
|
3194
|
-
throw new Error(
|
|
3332
|
+
const values = hasInclude ? filter.include : filter.exclude;
|
|
3333
|
+
if (!values || values.length === 0) {
|
|
3334
|
+
throw new Error('Pivot filter values cannot be empty');
|
|
3195
3335
|
}
|
|
3196
|
-
|
|
3197
|
-
throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);
|
|
3198
|
-
}
|
|
3199
|
-
assignment.sortOrder = order;
|
|
3336
|
+
this._filters.set(fieldName, filter);
|
|
3200
3337
|
return this;
|
|
3201
3338
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
const
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3339
|
+
toPivotCacheDefinitionXml() {
|
|
3340
|
+
const cacheData = this._buildPivotCacheData();
|
|
3341
|
+
return this._buildPivotCacheDefinitionXml(cacheData);
|
|
3342
|
+
}
|
|
3343
|
+
toPivotCacheRecordsXml() {
|
|
3344
|
+
const cacheData = this._buildPivotCacheData();
|
|
3345
|
+
return this._buildPivotCacheRecordsXml(cacheData);
|
|
3346
|
+
}
|
|
3347
|
+
toPivotCacheDefinitionRelsXml() {
|
|
3348
|
+
const relsRoot = createElement('Relationships', {
|
|
3349
|
+
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
3350
|
+
}, [
|
|
3351
|
+
createElement('Relationship', {
|
|
3352
|
+
Id: 'rId1',
|
|
3353
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
3354
|
+
Target: `pivotCacheRecords${this._cachePartIndex}.xml`
|
|
3355
|
+
}, [])
|
|
3356
|
+
]);
|
|
3357
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
3358
|
+
relsRoot
|
|
3359
|
+
])}`;
|
|
3220
3360
|
}
|
|
3221
3361
|
/**
|
|
3222
|
-
*
|
|
3223
|
-
*/
|
|
3362
|
+
* @internal
|
|
3363
|
+
*/ buildPivotPartsXml() {
|
|
3364
|
+
const cacheData = this._buildPivotCacheData();
|
|
3365
|
+
return {
|
|
3366
|
+
cacheDefinitionXml: this._buildPivotCacheDefinitionXml(cacheData),
|
|
3367
|
+
cacheRecordsXml: this._buildPivotCacheRecordsXml(cacheData),
|
|
3368
|
+
cacheRelsXml: this.toPivotCacheDefinitionRelsXml(),
|
|
3369
|
+
pivotTableXml: this._buildPivotTableDefinitionXml(cacheData)
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
toPivotTableDefinitionXml() {
|
|
3373
|
+
const cacheData = this._buildPivotCacheData();
|
|
3374
|
+
return this._buildPivotTableDefinitionXml(cacheData);
|
|
3375
|
+
}
|
|
3376
|
+
_buildPivotCacheDefinitionXml(cacheData) {
|
|
3377
|
+
const cacheFieldNodes = this._fields.map((field, index)=>this._buildCacheFieldNode(field, index, cacheData));
|
|
3378
|
+
const attrs = {
|
|
3379
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
3380
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3381
|
+
'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
|
|
3382
|
+
'mc:Ignorable': 'xr',
|
|
3383
|
+
'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
|
|
3384
|
+
'r:id': 'rId1',
|
|
3385
|
+
createdVersion: '8',
|
|
3386
|
+
minRefreshableVersion: '3',
|
|
3387
|
+
refreshedVersion: '8',
|
|
3388
|
+
refreshOnLoad: this._refreshOnLoad ? '1' : '0',
|
|
3389
|
+
recordCount: String(cacheData.rowCount)
|
|
3390
|
+
};
|
|
3391
|
+
const cacheSourceNode = createElement('cacheSource', {
|
|
3392
|
+
type: 'worksheet'
|
|
3393
|
+
}, [
|
|
3394
|
+
createElement('worksheetSource', {
|
|
3395
|
+
sheet: this._sourceSheetName,
|
|
3396
|
+
ref: toRange(this._sourceRange)
|
|
3397
|
+
}, [])
|
|
3398
|
+
]);
|
|
3399
|
+
const cacheFieldsNode = createElement('cacheFields', {
|
|
3400
|
+
count: String(cacheFieldNodes.length)
|
|
3401
|
+
}, cacheFieldNodes);
|
|
3402
|
+
const extLstNode = createElement('extLst', {}, [
|
|
3403
|
+
createElement('ext', {
|
|
3404
|
+
uri: '{725AE2AE-9491-48be-B2B4-4EB974FC3084}',
|
|
3405
|
+
'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'
|
|
3406
|
+
}, [
|
|
3407
|
+
createElement('x14:pivotCacheDefinition', {}, [])
|
|
3408
|
+
])
|
|
3409
|
+
]);
|
|
3410
|
+
const root = createElement('pivotCacheDefinition', attrs, [
|
|
3411
|
+
cacheSourceNode,
|
|
3412
|
+
cacheFieldsNode,
|
|
3413
|
+
extLstNode
|
|
3414
|
+
]);
|
|
3415
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
3416
|
+
root
|
|
3417
|
+
])}`;
|
|
3418
|
+
}
|
|
3419
|
+
_buildPivotCacheRecordsXml(cacheData) {
|
|
3420
|
+
const recordNodes = cacheData.recordNodes;
|
|
3421
|
+
const root = createElement('pivotCacheRecords', {
|
|
3422
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
3423
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3424
|
+
'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
|
|
3425
|
+
'mc:Ignorable': 'xr',
|
|
3426
|
+
'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
|
|
3427
|
+
count: String(recordNodes.length)
|
|
3428
|
+
}, recordNodes);
|
|
3429
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
3430
|
+
root
|
|
3431
|
+
])}`;
|
|
3432
|
+
}
|
|
3433
|
+
_buildPivotTableDefinitionXml(cacheData) {
|
|
3434
|
+
const effectiveValueFields = this._valueFields.length > 0 ? [
|
|
3435
|
+
this._valueFields[0]
|
|
3436
|
+
] : [];
|
|
3437
|
+
const sourceFieldCount = this._fields.length;
|
|
3438
|
+
const pivotFields = [];
|
|
3439
|
+
const effectiveRowFieldName = this._rowFields[0];
|
|
3440
|
+
const rowFieldIndexes = effectiveRowFieldName ? [
|
|
3441
|
+
this._fieldIndex(effectiveRowFieldName)
|
|
3442
|
+
] : [];
|
|
3443
|
+
const colFieldIndexes = this._columnFields.length > 0 ? [
|
|
3444
|
+
this._fieldIndex(this._columnFields[0])
|
|
3445
|
+
] : [];
|
|
3446
|
+
const valueFieldIndexes = new Set(effectiveValueFields.map((valueField)=>this._fieldIndex(valueField.field)));
|
|
3447
|
+
for(let index = 0; index < this._fields.length; index++){
|
|
3448
|
+
const field = this._fields[index];
|
|
3449
|
+
const attrs = {
|
|
3450
|
+
showAll: '0'
|
|
3451
|
+
};
|
|
3452
|
+
if (rowFieldIndexes.includes(index)) {
|
|
3453
|
+
attrs.axis = 'axisRow';
|
|
3454
|
+
} else if (colFieldIndexes.includes(index)) {
|
|
3455
|
+
attrs.axis = 'axisCol';
|
|
3456
|
+
}
|
|
3457
|
+
if (valueFieldIndexes.has(index)) {
|
|
3458
|
+
attrs.dataField = '1';
|
|
3459
|
+
}
|
|
3460
|
+
const sortOrder = this._sortOrders.get(field.name);
|
|
3461
|
+
if (sortOrder) {
|
|
3462
|
+
attrs.sortType = SORT_TO_XML[sortOrder];
|
|
3463
|
+
}
|
|
3464
|
+
const children = [];
|
|
3465
|
+
if (rowFieldIndexes.includes(index) || colFieldIndexes.includes(index)) {
|
|
3466
|
+
const distinctItems = cacheData.distinctItemsByField[index] ?? [];
|
|
3467
|
+
const itemNodes = distinctItems.map((_item, itemIndex)=>createElement('item', {
|
|
3468
|
+
x: String(itemIndex)
|
|
3469
|
+
}, []));
|
|
3470
|
+
itemNodes.push(createElement('item', {
|
|
3471
|
+
t: 'default'
|
|
3472
|
+
}, []));
|
|
3473
|
+
children.push(createElement('items', {
|
|
3474
|
+
count: String(itemNodes.length)
|
|
3475
|
+
}, itemNodes));
|
|
3476
|
+
}
|
|
3477
|
+
pivotFields.push(createElement('pivotField', attrs, children));
|
|
3478
|
+
}
|
|
3224
3479
|
const children = [];
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
// Calculate first data row/col offsets (1-based, relative to pivot table)
|
|
3228
|
-
// firstHeaderRow: row offset of column headers (usually 1)
|
|
3229
|
-
// firstDataRow: row offset where data starts (after filters and column headers)
|
|
3230
|
-
// firstDataCol: column offset where data starts (after row labels)
|
|
3231
|
-
const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;
|
|
3232
|
-
const headerRows = this._columnFields.length > 0 ? 1 : 0;
|
|
3233
|
-
const firstDataRow = filterRowCount + headerRows + 1;
|
|
3234
|
-
const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;
|
|
3235
|
-
const locationNode = createElement('location', {
|
|
3480
|
+
const locationRef = this._buildTargetAreaRef(cacheData);
|
|
3481
|
+
children.push(createElement('location', {
|
|
3236
3482
|
ref: locationRef,
|
|
3237
|
-
firstHeaderRow:
|
|
3238
|
-
firstDataRow:
|
|
3239
|
-
firstDataCol: String(
|
|
3240
|
-
}, []);
|
|
3241
|
-
children.push(locationNode);
|
|
3242
|
-
// Build pivotFields (one per source field)
|
|
3243
|
-
const pivotFieldNodes = [];
|
|
3244
|
-
for (const cacheField of this._cache.fields){
|
|
3245
|
-
const fieldNode = this._buildPivotFieldNode(cacheField.index);
|
|
3246
|
-
pivotFieldNodes.push(fieldNode);
|
|
3247
|
-
}
|
|
3483
|
+
firstHeaderRow: '1',
|
|
3484
|
+
firstDataRow: '1',
|
|
3485
|
+
firstDataCol: String(Math.max(1, this._rowFields.length + 1))
|
|
3486
|
+
}, []));
|
|
3248
3487
|
children.push(createElement('pivotFields', {
|
|
3249
|
-
count: String(
|
|
3250
|
-
},
|
|
3251
|
-
|
|
3252
|
-
if (this._rowFields.length > 0) {
|
|
3253
|
-
const rowFieldNodes = this._rowFields.map((f)=>createElement('field', {
|
|
3254
|
-
x: String(f.fieldIndex)
|
|
3255
|
-
}, []));
|
|
3488
|
+
count: String(sourceFieldCount)
|
|
3489
|
+
}, pivotFields));
|
|
3490
|
+
if (rowFieldIndexes.length > 0) {
|
|
3256
3491
|
children.push(createElement('rowFields', {
|
|
3257
|
-
count: String(
|
|
3258
|
-
},
|
|
3259
|
-
|
|
3260
|
-
|
|
3492
|
+
count: String(rowFieldIndexes.length)
|
|
3493
|
+
}, rowFieldIndexes.map((fieldIndex)=>createElement('field', {
|
|
3494
|
+
x: String(fieldIndex)
|
|
3495
|
+
}, []))));
|
|
3496
|
+
const distinctRowItems = cacheData.distinctItemsByField[rowFieldIndexes[0]] ?? [];
|
|
3497
|
+
const rowItemNodes = [];
|
|
3498
|
+
if (distinctRowItems.length > 0) {
|
|
3499
|
+
rowItemNodes.push(createElement('i', {}, [
|
|
3500
|
+
createElement('x', {}, [])
|
|
3501
|
+
]));
|
|
3502
|
+
for(let itemIndex = 1; itemIndex < distinctRowItems.length; itemIndex++){
|
|
3503
|
+
rowItemNodes.push(createElement('i', {}, [
|
|
3504
|
+
createElement('x', {
|
|
3505
|
+
v: String(itemIndex)
|
|
3506
|
+
}, [])
|
|
3507
|
+
]));
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
rowItemNodes.push(createElement('i', {
|
|
3511
|
+
t: 'grand'
|
|
3512
|
+
}, [
|
|
3513
|
+
createElement('x', {}, [])
|
|
3514
|
+
]));
|
|
3261
3515
|
children.push(createElement('rowItems', {
|
|
3262
3516
|
count: String(rowItemNodes.length)
|
|
3263
3517
|
}, rowItemNodes));
|
|
3264
3518
|
}
|
|
3265
|
-
|
|
3266
|
-
if (this._columnFields.length > 0) {
|
|
3267
|
-
const colFieldNodes = this._columnFields.map((f)=>createElement('field', {
|
|
3268
|
-
x: String(f.fieldIndex)
|
|
3269
|
-
}, []));
|
|
3270
|
-
// If we have multiple value fields, add -2 to indicate where "Values" header goes
|
|
3271
|
-
if (this._valueFields.length > 1) {
|
|
3272
|
-
colFieldNodes.push(createElement('field', {
|
|
3273
|
-
x: '-2'
|
|
3274
|
-
}, []));
|
|
3275
|
-
}
|
|
3519
|
+
if (colFieldIndexes.length > 0) {
|
|
3276
3520
|
children.push(createElement('colFields', {
|
|
3277
|
-
count: String(
|
|
3278
|
-
},
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
children.push(createElement('colItems', {
|
|
3282
|
-
count: String(colItemNodes.length)
|
|
3283
|
-
}, colItemNodes));
|
|
3284
|
-
} else if (this._valueFields.length > 1) {
|
|
3285
|
-
// If no column fields but we have multiple values, need colFields with -2 (data field indicator)
|
|
3286
|
-
children.push(createElement('colFields', {
|
|
3287
|
-
count: '1'
|
|
3288
|
-
}, [
|
|
3289
|
-
createElement('field', {
|
|
3290
|
-
x: '-2'
|
|
3291
|
-
}, [])
|
|
3292
|
-
]));
|
|
3293
|
-
// Column items for each value field
|
|
3294
|
-
const colItemNodes = [];
|
|
3295
|
-
for(let i = 0; i < this._valueFields.length; i++){
|
|
3296
|
-
colItemNodes.push(createElement('i', {}, [
|
|
3297
|
-
createElement('x', i === 0 ? {} : {
|
|
3298
|
-
v: String(i)
|
|
3299
|
-
}, [])
|
|
3300
|
-
]));
|
|
3301
|
-
}
|
|
3302
|
-
children.push(createElement('colItems', {
|
|
3303
|
-
count: String(colItemNodes.length)
|
|
3304
|
-
}, colItemNodes));
|
|
3305
|
-
} else if (this._valueFields.length === 1) {
|
|
3306
|
-
// Single value field - just add a single column item
|
|
3307
|
-
children.push(createElement('colItems', {
|
|
3308
|
-
count: '1'
|
|
3309
|
-
}, [
|
|
3310
|
-
createElement('i', {}, [])
|
|
3311
|
-
]));
|
|
3521
|
+
count: String(colFieldIndexes.length)
|
|
3522
|
+
}, colFieldIndexes.map((fieldIndex)=>createElement('field', {
|
|
3523
|
+
x: String(fieldIndex)
|
|
3524
|
+
}, []))));
|
|
3312
3525
|
}
|
|
3313
|
-
//
|
|
3526
|
+
// Excel expects colItems even when no explicit column fields are configured.
|
|
3527
|
+
children.push(createElement('colItems', {
|
|
3528
|
+
count: '1'
|
|
3529
|
+
}, [
|
|
3530
|
+
createElement('i', {}, [])
|
|
3531
|
+
]));
|
|
3314
3532
|
if (this._filterFields.length > 0) {
|
|
3315
|
-
const pageFieldNodes = this._filterFields.map((f)=>createElement('pageField', {
|
|
3316
|
-
fld: String(f.fieldIndex),
|
|
3317
|
-
hier: '-1'
|
|
3318
|
-
}, []));
|
|
3319
3533
|
children.push(createElement('pageFields', {
|
|
3320
|
-
count: String(
|
|
3321
|
-
},
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3534
|
+
count: String(this._filterFields.length)
|
|
3535
|
+
}, this._filterFields.map((field, index)=>createElement('pageField', {
|
|
3536
|
+
fld: String(this._fieldIndex(field)),
|
|
3537
|
+
hier: '-1',
|
|
3538
|
+
item: String(index)
|
|
3539
|
+
}, []))));
|
|
3540
|
+
}
|
|
3541
|
+
if (effectiveValueFields.length > 0) {
|
|
3542
|
+
children.push(createElement('dataFields', {
|
|
3543
|
+
count: String(effectiveValueFields.length)
|
|
3544
|
+
}, effectiveValueFields.map((valueField)=>{
|
|
3326
3545
|
const attrs = {
|
|
3327
|
-
name:
|
|
3328
|
-
fld: String(
|
|
3546
|
+
name: valueField.name,
|
|
3547
|
+
fld: String(this._fieldIndex(valueField.field)),
|
|
3329
3548
|
baseField: '0',
|
|
3330
3549
|
baseItem: '0',
|
|
3331
|
-
subtotal:
|
|
3550
|
+
subtotal: AGGREGATION_TO_XML[valueField.aggregation]
|
|
3332
3551
|
};
|
|
3333
|
-
if (
|
|
3334
|
-
attrs.numFmtId = String(
|
|
3552
|
+
if (valueField.numberFormat) {
|
|
3553
|
+
attrs.numFmtId = String(this._workbook.styles.getOrCreateNumFmtId(valueField.numberFormat));
|
|
3335
3554
|
}
|
|
3336
3555
|
return createElement('dataField', attrs, []);
|
|
3337
|
-
});
|
|
3338
|
-
children.push(createElement('dataFields', {
|
|
3339
|
-
count: String(dataFieldNodes.length)
|
|
3340
|
-
}, dataFieldNodes));
|
|
3556
|
+
})));
|
|
3341
3557
|
}
|
|
3342
|
-
// Pivot table style
|
|
3343
3558
|
children.push(createElement('pivotTableStyleInfo', {
|
|
3344
3559
|
name: 'PivotStyleMedium9',
|
|
3345
3560
|
showRowHeaders: '1',
|
|
@@ -3348,672 +3563,400 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
3348
3563
|
showColStripes: '0',
|
|
3349
3564
|
showLastColumn: '1'
|
|
3350
3565
|
}, []));
|
|
3351
|
-
const
|
|
3566
|
+
const attrs = {
|
|
3352
3567
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
3353
3568
|
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3354
3569
|
name: this._name,
|
|
3355
|
-
cacheId: String(this.
|
|
3570
|
+
cacheId: String(this._cacheId),
|
|
3571
|
+
dataCaption: 'Values',
|
|
3356
3572
|
applyNumberFormats: '1',
|
|
3357
3573
|
applyBorderFormats: '0',
|
|
3358
3574
|
applyFontFormats: '0',
|
|
3359
3575
|
applyPatternFormats: '0',
|
|
3360
3576
|
applyAlignmentFormats: '0',
|
|
3361
3577
|
applyWidthHeightFormats: '1',
|
|
3362
|
-
dataCaption: 'Values',
|
|
3363
3578
|
updatedVersion: '8',
|
|
3364
3579
|
minRefreshableVersion: '3',
|
|
3580
|
+
createdVersion: '8',
|
|
3365
3581
|
useAutoFormatting: '1',
|
|
3366
3582
|
rowGrandTotals: '1',
|
|
3367
3583
|
colGrandTotals: '1',
|
|
3368
3584
|
itemPrintTitles: '1',
|
|
3369
|
-
createdVersion: '8',
|
|
3370
3585
|
indent: '0',
|
|
3586
|
+
multipleFieldFilters: this._filters.size > 0 ? '1' : '0',
|
|
3371
3587
|
outline: '1',
|
|
3372
|
-
outlineData: '1'
|
|
3373
|
-
|
|
3374
|
-
|
|
3588
|
+
outlineData: '1'
|
|
3589
|
+
};
|
|
3590
|
+
const root = createElement('pivotTableDefinition', attrs, children);
|
|
3375
3591
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
3376
|
-
|
|
3592
|
+
root
|
|
3377
3593
|
])}`;
|
|
3378
3594
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
const
|
|
3383
|
-
const
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
if (
|
|
3396
|
-
attrs.
|
|
3397
|
-
}
|
|
3398
|
-
// Add items for shared values
|
|
3399
|
-
const cacheField = this._cache.fields[fieldIndex];
|
|
3400
|
-
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
3401
|
-
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
3402
|
-
children.push(createElement('items', {
|
|
3403
|
-
count: String(itemNodes.length)
|
|
3404
|
-
}, itemNodes));
|
|
3405
|
-
}
|
|
3406
|
-
} else if (colField) {
|
|
3407
|
-
attrs.axis = 'axisCol';
|
|
3408
|
-
attrs.showAll = '0';
|
|
3409
|
-
// Add sort order if specified
|
|
3410
|
-
if (colField.sortOrder) {
|
|
3411
|
-
attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
3412
|
-
}
|
|
3413
|
-
const cacheField = this._cache.fields[fieldIndex];
|
|
3414
|
-
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
3415
|
-
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
3416
|
-
children.push(createElement('items', {
|
|
3417
|
-
count: String(itemNodes.length)
|
|
3418
|
-
}, itemNodes));
|
|
3595
|
+
_buildCacheFieldNode(field, fieldIndex, cacheData) {
|
|
3596
|
+
const info = cacheData.numericInfoByField[fieldIndex];
|
|
3597
|
+
const isAxisField = cacheData.isAxisFieldByIndex[fieldIndex];
|
|
3598
|
+
const isValueField = cacheData.isValueFieldByIndex[fieldIndex];
|
|
3599
|
+
const allNonNullAreNumbers = info.nonNullCount > 0 && info.numericCount === info.nonNullCount;
|
|
3600
|
+
if (isValueField || !isAxisField && allNonNullAreNumbers) {
|
|
3601
|
+
const minValue = info.hasNumeric ? info.min : 0;
|
|
3602
|
+
const maxValue = info.hasNumeric ? info.max : 0;
|
|
3603
|
+
const hasInteger = info.hasNumeric ? info.allIntegers : true;
|
|
3604
|
+
const attrs = {
|
|
3605
|
+
containsSemiMixedTypes: '0',
|
|
3606
|
+
containsString: '0',
|
|
3607
|
+
containsNumber: '1',
|
|
3608
|
+
minValue: String(minValue),
|
|
3609
|
+
maxValue: String(maxValue)
|
|
3610
|
+
};
|
|
3611
|
+
if (hasInteger) {
|
|
3612
|
+
attrs.containsInteger = '1';
|
|
3419
3613
|
}
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
attrs.showAll = '0';
|
|
3614
|
+
return createElement('cacheField', {
|
|
3615
|
+
name: field.name,
|
|
3616
|
+
numFmtId: '0'
|
|
3617
|
+
}, [
|
|
3618
|
+
createElement('sharedItems', attrs, [])
|
|
3619
|
+
]);
|
|
3620
|
+
}
|
|
3621
|
+
if (!isAxisField) {
|
|
3622
|
+
return createElement('cacheField', {
|
|
3623
|
+
name: field.name,
|
|
3624
|
+
numFmtId: '0'
|
|
3625
|
+
}, [
|
|
3626
|
+
createElement('sharedItems', {}, [])
|
|
3627
|
+
]);
|
|
3435
3628
|
}
|
|
3436
|
-
|
|
3629
|
+
const sharedItems = cacheData.sharedItemsByField[fieldIndex] ?? [];
|
|
3630
|
+
return createElement('cacheField', {
|
|
3631
|
+
name: field.name,
|
|
3632
|
+
numFmtId: '0'
|
|
3633
|
+
}, [
|
|
3634
|
+
createElement('sharedItems', {
|
|
3635
|
+
count: String(sharedItems.length)
|
|
3636
|
+
}, sharedItems)
|
|
3637
|
+
]);
|
|
3437
3638
|
}
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
const
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3639
|
+
_buildTargetAreaRef(cacheData) {
|
|
3640
|
+
const start = this._targetCell;
|
|
3641
|
+
const estimatedRows = Math.max(3, this._estimateOutputRows(cacheData));
|
|
3642
|
+
const estimatedCols = Math.max(1, this._rowFields.length + Math.max(1, this._valueFields.length));
|
|
3643
|
+
const endRow = start.row + estimatedRows - 1;
|
|
3644
|
+
const endCol = start.col + estimatedCols - 1;
|
|
3645
|
+
return `${toAddress(start.row, start.col)}:${toAddress(endRow, endCol)}`;
|
|
3646
|
+
}
|
|
3647
|
+
_estimateOutputRows(cacheData) {
|
|
3648
|
+
if (this._rowFields.length === 0) {
|
|
3649
|
+
return 3;
|
|
3650
|
+
}
|
|
3651
|
+
const rowFieldIndex = this._fieldIndex(this._rowFields[0]);
|
|
3652
|
+
const distinctItems = cacheData.distinctItemsByField[rowFieldIndex] ?? [];
|
|
3653
|
+
return Math.max(3, distinctItems.length + 2);
|
|
3654
|
+
}
|
|
3655
|
+
_buildPivotCacheData() {
|
|
3656
|
+
const rowCount = Math.max(0, this._sourceRange.end.row - this._sourceRange.start.row);
|
|
3657
|
+
const fieldCount = this._fields.length;
|
|
3658
|
+
const recordNodes = new Array(rowCount);
|
|
3659
|
+
const sharedItemIndexByField = new Array(fieldCount).fill(null);
|
|
3660
|
+
const sharedItemsByField = new Array(fieldCount).fill(null);
|
|
3661
|
+
const distinctItemsByField = new Array(fieldCount).fill(null);
|
|
3662
|
+
const numericInfoByField = new Array(fieldCount);
|
|
3663
|
+
const isAxisFieldByIndex = new Array(fieldCount);
|
|
3664
|
+
const isValueFieldByIndex = new Array(fieldCount);
|
|
3665
|
+
const effectiveRowField = this._rowFields[0] ?? null;
|
|
3666
|
+
const effectiveColumnField = this._columnFields[0] ?? null;
|
|
3667
|
+
const filterFields = new Set(this._filterFields);
|
|
3668
|
+
const valueFields = new Set(this._valueFields.map((valueField)=>valueField.field));
|
|
3669
|
+
for(let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++){
|
|
3670
|
+
const fieldName = this._fields[fieldIndex].name;
|
|
3671
|
+
const isAxisField = fieldName === effectiveRowField || fieldName === effectiveColumnField || filterFields.has(fieldName);
|
|
3672
|
+
const isValueField = valueFields.has(fieldName);
|
|
3673
|
+
isAxisFieldByIndex[fieldIndex] = isAxisField;
|
|
3674
|
+
isValueFieldByIndex[fieldIndex] = isValueField;
|
|
3675
|
+
if (isAxisField) {
|
|
3676
|
+
sharedItemIndexByField[fieldIndex] = new Map();
|
|
3677
|
+
sharedItemsByField[fieldIndex] = [];
|
|
3678
|
+
distinctItemsByField[fieldIndex] = [];
|
|
3458
3679
|
}
|
|
3459
|
-
|
|
3680
|
+
numericInfoByField[fieldIndex] = {
|
|
3681
|
+
nonNullCount: 0,
|
|
3682
|
+
numericCount: 0,
|
|
3683
|
+
min: 0,
|
|
3684
|
+
max: 0,
|
|
3685
|
+
hasNumeric: false,
|
|
3686
|
+
allIntegers: true
|
|
3687
|
+
};
|
|
3460
3688
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3689
|
+
for(let rowOffset = 0; rowOffset < rowCount; rowOffset++){
|
|
3690
|
+
const row = this._sourceRange.start.row + 1 + rowOffset;
|
|
3691
|
+
const valueNodes = [];
|
|
3692
|
+
for(let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++){
|
|
3693
|
+
const field = this._fields[fieldIndex];
|
|
3694
|
+
const cellValue = this._sourceSheet.getCellIfExists(row, field.sourceCol)?.value ?? null;
|
|
3695
|
+
if (cellValue !== null) {
|
|
3696
|
+
const numericInfo = numericInfoByField[fieldIndex];
|
|
3697
|
+
numericInfo.nonNullCount++;
|
|
3698
|
+
if (typeof cellValue === 'number' && Number.isFinite(cellValue)) {
|
|
3699
|
+
numericInfo.numericCount++;
|
|
3700
|
+
if (!numericInfo.hasNumeric) {
|
|
3701
|
+
numericInfo.min = cellValue;
|
|
3702
|
+
numericInfo.max = cellValue;
|
|
3703
|
+
numericInfo.hasNumeric = true;
|
|
3704
|
+
} else {
|
|
3705
|
+
if (cellValue < numericInfo.min) numericInfo.min = cellValue;
|
|
3706
|
+
if (cellValue > numericInfo.max) numericInfo.max = cellValue;
|
|
3707
|
+
}
|
|
3708
|
+
if (!Number.isInteger(cellValue)) {
|
|
3709
|
+
numericInfo.allIntegers = false;
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
if (isAxisFieldByIndex[fieldIndex]) {
|
|
3713
|
+
const distinctMap = sharedItemIndexByField[fieldIndex];
|
|
3714
|
+
const key = this._distinctKey(cellValue);
|
|
3715
|
+
let index = distinctMap.get(key);
|
|
3716
|
+
if (index === undefined) {
|
|
3717
|
+
index = distinctMap.size;
|
|
3718
|
+
distinctMap.set(key, index);
|
|
3719
|
+
distinctItemsByField[fieldIndex].push(cellValue);
|
|
3720
|
+
const sharedNode = this._buildSharedItemNode(cellValue);
|
|
3721
|
+
if (sharedNode) {
|
|
3722
|
+
sharedItemsByField[fieldIndex].push(sharedNode);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
valueNodes.push(createElement('x', {
|
|
3726
|
+
v: String(index)
|
|
3727
|
+
}, []));
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
valueNodes.push(this._buildRawCacheValueNode(cellValue));
|
|
3482
3732
|
}
|
|
3733
|
+
recordNodes[rowOffset] = createElement('r', {}, valueNodes);
|
|
3483
3734
|
}
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
if (
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
if (this._valueFields.length > 1) {
|
|
3502
|
-
// Multiple value fields - need nested items for each column value + value field combination
|
|
3503
|
-
for(let colIdx = 0; colIdx < cacheField.sharedItems.length; colIdx++){
|
|
3504
|
-
for(let valIdx = 0; valIdx < this._valueFields.length; valIdx++){
|
|
3505
|
-
const xNodes = [
|
|
3506
|
-
createElement('x', colIdx === 0 ? {} : {
|
|
3507
|
-
v: String(colIdx)
|
|
3508
|
-
}, []),
|
|
3509
|
-
createElement('x', valIdx === 0 ? {} : {
|
|
3510
|
-
v: String(valIdx)
|
|
3511
|
-
}, [])
|
|
3512
|
-
];
|
|
3513
|
-
items.push(createElement('i', {}, xNodes));
|
|
3514
|
-
}
|
|
3735
|
+
return {
|
|
3736
|
+
rowCount,
|
|
3737
|
+
recordNodes,
|
|
3738
|
+
sharedItemIndexByField,
|
|
3739
|
+
sharedItemsByField,
|
|
3740
|
+
distinctItemsByField,
|
|
3741
|
+
numericInfoByField,
|
|
3742
|
+
isAxisFieldByIndex,
|
|
3743
|
+
isValueFieldByIndex
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
_buildSharedItemNode(value) {
|
|
3747
|
+
if (typeof value === 'string') {
|
|
3748
|
+
return {
|
|
3749
|
+
s: [],
|
|
3750
|
+
':@': {
|
|
3751
|
+
'@_v': value
|
|
3515
3752
|
}
|
|
3516
|
-
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
if (typeof value === 'number') {
|
|
3756
|
+
return createElement('n', {
|
|
3757
|
+
v: String(value)
|
|
3758
|
+
}, []);
|
|
3759
|
+
}
|
|
3760
|
+
if (typeof value === 'boolean') {
|
|
3761
|
+
return createElement('b', {
|
|
3762
|
+
v: value ? '1' : '0'
|
|
3763
|
+
}, []);
|
|
3764
|
+
}
|
|
3765
|
+
if (value instanceof Date) {
|
|
3766
|
+
return createElement('d', {
|
|
3767
|
+
v: value.toISOString()
|
|
3768
|
+
}, []);
|
|
3769
|
+
}
|
|
3770
|
+
return null;
|
|
3771
|
+
}
|
|
3772
|
+
_buildRawCacheValueNode(value) {
|
|
3773
|
+
if (value === null) {
|
|
3774
|
+
return createElement('m', {}, []);
|
|
3775
|
+
}
|
|
3776
|
+
if (typeof value === 'string') {
|
|
3777
|
+
return {
|
|
3778
|
+
s: [],
|
|
3779
|
+
':@': {
|
|
3780
|
+
'@_v': value
|
|
3524
3781
|
}
|
|
3525
|
-
}
|
|
3782
|
+
};
|
|
3526
3783
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
const xNodes = [
|
|
3532
|
-
createElement('x', {}, []),
|
|
3533
|
-
createElement('x', valIdx === 0 ? {} : {
|
|
3534
|
-
v: String(valIdx)
|
|
3535
|
-
}, [])
|
|
3536
|
-
];
|
|
3537
|
-
items.push(createElement('i', {
|
|
3538
|
-
t: 'grand'
|
|
3539
|
-
}, xNodes));
|
|
3540
|
-
}
|
|
3541
|
-
} else {
|
|
3542
|
-
items.push(createElement('i', {
|
|
3543
|
-
t: 'grand'
|
|
3544
|
-
}, [
|
|
3545
|
-
createElement('x', {}, [])
|
|
3546
|
-
]));
|
|
3784
|
+
if (typeof value === 'number') {
|
|
3785
|
+
return createElement('n', {
|
|
3786
|
+
v: String(value)
|
|
3787
|
+
}, []);
|
|
3547
3788
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
const startCol = this._targetCol;
|
|
3558
|
-
const endRow = startRow + numRows - 1;
|
|
3559
|
-
const endCol = startCol + numCols - 1;
|
|
3560
|
-
return `${this._colToLetter(startCol)}${startRow}:${this._colToLetter(endCol)}${endRow}`;
|
|
3561
|
-
}
|
|
3562
|
-
/**
|
|
3563
|
-
* Estimate number of rows in pivot table output
|
|
3564
|
-
*/ _estimateRowCount() {
|
|
3565
|
-
let count = 1; // Header row
|
|
3566
|
-
// Add filter area rows
|
|
3567
|
-
count += this._filterFields.length;
|
|
3568
|
-
// Add row labels (unique values in row fields)
|
|
3569
|
-
if (this._rowFields.length > 0) {
|
|
3570
|
-
const firstRowField = this._rowFields[0];
|
|
3571
|
-
const cacheField = this._cache.fields[firstRowField.fieldIndex];
|
|
3572
|
-
count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
|
|
3573
|
-
} else {
|
|
3574
|
-
count += 1; // At least one data row
|
|
3789
|
+
if (typeof value === 'boolean') {
|
|
3790
|
+
return createElement('b', {
|
|
3791
|
+
v: value ? '1' : '0'
|
|
3792
|
+
}, []);
|
|
3793
|
+
}
|
|
3794
|
+
if (value instanceof Date) {
|
|
3795
|
+
return createElement('d', {
|
|
3796
|
+
v: value.toISOString()
|
|
3797
|
+
}, []);
|
|
3575
3798
|
}
|
|
3576
|
-
return
|
|
3799
|
+
return createElement('m', {}, []);
|
|
3577
3800
|
}
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
let count = 0;
|
|
3582
|
-
// Row label columns
|
|
3583
|
-
count += Math.max(this._rowFields.length, 1);
|
|
3584
|
-
// Column labels (unique values in column fields)
|
|
3585
|
-
if (this._columnFields.length > 0) {
|
|
3586
|
-
const firstColField = this._columnFields[0];
|
|
3587
|
-
const cacheField = this._cache.fields[firstColField.fieldIndex];
|
|
3588
|
-
count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
|
|
3589
|
-
} else {
|
|
3590
|
-
// Value columns
|
|
3591
|
-
count += Math.max(this._valueFields.length, 1);
|
|
3801
|
+
_assertFieldExists(fieldName) {
|
|
3802
|
+
if (!this._fields.some((field)=>field.name === fieldName)) {
|
|
3803
|
+
throw new Error(`Pivot field not found: ${fieldName}`);
|
|
3592
3804
|
}
|
|
3593
|
-
return Math.max(count, 2);
|
|
3594
3805
|
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
let n = col;
|
|
3600
|
-
while(n >= 0){
|
|
3601
|
-
result = String.fromCharCode(n % 26 + 65) + result;
|
|
3602
|
-
n = Math.floor(n / 26) - 1;
|
|
3806
|
+
_fieldIndex(fieldName) {
|
|
3807
|
+
const index = this._fields.findIndex((field)=>field.name === fieldName);
|
|
3808
|
+
if (index < 0) {
|
|
3809
|
+
throw new Error(`Pivot field not found: ${fieldName}`);
|
|
3603
3810
|
}
|
|
3604
|
-
return
|
|
3811
|
+
return index;
|
|
3812
|
+
}
|
|
3813
|
+
_aggregationLabel(aggregation) {
|
|
3814
|
+
switch(aggregation){
|
|
3815
|
+
case 'sum':
|
|
3816
|
+
return 'Sum';
|
|
3817
|
+
case 'count':
|
|
3818
|
+
return 'Count';
|
|
3819
|
+
case 'average':
|
|
3820
|
+
return 'Average';
|
|
3821
|
+
case 'min':
|
|
3822
|
+
return 'Min';
|
|
3823
|
+
case 'max':
|
|
3824
|
+
return 'Max';
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
_distinctKey(value) {
|
|
3828
|
+
if (value instanceof Date) {
|
|
3829
|
+
return `d:${value.toISOString()}`;
|
|
3830
|
+
}
|
|
3831
|
+
if (typeof value === 'string') {
|
|
3832
|
+
return `s:${value}`;
|
|
3833
|
+
}
|
|
3834
|
+
if (typeof value === 'number') {
|
|
3835
|
+
return `n:${value}`;
|
|
3836
|
+
}
|
|
3837
|
+
if (typeof value === 'boolean') {
|
|
3838
|
+
return `b:${value ? 1 : 0}`;
|
|
3839
|
+
}
|
|
3840
|
+
if (typeof value === 'object' && value && 'error' in value) {
|
|
3841
|
+
return `e:${value.error}`;
|
|
3842
|
+
}
|
|
3843
|
+
return 'u:';
|
|
3605
3844
|
}
|
|
3606
3845
|
}
|
|
3607
3846
|
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
*/ class PivotCache {
|
|
3612
|
-
constructor(cacheId, sourceSheet, sourceRange, fileIndex){
|
|
3613
|
-
this._fields = [];
|
|
3614
|
-
this._records = [];
|
|
3615
|
-
this._recordCount = 0;
|
|
3616
|
-
this._saveData = true;
|
|
3617
|
-
this._refreshOnLoad = true; // Default to true
|
|
3618
|
-
// Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
|
|
3619
|
-
this._sharedItemsIndexMap = new Map();
|
|
3620
|
-
this._blankItemIndexMap = new Map();
|
|
3621
|
-
this._styles = null;
|
|
3622
|
-
this._cacheId = cacheId;
|
|
3623
|
-
this._fileIndex = fileIndex;
|
|
3624
|
-
this._sourceSheet = sourceSheet;
|
|
3625
|
-
this._sourceRange = sourceRange;
|
|
3847
|
+
class EagerZipStore {
|
|
3848
|
+
constructor(files){
|
|
3849
|
+
this._files = files;
|
|
3626
3850
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
* @internal
|
|
3630
|
-
*/ setStyles(styles) {
|
|
3631
|
-
this._styles = styles;
|
|
3851
|
+
get(path) {
|
|
3852
|
+
return this._files.get(path);
|
|
3632
3853
|
}
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
*/ get cacheId() {
|
|
3636
|
-
return this._cacheId;
|
|
3854
|
+
set(path, content) {
|
|
3855
|
+
this._files.set(path, content);
|
|
3637
3856
|
}
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
*/ get fileIndex() {
|
|
3641
|
-
return this._fileIndex;
|
|
3857
|
+
has(path) {
|
|
3858
|
+
return this._files.has(path);
|
|
3642
3859
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
*/ set refreshOnLoad(value) {
|
|
3646
|
-
this._refreshOnLoad = value;
|
|
3860
|
+
delete(path) {
|
|
3861
|
+
this._files.delete(path);
|
|
3647
3862
|
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3863
|
+
getText(path) {
|
|
3864
|
+
const data = this._files.get(path);
|
|
3865
|
+
if (!data) return undefined;
|
|
3866
|
+
return strFromU8(data);
|
|
3652
3867
|
}
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
*/ get refreshOnLoad() {
|
|
3656
|
-
return this._refreshOnLoad;
|
|
3868
|
+
setText(path, content) {
|
|
3869
|
+
this._files.set(path, strToU8(content));
|
|
3657
3870
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
*/ get saveData() {
|
|
3661
|
-
return this._saveData;
|
|
3662
|
-
}
|
|
3663
|
-
/**
|
|
3664
|
-
* Get the source sheet name
|
|
3665
|
-
*/ get sourceSheet() {
|
|
3666
|
-
return this._sourceSheet;
|
|
3667
|
-
}
|
|
3668
|
-
/**
|
|
3669
|
-
* Get the source range
|
|
3670
|
-
*/ get sourceRange() {
|
|
3671
|
-
return this._sourceRange;
|
|
3672
|
-
}
|
|
3673
|
-
/**
|
|
3674
|
-
* Get the full source reference (Sheet!Range)
|
|
3675
|
-
*/ get sourceRef() {
|
|
3676
|
-
return `${this._sourceSheet}!${this._sourceRange}`;
|
|
3677
|
-
}
|
|
3678
|
-
/**
|
|
3679
|
-
* Get the fields in this cache
|
|
3680
|
-
*/ get fields() {
|
|
3681
|
-
return this._fields;
|
|
3871
|
+
toFiles() {
|
|
3872
|
+
return Promise.resolve(this._files);
|
|
3682
3873
|
}
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
this.
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
sharedItems: [],
|
|
3704
|
-
minValue: undefined,
|
|
3705
|
-
maxValue: undefined,
|
|
3706
|
-
minDate: undefined,
|
|
3707
|
-
maxDate: undefined
|
|
3708
|
-
}));
|
|
3709
|
-
// Use Maps for unique value collection during analysis
|
|
3710
|
-
const sharedItemsMaps = this._fields.map(()=>new Map());
|
|
3711
|
-
// Analyze data to determine field types and collect unique values
|
|
3712
|
-
for (const row of data){
|
|
3713
|
-
for(let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++){
|
|
3714
|
-
const value = row[colIdx];
|
|
3715
|
-
const field = this._fields[colIdx];
|
|
3716
|
-
if (value === null || value === undefined) {
|
|
3717
|
-
field.hasBlank = true;
|
|
3718
|
-
continue;
|
|
3719
|
-
}
|
|
3720
|
-
if (typeof value === 'string') {
|
|
3721
|
-
field.isNumeric = false;
|
|
3722
|
-
const map = sharedItemsMaps[colIdx];
|
|
3723
|
-
if (!map.has(value)) {
|
|
3724
|
-
map.set(value, value);
|
|
3725
|
-
}
|
|
3726
|
-
} else if (typeof value === 'number') {
|
|
3727
|
-
if (field.isDate) {
|
|
3728
|
-
const d = this._excelSerialToDate(value);
|
|
3729
|
-
if (!field.minDate || d < field.minDate) {
|
|
3730
|
-
field.minDate = d;
|
|
3731
|
-
}
|
|
3732
|
-
if (!field.maxDate || d > field.maxDate) {
|
|
3733
|
-
field.maxDate = d;
|
|
3734
|
-
}
|
|
3735
|
-
} else {
|
|
3736
|
-
if (field.minValue === undefined || value < field.minValue) {
|
|
3737
|
-
field.minValue = value;
|
|
3738
|
-
}
|
|
3739
|
-
if (field.maxValue === undefined || value > field.maxValue) {
|
|
3740
|
-
field.maxValue = value;
|
|
3741
|
-
}
|
|
3742
|
-
}
|
|
3743
|
-
} else if (value instanceof Date) {
|
|
3744
|
-
field.isDate = true;
|
|
3745
|
-
field.isNumeric = false;
|
|
3746
|
-
if (!field.minDate || value < field.minDate) {
|
|
3747
|
-
field.minDate = value;
|
|
3748
|
-
}
|
|
3749
|
-
if (!field.maxDate || value > field.maxDate) {
|
|
3750
|
-
field.maxDate = value;
|
|
3751
|
-
}
|
|
3752
|
-
} else if (typeof value === 'boolean') {
|
|
3753
|
-
field.isNumeric = false;
|
|
3754
|
-
field.hasBoolean = true;
|
|
3755
|
-
}
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
// Resolve number formats if styles are available
|
|
3759
|
-
if (this._styles) {
|
|
3760
|
-
const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');
|
|
3761
|
-
for (const field of this._fields){
|
|
3762
|
-
if (field.isDate) {
|
|
3763
|
-
field.numFmtId = dateFmtId;
|
|
3764
|
-
}
|
|
3765
|
-
}
|
|
3766
|
-
}
|
|
3767
|
-
// Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
|
|
3768
|
-
this._sharedItemsIndexMap.clear();
|
|
3769
|
-
this._blankItemIndexMap.clear();
|
|
3770
|
-
for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
|
|
3771
|
-
const field = this._fields[colIdx];
|
|
3772
|
-
const map = sharedItemsMaps[colIdx];
|
|
3773
|
-
// Convert Map values to array (maintains insertion order in ES6+)
|
|
3774
|
-
field.sharedItems = Array.from(map.values());
|
|
3775
|
-
// Build reverse lookup Map: value -> index
|
|
3776
|
-
if (field.sharedItems.length > 0) {
|
|
3777
|
-
const indexMap = new Map();
|
|
3778
|
-
for(let i = 0; i < field.sharedItems.length; i++){
|
|
3779
|
-
indexMap.set(field.sharedItems[i], i);
|
|
3780
|
-
}
|
|
3781
|
-
this._sharedItemsIndexMap.set(colIdx, indexMap);
|
|
3782
|
-
if (field.hasBlank) {
|
|
3783
|
-
const blankIndex = field.sharedItems.length;
|
|
3784
|
-
this._blankItemIndexMap.set(colIdx, blankIndex);
|
|
3785
|
-
}
|
|
3786
|
-
}
|
|
3874
|
+
}
|
|
3875
|
+
class LazyZipStore {
|
|
3876
|
+
constructor(data){
|
|
3877
|
+
this._files = new Map();
|
|
3878
|
+
this._deleted = new Set();
|
|
3879
|
+
this._entryNames = null;
|
|
3880
|
+
this._data = data;
|
|
3881
|
+
}
|
|
3882
|
+
get(path) {
|
|
3883
|
+
if (this._deleted.has(path)) return undefined;
|
|
3884
|
+
const cached = this._files.get(path);
|
|
3885
|
+
if (cached) return cached;
|
|
3886
|
+
this._ensureIndex();
|
|
3887
|
+
if (this._entryNames && !this._entryNames.has(path)) return undefined;
|
|
3888
|
+
const result = unzipSync(this._data, {
|
|
3889
|
+
filter: (file)=>file.name === path
|
|
3890
|
+
});
|
|
3891
|
+
const data = result[path];
|
|
3892
|
+
if (data) {
|
|
3893
|
+
this._files.set(path, data);
|
|
3787
3894
|
}
|
|
3788
|
-
|
|
3789
|
-
this._records = data;
|
|
3895
|
+
return data;
|
|
3790
3896
|
}
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3897
|
+
set(path, content) {
|
|
3898
|
+
this._files.set(path, content);
|
|
3899
|
+
this._deleted.delete(path);
|
|
3900
|
+
if (this._entryNames) {
|
|
3901
|
+
this._entryNames.add(path);
|
|
3902
|
+
}
|
|
3795
3903
|
}
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
return
|
|
3904
|
+
has(path) {
|
|
3905
|
+
if (this._deleted.has(path)) return false;
|
|
3906
|
+
if (this._files.has(path)) return true;
|
|
3907
|
+
this._ensureIndex();
|
|
3908
|
+
return this._entryNames?.has(path) ?? false;
|
|
3801
3909
|
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
const sharedItemChildren = [];
|
|
3808
|
-
if (field.sharedItems.length > 0) {
|
|
3809
|
-
// String field with shared items
|
|
3810
|
-
const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;
|
|
3811
|
-
sharedItemsAttrs.count = String(total);
|
|
3812
|
-
sharedItemsAttrs.containsString = '1';
|
|
3813
|
-
if (field.hasBlank) {
|
|
3814
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3815
|
-
}
|
|
3816
|
-
for (const item of field.sharedItems){
|
|
3817
|
-
sharedItemChildren.push(createElement('s', {
|
|
3818
|
-
v: item
|
|
3819
|
-
}, []));
|
|
3820
|
-
}
|
|
3821
|
-
if (field.hasBlank) {
|
|
3822
|
-
sharedItemChildren.push(createElement('m', {}, []));
|
|
3823
|
-
}
|
|
3824
|
-
} else if (field.isDate) {
|
|
3825
|
-
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
3826
|
-
sharedItemsAttrs.containsString = '0';
|
|
3827
|
-
sharedItemsAttrs.containsDate = '1';
|
|
3828
|
-
sharedItemsAttrs.containsNonDate = '0';
|
|
3829
|
-
if (field.hasBlank) {
|
|
3830
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3831
|
-
}
|
|
3832
|
-
if (field.minDate) {
|
|
3833
|
-
sharedItemsAttrs.minDate = this._formatDate(field.minDate);
|
|
3834
|
-
}
|
|
3835
|
-
if (field.maxDate) {
|
|
3836
|
-
const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);
|
|
3837
|
-
sharedItemsAttrs.maxDate = this._formatDate(maxDate);
|
|
3838
|
-
}
|
|
3839
|
-
} else if (field.isNumeric) {
|
|
3840
|
-
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
3841
|
-
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
3842
|
-
sharedItemsAttrs.containsString = '0';
|
|
3843
|
-
sharedItemsAttrs.containsNumber = '1';
|
|
3844
|
-
if (field.hasBlank) {
|
|
3845
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3846
|
-
}
|
|
3847
|
-
// Check if all values are integers
|
|
3848
|
-
if (field.minValue !== undefined && field.maxValue !== undefined) {
|
|
3849
|
-
const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
|
|
3850
|
-
if (isInteger) {
|
|
3851
|
-
sharedItemsAttrs.containsInteger = '1';
|
|
3852
|
-
}
|
|
3853
|
-
sharedItemsAttrs.minValue = this._formatNumber(field.minValue);
|
|
3854
|
-
sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);
|
|
3855
|
-
}
|
|
3856
|
-
} else if (field.hasBoolean) {
|
|
3857
|
-
// Boolean-only field (no strings, no numbers)
|
|
3858
|
-
if (field.hasBlank) {
|
|
3859
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3860
|
-
}
|
|
3861
|
-
sharedItemsAttrs.count = field.hasBlank ? '3' : '2';
|
|
3862
|
-
sharedItemChildren.push(createElement('b', {
|
|
3863
|
-
v: '0'
|
|
3864
|
-
}, []));
|
|
3865
|
-
sharedItemChildren.push(createElement('b', {
|
|
3866
|
-
v: '1'
|
|
3867
|
-
}, []));
|
|
3868
|
-
if (field.hasBlank) {
|
|
3869
|
-
sharedItemChildren.push(createElement('m', {}, []));
|
|
3870
|
-
}
|
|
3871
|
-
} else if (field.hasBlank) {
|
|
3872
|
-
// Field that only contains blanks
|
|
3873
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3874
|
-
}
|
|
3875
|
-
const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
|
|
3876
|
-
const cacheFieldAttrs = {
|
|
3877
|
-
name: field.name,
|
|
3878
|
-
numFmtId: String(field.numFmtId ?? 0)
|
|
3879
|
-
};
|
|
3880
|
-
return createElement('cacheField', cacheFieldAttrs, [
|
|
3881
|
-
sharedItemsNode
|
|
3882
|
-
]);
|
|
3883
|
-
});
|
|
3884
|
-
const cacheFieldsNode = createElement('cacheFields', {
|
|
3885
|
-
count: String(this._fields.length)
|
|
3886
|
-
}, cacheFieldNodes);
|
|
3887
|
-
const worksheetSourceNode = createElement('worksheetSource', {
|
|
3888
|
-
ref: this._sourceRange,
|
|
3889
|
-
sheet: this._sourceSheet
|
|
3890
|
-
}, []);
|
|
3891
|
-
const cacheSourceNode = createElement('cacheSource', {
|
|
3892
|
-
type: 'worksheet'
|
|
3893
|
-
}, [
|
|
3894
|
-
worksheetSourceNode
|
|
3895
|
-
]);
|
|
3896
|
-
// Build attributes - align with Excel expectations
|
|
3897
|
-
const definitionAttrs = {
|
|
3898
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
3899
|
-
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3900
|
-
'r:id': recordsRelId
|
|
3901
|
-
};
|
|
3902
|
-
if (this._refreshOnLoad) {
|
|
3903
|
-
definitionAttrs.refreshOnLoad = '1';
|
|
3904
|
-
}
|
|
3905
|
-
definitionAttrs.refreshedBy = 'User';
|
|
3906
|
-
definitionAttrs.refreshedVersion = '8';
|
|
3907
|
-
definitionAttrs.minRefreshableVersion = '3';
|
|
3908
|
-
definitionAttrs.createdVersion = '8';
|
|
3909
|
-
if (!this._saveData) {
|
|
3910
|
-
definitionAttrs.saveData = '0';
|
|
3911
|
-
definitionAttrs.recordCount = '0';
|
|
3912
|
-
} else {
|
|
3913
|
-
definitionAttrs.recordCount = String(this._recordCount);
|
|
3910
|
+
delete(path) {
|
|
3911
|
+
this._files.delete(path);
|
|
3912
|
+
this._deleted.add(path);
|
|
3913
|
+
if (this._entryNames) {
|
|
3914
|
+
this._entryNames.delete(path);
|
|
3914
3915
|
}
|
|
3915
|
-
const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [
|
|
3916
|
-
cacheSourceNode,
|
|
3917
|
-
cacheFieldsNode
|
|
3918
|
-
]);
|
|
3919
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
3920
|
-
definitionNode
|
|
3921
|
-
])}`;
|
|
3922
3916
|
}
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
for (const row of this._records){
|
|
3928
|
-
const fieldNodes = [];
|
|
3929
|
-
for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
|
|
3930
|
-
const value = colIdx < row.length ? row[colIdx] : null;
|
|
3931
|
-
if (value === null || value === undefined) {
|
|
3932
|
-
// Missing value
|
|
3933
|
-
const blankIndex = this._blankItemIndexMap.get(colIdx);
|
|
3934
|
-
if (blankIndex !== undefined) {
|
|
3935
|
-
fieldNodes.push(createElement('x', {
|
|
3936
|
-
v: String(blankIndex)
|
|
3937
|
-
}, []));
|
|
3938
|
-
} else {
|
|
3939
|
-
fieldNodes.push(createElement('m', {}, []));
|
|
3940
|
-
}
|
|
3941
|
-
} else if (typeof value === 'string') {
|
|
3942
|
-
// String value - use index into sharedItems via O(1) Map lookup
|
|
3943
|
-
const indexMap = this._sharedItemsIndexMap.get(colIdx);
|
|
3944
|
-
const idx = indexMap?.get(value);
|
|
3945
|
-
if (idx !== undefined) {
|
|
3946
|
-
fieldNodes.push(createElement('x', {
|
|
3947
|
-
v: String(idx)
|
|
3948
|
-
}, []));
|
|
3949
|
-
} else {
|
|
3950
|
-
// Direct string value (shouldn't happen if cache is built correctly)
|
|
3951
|
-
fieldNodes.push(createElement('s', {
|
|
3952
|
-
v: value
|
|
3953
|
-
}, []));
|
|
3954
|
-
}
|
|
3955
|
-
} else if (typeof value === 'number') {
|
|
3956
|
-
if (this._fields[colIdx]?.isDate) {
|
|
3957
|
-
const d = this._excelSerialToDate(value);
|
|
3958
|
-
fieldNodes.push(createElement('d', {
|
|
3959
|
-
v: this._formatDate(d)
|
|
3960
|
-
}, []));
|
|
3961
|
-
} else {
|
|
3962
|
-
fieldNodes.push(createElement('n', {
|
|
3963
|
-
v: String(value)
|
|
3964
|
-
}, []));
|
|
3965
|
-
}
|
|
3966
|
-
} else if (typeof value === 'boolean') {
|
|
3967
|
-
fieldNodes.push(createElement('b', {
|
|
3968
|
-
v: value ? '1' : '0'
|
|
3969
|
-
}, []));
|
|
3970
|
-
} else if (value instanceof Date) {
|
|
3971
|
-
fieldNodes.push(createElement('d', {
|
|
3972
|
-
v: this._formatDate(value)
|
|
3973
|
-
}, []));
|
|
3974
|
-
} else {
|
|
3975
|
-
// Unknown type, treat as missing
|
|
3976
|
-
fieldNodes.push(createElement('m', {}, []));
|
|
3977
|
-
}
|
|
3978
|
-
}
|
|
3979
|
-
recordNodes.push(createElement('r', {}, fieldNodes));
|
|
3980
|
-
}
|
|
3981
|
-
const recordsNode = createElement('pivotCacheRecords', {
|
|
3982
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
3983
|
-
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3984
|
-
count: String(this._recordCount)
|
|
3985
|
-
}, recordNodes);
|
|
3986
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
3987
|
-
recordsNode
|
|
3988
|
-
])}`;
|
|
3917
|
+
getText(path) {
|
|
3918
|
+
const data = this.get(path);
|
|
3919
|
+
if (!data) return undefined;
|
|
3920
|
+
return strFromU8(data);
|
|
3989
3921
|
}
|
|
3990
|
-
|
|
3991
|
-
|
|
3922
|
+
setText(path, content) {
|
|
3923
|
+
this.set(path, strToU8(content));
|
|
3992
3924
|
}
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3925
|
+
async toFiles() {
|
|
3926
|
+
const unzipped = unzipSync(this._data);
|
|
3927
|
+
const files = new Map(Object.entries(unzipped));
|
|
3928
|
+
for (const path of this._deleted){
|
|
3929
|
+
files.delete(path);
|
|
3996
3930
|
}
|
|
3997
|
-
|
|
3998
|
-
|
|
3931
|
+
for (const [path, content] of this._files){
|
|
3932
|
+
files.set(path, content);
|
|
3999
3933
|
}
|
|
4000
|
-
return
|
|
3934
|
+
return files;
|
|
4001
3935
|
}
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
const
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
3936
|
+
_ensureIndex() {
|
|
3937
|
+
if (this._entryNames) return;
|
|
3938
|
+
const names = new Set();
|
|
3939
|
+
unzipSync(this._data, {
|
|
3940
|
+
filter: (file)=>{
|
|
3941
|
+
names.add(file.name);
|
|
3942
|
+
return false;
|
|
3943
|
+
}
|
|
3944
|
+
});
|
|
3945
|
+
this._entryNames = names;
|
|
4009
3946
|
}
|
|
4010
3947
|
}
|
|
4011
|
-
|
|
3948
|
+
const createZipStore = ()=>{
|
|
3949
|
+
return new EagerZipStore(new Map());
|
|
3950
|
+
};
|
|
4012
3951
|
/**
|
|
4013
3952
|
* Reads a ZIP file and returns a map of path -> content
|
|
4014
3953
|
* @param data - ZIP file as Uint8Array
|
|
4015
3954
|
* @returns Promise resolving to a map of file paths to contents
|
|
4016
|
-
*/ const readZip = (data)=>{
|
|
3955
|
+
*/ const readZip = (data, options)=>{
|
|
3956
|
+
const lazy = options?.lazy ?? false;
|
|
3957
|
+
if (lazy) {
|
|
3958
|
+
return Promise.resolve(new LazyZipStore(data));
|
|
3959
|
+
}
|
|
4017
3960
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
4018
3961
|
if (isBun) {
|
|
4019
3962
|
try {
|
|
@@ -4022,7 +3965,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4022
3965
|
for (const [path, content] of Object.entries(result)){
|
|
4023
3966
|
files.set(path, content);
|
|
4024
3967
|
}
|
|
4025
|
-
return Promise.resolve(files);
|
|
3968
|
+
return Promise.resolve(new EagerZipStore(files));
|
|
4026
3969
|
} catch (error) {
|
|
4027
3970
|
return Promise.reject(error);
|
|
4028
3971
|
}
|
|
@@ -4037,7 +3980,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4037
3980
|
for (const [path, content] of Object.entries(result)){
|
|
4038
3981
|
files.set(path, content);
|
|
4039
3982
|
}
|
|
4040
|
-
resolve(files);
|
|
3983
|
+
resolve(new EagerZipStore(files));
|
|
4041
3984
|
});
|
|
4042
3985
|
});
|
|
4043
3986
|
};
|
|
@@ -4045,9 +3988,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4045
3988
|
* Creates a ZIP file from a map of path -> content
|
|
4046
3989
|
* @param files - Map of file paths to contents
|
|
4047
3990
|
* @returns Promise resolving to ZIP file as Uint8Array
|
|
4048
|
-
*/ const writeZip = (files)=>{
|
|
3991
|
+
*/ const writeZip = async (files)=>{
|
|
3992
|
+
const resolved = await files.toFiles();
|
|
4049
3993
|
const zipData = {};
|
|
4050
|
-
for (const [path, content] of
|
|
3994
|
+
for (const [path, content] of resolved){
|
|
4051
3995
|
zipData[path] = content;
|
|
4052
3996
|
}
|
|
4053
3997
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
@@ -4071,49 +4015,53 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4071
4015
|
/**
|
|
4072
4016
|
* Reads a file from the ZIP as a UTF-8 string
|
|
4073
4017
|
*/ const readZipText = (files, path)=>{
|
|
4074
|
-
|
|
4075
|
-
if (!data) return undefined;
|
|
4076
|
-
return strFromU8(data);
|
|
4018
|
+
return files.getText(path);
|
|
4077
4019
|
};
|
|
4078
4020
|
/**
|
|
4079
4021
|
* Writes a UTF-8 string to the ZIP files map
|
|
4080
4022
|
*/ const writeZipText = (files, path, content)=>{
|
|
4081
|
-
files.
|
|
4023
|
+
files.setText(path, content);
|
|
4082
4024
|
};
|
|
4083
4025
|
|
|
4084
4026
|
/**
|
|
4085
4027
|
* Represents an Excel workbook (.xlsx file)
|
|
4086
4028
|
*/ class Workbook {
|
|
4087
4029
|
constructor(){
|
|
4088
|
-
this._files =
|
|
4030
|
+
this._files = createZipStore();
|
|
4089
4031
|
this._sheets = new Map();
|
|
4090
4032
|
this._sheetDefs = [];
|
|
4091
4033
|
this._relationships = [];
|
|
4034
|
+
this._sharedStrings = null;
|
|
4035
|
+
this._styles = null;
|
|
4036
|
+
this._sharedStringsXml = null;
|
|
4037
|
+
this._stylesXml = null;
|
|
4038
|
+
this._lazy = true;
|
|
4092
4039
|
this._dirty = false;
|
|
4093
|
-
// Pivot table support
|
|
4094
|
-
this._pivotTables = [];
|
|
4095
|
-
this._pivotCaches = [];
|
|
4096
|
-
this._nextCacheId = 5;
|
|
4097
|
-
this._nextCacheFileIndex = 1;
|
|
4098
4040
|
// Table support
|
|
4099
4041
|
this._nextTableId = 1;
|
|
4042
|
+
// Pivot table support
|
|
4043
|
+
this._pivotTables = [];
|
|
4044
|
+
this._nextPivotTableId = 1;
|
|
4045
|
+
this._nextPivotCacheId = 1;
|
|
4100
4046
|
// Date serialization handling
|
|
4101
4047
|
this._dateHandling = 'jsDate';
|
|
4102
4048
|
this._locale = 'fr-FR';
|
|
4103
|
-
|
|
4104
|
-
this._styles = Styles.createDefault();
|
|
4049
|
+
// Lazy init
|
|
4105
4050
|
}
|
|
4106
4051
|
/**
|
|
4107
4052
|
* Load a workbook from a file path
|
|
4108
|
-
*/ static async fromFile(path) {
|
|
4053
|
+
*/ static async fromFile(path, options = {}) {
|
|
4109
4054
|
const data = await readFile(path);
|
|
4110
|
-
return Workbook.fromBuffer(new Uint8Array(data));
|
|
4055
|
+
return Workbook.fromBuffer(new Uint8Array(data), options);
|
|
4111
4056
|
}
|
|
4112
4057
|
/**
|
|
4113
4058
|
* Load a workbook from a buffer
|
|
4114
|
-
*/ static async fromBuffer(data) {
|
|
4059
|
+
*/ static async fromBuffer(data, options = {}) {
|
|
4115
4060
|
const workbook = new Workbook();
|
|
4116
|
-
workbook.
|
|
4061
|
+
workbook._lazy = options.lazy ?? true;
|
|
4062
|
+
workbook._files = await readZip(data, {
|
|
4063
|
+
lazy: workbook._lazy
|
|
4064
|
+
});
|
|
4117
4065
|
// Parse workbook.xml for sheet definitions
|
|
4118
4066
|
const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');
|
|
4119
4067
|
if (workbookXml) {
|
|
@@ -4125,15 +4073,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4125
4073
|
workbook._parseRelationships(relsXml);
|
|
4126
4074
|
}
|
|
4127
4075
|
// Parse shared strings
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
}
|
|
4132
|
-
// Parse styles
|
|
4133
|
-
const stylesXml = readZipText(workbook._files, 'xl/styles.xml');
|
|
4134
|
-
if (stylesXml) {
|
|
4135
|
-
workbook._styles = Styles.parse(stylesXml);
|
|
4136
|
-
}
|
|
4076
|
+
// Store shared strings/styles XML for lazy parse
|
|
4077
|
+
workbook._sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml') ?? null;
|
|
4078
|
+
workbook._stylesXml = readZipText(workbook._files, 'xl/styles.xml') ?? null;
|
|
4137
4079
|
return workbook;
|
|
4138
4080
|
}
|
|
4139
4081
|
/**
|
|
@@ -4141,6 +4083,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4141
4083
|
*/ static create() {
|
|
4142
4084
|
const workbook = new Workbook();
|
|
4143
4085
|
workbook._dirty = true;
|
|
4086
|
+
workbook._lazy = false;
|
|
4087
|
+
workbook._sharedStrings = new SharedStrings();
|
|
4088
|
+
workbook._styles = Styles.createDefault();
|
|
4144
4089
|
return workbook;
|
|
4145
4090
|
}
|
|
4146
4091
|
/**
|
|
@@ -4156,11 +4101,25 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4156
4101
|
/**
|
|
4157
4102
|
* Get shared strings table
|
|
4158
4103
|
*/ get sharedStrings() {
|
|
4104
|
+
if (!this._sharedStrings) {
|
|
4105
|
+
if (this._sharedStringsXml) {
|
|
4106
|
+
this._sharedStrings = SharedStrings.parse(this._sharedStringsXml);
|
|
4107
|
+
} else {
|
|
4108
|
+
this._sharedStrings = new SharedStrings();
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4159
4111
|
return this._sharedStrings;
|
|
4160
4112
|
}
|
|
4161
4113
|
/**
|
|
4162
4114
|
* Get styles
|
|
4163
4115
|
*/ get styles() {
|
|
4116
|
+
if (!this._styles) {
|
|
4117
|
+
if (this._stylesXml) {
|
|
4118
|
+
this._styles = Styles.parse(this._stylesXml);
|
|
4119
|
+
} else {
|
|
4120
|
+
this._styles = Styles.createDefault();
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4164
4123
|
return this._styles;
|
|
4165
4124
|
}
|
|
4166
4125
|
/**
|
|
@@ -4191,6 +4150,72 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4191
4150
|
return this._nextTableId++;
|
|
4192
4151
|
}
|
|
4193
4152
|
/**
|
|
4153
|
+
* Get all pivot tables in the workbook.
|
|
4154
|
+
*/ get pivotTables() {
|
|
4155
|
+
return [
|
|
4156
|
+
...this._pivotTables
|
|
4157
|
+
];
|
|
4158
|
+
}
|
|
4159
|
+
/**
|
|
4160
|
+
* Create a new pivot table.
|
|
4161
|
+
*/ createPivotTable(config) {
|
|
4162
|
+
if (!config.name || config.name.trim().length === 0) {
|
|
4163
|
+
throw new Error('Pivot table name is required');
|
|
4164
|
+
}
|
|
4165
|
+
if (this._pivotTables.some((pivot)=>pivot.name === config.name)) {
|
|
4166
|
+
throw new Error(`Pivot table name already exists: ${config.name}`);
|
|
4167
|
+
}
|
|
4168
|
+
const sourceRef = parseSheetRange(config.source);
|
|
4169
|
+
const targetRef = parseSheetAddress(config.target);
|
|
4170
|
+
const sourceSheet = this.sheet(sourceRef.sheet);
|
|
4171
|
+
this.sheet(targetRef.sheet);
|
|
4172
|
+
const sourceRange = this._normalizeRange(sourceRef.range);
|
|
4173
|
+
if (sourceRange.start.row >= sourceRange.end.row) {
|
|
4174
|
+
throw new Error('Pivot source range must include a header row and at least one data row');
|
|
4175
|
+
}
|
|
4176
|
+
const fields = this._extractPivotFields(sourceSheet, sourceRange);
|
|
4177
|
+
const cacheId = this._nextPivotCacheId++;
|
|
4178
|
+
const pivotId = this._nextPivotTableId++;
|
|
4179
|
+
const cachePartIndex = this._pivotTables.length + 1;
|
|
4180
|
+
const pivot = new PivotTable(this, config, sourceRef.sheet, sourceSheet, sourceRange, targetRef.sheet, targetRef.address, cacheId, pivotId, cachePartIndex, fields);
|
|
4181
|
+
this._pivotTables.push(pivot);
|
|
4182
|
+
this._dirty = true;
|
|
4183
|
+
return pivot;
|
|
4184
|
+
}
|
|
4185
|
+
_extractPivotFields(sourceSheet, sourceRange) {
|
|
4186
|
+
const fields = [];
|
|
4187
|
+
const seen = new Set();
|
|
4188
|
+
for(let col = sourceRange.start.col; col <= sourceRange.end.col; col++){
|
|
4189
|
+
const headerCell = sourceSheet.getCellIfExists(sourceRange.start.row, col);
|
|
4190
|
+
const rawHeader = headerCell?.value;
|
|
4191
|
+
const name = rawHeader == null ? `Column${col - sourceRange.start.col + 1}` : String(rawHeader).trim();
|
|
4192
|
+
if (!name) {
|
|
4193
|
+
throw new Error(`Pivot source header is empty at column ${col + 1}`);
|
|
4194
|
+
}
|
|
4195
|
+
if (seen.has(name)) {
|
|
4196
|
+
throw new Error(`Duplicate pivot source header: ${name}`);
|
|
4197
|
+
}
|
|
4198
|
+
seen.add(name);
|
|
4199
|
+
fields.push({
|
|
4200
|
+
name,
|
|
4201
|
+
sourceCol: col
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
return fields;
|
|
4205
|
+
}
|
|
4206
|
+
_normalizeRange(range) {
|
|
4207
|
+
return {
|
|
4208
|
+
start: {
|
|
4209
|
+
row: Math.min(range.start.row, range.end.row),
|
|
4210
|
+
col: Math.min(range.start.col, range.end.col)
|
|
4211
|
+
},
|
|
4212
|
+
end: {
|
|
4213
|
+
row: Math.max(range.start.row, range.end.row),
|
|
4214
|
+
col: Math.max(range.start.col, range.end.col)
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4194
4219
|
* Get a worksheet by name or index
|
|
4195
4220
|
*/ sheet(nameOrIndex) {
|
|
4196
4221
|
let def;
|
|
@@ -4214,7 +4239,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4214
4239
|
const sheetPath = `xl/${rel.target}`;
|
|
4215
4240
|
const sheetXml = readZipText(this._files, sheetPath);
|
|
4216
4241
|
if (sheetXml) {
|
|
4217
|
-
worksheet.parse(sheetXml
|
|
4242
|
+
worksheet.parse(sheetXml, {
|
|
4243
|
+
lazy: this._lazy
|
|
4244
|
+
});
|
|
4218
4245
|
}
|
|
4219
4246
|
}
|
|
4220
4247
|
this._sheets.set(def.name, worksheet);
|
|
@@ -4525,100 +4552,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4525
4552
|
return String(value);
|
|
4526
4553
|
}
|
|
4527
4554
|
/**
|
|
4528
|
-
* Create a pivot table from source data.
|
|
4529
|
-
*
|
|
4530
|
-
* @param config - Pivot table configuration
|
|
4531
|
-
* @returns PivotTable instance for fluent configuration
|
|
4532
|
-
*
|
|
4533
|
-
* @example
|
|
4534
|
-
* ```typescript
|
|
4535
|
-
* const pivot = wb.createPivotTable({
|
|
4536
|
-
* name: 'SalesPivot',
|
|
4537
|
-
* source: 'DataSheet!A1:D100',
|
|
4538
|
-
* target: 'PivotSheet!A3',
|
|
4539
|
-
* });
|
|
4540
|
-
*
|
|
4541
|
-
* pivot
|
|
4542
|
-
* .addRowField('Region')
|
|
4543
|
-
* .addColumnField('Product')
|
|
4544
|
-
* .addValueField('Sales', 'sum', 'Total Sales');
|
|
4545
|
-
* ```
|
|
4546
|
-
*/ createPivotTable(config) {
|
|
4547
|
-
this._dirty = true;
|
|
4548
|
-
// Parse source reference (Sheet!Range)
|
|
4549
|
-
const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);
|
|
4550
|
-
// Parse target reference
|
|
4551
|
-
const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);
|
|
4552
|
-
// Ensure target sheet exists
|
|
4553
|
-
if (!this._sheetDefs.some((s)=>s.name === targetSheet)) {
|
|
4554
|
-
this.addSheet(targetSheet);
|
|
4555
|
-
}
|
|
4556
|
-
// Parse target cell address
|
|
4557
|
-
const targetAddr = parseAddress(targetCell);
|
|
4558
|
-
// Get source worksheet and extract data
|
|
4559
|
-
const sourceWs = this.sheet(sourceSheet);
|
|
4560
|
-
const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
|
|
4561
|
-
// Create pivot cache
|
|
4562
|
-
const cacheId = this._nextCacheId++;
|
|
4563
|
-
const cacheFileIndex = this._nextCacheFileIndex++;
|
|
4564
|
-
const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
|
|
4565
|
-
cache.setStyles(this._styles);
|
|
4566
|
-
cache.buildFromData(headers, data);
|
|
4567
|
-
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
4568
|
-
if (config.refreshOnLoad === false) {
|
|
4569
|
-
cache.refreshOnLoad = false;
|
|
4570
|
-
}
|
|
4571
|
-
// saveData defaults to true; only disable if explicitly set to false
|
|
4572
|
-
if (config.saveData === false) {
|
|
4573
|
-
cache.saveData = false;
|
|
4574
|
-
}
|
|
4575
|
-
this._pivotCaches.push(cache);
|
|
4576
|
-
// Create pivot table
|
|
4577
|
-
const pivotTableIndex = this._pivotTables.length + 1;
|
|
4578
|
-
const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex, cacheFileIndex);
|
|
4579
|
-
// Set styles reference for number format resolution
|
|
4580
|
-
pivotTable.setStyles(this._styles);
|
|
4581
|
-
this._pivotTables.push(pivotTable);
|
|
4582
|
-
return pivotTable;
|
|
4583
|
-
}
|
|
4584
|
-
/**
|
|
4585
|
-
* Parse a sheet reference like "Sheet1!A1:D100" into sheet name and range
|
|
4586
|
-
*/ _parseSheetRef(ref) {
|
|
4587
|
-
const match = ref.match(/^(.+?)!(.+)$/);
|
|
4588
|
-
if (!match) {
|
|
4589
|
-
throw new Error(`Invalid reference format: ${ref}. Expected "SheetName!Range"`);
|
|
4590
|
-
}
|
|
4591
|
-
return {
|
|
4592
|
-
sheetName: match[1],
|
|
4593
|
-
range: match[2]
|
|
4594
|
-
};
|
|
4595
|
-
}
|
|
4596
|
-
/**
|
|
4597
|
-
* Extract headers and data from a source range
|
|
4598
|
-
*/ _extractSourceData(sheet, rangeStr) {
|
|
4599
|
-
const range = parseRange(rangeStr);
|
|
4600
|
-
const headers = [];
|
|
4601
|
-
const data = [];
|
|
4602
|
-
// First row is headers
|
|
4603
|
-
for(let col = range.start.col; col <= range.end.col; col++){
|
|
4604
|
-
const cell = sheet.cell(toAddress(range.start.row, col));
|
|
4605
|
-
headers.push(String(cell.value ?? `Column${col + 1}`));
|
|
4606
|
-
}
|
|
4607
|
-
// Remaining rows are data
|
|
4608
|
-
for(let row = range.start.row + 1; row <= range.end.row; row++){
|
|
4609
|
-
const rowData = [];
|
|
4610
|
-
for(let col = range.start.col; col <= range.end.col; col++){
|
|
4611
|
-
const cell = sheet.cell(toAddress(row, col));
|
|
4612
|
-
rowData.push(cell.value);
|
|
4613
|
-
}
|
|
4614
|
-
data.push(rowData);
|
|
4615
|
-
}
|
|
4616
|
-
return {
|
|
4617
|
-
headers,
|
|
4618
|
-
data
|
|
4619
|
-
};
|
|
4620
|
-
}
|
|
4621
|
-
/**
|
|
4622
4555
|
* Save the workbook to a file
|
|
4623
4556
|
*/ async toFile(path) {
|
|
4624
4557
|
const buffer = await this.toBuffer();
|
|
@@ -4676,20 +4609,28 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4676
4609
|
_updateFiles() {
|
|
4677
4610
|
const relationshipInfo = this._buildRelationshipInfo();
|
|
4678
4611
|
// Update workbook.xml
|
|
4679
|
-
this._updateWorkbookXml(relationshipInfo.
|
|
4612
|
+
this._updateWorkbookXml(relationshipInfo.pivotCacheRelByTarget);
|
|
4680
4613
|
// Update relationships
|
|
4681
4614
|
this._updateRelationshipsXml(relationshipInfo.relNodes);
|
|
4682
4615
|
// Update content types
|
|
4683
4616
|
this._updateContentTypes();
|
|
4684
4617
|
// Update shared strings if modified
|
|
4685
|
-
if (this._sharedStrings
|
|
4686
|
-
|
|
4618
|
+
if (this._sharedStrings) {
|
|
4619
|
+
if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {
|
|
4620
|
+
writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());
|
|
4621
|
+
}
|
|
4622
|
+
} else if (this._sharedStringsXml) {
|
|
4623
|
+
writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStringsXml);
|
|
4687
4624
|
}
|
|
4688
4625
|
// Update styles if modified or if file doesn't exist yet
|
|
4689
|
-
if (this._styles
|
|
4690
|
-
|
|
4626
|
+
if (this._styles) {
|
|
4627
|
+
if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
|
|
4628
|
+
writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
|
|
4629
|
+
}
|
|
4630
|
+
} else if (this._stylesXml) {
|
|
4631
|
+
writeZipText(this._files, 'xl/styles.xml', this._stylesXml);
|
|
4691
4632
|
}
|
|
4692
|
-
// Update worksheets
|
|
4633
|
+
// Update worksheets
|
|
4693
4634
|
for (const [name, worksheet] of this._sheets){
|
|
4694
4635
|
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
4695
4636
|
const def = this._sheetDefs.find((s)=>s.name === name);
|
|
@@ -4702,15 +4643,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4702
4643
|
}
|
|
4703
4644
|
}
|
|
4704
4645
|
}
|
|
4705
|
-
// Update pivot tables
|
|
4706
|
-
if (this._pivotTables.length > 0) {
|
|
4707
|
-
this._updatePivotTableFiles();
|
|
4708
|
-
}
|
|
4709
4646
|
// Update tables (sets table rel IDs for tableParts)
|
|
4710
4647
|
this._updateTableFiles();
|
|
4648
|
+
// Update pivot tables (sets pivot rel IDs for pivotTableParts)
|
|
4649
|
+
this._updatePivotFiles();
|
|
4711
4650
|
// Update worksheets to align tableParts with relationship IDs
|
|
4712
4651
|
for (const [name, worksheet] of this._sheets){
|
|
4713
|
-
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
4652
|
+
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0 || this._pivotTables.length > 0) {
|
|
4714
4653
|
const def = this._sheetDefs.find((s)=>s.name === name);
|
|
4715
4654
|
if (def) {
|
|
4716
4655
|
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
@@ -4722,7 +4661,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4722
4661
|
}
|
|
4723
4662
|
}
|
|
4724
4663
|
}
|
|
4725
|
-
_updateWorkbookXml(
|
|
4664
|
+
_updateWorkbookXml(pivotCacheRelByTarget) {
|
|
4726
4665
|
const sheetNodes = this._sheetDefs.map((def)=>createElement('sheet', {
|
|
4727
4666
|
name: def.name,
|
|
4728
4667
|
sheetId: String(def.sheetId),
|
|
@@ -4732,19 +4671,20 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4732
4671
|
const children = [
|
|
4733
4672
|
sheetsNode
|
|
4734
4673
|
];
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
const
|
|
4738
|
-
const
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4674
|
+
if (this._pivotTables.length > 0) {
|
|
4675
|
+
const pivotCacheNodes = [];
|
|
4676
|
+
for (const pivot of this._pivotTables){
|
|
4677
|
+
const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
|
|
4678
|
+
const relId = pivotCacheRelByTarget.get(target);
|
|
4679
|
+
if (!relId) continue;
|
|
4680
|
+
pivotCacheNodes.push(createElement('pivotCache', {
|
|
4681
|
+
cacheId: String(pivot.cacheId),
|
|
4682
|
+
'r:id': relId
|
|
4683
|
+
}, []));
|
|
4684
|
+
}
|
|
4685
|
+
if (pivotCacheNodes.length > 0) {
|
|
4686
|
+
children.push(createElement('pivotCaches', {}, pivotCacheNodes));
|
|
4687
|
+
}
|
|
4748
4688
|
}
|
|
4749
4689
|
const workbookNode = createElement('workbook', {
|
|
4750
4690
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
@@ -4770,6 +4710,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4770
4710
|
Type: rel.type,
|
|
4771
4711
|
Target: rel.target
|
|
4772
4712
|
}, []));
|
|
4713
|
+
const pivotCacheRelByTarget = new Map();
|
|
4773
4714
|
const reservedRelIds = new Set(relNodes.map((node)=>getAttr(node, 'Id') || '').filter(Boolean));
|
|
4774
4715
|
let nextRelId = Math.max(0, ...this._relationships.map((r)=>parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
|
|
4775
4716
|
const allocateRelId = ()=>{
|
|
@@ -4782,7 +4723,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4782
4723
|
return id;
|
|
4783
4724
|
};
|
|
4784
4725
|
// Add shared strings relationship if needed
|
|
4785
|
-
|
|
4726
|
+
const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
|
|
4727
|
+
if (shouldIncludeSharedStrings) {
|
|
4786
4728
|
const hasSharedStrings = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
|
|
4787
4729
|
if (!hasSharedStrings) {
|
|
4788
4730
|
relNodes.push(createElement('Relationship', {
|
|
@@ -4801,23 +4743,32 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4801
4743
|
Target: 'styles.xml'
|
|
4802
4744
|
}, []));
|
|
4803
4745
|
}
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4746
|
+
for (const pivot of this._pivotTables){
|
|
4747
|
+
const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
|
|
4748
|
+
const hasPivotCacheRel = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' && getAttr(node, 'Target') === target);
|
|
4749
|
+
if (!hasPivotCacheRel) {
|
|
4750
|
+
const id = allocateRelId();
|
|
4751
|
+
pivotCacheRelByTarget.set(target, id);
|
|
4752
|
+
relNodes.push(createElement('Relationship', {
|
|
4753
|
+
Id: id,
|
|
4754
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
4755
|
+
Target: target
|
|
4756
|
+
}, []));
|
|
4757
|
+
} else {
|
|
4758
|
+
const existing = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' && getAttr(node, 'Target') === target);
|
|
4759
|
+
const existingId = existing ? getAttr(existing, 'Id') : undefined;
|
|
4760
|
+
if (existingId) {
|
|
4761
|
+
pivotCacheRelByTarget.set(target, existingId);
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4814
4764
|
}
|
|
4815
4765
|
return {
|
|
4816
4766
|
relNodes,
|
|
4817
|
-
|
|
4767
|
+
pivotCacheRelByTarget
|
|
4818
4768
|
};
|
|
4819
4769
|
}
|
|
4820
4770
|
_updateContentTypes() {
|
|
4771
|
+
const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
|
|
4821
4772
|
const types = [
|
|
4822
4773
|
createElement('Default', {
|
|
4823
4774
|
Extension: 'rels',
|
|
@@ -4837,7 +4788,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4837
4788
|
}, [])
|
|
4838
4789
|
];
|
|
4839
4790
|
// Add shared strings if present
|
|
4840
|
-
if (
|
|
4791
|
+
if (shouldIncludeSharedStrings) {
|
|
4841
4792
|
types.push(createElement('Override', {
|
|
4842
4793
|
PartName: '/xl/sharedStrings.xml',
|
|
4843
4794
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'
|
|
@@ -4853,24 +4804,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4853
4804
|
}, []));
|
|
4854
4805
|
}
|
|
4855
4806
|
}
|
|
4856
|
-
// Add pivot cache definitions and records
|
|
4857
|
-
for (const cache of this._pivotCaches){
|
|
4858
|
-
types.push(createElement('Override', {
|
|
4859
|
-
PartName: `/xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
|
|
4860
|
-
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
|
|
4861
|
-
}, []));
|
|
4862
|
-
types.push(createElement('Override', {
|
|
4863
|
-
PartName: `/xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`,
|
|
4864
|
-
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
|
|
4865
|
-
}, []));
|
|
4866
|
-
}
|
|
4867
|
-
// Add pivot tables
|
|
4868
|
-
for (const pivotTable of this._pivotTables){
|
|
4869
|
-
types.push(createElement('Override', {
|
|
4870
|
-
PartName: `/xl/pivotTables/pivotTable${pivotTable.index}.xml`,
|
|
4871
|
-
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
|
|
4872
|
-
}, []));
|
|
4873
|
-
}
|
|
4874
4807
|
// Add tables
|
|
4875
4808
|
let tableIndex = 1;
|
|
4876
4809
|
for (const def of this._sheetDefs){
|
|
@@ -4885,6 +4818,21 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4885
4818
|
}
|
|
4886
4819
|
}
|
|
4887
4820
|
}
|
|
4821
|
+
// Add pivot caches and pivot tables
|
|
4822
|
+
for (const pivot of this._pivotTables){
|
|
4823
|
+
types.push(createElement('Override', {
|
|
4824
|
+
PartName: `/xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`,
|
|
4825
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
|
|
4826
|
+
}, []));
|
|
4827
|
+
types.push(createElement('Override', {
|
|
4828
|
+
PartName: `/xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`,
|
|
4829
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
|
|
4830
|
+
}, []));
|
|
4831
|
+
types.push(createElement('Override', {
|
|
4832
|
+
PartName: `/xl/pivotTables/pivotTable${pivot.pivotId}.xml`,
|
|
4833
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
|
|
4834
|
+
}, []));
|
|
4835
|
+
}
|
|
4888
4836
|
const existingTypesXml = readZipText(this._files, '[Content_Types].xml');
|
|
4889
4837
|
const existingKeys = new Set(types.map((t)=>{
|
|
4890
4838
|
if ('Default' in t) {
|
|
@@ -4940,66 +4888,35 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4940
4888
|
}
|
|
4941
4889
|
}
|
|
4942
4890
|
/**
|
|
4943
|
-
* Generate all
|
|
4944
|
-
*/
|
|
4945
|
-
//
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4891
|
+
* Generate all table related files
|
|
4892
|
+
*/ _updateTableFiles() {
|
|
4893
|
+
// Collect all tables with their global indices
|
|
4894
|
+
let globalTableIndex = 1;
|
|
4895
|
+
const sheetTables = new Map();
|
|
4896
|
+
for (const def of this._sheetDefs){
|
|
4897
|
+
const worksheet = this._sheets.get(def.name);
|
|
4898
|
+
if (!worksheet) continue;
|
|
4899
|
+
const tables = worksheet.tables;
|
|
4900
|
+
if (tables.length === 0) continue;
|
|
4901
|
+
const tableInfos = [];
|
|
4902
|
+
for (const table of tables){
|
|
4903
|
+
tableInfos.push({
|
|
4904
|
+
table,
|
|
4905
|
+
globalIndex: globalTableIndex
|
|
4906
|
+
});
|
|
4907
|
+
globalTableIndex++;
|
|
4951
4908
|
}
|
|
4952
|
-
|
|
4953
|
-
}
|
|
4954
|
-
// Generate pivot cache files
|
|
4955
|
-
for(let i = 0; i < this._pivotCaches.length; i++){
|
|
4956
|
-
const cache = this._pivotCaches[i];
|
|
4957
|
-
// Pivot cache definition
|
|
4958
|
-
const definitionPath = `xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`;
|
|
4959
|
-
writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
|
|
4960
|
-
// Pivot cache records
|
|
4961
|
-
const recordsPath = `xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`;
|
|
4962
|
-
writeZipText(this._files, recordsPath, cache.toRecordsXml());
|
|
4963
|
-
// Pivot cache definition relationships (link to records)
|
|
4964
|
-
const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cache.fileIndex}.xml.rels`;
|
|
4965
|
-
const cacheRels = createElement('Relationships', {
|
|
4966
|
-
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
4967
|
-
}, [
|
|
4968
|
-
createElement('Relationship', {
|
|
4969
|
-
Id: 'rId1',
|
|
4970
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
4971
|
-
Target: `pivotCacheRecords${cache.fileIndex}.xml`
|
|
4972
|
-
}, [])
|
|
4973
|
-
]);
|
|
4974
|
-
writeZipText(this._files, cacheRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
4975
|
-
cacheRels
|
|
4976
|
-
])}`);
|
|
4909
|
+
sheetTables.set(def.name, tableInfos);
|
|
4977
4910
|
}
|
|
4978
|
-
// Generate
|
|
4979
|
-
for(
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
writeZipText(this._files, ptPath, pivotTable.toXml());
|
|
4985
|
-
// Pivot table relationships (link to cache definition)
|
|
4986
|
-
const cacheIdx = pivotTable.cacheFileIndex;
|
|
4987
|
-
const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
|
|
4988
|
-
const ptRels = createElement('Relationships', {
|
|
4989
|
-
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
4990
|
-
}, [
|
|
4991
|
-
createElement('Relationship', {
|
|
4992
|
-
Id: 'rId1',
|
|
4993
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
4994
|
-
Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`
|
|
4995
|
-
}, [])
|
|
4996
|
-
]);
|
|
4997
|
-
writeZipText(this._files, ptRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
4998
|
-
ptRels
|
|
4999
|
-
])}`);
|
|
4911
|
+
// Generate table files
|
|
4912
|
+
for (const [, tableInfos] of sheetTables){
|
|
4913
|
+
for (const { table, globalIndex } of tableInfos){
|
|
4914
|
+
const tablePath = `xl/tables/table${globalIndex}.xml`;
|
|
4915
|
+
writeZipText(this._files, tablePath, table.toXml());
|
|
4916
|
+
}
|
|
5000
4917
|
}
|
|
5001
|
-
// Generate worksheet relationships for
|
|
5002
|
-
for (const [sheetName,
|
|
4918
|
+
// Generate worksheet relationships for tables
|
|
4919
|
+
for (const [sheetName, tableInfos] of sheetTables){
|
|
5003
4920
|
const def = this._sheetDefs.find((s)=>s.name === sheetName);
|
|
5004
4921
|
if (!def) continue;
|
|
5005
4922
|
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
@@ -5007,11 +4924,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5007
4924
|
// Extract sheet file name from target path
|
|
5008
4925
|
const sheetFileName = rel.target.split('/').pop();
|
|
5009
4926
|
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
4927
|
+
// Check if there are already pivot table relationships for this sheet
|
|
5010
4928
|
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
5011
|
-
let relNodes = [];
|
|
5012
4929
|
let nextRelId = 1;
|
|
4930
|
+
const relNodes = [];
|
|
5013
4931
|
const reservedRelIds = new Set();
|
|
5014
4932
|
if (existingRelsXml) {
|
|
4933
|
+
// Parse existing rels and find max rId
|
|
5015
4934
|
const parsed = parseXml(existingRelsXml);
|
|
5016
4935
|
const relsElement = findElement(parsed, 'Relationships');
|
|
5017
4936
|
if (relsElement) {
|
|
@@ -5040,16 +4959,29 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5040
4959
|
reservedRelIds.add(id);
|
|
5041
4960
|
return id;
|
|
5042
4961
|
};
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
4962
|
+
// Add table relationships
|
|
4963
|
+
const tableRelIds = [];
|
|
4964
|
+
for (const { globalIndex } of tableInfos){
|
|
4965
|
+
const target = `../tables/table${globalIndex}.xml`;
|
|
4966
|
+
const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
|
|
4967
|
+
if (existing) {
|
|
4968
|
+
const existingRel = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
|
|
4969
|
+
const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
|
|
4970
|
+
tableRelIds.push(existingId ?? allocateRelId());
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4973
|
+
const id = allocateRelId();
|
|
4974
|
+
tableRelIds.push(id);
|
|
5047
4975
|
relNodes.push(createElement('Relationship', {
|
|
5048
|
-
Id:
|
|
5049
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/
|
|
4976
|
+
Id: id,
|
|
4977
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
|
|
5050
4978
|
Target: target
|
|
5051
4979
|
}, []));
|
|
5052
4980
|
}
|
|
4981
|
+
const worksheet = this._sheets.get(sheetName);
|
|
4982
|
+
if (worksheet) {
|
|
4983
|
+
worksheet.setTableRelIds(tableRelIds);
|
|
4984
|
+
}
|
|
5053
4985
|
const sheetRels = createElement('Relationships', {
|
|
5054
4986
|
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
5055
4987
|
}, relNodes);
|
|
@@ -5059,54 +4991,45 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5059
4991
|
}
|
|
5060
4992
|
}
|
|
5061
4993
|
/**
|
|
5062
|
-
* Generate
|
|
5063
|
-
*/
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
const sheetTables = new Map();
|
|
5067
|
-
for (const def of this._sheetDefs){
|
|
5068
|
-
const worksheet = this._sheets.get(def.name);
|
|
5069
|
-
if (!worksheet) continue;
|
|
5070
|
-
const tables = worksheet.tables;
|
|
5071
|
-
if (tables.length === 0) continue;
|
|
5072
|
-
const tableInfos = [];
|
|
5073
|
-
for (const table of tables){
|
|
5074
|
-
tableInfos.push({
|
|
5075
|
-
table,
|
|
5076
|
-
globalIndex: globalTableIndex
|
|
5077
|
-
});
|
|
5078
|
-
globalTableIndex++;
|
|
5079
|
-
}
|
|
5080
|
-
sheetTables.set(def.name, tableInfos);
|
|
5081
|
-
}
|
|
5082
|
-
// Generate table files
|
|
5083
|
-
for (const [, tableInfos] of sheetTables){
|
|
5084
|
-
for (const { table, globalIndex } of tableInfos){
|
|
5085
|
-
const tablePath = `xl/tables/table${globalIndex}.xml`;
|
|
5086
|
-
writeZipText(this._files, tablePath, table.toXml());
|
|
5087
|
-
}
|
|
4994
|
+
* Generate pivot cache/table parts and worksheet relationships.
|
|
4995
|
+
*/ _updatePivotFiles() {
|
|
4996
|
+
if (this._pivotTables.length === 0) {
|
|
4997
|
+
return;
|
|
5088
4998
|
}
|
|
5089
|
-
|
|
5090
|
-
|
|
4999
|
+
for (const pivot of this._pivotTables){
|
|
5000
|
+
const pivotParts = pivot.buildPivotPartsXml();
|
|
5001
|
+
const pivotCachePath = `xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
|
|
5002
|
+
writeZipText(this._files, pivotCachePath, pivotParts.cacheDefinitionXml);
|
|
5003
|
+
const pivotCacheRecordsPath = `xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`;
|
|
5004
|
+
writeZipText(this._files, pivotCacheRecordsPath, pivotParts.cacheRecordsXml);
|
|
5005
|
+
const pivotCacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${pivot.cachePartIndex}.xml.rels`;
|
|
5006
|
+
writeZipText(this._files, pivotCacheRelsPath, pivotParts.cacheRelsXml);
|
|
5007
|
+
const pivotTablePath = `xl/pivotTables/pivotTable${pivot.pivotId}.xml`;
|
|
5008
|
+
writeZipText(this._files, pivotTablePath, pivotParts.pivotTableXml);
|
|
5009
|
+
}
|
|
5010
|
+
const pivotsBySheet = new Map();
|
|
5011
|
+
for (const pivot of this._pivotTables){
|
|
5012
|
+
const existing = pivotsBySheet.get(pivot.targetSheetName) ?? [];
|
|
5013
|
+
existing.push(pivot);
|
|
5014
|
+
pivotsBySheet.set(pivot.targetSheetName, existing);
|
|
5015
|
+
}
|
|
5016
|
+
for (const [sheetName, pivots] of pivotsBySheet){
|
|
5091
5017
|
const def = this._sheetDefs.find((s)=>s.name === sheetName);
|
|
5092
5018
|
if (!def) continue;
|
|
5093
5019
|
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
5094
5020
|
if (!rel) continue;
|
|
5095
|
-
// Extract sheet file name from target path
|
|
5096
5021
|
const sheetFileName = rel.target.split('/').pop();
|
|
5022
|
+
if (!sheetFileName) continue;
|
|
5097
5023
|
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
5098
|
-
// Check if there are already pivot table relationships for this sheet
|
|
5099
5024
|
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
5100
5025
|
let nextRelId = 1;
|
|
5101
5026
|
const relNodes = [];
|
|
5102
5027
|
const reservedRelIds = new Set();
|
|
5103
5028
|
if (existingRelsXml) {
|
|
5104
|
-
// Parse existing rels and find max rId
|
|
5105
5029
|
const parsed = parseXml(existingRelsXml);
|
|
5106
5030
|
const relsElement = findElement(parsed, 'Relationships');
|
|
5107
5031
|
if (relsElement) {
|
|
5108
|
-
const
|
|
5109
|
-
for (const relNode of existingRelNodes){
|
|
5032
|
+
for (const relNode of getChildren(relsElement, 'Relationships')){
|
|
5110
5033
|
if ('Relationship' in relNode) {
|
|
5111
5034
|
relNodes.push(relNode);
|
|
5112
5035
|
const id = getAttr(relNode, 'Id');
|
|
@@ -5130,28 +5053,26 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5130
5053
|
reservedRelIds.add(id);
|
|
5131
5054
|
return id;
|
|
5132
5055
|
};
|
|
5133
|
-
|
|
5134
|
-
const
|
|
5135
|
-
|
|
5136
|
-
const
|
|
5137
|
-
const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
|
|
5056
|
+
const pivotRelIds = [];
|
|
5057
|
+
for (const pivot of pivots){
|
|
5058
|
+
const target = `../pivotTables/pivotTable${pivot.pivotId}.xml`;
|
|
5059
|
+
const existing = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' && getAttr(node, 'Target') === target);
|
|
5138
5060
|
if (existing) {
|
|
5139
|
-
const
|
|
5140
|
-
|
|
5141
|
-
tableRelIds.push(existingId ?? allocateRelId());
|
|
5061
|
+
const existingId = getAttr(existing, 'Id');
|
|
5062
|
+
pivotRelIds.push(existingId ?? allocateRelId());
|
|
5142
5063
|
continue;
|
|
5143
5064
|
}
|
|
5144
5065
|
const id = allocateRelId();
|
|
5145
|
-
|
|
5066
|
+
pivotRelIds.push(id);
|
|
5146
5067
|
relNodes.push(createElement('Relationship', {
|
|
5147
5068
|
Id: id,
|
|
5148
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/
|
|
5069
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
|
|
5149
5070
|
Target: target
|
|
5150
5071
|
}, []));
|
|
5151
5072
|
}
|
|
5152
5073
|
const worksheet = this._sheets.get(sheetName);
|
|
5153
5074
|
if (worksheet) {
|
|
5154
|
-
worksheet.
|
|
5075
|
+
worksheet.setPivotTableRelIds(pivotRelIds);
|
|
5155
5076
|
}
|
|
5156
5077
|
const sheetRels = createElement('Relationships', {
|
|
5157
5078
|
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
@@ -5163,4 +5084,4 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5163
5084
|
}
|
|
5164
5085
|
}
|
|
5165
5086
|
|
|
5166
|
-
export { Cell,
|
|
5087
|
+
export { Cell, PivotTable, Range, SharedStrings, Styles, Table, Workbook, Worksheet, parseAddress, parseRange, parseSheetAddress, parseSheetRange, toAddress, toRange };
|