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