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