@niicojs/excel 0.3.3 → 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 -1309
- 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 -1310
- 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 -501
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,716 +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
|
-
children.push(createElement('items', {
|
|
3429
|
-
count: String(itemNodes.length)
|
|
3430
|
-
}, itemNodes));
|
|
3431
|
-
}
|
|
3432
|
-
} else if (valueField) {
|
|
3433
|
-
attrs.dataField = '1';
|
|
3434
|
-
attrs.showAll = '0';
|
|
3435
|
-
} else {
|
|
3436
|
-
attrs.showAll = '0';
|
|
3616
|
+
return createElement('cacheField', {
|
|
3617
|
+
name: field.name,
|
|
3618
|
+
numFmtId: '0'
|
|
3619
|
+
}, [
|
|
3620
|
+
createElement('sharedItems', attrs, [])
|
|
3621
|
+
]);
|
|
3437
3622
|
}
|
|
3438
|
-
|
|
3623
|
+
if (!isAxisField) {
|
|
3624
|
+
return createElement('cacheField', {
|
|
3625
|
+
name: field.name,
|
|
3626
|
+
numFmtId: '0'
|
|
3627
|
+
}, [
|
|
3628
|
+
createElement('sharedItems', {}, [])
|
|
3629
|
+
]);
|
|
3630
|
+
}
|
|
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;
|
|
3628
|
-
}
|
|
3629
|
-
/**
|
|
3630
|
-
* Set styles reference for number format resolution.
|
|
3631
|
-
* @internal
|
|
3632
|
-
*/ setStyles(styles) {
|
|
3633
|
-
this._styles = styles;
|
|
3634
|
-
}
|
|
3635
|
-
/**
|
|
3636
|
-
* Get the cache ID
|
|
3637
|
-
*/ get cacheId() {
|
|
3638
|
-
return this._cacheId;
|
|
3639
|
-
}
|
|
3640
|
-
/**
|
|
3641
|
-
* Get the file index for this cache (used for file naming).
|
|
3642
|
-
*/ get fileIndex() {
|
|
3643
|
-
return this._fileIndex;
|
|
3644
|
-
}
|
|
3645
|
-
/**
|
|
3646
|
-
* Set refreshOnLoad option
|
|
3647
|
-
*/ set refreshOnLoad(value) {
|
|
3648
|
-
this._refreshOnLoad = value;
|
|
3649
|
-
}
|
|
3650
|
-
/**
|
|
3651
|
-
* Set saveData option
|
|
3652
|
-
*/ set saveData(value) {
|
|
3653
|
-
this._saveData = value;
|
|
3849
|
+
class EagerZipStore {
|
|
3850
|
+
constructor(files){
|
|
3851
|
+
this._files = files;
|
|
3654
3852
|
}
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
*/ get refreshOnLoad() {
|
|
3658
|
-
return this._refreshOnLoad;
|
|
3853
|
+
get(path) {
|
|
3854
|
+
return this._files.get(path);
|
|
3659
3855
|
}
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
*/ get saveData() {
|
|
3663
|
-
return this._saveData;
|
|
3856
|
+
set(path, content) {
|
|
3857
|
+
this._files.set(path, content);
|
|
3664
3858
|
}
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
*/ get sourceSheet() {
|
|
3668
|
-
return this._sourceSheet;
|
|
3859
|
+
has(path) {
|
|
3860
|
+
return this._files.has(path);
|
|
3669
3861
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
*/ get sourceRange() {
|
|
3673
|
-
return this._sourceRange;
|
|
3862
|
+
delete(path) {
|
|
3863
|
+
this._files.delete(path);
|
|
3674
3864
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
return
|
|
3865
|
+
getText(path) {
|
|
3866
|
+
const data = this._files.get(path);
|
|
3867
|
+
if (!data) return undefined;
|
|
3868
|
+
return fflate.strFromU8(data);
|
|
3679
3869
|
}
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
*/ get fields() {
|
|
3683
|
-
return this._fields;
|
|
3870
|
+
setText(path, content) {
|
|
3871
|
+
this._files.set(path, fflate.strToU8(content));
|
|
3684
3872
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
*/ get recordCount() {
|
|
3688
|
-
return this._recordCount;
|
|
3873
|
+
toFiles() {
|
|
3874
|
+
return Promise.resolve(this._files);
|
|
3689
3875
|
}
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
this.
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
}));
|
|
3711
|
-
// Use Maps for case-insensitive 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
|
-
// Preserve original behavior: only build shared items for select string fields
|
|
3725
|
-
if (field.name === 'top') {
|
|
3726
|
-
const normalized = value.toLocaleLowerCase();
|
|
3727
|
-
const map = sharedItemsMaps[colIdx];
|
|
3728
|
-
if (!map.has(normalized)) {
|
|
3729
|
-
map.set(normalized, value);
|
|
3730
|
-
}
|
|
3731
|
-
}
|
|
3732
|
-
} else if (typeof value === 'number') {
|
|
3733
|
-
if (field.minValue === undefined || value < field.minValue) {
|
|
3734
|
-
field.minValue = value;
|
|
3735
|
-
}
|
|
3736
|
-
if (field.maxValue === undefined || value > field.maxValue) {
|
|
3737
|
-
field.maxValue = value;
|
|
3738
|
-
}
|
|
3739
|
-
if (field.name === 'date') {
|
|
3740
|
-
const d = this._excelSerialToDate(value);
|
|
3741
|
-
field.isDate = true;
|
|
3742
|
-
field.isNumeric = false;
|
|
3743
|
-
if (!field.minDate || d < field.minDate) {
|
|
3744
|
-
field.minDate = d;
|
|
3745
|
-
}
|
|
3746
|
-
if (!field.maxDate || d > field.maxDate) {
|
|
3747
|
-
field.maxDate = d;
|
|
3748
|
-
}
|
|
3749
|
-
}
|
|
3750
|
-
} else if (value instanceof Date) {
|
|
3751
|
-
field.isDate = true;
|
|
3752
|
-
field.isNumeric = false;
|
|
3753
|
-
if (!field.minDate || value < field.minDate) {
|
|
3754
|
-
field.minDate = value;
|
|
3755
|
-
}
|
|
3756
|
-
if (!field.maxDate || value > field.maxDate) {
|
|
3757
|
-
field.maxDate = value;
|
|
3758
|
-
}
|
|
3759
|
-
} else if (typeof value === 'boolean') {
|
|
3760
|
-
field.isNumeric = false;
|
|
3761
|
-
field.hasBoolean = true;
|
|
3762
|
-
}
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
// Resolve number formats if styles are available
|
|
3766
|
-
if (this._styles) {
|
|
3767
|
-
const numericFmtId = 164;
|
|
3768
|
-
const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');
|
|
3769
|
-
for (const field of this._fields){
|
|
3770
|
-
if (field.isDate) {
|
|
3771
|
-
field.numFmtId = dateFmtId;
|
|
3772
|
-
continue;
|
|
3773
|
-
}
|
|
3774
|
-
if (field.isNumeric) {
|
|
3775
|
-
if (field.name === 'jours') {
|
|
3776
|
-
field.numFmtId = 0;
|
|
3777
|
-
} else {
|
|
3778
|
-
field.numFmtId = numericFmtId;
|
|
3779
|
-
}
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
// Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
|
|
3784
|
-
this._sharedItemsIndexMap.clear();
|
|
3785
|
-
this._blankItemIndexMap.clear();
|
|
3786
|
-
for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
|
|
3787
|
-
const field = this._fields[colIdx];
|
|
3788
|
-
const map = sharedItemsMaps[colIdx];
|
|
3789
|
-
// Convert Map values to array (maintains insertion order in ES6+)
|
|
3790
|
-
field.sharedItems = Array.from(map.values());
|
|
3791
|
-
if (field.name !== 'top') {
|
|
3792
|
-
field.sharedItems = [];
|
|
3793
|
-
}
|
|
3794
|
-
// Build reverse lookup Map: value -> index
|
|
3795
|
-
if (field.sharedItems.length > 0) {
|
|
3796
|
-
const indexMap = new Map();
|
|
3797
|
-
for(let i = 0; i < field.sharedItems.length; i++){
|
|
3798
|
-
indexMap.set(field.sharedItems[i], i);
|
|
3799
|
-
}
|
|
3800
|
-
this._sharedItemsIndexMap.set(colIdx, indexMap);
|
|
3801
|
-
if (field.hasBlank) {
|
|
3802
|
-
const blankIndex = field.name === 'secteur' ? 1 : field.sharedItems.length;
|
|
3803
|
-
this._blankItemIndexMap.set(colIdx, blankIndex);
|
|
3804
|
-
}
|
|
3805
|
-
}
|
|
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);
|
|
3806
3896
|
}
|
|
3807
|
-
|
|
3808
|
-
this._records = data;
|
|
3897
|
+
return data;
|
|
3809
3898
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
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
|
+
}
|
|
3814
3905
|
}
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
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;
|
|
3820
3911
|
}
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
const sharedItemChildren = [];
|
|
3827
|
-
if (field.sharedItems.length > 0 && field.name === 'top') {
|
|
3828
|
-
// String field with shared items
|
|
3829
|
-
const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;
|
|
3830
|
-
sharedItemsAttrs.count = String(total);
|
|
3831
|
-
if (field.hasBlank) {
|
|
3832
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3833
|
-
}
|
|
3834
|
-
for (const item of field.sharedItems){
|
|
3835
|
-
sharedItemChildren.push(createElement('s', {
|
|
3836
|
-
v: item
|
|
3837
|
-
}, []));
|
|
3838
|
-
}
|
|
3839
|
-
if (field.hasBlank) {
|
|
3840
|
-
if (field.name === 'secteur') {
|
|
3841
|
-
sharedItemChildren.splice(1, 0, createElement('m', {}, []));
|
|
3842
|
-
} else {
|
|
3843
|
-
sharedItemChildren.push(createElement('m', {}, []));
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
} else if (field.name !== 'top' && field.sharedItems.length > 0) {
|
|
3847
|
-
// For non-top string fields, avoid sharedItems count/items to match Excel output
|
|
3848
|
-
sharedItemsAttrs.containsString = '0';
|
|
3849
|
-
} else if (field.isDate) {
|
|
3850
|
-
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
3851
|
-
sharedItemsAttrs.containsString = '0';
|
|
3852
|
-
sharedItemsAttrs.containsDate = '1';
|
|
3853
|
-
sharedItemsAttrs.containsNonDate = '0';
|
|
3854
|
-
if (field.hasBlank) {
|
|
3855
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3856
|
-
}
|
|
3857
|
-
if (field.minDate) {
|
|
3858
|
-
sharedItemsAttrs.minDate = this._formatDate(field.minDate);
|
|
3859
|
-
}
|
|
3860
|
-
if (field.maxDate) {
|
|
3861
|
-
const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);
|
|
3862
|
-
sharedItemsAttrs.maxDate = this._formatDate(maxDate);
|
|
3863
|
-
}
|
|
3864
|
-
} else if (field.isNumeric) {
|
|
3865
|
-
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
3866
|
-
if (field.name === 'cost') {
|
|
3867
|
-
sharedItemsAttrs.containsMixedTypes = '1';
|
|
3868
|
-
} else {
|
|
3869
|
-
if (field.name !== 'jours') {
|
|
3870
|
-
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
3871
|
-
}
|
|
3872
|
-
sharedItemsAttrs.containsString = '0';
|
|
3873
|
-
}
|
|
3874
|
-
sharedItemsAttrs.containsNumber = '1';
|
|
3875
|
-
if (field.hasBlank) {
|
|
3876
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3877
|
-
}
|
|
3878
|
-
// Check if all values are integers
|
|
3879
|
-
if (field.minValue !== undefined && field.maxValue !== undefined) {
|
|
3880
|
-
const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
|
|
3881
|
-
if (isInteger) {
|
|
3882
|
-
sharedItemsAttrs.containsInteger = '1';
|
|
3883
|
-
}
|
|
3884
|
-
sharedItemsAttrs.minValue = this._formatNumber(field.minValue);
|
|
3885
|
-
sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);
|
|
3886
|
-
}
|
|
3887
|
-
} else if (field.hasBoolean) {
|
|
3888
|
-
// Boolean-only field (no strings, no numbers)
|
|
3889
|
-
// Excel does not add contains* flags for ww in this dataset
|
|
3890
|
-
if (field.hasBlank) {
|
|
3891
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3892
|
-
}
|
|
3893
|
-
if (field.name === 'ww') {
|
|
3894
|
-
sharedItemsAttrs.count = field.hasBlank ? '3' : '2';
|
|
3895
|
-
sharedItemChildren.push(createElement('b', {
|
|
3896
|
-
v: '0'
|
|
3897
|
-
}, []));
|
|
3898
|
-
sharedItemChildren.push(createElement('b', {
|
|
3899
|
-
v: '1'
|
|
3900
|
-
}, []));
|
|
3901
|
-
if (field.hasBlank) {
|
|
3902
|
-
sharedItemChildren.push(createElement('m', {}, []));
|
|
3903
|
-
}
|
|
3904
|
-
}
|
|
3905
|
-
} else if (field.hasBlank) {
|
|
3906
|
-
// Field that only contains blanks
|
|
3907
|
-
if (field.name === 'contratClient' || field.name === 'secteur' || field.name === 'vertical' || field.name === 'parentOppy' || field.name === 'pole' || field.name === 'oppyClosed' || field.name === 'domain' || field.name === 'businessOwner') {
|
|
3908
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3909
|
-
} else {
|
|
3910
|
-
sharedItemsAttrs.containsNonDate = '0';
|
|
3911
|
-
sharedItemsAttrs.containsString = '0';
|
|
3912
|
-
sharedItemsAttrs.containsBlank = '1';
|
|
3913
|
-
}
|
|
3914
|
-
}
|
|
3915
|
-
const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
|
|
3916
|
-
const cacheFieldAttrs = {
|
|
3917
|
-
name: field.name,
|
|
3918
|
-
numFmtId: String(field.numFmtId ?? 0)
|
|
3919
|
-
};
|
|
3920
|
-
return createElement('cacheField', cacheFieldAttrs, [
|
|
3921
|
-
sharedItemsNode
|
|
3922
|
-
]);
|
|
3923
|
-
});
|
|
3924
|
-
const cacheFieldsNode = createElement('cacheFields', {
|
|
3925
|
-
count: String(this._fields.length)
|
|
3926
|
-
}, cacheFieldNodes);
|
|
3927
|
-
const worksheetSourceNode = createElement('worksheetSource', {
|
|
3928
|
-
ref: this._sourceRange,
|
|
3929
|
-
sheet: this._sourceSheet
|
|
3930
|
-
}, []);
|
|
3931
|
-
const cacheSourceNode = createElement('cacheSource', {
|
|
3932
|
-
type: 'worksheet'
|
|
3933
|
-
}, [
|
|
3934
|
-
worksheetSourceNode
|
|
3935
|
-
]);
|
|
3936
|
-
// Build attributes - align with Excel expectations
|
|
3937
|
-
const definitionAttrs = {
|
|
3938
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
3939
|
-
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3940
|
-
'r:id': recordsRelId
|
|
3941
|
-
};
|
|
3942
|
-
if (this._refreshOnLoad) {
|
|
3943
|
-
definitionAttrs.refreshOnLoad = '1';
|
|
3944
|
-
}
|
|
3945
|
-
definitionAttrs.refreshedBy = 'User';
|
|
3946
|
-
definitionAttrs.refreshedVersion = '8';
|
|
3947
|
-
definitionAttrs.minRefreshableVersion = '3';
|
|
3948
|
-
definitionAttrs.createdVersion = '8';
|
|
3949
|
-
if (!this._saveData) {
|
|
3950
|
-
definitionAttrs.saveData = '0';
|
|
3951
|
-
definitionAttrs.recordCount = '0';
|
|
3952
|
-
} else {
|
|
3953
|
-
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);
|
|
3954
3917
|
}
|
|
3955
|
-
const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [
|
|
3956
|
-
cacheSourceNode,
|
|
3957
|
-
cacheFieldsNode
|
|
3958
|
-
]);
|
|
3959
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
3960
|
-
definitionNode
|
|
3961
|
-
])}`;
|
|
3962
3918
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
for (const row of this._records){
|
|
3968
|
-
const fieldNodes = [];
|
|
3969
|
-
for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
|
|
3970
|
-
const value = colIdx < row.length ? row[colIdx] : null;
|
|
3971
|
-
if (value === null || value === undefined) {
|
|
3972
|
-
// Missing value
|
|
3973
|
-
const blankIndex = this._blankItemIndexMap.get(colIdx);
|
|
3974
|
-
if (blankIndex !== undefined) {
|
|
3975
|
-
fieldNodes.push(createElement('x', {
|
|
3976
|
-
v: String(blankIndex)
|
|
3977
|
-
}, []));
|
|
3978
|
-
} else {
|
|
3979
|
-
fieldNodes.push(createElement('m', {}, []));
|
|
3980
|
-
}
|
|
3981
|
-
} else if (typeof value === 'string') {
|
|
3982
|
-
// String value - use index into sharedItems via O(1) Map lookup
|
|
3983
|
-
const indexMap = this._sharedItemsIndexMap.get(colIdx);
|
|
3984
|
-
const idx = indexMap?.get(value);
|
|
3985
|
-
if (idx !== undefined) {
|
|
3986
|
-
fieldNodes.push(createElement('x', {
|
|
3987
|
-
v: String(idx)
|
|
3988
|
-
}, []));
|
|
3989
|
-
} else {
|
|
3990
|
-
// Direct string value (shouldn't happen if cache is built correctly)
|
|
3991
|
-
fieldNodes.push(createElement('s', {
|
|
3992
|
-
v: value
|
|
3993
|
-
}, []));
|
|
3994
|
-
}
|
|
3995
|
-
} else if (typeof value === 'number') {
|
|
3996
|
-
if (this._fields[colIdx]?.name === 'date') {
|
|
3997
|
-
const d = this._excelSerialToDate(value);
|
|
3998
|
-
fieldNodes.push(createElement('d', {
|
|
3999
|
-
v: this._formatDate(d)
|
|
4000
|
-
}, []));
|
|
4001
|
-
} else {
|
|
4002
|
-
fieldNodes.push(createElement('n', {
|
|
4003
|
-
v: String(value)
|
|
4004
|
-
}, []));
|
|
4005
|
-
}
|
|
4006
|
-
} else if (typeof value === 'boolean') {
|
|
4007
|
-
if (this._fields[colIdx]?.name === 'ww') {
|
|
4008
|
-
fieldNodes.push(createElement('x', {
|
|
4009
|
-
v: value ? '1' : '0'
|
|
4010
|
-
}, []));
|
|
4011
|
-
} else {
|
|
4012
|
-
fieldNodes.push(createElement('b', {
|
|
4013
|
-
v: value ? '1' : '0'
|
|
4014
|
-
}, []));
|
|
4015
|
-
}
|
|
4016
|
-
} else if (value instanceof Date) {
|
|
4017
|
-
fieldNodes.push(createElement('d', {
|
|
4018
|
-
v: this._formatDate(value)
|
|
4019
|
-
}, []));
|
|
4020
|
-
} else {
|
|
4021
|
-
// Unknown type, treat as missing
|
|
4022
|
-
fieldNodes.push(createElement('m', {}, []));
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
recordNodes.push(createElement('r', {}, fieldNodes));
|
|
4026
|
-
}
|
|
4027
|
-
const recordsNode = createElement('pivotCacheRecords', {
|
|
4028
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
4029
|
-
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
4030
|
-
count: String(this._recordCount)
|
|
4031
|
-
}, recordNodes);
|
|
4032
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
4033
|
-
recordsNode
|
|
4034
|
-
])}`;
|
|
3919
|
+
getText(path) {
|
|
3920
|
+
const data = this.get(path);
|
|
3921
|
+
if (!data) return undefined;
|
|
3922
|
+
return fflate.strFromU8(data);
|
|
4035
3923
|
}
|
|
4036
|
-
|
|
4037
|
-
|
|
3924
|
+
setText(path, content) {
|
|
3925
|
+
this.set(path, fflate.strToU8(content));
|
|
4038
3926
|
}
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
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);
|
|
4042
3932
|
}
|
|
4043
|
-
|
|
4044
|
-
|
|
3933
|
+
for (const [path, content] of this._files){
|
|
3934
|
+
files.set(path, content);
|
|
4045
3935
|
}
|
|
4046
|
-
return
|
|
3936
|
+
return files;
|
|
4047
3937
|
}
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
const
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
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;
|
|
4055
3948
|
}
|
|
4056
3949
|
}
|
|
4057
|
-
|
|
3950
|
+
const createZipStore = ()=>{
|
|
3951
|
+
return new EagerZipStore(new Map());
|
|
3952
|
+
};
|
|
4058
3953
|
/**
|
|
4059
3954
|
* Reads a ZIP file and returns a map of path -> content
|
|
4060
3955
|
* @param data - ZIP file as Uint8Array
|
|
4061
3956
|
* @returns Promise resolving to a map of file paths to contents
|
|
4062
|
-
*/ 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
|
+
}
|
|
4063
3962
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
4064
3963
|
if (isBun) {
|
|
4065
3964
|
try {
|
|
@@ -4068,7 +3967,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4068
3967
|
for (const [path, content] of Object.entries(result)){
|
|
4069
3968
|
files.set(path, content);
|
|
4070
3969
|
}
|
|
4071
|
-
return Promise.resolve(files);
|
|
3970
|
+
return Promise.resolve(new EagerZipStore(files));
|
|
4072
3971
|
} catch (error) {
|
|
4073
3972
|
return Promise.reject(error);
|
|
4074
3973
|
}
|
|
@@ -4083,7 +3982,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4083
3982
|
for (const [path, content] of Object.entries(result)){
|
|
4084
3983
|
files.set(path, content);
|
|
4085
3984
|
}
|
|
4086
|
-
resolve(files);
|
|
3985
|
+
resolve(new EagerZipStore(files));
|
|
4087
3986
|
});
|
|
4088
3987
|
});
|
|
4089
3988
|
};
|
|
@@ -4091,9 +3990,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4091
3990
|
* Creates a ZIP file from a map of path -> content
|
|
4092
3991
|
* @param files - Map of file paths to contents
|
|
4093
3992
|
* @returns Promise resolving to ZIP file as Uint8Array
|
|
4094
|
-
*/ const writeZip = (files)=>{
|
|
3993
|
+
*/ const writeZip = async (files)=>{
|
|
3994
|
+
const resolved = await files.toFiles();
|
|
4095
3995
|
const zipData = {};
|
|
4096
|
-
for (const [path, content] of
|
|
3996
|
+
for (const [path, content] of resolved){
|
|
4097
3997
|
zipData[path] = content;
|
|
4098
3998
|
}
|
|
4099
3999
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
@@ -4117,49 +4017,53 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4117
4017
|
/**
|
|
4118
4018
|
* Reads a file from the ZIP as a UTF-8 string
|
|
4119
4019
|
*/ const readZipText = (files, path)=>{
|
|
4120
|
-
|
|
4121
|
-
if (!data) return undefined;
|
|
4122
|
-
return fflate.strFromU8(data);
|
|
4020
|
+
return files.getText(path);
|
|
4123
4021
|
};
|
|
4124
4022
|
/**
|
|
4125
4023
|
* Writes a UTF-8 string to the ZIP files map
|
|
4126
4024
|
*/ const writeZipText = (files, path, content)=>{
|
|
4127
|
-
files.
|
|
4025
|
+
files.setText(path, content);
|
|
4128
4026
|
};
|
|
4129
4027
|
|
|
4130
4028
|
/**
|
|
4131
4029
|
* Represents an Excel workbook (.xlsx file)
|
|
4132
4030
|
*/ class Workbook {
|
|
4133
4031
|
constructor(){
|
|
4134
|
-
this._files =
|
|
4032
|
+
this._files = createZipStore();
|
|
4135
4033
|
this._sheets = new Map();
|
|
4136
4034
|
this._sheetDefs = [];
|
|
4137
4035
|
this._relationships = [];
|
|
4036
|
+
this._sharedStrings = null;
|
|
4037
|
+
this._styles = null;
|
|
4038
|
+
this._sharedStringsXml = null;
|
|
4039
|
+
this._stylesXml = null;
|
|
4040
|
+
this._lazy = true;
|
|
4138
4041
|
this._dirty = false;
|
|
4139
|
-
// Pivot table support
|
|
4140
|
-
this._pivotTables = [];
|
|
4141
|
-
this._pivotCaches = [];
|
|
4142
|
-
this._nextCacheId = 5;
|
|
4143
|
-
this._nextCacheFileIndex = 1;
|
|
4144
4042
|
// Table support
|
|
4145
4043
|
this._nextTableId = 1;
|
|
4044
|
+
// Pivot table support
|
|
4045
|
+
this._pivotTables = [];
|
|
4046
|
+
this._nextPivotTableId = 1;
|
|
4047
|
+
this._nextPivotCacheId = 1;
|
|
4146
4048
|
// Date serialization handling
|
|
4147
4049
|
this._dateHandling = 'jsDate';
|
|
4148
4050
|
this._locale = 'fr-FR';
|
|
4149
|
-
|
|
4150
|
-
this._styles = Styles.createDefault();
|
|
4051
|
+
// Lazy init
|
|
4151
4052
|
}
|
|
4152
4053
|
/**
|
|
4153
4054
|
* Load a workbook from a file path
|
|
4154
|
-
*/ static async fromFile(path) {
|
|
4055
|
+
*/ static async fromFile(path, options = {}) {
|
|
4155
4056
|
const data = await promises.readFile(path);
|
|
4156
|
-
return Workbook.fromBuffer(new Uint8Array(data));
|
|
4057
|
+
return Workbook.fromBuffer(new Uint8Array(data), options);
|
|
4157
4058
|
}
|
|
4158
4059
|
/**
|
|
4159
4060
|
* Load a workbook from a buffer
|
|
4160
|
-
*/ static async fromBuffer(data) {
|
|
4061
|
+
*/ static async fromBuffer(data, options = {}) {
|
|
4161
4062
|
const workbook = new Workbook();
|
|
4162
|
-
workbook.
|
|
4063
|
+
workbook._lazy = options.lazy ?? true;
|
|
4064
|
+
workbook._files = await readZip(data, {
|
|
4065
|
+
lazy: workbook._lazy
|
|
4066
|
+
});
|
|
4163
4067
|
// Parse workbook.xml for sheet definitions
|
|
4164
4068
|
const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');
|
|
4165
4069
|
if (workbookXml) {
|
|
@@ -4171,15 +4075,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4171
4075
|
workbook._parseRelationships(relsXml);
|
|
4172
4076
|
}
|
|
4173
4077
|
// Parse shared strings
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
}
|
|
4178
|
-
// Parse styles
|
|
4179
|
-
const stylesXml = readZipText(workbook._files, 'xl/styles.xml');
|
|
4180
|
-
if (stylesXml) {
|
|
4181
|
-
workbook._styles = Styles.parse(stylesXml);
|
|
4182
|
-
}
|
|
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;
|
|
4183
4081
|
return workbook;
|
|
4184
4082
|
}
|
|
4185
4083
|
/**
|
|
@@ -4187,6 +4085,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4187
4085
|
*/ static create() {
|
|
4188
4086
|
const workbook = new Workbook();
|
|
4189
4087
|
workbook._dirty = true;
|
|
4088
|
+
workbook._lazy = false;
|
|
4089
|
+
workbook._sharedStrings = new SharedStrings();
|
|
4090
|
+
workbook._styles = Styles.createDefault();
|
|
4190
4091
|
return workbook;
|
|
4191
4092
|
}
|
|
4192
4093
|
/**
|
|
@@ -4202,11 +4103,25 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4202
4103
|
/**
|
|
4203
4104
|
* Get shared strings table
|
|
4204
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
|
+
}
|
|
4205
4113
|
return this._sharedStrings;
|
|
4206
4114
|
}
|
|
4207
4115
|
/**
|
|
4208
4116
|
* Get styles
|
|
4209
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
|
+
}
|
|
4210
4125
|
return this._styles;
|
|
4211
4126
|
}
|
|
4212
4127
|
/**
|
|
@@ -4237,6 +4152,72 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4237
4152
|
return this._nextTableId++;
|
|
4238
4153
|
}
|
|
4239
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
|
+
/**
|
|
4240
4221
|
* Get a worksheet by name or index
|
|
4241
4222
|
*/ sheet(nameOrIndex) {
|
|
4242
4223
|
let def;
|
|
@@ -4260,7 +4241,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4260
4241
|
const sheetPath = `xl/${rel.target}`;
|
|
4261
4242
|
const sheetXml = readZipText(this._files, sheetPath);
|
|
4262
4243
|
if (sheetXml) {
|
|
4263
|
-
worksheet.parse(sheetXml
|
|
4244
|
+
worksheet.parse(sheetXml, {
|
|
4245
|
+
lazy: this._lazy
|
|
4246
|
+
});
|
|
4264
4247
|
}
|
|
4265
4248
|
}
|
|
4266
4249
|
this._sheets.set(def.name, worksheet);
|
|
@@ -4571,100 +4554,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4571
4554
|
return String(value);
|
|
4572
4555
|
}
|
|
4573
4556
|
/**
|
|
4574
|
-
* Create a pivot table from source data.
|
|
4575
|
-
*
|
|
4576
|
-
* @param config - Pivot table configuration
|
|
4577
|
-
* @returns PivotTable instance for fluent configuration
|
|
4578
|
-
*
|
|
4579
|
-
* @example
|
|
4580
|
-
* ```typescript
|
|
4581
|
-
* const pivot = wb.createPivotTable({
|
|
4582
|
-
* name: 'SalesPivot',
|
|
4583
|
-
* source: 'DataSheet!A1:D100',
|
|
4584
|
-
* target: 'PivotSheet!A3',
|
|
4585
|
-
* });
|
|
4586
|
-
*
|
|
4587
|
-
* pivot
|
|
4588
|
-
* .addRowField('Region')
|
|
4589
|
-
* .addColumnField('Product')
|
|
4590
|
-
* .addValueField('Sales', 'sum', 'Total Sales');
|
|
4591
|
-
* ```
|
|
4592
|
-
*/ createPivotTable(config) {
|
|
4593
|
-
this._dirty = true;
|
|
4594
|
-
// Parse source reference (Sheet!Range)
|
|
4595
|
-
const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);
|
|
4596
|
-
// Parse target reference
|
|
4597
|
-
const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);
|
|
4598
|
-
// Ensure target sheet exists
|
|
4599
|
-
if (!this._sheetDefs.some((s)=>s.name === targetSheet)) {
|
|
4600
|
-
this.addSheet(targetSheet);
|
|
4601
|
-
}
|
|
4602
|
-
// Parse target cell address
|
|
4603
|
-
const targetAddr = parseAddress(targetCell);
|
|
4604
|
-
// Get source worksheet and extract data
|
|
4605
|
-
const sourceWs = this.sheet(sourceSheet);
|
|
4606
|
-
const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
|
|
4607
|
-
// Create pivot cache
|
|
4608
|
-
const cacheId = this._nextCacheId++;
|
|
4609
|
-
const cacheFileIndex = this._nextCacheFileIndex++;
|
|
4610
|
-
const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
|
|
4611
|
-
cache.setStyles(this._styles);
|
|
4612
|
-
cache.buildFromData(headers, data);
|
|
4613
|
-
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
4614
|
-
if (config.refreshOnLoad === false) {
|
|
4615
|
-
cache.refreshOnLoad = false;
|
|
4616
|
-
}
|
|
4617
|
-
// saveData defaults to true; only disable if explicitly set to false
|
|
4618
|
-
if (config.saveData === false) {
|
|
4619
|
-
cache.saveData = false;
|
|
4620
|
-
}
|
|
4621
|
-
this._pivotCaches.push(cache);
|
|
4622
|
-
// Create pivot table
|
|
4623
|
-
const pivotTableIndex = this._pivotTables.length + 1;
|
|
4624
|
-
const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex, cacheFileIndex);
|
|
4625
|
-
// Set styles reference for number format resolution
|
|
4626
|
-
pivotTable.setStyles(this._styles);
|
|
4627
|
-
this._pivotTables.push(pivotTable);
|
|
4628
|
-
return pivotTable;
|
|
4629
|
-
}
|
|
4630
|
-
/**
|
|
4631
|
-
* Parse a sheet reference like "Sheet1!A1:D100" into sheet name and range
|
|
4632
|
-
*/ _parseSheetRef(ref) {
|
|
4633
|
-
const match = ref.match(/^(.+?)!(.+)$/);
|
|
4634
|
-
if (!match) {
|
|
4635
|
-
throw new Error(`Invalid reference format: ${ref}. Expected "SheetName!Range"`);
|
|
4636
|
-
}
|
|
4637
|
-
return {
|
|
4638
|
-
sheetName: match[1],
|
|
4639
|
-
range: match[2]
|
|
4640
|
-
};
|
|
4641
|
-
}
|
|
4642
|
-
/**
|
|
4643
|
-
* Extract headers and data from a source range
|
|
4644
|
-
*/ _extractSourceData(sheet, rangeStr) {
|
|
4645
|
-
const range = parseRange(rangeStr);
|
|
4646
|
-
const headers = [];
|
|
4647
|
-
const data = [];
|
|
4648
|
-
// First row is headers
|
|
4649
|
-
for(let col = range.start.col; col <= range.end.col; col++){
|
|
4650
|
-
const cell = sheet.cell(toAddress(range.start.row, col));
|
|
4651
|
-
headers.push(String(cell.value ?? `Column${col + 1}`));
|
|
4652
|
-
}
|
|
4653
|
-
// Remaining rows are data
|
|
4654
|
-
for(let row = range.start.row + 1; row <= range.end.row; row++){
|
|
4655
|
-
const rowData = [];
|
|
4656
|
-
for(let col = range.start.col; col <= range.end.col; col++){
|
|
4657
|
-
const cell = sheet.cell(toAddress(row, col));
|
|
4658
|
-
rowData.push(cell.value);
|
|
4659
|
-
}
|
|
4660
|
-
data.push(rowData);
|
|
4661
|
-
}
|
|
4662
|
-
return {
|
|
4663
|
-
headers,
|
|
4664
|
-
data
|
|
4665
|
-
};
|
|
4666
|
-
}
|
|
4667
|
-
/**
|
|
4668
4557
|
* Save the workbook to a file
|
|
4669
4558
|
*/ async toFile(path) {
|
|
4670
4559
|
const buffer = await this.toBuffer();
|
|
@@ -4722,20 +4611,28 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4722
4611
|
_updateFiles() {
|
|
4723
4612
|
const relationshipInfo = this._buildRelationshipInfo();
|
|
4724
4613
|
// Update workbook.xml
|
|
4725
|
-
this._updateWorkbookXml(relationshipInfo.
|
|
4614
|
+
this._updateWorkbookXml(relationshipInfo.pivotCacheRelByTarget);
|
|
4726
4615
|
// Update relationships
|
|
4727
4616
|
this._updateRelationshipsXml(relationshipInfo.relNodes);
|
|
4728
4617
|
// Update content types
|
|
4729
4618
|
this._updateContentTypes();
|
|
4730
4619
|
// Update shared strings if modified
|
|
4731
|
-
if (this._sharedStrings
|
|
4732
|
-
|
|
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);
|
|
4733
4626
|
}
|
|
4734
4627
|
// Update styles if modified or if file doesn't exist yet
|
|
4735
|
-
if (this._styles
|
|
4736
|
-
|
|
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);
|
|
4737
4634
|
}
|
|
4738
|
-
// Update worksheets
|
|
4635
|
+
// Update worksheets
|
|
4739
4636
|
for (const [name, worksheet] of this._sheets){
|
|
4740
4637
|
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
4741
4638
|
const def = this._sheetDefs.find((s)=>s.name === name);
|
|
@@ -4748,15 +4645,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4748
4645
|
}
|
|
4749
4646
|
}
|
|
4750
4647
|
}
|
|
4751
|
-
// Update pivot tables
|
|
4752
|
-
if (this._pivotTables.length > 0) {
|
|
4753
|
-
this._updatePivotTableFiles();
|
|
4754
|
-
}
|
|
4755
4648
|
// Update tables (sets table rel IDs for tableParts)
|
|
4756
4649
|
this._updateTableFiles();
|
|
4650
|
+
// Update pivot tables (sets pivot rel IDs for pivotTableParts)
|
|
4651
|
+
this._updatePivotFiles();
|
|
4757
4652
|
// Update worksheets to align tableParts with relationship IDs
|
|
4758
4653
|
for (const [name, worksheet] of this._sheets){
|
|
4759
|
-
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
4654
|
+
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0 || this._pivotTables.length > 0) {
|
|
4760
4655
|
const def = this._sheetDefs.find((s)=>s.name === name);
|
|
4761
4656
|
if (def) {
|
|
4762
4657
|
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
@@ -4768,7 +4663,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4768
4663
|
}
|
|
4769
4664
|
}
|
|
4770
4665
|
}
|
|
4771
|
-
_updateWorkbookXml(
|
|
4666
|
+
_updateWorkbookXml(pivotCacheRelByTarget) {
|
|
4772
4667
|
const sheetNodes = this._sheetDefs.map((def)=>createElement('sheet', {
|
|
4773
4668
|
name: def.name,
|
|
4774
4669
|
sheetId: String(def.sheetId),
|
|
@@ -4778,19 +4673,20 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4778
4673
|
const children = [
|
|
4779
4674
|
sheetsNode
|
|
4780
4675
|
];
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
const
|
|
4784
|
-
const
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
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
|
+
}
|
|
4794
4690
|
}
|
|
4795
4691
|
const workbookNode = createElement('workbook', {
|
|
4796
4692
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
@@ -4816,6 +4712,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4816
4712
|
Type: rel.type,
|
|
4817
4713
|
Target: rel.target
|
|
4818
4714
|
}, []));
|
|
4715
|
+
const pivotCacheRelByTarget = new Map();
|
|
4819
4716
|
const reservedRelIds = new Set(relNodes.map((node)=>getAttr(node, 'Id') || '').filter(Boolean));
|
|
4820
4717
|
let nextRelId = Math.max(0, ...this._relationships.map((r)=>parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
|
|
4821
4718
|
const allocateRelId = ()=>{
|
|
@@ -4828,7 +4725,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4828
4725
|
return id;
|
|
4829
4726
|
};
|
|
4830
4727
|
// Add shared strings relationship if needed
|
|
4831
|
-
|
|
4728
|
+
const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
|
|
4729
|
+
if (shouldIncludeSharedStrings) {
|
|
4832
4730
|
const hasSharedStrings = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
|
|
4833
4731
|
if (!hasSharedStrings) {
|
|
4834
4732
|
relNodes.push(createElement('Relationship', {
|
|
@@ -4847,23 +4745,32 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4847
4745
|
Target: 'styles.xml'
|
|
4848
4746
|
}, []));
|
|
4849
4747
|
}
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
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
|
+
}
|
|
4860
4766
|
}
|
|
4861
4767
|
return {
|
|
4862
4768
|
relNodes,
|
|
4863
|
-
|
|
4769
|
+
pivotCacheRelByTarget
|
|
4864
4770
|
};
|
|
4865
4771
|
}
|
|
4866
4772
|
_updateContentTypes() {
|
|
4773
|
+
const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
|
|
4867
4774
|
const types = [
|
|
4868
4775
|
createElement('Default', {
|
|
4869
4776
|
Extension: 'rels',
|
|
@@ -4883,7 +4790,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4883
4790
|
}, [])
|
|
4884
4791
|
];
|
|
4885
4792
|
// Add shared strings if present
|
|
4886
|
-
if (
|
|
4793
|
+
if (shouldIncludeSharedStrings) {
|
|
4887
4794
|
types.push(createElement('Override', {
|
|
4888
4795
|
PartName: '/xl/sharedStrings.xml',
|
|
4889
4796
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'
|
|
@@ -4899,24 +4806,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4899
4806
|
}, []));
|
|
4900
4807
|
}
|
|
4901
4808
|
}
|
|
4902
|
-
// Add pivot cache definitions and records
|
|
4903
|
-
for (const cache of this._pivotCaches){
|
|
4904
|
-
types.push(createElement('Override', {
|
|
4905
|
-
PartName: `/xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
|
|
4906
|
-
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
|
|
4907
|
-
}, []));
|
|
4908
|
-
types.push(createElement('Override', {
|
|
4909
|
-
PartName: `/xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`,
|
|
4910
|
-
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
|
|
4911
|
-
}, []));
|
|
4912
|
-
}
|
|
4913
|
-
// Add pivot tables
|
|
4914
|
-
for (const pivotTable of this._pivotTables){
|
|
4915
|
-
types.push(createElement('Override', {
|
|
4916
|
-
PartName: `/xl/pivotTables/pivotTable${pivotTable.index}.xml`,
|
|
4917
|
-
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
|
|
4918
|
-
}, []));
|
|
4919
|
-
}
|
|
4920
4809
|
// Add tables
|
|
4921
4810
|
let tableIndex = 1;
|
|
4922
4811
|
for (const def of this._sheetDefs){
|
|
@@ -4931,6 +4820,21 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4931
4820
|
}
|
|
4932
4821
|
}
|
|
4933
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
|
+
}
|
|
4934
4838
|
const existingTypesXml = readZipText(this._files, '[Content_Types].xml');
|
|
4935
4839
|
const existingKeys = new Set(types.map((t)=>{
|
|
4936
4840
|
if ('Default' in t) {
|
|
@@ -4986,66 +4890,35 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
4986
4890
|
}
|
|
4987
4891
|
}
|
|
4988
4892
|
/**
|
|
4989
|
-
* Generate all
|
|
4990
|
-
*/
|
|
4991
|
-
//
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
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++;
|
|
4997
4910
|
}
|
|
4998
|
-
|
|
4999
|
-
}
|
|
5000
|
-
// Generate pivot cache files
|
|
5001
|
-
for(let i = 0; i < this._pivotCaches.length; i++){
|
|
5002
|
-
const cache = this._pivotCaches[i];
|
|
5003
|
-
// Pivot cache definition
|
|
5004
|
-
const definitionPath = `xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`;
|
|
5005
|
-
writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
|
|
5006
|
-
// Pivot cache records
|
|
5007
|
-
const recordsPath = `xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`;
|
|
5008
|
-
writeZipText(this._files, recordsPath, cache.toRecordsXml());
|
|
5009
|
-
// Pivot cache definition relationships (link to records)
|
|
5010
|
-
const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cache.fileIndex}.xml.rels`;
|
|
5011
|
-
const cacheRels = createElement('Relationships', {
|
|
5012
|
-
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
5013
|
-
}, [
|
|
5014
|
-
createElement('Relationship', {
|
|
5015
|
-
Id: 'rId1',
|
|
5016
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
5017
|
-
Target: `pivotCacheRecords${cache.fileIndex}.xml`
|
|
5018
|
-
}, [])
|
|
5019
|
-
]);
|
|
5020
|
-
writeZipText(this._files, cacheRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
5021
|
-
cacheRels
|
|
5022
|
-
])}`);
|
|
4911
|
+
sheetTables.set(def.name, tableInfos);
|
|
5023
4912
|
}
|
|
5024
|
-
// Generate
|
|
5025
|
-
for(
|
|
5026
|
-
const
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
writeZipText(this._files, ptPath, pivotTable.toXml());
|
|
5031
|
-
// Pivot table relationships (link to cache definition)
|
|
5032
|
-
const cacheIdx = pivotTable.cacheFileIndex;
|
|
5033
|
-
const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
|
|
5034
|
-
const ptRels = createElement('Relationships', {
|
|
5035
|
-
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
5036
|
-
}, [
|
|
5037
|
-
createElement('Relationship', {
|
|
5038
|
-
Id: 'rId1',
|
|
5039
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
5040
|
-
Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`
|
|
5041
|
-
}, [])
|
|
5042
|
-
]);
|
|
5043
|
-
writeZipText(this._files, ptRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
5044
|
-
ptRels
|
|
5045
|
-
])}`);
|
|
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
|
+
}
|
|
5046
4919
|
}
|
|
5047
|
-
// Generate worksheet relationships for
|
|
5048
|
-
for (const [sheetName,
|
|
4920
|
+
// Generate worksheet relationships for tables
|
|
4921
|
+
for (const [sheetName, tableInfos] of sheetTables){
|
|
5049
4922
|
const def = this._sheetDefs.find((s)=>s.name === sheetName);
|
|
5050
4923
|
if (!def) continue;
|
|
5051
4924
|
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
@@ -5053,11 +4926,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5053
4926
|
// Extract sheet file name from target path
|
|
5054
4927
|
const sheetFileName = rel.target.split('/').pop();
|
|
5055
4928
|
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
4929
|
+
// Check if there are already pivot table relationships for this sheet
|
|
5056
4930
|
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
5057
|
-
let relNodes = [];
|
|
5058
4931
|
let nextRelId = 1;
|
|
4932
|
+
const relNodes = [];
|
|
5059
4933
|
const reservedRelIds = new Set();
|
|
5060
4934
|
if (existingRelsXml) {
|
|
4935
|
+
// Parse existing rels and find max rId
|
|
5061
4936
|
const parsed = parseXml(existingRelsXml);
|
|
5062
4937
|
const relsElement = findElement(parsed, 'Relationships');
|
|
5063
4938
|
if (relsElement) {
|
|
@@ -5086,16 +4961,29 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5086
4961
|
reservedRelIds.add(id);
|
|
5087
4962
|
return id;
|
|
5088
4963
|
};
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
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);
|
|
5093
4977
|
relNodes.push(createElement('Relationship', {
|
|
5094
|
-
Id:
|
|
5095
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/
|
|
4978
|
+
Id: id,
|
|
4979
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
|
|
5096
4980
|
Target: target
|
|
5097
4981
|
}, []));
|
|
5098
4982
|
}
|
|
4983
|
+
const worksheet = this._sheets.get(sheetName);
|
|
4984
|
+
if (worksheet) {
|
|
4985
|
+
worksheet.setTableRelIds(tableRelIds);
|
|
4986
|
+
}
|
|
5099
4987
|
const sheetRels = createElement('Relationships', {
|
|
5100
4988
|
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
5101
4989
|
}, relNodes);
|
|
@@ -5105,54 +4993,45 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5105
4993
|
}
|
|
5106
4994
|
}
|
|
5107
4995
|
/**
|
|
5108
|
-
* Generate
|
|
5109
|
-
*/
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
const sheetTables = new Map();
|
|
5113
|
-
for (const def of this._sheetDefs){
|
|
5114
|
-
const worksheet = this._sheets.get(def.name);
|
|
5115
|
-
if (!worksheet) continue;
|
|
5116
|
-
const tables = worksheet.tables;
|
|
5117
|
-
if (tables.length === 0) continue;
|
|
5118
|
-
const tableInfos = [];
|
|
5119
|
-
for (const table of tables){
|
|
5120
|
-
tableInfos.push({
|
|
5121
|
-
table,
|
|
5122
|
-
globalIndex: globalTableIndex
|
|
5123
|
-
});
|
|
5124
|
-
globalTableIndex++;
|
|
5125
|
-
}
|
|
5126
|
-
sheetTables.set(def.name, tableInfos);
|
|
5127
|
-
}
|
|
5128
|
-
// Generate table files
|
|
5129
|
-
for (const [, tableInfos] of sheetTables){
|
|
5130
|
-
for (const { table, globalIndex } of tableInfos){
|
|
5131
|
-
const tablePath = `xl/tables/table${globalIndex}.xml`;
|
|
5132
|
-
writeZipText(this._files, tablePath, table.toXml());
|
|
5133
|
-
}
|
|
4996
|
+
* Generate pivot cache/table parts and worksheet relationships.
|
|
4997
|
+
*/ _updatePivotFiles() {
|
|
4998
|
+
if (this._pivotTables.length === 0) {
|
|
4999
|
+
return;
|
|
5134
5000
|
}
|
|
5135
|
-
|
|
5136
|
-
|
|
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){
|
|
5137
5019
|
const def = this._sheetDefs.find((s)=>s.name === sheetName);
|
|
5138
5020
|
if (!def) continue;
|
|
5139
5021
|
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
5140
5022
|
if (!rel) continue;
|
|
5141
|
-
// Extract sheet file name from target path
|
|
5142
5023
|
const sheetFileName = rel.target.split('/').pop();
|
|
5024
|
+
if (!sheetFileName) continue;
|
|
5143
5025
|
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
5144
|
-
// Check if there are already pivot table relationships for this sheet
|
|
5145
5026
|
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
5146
5027
|
let nextRelId = 1;
|
|
5147
5028
|
const relNodes = [];
|
|
5148
5029
|
const reservedRelIds = new Set();
|
|
5149
5030
|
if (existingRelsXml) {
|
|
5150
|
-
// Parse existing rels and find max rId
|
|
5151
5031
|
const parsed = parseXml(existingRelsXml);
|
|
5152
5032
|
const relsElement = findElement(parsed, 'Relationships');
|
|
5153
5033
|
if (relsElement) {
|
|
5154
|
-
const
|
|
5155
|
-
for (const relNode of existingRelNodes){
|
|
5034
|
+
for (const relNode of getChildren(relsElement, 'Relationships')){
|
|
5156
5035
|
if ('Relationship' in relNode) {
|
|
5157
5036
|
relNodes.push(relNode);
|
|
5158
5037
|
const id = getAttr(relNode, 'Id');
|
|
@@ -5176,28 +5055,26 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5176
5055
|
reservedRelIds.add(id);
|
|
5177
5056
|
return id;
|
|
5178
5057
|
};
|
|
5179
|
-
|
|
5180
|
-
const
|
|
5181
|
-
|
|
5182
|
-
const
|
|
5183
|
-
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);
|
|
5184
5062
|
if (existing) {
|
|
5185
|
-
const
|
|
5186
|
-
|
|
5187
|
-
tableRelIds.push(existingId ?? allocateRelId());
|
|
5063
|
+
const existingId = getAttr(existing, 'Id');
|
|
5064
|
+
pivotRelIds.push(existingId ?? allocateRelId());
|
|
5188
5065
|
continue;
|
|
5189
5066
|
}
|
|
5190
5067
|
const id = allocateRelId();
|
|
5191
|
-
|
|
5068
|
+
pivotRelIds.push(id);
|
|
5192
5069
|
relNodes.push(createElement('Relationship', {
|
|
5193
5070
|
Id: id,
|
|
5194
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/
|
|
5071
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
|
|
5195
5072
|
Target: target
|
|
5196
5073
|
}, []));
|
|
5197
5074
|
}
|
|
5198
5075
|
const worksheet = this._sheets.get(sheetName);
|
|
5199
5076
|
if (worksheet) {
|
|
5200
|
-
worksheet.
|
|
5077
|
+
worksheet.setPivotTableRelIds(pivotRelIds);
|
|
5201
5078
|
}
|
|
5202
5079
|
const sheetRels = createElement('Relationships', {
|
|
5203
5080
|
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
@@ -5210,7 +5087,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
|
5210
5087
|
}
|
|
5211
5088
|
|
|
5212
5089
|
exports.Cell = Cell;
|
|
5213
|
-
exports.PivotCache = PivotCache;
|
|
5214
5090
|
exports.PivotTable = PivotTable;
|
|
5215
5091
|
exports.Range = Range;
|
|
5216
5092
|
exports.SharedStrings = SharedStrings;
|
|
@@ -5220,5 +5096,7 @@ exports.Workbook = Workbook;
|
|
|
5220
5096
|
exports.Worksheet = Worksheet;
|
|
5221
5097
|
exports.parseAddress = parseAddress;
|
|
5222
5098
|
exports.parseRange = parseRange;
|
|
5099
|
+
exports.parseSheetAddress = parseSheetAddress;
|
|
5100
|
+
exports.parseSheetRange = parseSheetRange;
|
|
5223
5101
|
exports.toAddress = toAddress;
|
|
5224
5102
|
exports.toRange = toRange;
|