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