@niicojs/excel 0.2.3 → 0.2.4
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/README.md +232 -8
- package/dist/index.cjs +414 -28
- package/dist/index.d.cts +116 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +116 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +411 -29
- package/package.json +1 -1
- package/src/index.ts +15 -1
- package/src/pivot-cache.ts +11 -1
- package/src/pivot-table.ts +122 -12
- package/src/range.ts +15 -2
- package/src/styles.ts +60 -1
- package/src/types.ts +23 -0
- package/src/utils/address.ts +4 -1
- package/src/utils/xml.ts +0 -7
- package/src/workbook.ts +18 -0
- package/src/worksheet.ts +235 -7
package/dist/index.js
CHANGED
|
@@ -38,8 +38,10 @@ import { unzip, strFromU8, zip, strToU8 } from 'fflate';
|
|
|
38
38
|
if (!match) {
|
|
39
39
|
throw new Error(`Invalid cell address: ${address}`);
|
|
40
40
|
}
|
|
41
|
+
const rowNumber = +match[2];
|
|
42
|
+
if (rowNumber <= 0) throw new Error(`Invalid cell address: ${address}`);
|
|
41
43
|
const col = letterToCol(match[1].toUpperCase());
|
|
42
|
-
const row =
|
|
44
|
+
const row = rowNumber - 1; // Convert to 0-based
|
|
43
45
|
return {
|
|
44
46
|
row,
|
|
45
47
|
col
|
|
@@ -101,6 +103,12 @@ import { unzip, strFromU8, zip, strToU8 } from 'fflate';
|
|
|
101
103
|
}
|
|
102
104
|
};
|
|
103
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Checks if an address is within a range
|
|
108
|
+
*/ const isInRange = (addr, range)=>{
|
|
109
|
+
const norm = normalizeRange(range);
|
|
110
|
+
return addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col;
|
|
111
|
+
};
|
|
104
112
|
|
|
105
113
|
// Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
|
|
106
114
|
const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
|
|
@@ -433,12 +441,23 @@ _computedKey = Symbol.iterator;
|
|
|
433
441
|
/**
|
|
434
442
|
* Get all values in the range as a 2D array
|
|
435
443
|
*/ get values() {
|
|
444
|
+
return this.getValues();
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get all values in the range as a 2D array with options
|
|
448
|
+
*/ getValues(options = {}) {
|
|
449
|
+
const { createMissing = true } = options;
|
|
436
450
|
const result = [];
|
|
437
451
|
for(let r = this._range.start.row; r <= this._range.end.row; r++){
|
|
438
452
|
const row = [];
|
|
439
453
|
for(let c = this._range.start.col; c <= this._range.end.col; c++){
|
|
440
|
-
|
|
441
|
-
|
|
454
|
+
if (createMissing) {
|
|
455
|
+
const cell = this._worksheet.cell(r, c);
|
|
456
|
+
row.push(cell.value);
|
|
457
|
+
} else {
|
|
458
|
+
const cell = this._worksheet.getCellIfExists(r, c);
|
|
459
|
+
row.push(cell?.value ?? null);
|
|
460
|
+
}
|
|
442
461
|
}
|
|
443
462
|
result.push(row);
|
|
444
463
|
}
|
|
@@ -611,6 +630,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
611
630
|
this._dirty = false;
|
|
612
631
|
this._mergedCells = new Set();
|
|
613
632
|
this._sheetData = [];
|
|
633
|
+
this._columnWidths = new Map();
|
|
634
|
+
this._rowHeights = new Map();
|
|
635
|
+
this._frozenPane = null;
|
|
636
|
+
this._dataBoundsCache = null;
|
|
637
|
+
this._boundsDirty = true;
|
|
614
638
|
this._workbook = workbook;
|
|
615
639
|
this._name = name;
|
|
616
640
|
}
|
|
@@ -637,12 +661,49 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
637
661
|
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
638
662
|
if (!worksheet) return;
|
|
639
663
|
const worksheetChildren = getChildren(worksheet, 'worksheet');
|
|
664
|
+
// Parse sheet views (freeze panes)
|
|
665
|
+
const sheetViews = findElement(worksheetChildren, 'sheetViews');
|
|
666
|
+
if (sheetViews) {
|
|
667
|
+
const viewChildren = getChildren(sheetViews, 'sheetViews');
|
|
668
|
+
const sheetView = findElement(viewChildren, 'sheetView');
|
|
669
|
+
if (sheetView) {
|
|
670
|
+
const sheetViewChildren = getChildren(sheetView, 'sheetView');
|
|
671
|
+
const pane = findElement(sheetViewChildren, 'pane');
|
|
672
|
+
if (pane && getAttr(pane, 'state') === 'frozen') {
|
|
673
|
+
const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);
|
|
674
|
+
const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);
|
|
675
|
+
if (xSplit > 0 || ySplit > 0) {
|
|
676
|
+
this._frozenPane = {
|
|
677
|
+
row: ySplit,
|
|
678
|
+
col: xSplit
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
640
684
|
// Parse sheet data (cells)
|
|
641
685
|
const sheetData = findElement(worksheetChildren, 'sheetData');
|
|
642
686
|
if (sheetData) {
|
|
643
687
|
this._sheetData = getChildren(sheetData, 'sheetData');
|
|
644
688
|
this._parseSheetData(this._sheetData);
|
|
645
689
|
}
|
|
690
|
+
// Parse column widths
|
|
691
|
+
const cols = findElement(worksheetChildren, 'cols');
|
|
692
|
+
if (cols) {
|
|
693
|
+
const colChildren = getChildren(cols, 'cols');
|
|
694
|
+
for (const col of colChildren){
|
|
695
|
+
if (!('col' in col)) continue;
|
|
696
|
+
const min = parseInt(getAttr(col, 'min') || '0', 10);
|
|
697
|
+
const max = parseInt(getAttr(col, 'max') || '0', 10);
|
|
698
|
+
const width = parseFloat(getAttr(col, 'width') || '0');
|
|
699
|
+
if (!Number.isFinite(width) || width <= 0) continue;
|
|
700
|
+
if (min > 0 && max > 0) {
|
|
701
|
+
for(let idx = min; idx <= max; idx++){
|
|
702
|
+
this._columnWidths.set(idx - 1, width);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
646
707
|
// Parse merged cells
|
|
647
708
|
const mergeCells = findElement(worksheetChildren, 'mergeCells');
|
|
648
709
|
if (mergeCells) {
|
|
@@ -662,6 +723,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
662
723
|
*/ _parseSheetData(rows) {
|
|
663
724
|
for (const rowNode of rows){
|
|
664
725
|
if (!('row' in rowNode)) continue;
|
|
726
|
+
const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;
|
|
727
|
+
const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');
|
|
728
|
+
if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {
|
|
729
|
+
this._rowHeights.set(rowIndex, rowHeight);
|
|
730
|
+
}
|
|
665
731
|
const rowChildren = getChildren(rowNode, 'row');
|
|
666
732
|
for (const cellNode of rowChildren){
|
|
667
733
|
if (!('c' in cellNode)) continue;
|
|
@@ -673,6 +739,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
673
739
|
this._cells.set(ref, cell);
|
|
674
740
|
}
|
|
675
741
|
}
|
|
742
|
+
this._boundsDirty = true;
|
|
676
743
|
}
|
|
677
744
|
/**
|
|
678
745
|
* Parse a cell XML node to CellData
|
|
@@ -759,9 +826,17 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
759
826
|
if (!cell) {
|
|
760
827
|
cell = new Cell(this, row, c);
|
|
761
828
|
this._cells.set(address, cell);
|
|
829
|
+
this._boundsDirty = true;
|
|
762
830
|
}
|
|
763
831
|
return cell;
|
|
764
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Get an existing cell without creating it.
|
|
835
|
+
*/ getCellIfExists(rowOrAddress, col) {
|
|
836
|
+
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
837
|
+
const address = toAddress(row, c);
|
|
838
|
+
return this._cells.get(address);
|
|
839
|
+
}
|
|
765
840
|
range(startRowOrRange, startCol, endRow, endCol) {
|
|
766
841
|
let rangeAddr;
|
|
767
842
|
if (typeof startRowOrRange === 'string') {
|
|
@@ -821,6 +896,65 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
821
896
|
return this._cells;
|
|
822
897
|
}
|
|
823
898
|
/**
|
|
899
|
+
* Set a column width (0-based index or column letter)
|
|
900
|
+
*/ setColumnWidth(col, width) {
|
|
901
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
902
|
+
throw new Error('Column width must be a positive number');
|
|
903
|
+
}
|
|
904
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
905
|
+
if (colIndex < 0) {
|
|
906
|
+
throw new Error(`Invalid column: ${col}`);
|
|
907
|
+
}
|
|
908
|
+
this._columnWidths.set(colIndex, width);
|
|
909
|
+
this._dirty = true;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get a column width if set
|
|
913
|
+
*/ getColumnWidth(col) {
|
|
914
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
915
|
+
return this._columnWidths.get(colIndex);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Set a row height (0-based index)
|
|
919
|
+
*/ setRowHeight(row, height) {
|
|
920
|
+
if (!Number.isFinite(height) || height <= 0) {
|
|
921
|
+
throw new Error('Row height must be a positive number');
|
|
922
|
+
}
|
|
923
|
+
if (row < 0) {
|
|
924
|
+
throw new Error('Row index must be >= 0');
|
|
925
|
+
}
|
|
926
|
+
this._rowHeights.set(row, height);
|
|
927
|
+
this._dirty = true;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Get a row height if set
|
|
931
|
+
*/ getRowHeight(row) {
|
|
932
|
+
return this._rowHeights.get(row);
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Freeze panes at a given row/column split (counts from top-left)
|
|
936
|
+
*/ freezePane(rowSplit, colSplit) {
|
|
937
|
+
if (rowSplit < 0 || colSplit < 0) {
|
|
938
|
+
throw new Error('Freeze pane splits must be >= 0');
|
|
939
|
+
}
|
|
940
|
+
if (rowSplit === 0 && colSplit === 0) {
|
|
941
|
+
this._frozenPane = null;
|
|
942
|
+
} else {
|
|
943
|
+
this._frozenPane = {
|
|
944
|
+
row: rowSplit,
|
|
945
|
+
col: colSplit
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
this._dirty = true;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Get current frozen pane configuration
|
|
952
|
+
*/ getFrozenPane() {
|
|
953
|
+
return this._frozenPane ? {
|
|
954
|
+
...this._frozenPane
|
|
955
|
+
} : null;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
824
958
|
* Convert sheet data to an array of JSON objects.
|
|
825
959
|
*
|
|
826
960
|
* @param config - Configuration options
|
|
@@ -838,7 +972,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
838
972
|
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
839
973
|
* ```
|
|
840
974
|
*/ toJson(config = {}) {
|
|
841
|
-
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
|
|
975
|
+
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
|
|
842
976
|
// Get the bounds of data in the sheet
|
|
843
977
|
const bounds = this._getDataBounds();
|
|
844
978
|
if (!bounds) {
|
|
@@ -871,7 +1005,10 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
871
1005
|
for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
|
|
872
1006
|
const col = startCol + colOffset;
|
|
873
1007
|
const cell = this._cells.get(toAddress(row, col));
|
|
874
|
-
|
|
1008
|
+
let value = cell?.value ?? null;
|
|
1009
|
+
if (value instanceof Date) {
|
|
1010
|
+
value = this._serializeDate(value, dateHandling, cell);
|
|
1011
|
+
}
|
|
875
1012
|
if (value !== null) {
|
|
876
1013
|
hasData = true;
|
|
877
1014
|
}
|
|
@@ -888,10 +1025,24 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
888
1025
|
}
|
|
889
1026
|
return result;
|
|
890
1027
|
}
|
|
1028
|
+
_serializeDate(value, dateHandling, cell) {
|
|
1029
|
+
if (dateHandling === 'excelSerial') {
|
|
1030
|
+
return cell?._jsDateToExcel(value) ?? value;
|
|
1031
|
+
}
|
|
1032
|
+
if (dateHandling === 'isoString') {
|
|
1033
|
+
return value.toISOString();
|
|
1034
|
+
}
|
|
1035
|
+
return value;
|
|
1036
|
+
}
|
|
891
1037
|
/**
|
|
892
1038
|
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
893
1039
|
*/ _getDataBounds() {
|
|
1040
|
+
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
1041
|
+
return this._dataBoundsCache;
|
|
1042
|
+
}
|
|
894
1043
|
if (this._cells.size === 0) {
|
|
1044
|
+
this._dataBoundsCache = null;
|
|
1045
|
+
this._boundsDirty = false;
|
|
895
1046
|
return null;
|
|
896
1047
|
}
|
|
897
1048
|
let minRow = Infinity;
|
|
@@ -907,14 +1058,18 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
907
1058
|
}
|
|
908
1059
|
}
|
|
909
1060
|
if (minRow === Infinity) {
|
|
1061
|
+
this._dataBoundsCache = null;
|
|
1062
|
+
this._boundsDirty = false;
|
|
910
1063
|
return null;
|
|
911
1064
|
}
|
|
912
|
-
|
|
1065
|
+
this._dataBoundsCache = {
|
|
913
1066
|
minRow,
|
|
914
1067
|
maxRow,
|
|
915
1068
|
minCol,
|
|
916
1069
|
maxCol
|
|
917
1070
|
};
|
|
1071
|
+
this._boundsDirty = false;
|
|
1072
|
+
return this._dataBoundsCache;
|
|
918
1073
|
}
|
|
919
1074
|
/**
|
|
920
1075
|
* Generate XML for this worksheet
|
|
@@ -928,6 +1083,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
928
1083
|
}
|
|
929
1084
|
rowMap.get(row).push(cell);
|
|
930
1085
|
}
|
|
1086
|
+
for (const rowIdx of this._rowHeights.keys()){
|
|
1087
|
+
if (!rowMap.has(rowIdx)) {
|
|
1088
|
+
rowMap.set(rowIdx, []);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
931
1091
|
// Sort rows and cells
|
|
932
1092
|
const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
|
|
933
1093
|
const rowNodes = [];
|
|
@@ -938,16 +1098,71 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
938
1098
|
const cellNode = this._buildCellNode(cell);
|
|
939
1099
|
cellNodes.push(cellNode);
|
|
940
1100
|
}
|
|
941
|
-
const
|
|
1101
|
+
const rowAttrs = {
|
|
942
1102
|
r: String(rowIdx + 1)
|
|
943
|
-
}
|
|
1103
|
+
};
|
|
1104
|
+
const rowHeight = this._rowHeights.get(rowIdx);
|
|
1105
|
+
if (rowHeight !== undefined) {
|
|
1106
|
+
rowAttrs.ht = String(rowHeight);
|
|
1107
|
+
rowAttrs.customHeight = '1';
|
|
1108
|
+
}
|
|
1109
|
+
const rowNode = createElement('row', rowAttrs, cellNodes);
|
|
944
1110
|
rowNodes.push(rowNode);
|
|
945
1111
|
}
|
|
946
1112
|
const sheetDataNode = createElement('sheetData', {}, rowNodes);
|
|
947
1113
|
// Build worksheet structure
|
|
948
|
-
const worksheetChildren = [
|
|
949
|
-
|
|
950
|
-
|
|
1114
|
+
const worksheetChildren = [];
|
|
1115
|
+
// Sheet views (freeze panes)
|
|
1116
|
+
if (this._frozenPane) {
|
|
1117
|
+
const paneAttrs = {
|
|
1118
|
+
state: 'frozen'
|
|
1119
|
+
};
|
|
1120
|
+
const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
|
|
1121
|
+
paneAttrs.topLeftCell = topLeftCell;
|
|
1122
|
+
if (this._frozenPane.col > 0) {
|
|
1123
|
+
paneAttrs.xSplit = String(this._frozenPane.col);
|
|
1124
|
+
}
|
|
1125
|
+
if (this._frozenPane.row > 0) {
|
|
1126
|
+
paneAttrs.ySplit = String(this._frozenPane.row);
|
|
1127
|
+
}
|
|
1128
|
+
let activePane = 'bottomRight';
|
|
1129
|
+
if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
|
|
1130
|
+
activePane = 'bottomLeft';
|
|
1131
|
+
} else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
|
|
1132
|
+
activePane = 'topRight';
|
|
1133
|
+
}
|
|
1134
|
+
paneAttrs.activePane = activePane;
|
|
1135
|
+
const paneNode = createElement('pane', paneAttrs, []);
|
|
1136
|
+
const selectionNode = createElement('selection', {
|
|
1137
|
+
pane: activePane,
|
|
1138
|
+
activeCell: topLeftCell,
|
|
1139
|
+
sqref: topLeftCell
|
|
1140
|
+
}, []);
|
|
1141
|
+
const sheetViewNode = createElement('sheetView', {
|
|
1142
|
+
workbookViewId: '0'
|
|
1143
|
+
}, [
|
|
1144
|
+
paneNode,
|
|
1145
|
+
selectionNode
|
|
1146
|
+
]);
|
|
1147
|
+
worksheetChildren.push(createElement('sheetViews', {}, [
|
|
1148
|
+
sheetViewNode
|
|
1149
|
+
]));
|
|
1150
|
+
}
|
|
1151
|
+
// Column widths
|
|
1152
|
+
if (this._columnWidths.size > 0) {
|
|
1153
|
+
const colNodes = [];
|
|
1154
|
+
const entries = Array.from(this._columnWidths.entries()).sort((a, b)=>a[0] - b[0]);
|
|
1155
|
+
for (const [colIndex, width] of entries){
|
|
1156
|
+
colNodes.push(createElement('col', {
|
|
1157
|
+
min: String(colIndex + 1),
|
|
1158
|
+
max: String(colIndex + 1),
|
|
1159
|
+
width: String(width),
|
|
1160
|
+
customWidth: '1'
|
|
1161
|
+
}, []));
|
|
1162
|
+
}
|
|
1163
|
+
worksheetChildren.push(createElement('cols', {}, colNodes));
|
|
1164
|
+
}
|
|
1165
|
+
worksheetChildren.push(sheetDataNode);
|
|
951
1166
|
// Add merged cells if any
|
|
952
1167
|
if (this._mergedCells.size > 0) {
|
|
953
1168
|
const mergeCellNodes = [];
|
|
@@ -1134,6 +1349,40 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1134
1349
|
* Manages the styles (xl/styles.xml)
|
|
1135
1350
|
*/ class Styles {
|
|
1136
1351
|
/**
|
|
1352
|
+
* Generate a deterministic cache key for a style object.
|
|
1353
|
+
* More efficient than JSON.stringify as it avoids the overhead of
|
|
1354
|
+
* full JSON serialization and produces a consistent key regardless
|
|
1355
|
+
* of property order.
|
|
1356
|
+
*/ _getStyleKey(style) {
|
|
1357
|
+
// Use a delimiter that won't appear in values
|
|
1358
|
+
const SEP = '\x00';
|
|
1359
|
+
// Build key from all style properties in a fixed order
|
|
1360
|
+
const parts = [
|
|
1361
|
+
style.bold ? '1' : '0',
|
|
1362
|
+
style.italic ? '1' : '0',
|
|
1363
|
+
style.underline === true ? '1' : style.underline === 'single' ? 's' : style.underline === 'double' ? 'd' : '0',
|
|
1364
|
+
style.strike ? '1' : '0',
|
|
1365
|
+
style.fontSize?.toString() ?? '',
|
|
1366
|
+
style.fontName ?? '',
|
|
1367
|
+
style.fontColor ?? '',
|
|
1368
|
+
style.fill ?? '',
|
|
1369
|
+
style.numberFormat ?? ''
|
|
1370
|
+
];
|
|
1371
|
+
// Border properties
|
|
1372
|
+
if (style.border) {
|
|
1373
|
+
parts.push(style.border.top ?? '', style.border.bottom ?? '', style.border.left ?? '', style.border.right ?? '');
|
|
1374
|
+
} else {
|
|
1375
|
+
parts.push('', '', '', '');
|
|
1376
|
+
}
|
|
1377
|
+
// Alignment properties
|
|
1378
|
+
if (style.alignment) {
|
|
1379
|
+
parts.push(style.alignment.horizontal ?? '', style.alignment.vertical ?? '', style.alignment.wrapText ? '1' : '0', style.alignment.textRotation?.toString() ?? '');
|
|
1380
|
+
} else {
|
|
1381
|
+
parts.push('', '', '0', '');
|
|
1382
|
+
}
|
|
1383
|
+
return parts.join(SEP);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1137
1386
|
* Parse styles from XML content
|
|
1138
1387
|
*/ static parse(xml) {
|
|
1139
1388
|
const styles = new Styles();
|
|
@@ -1300,6 +1549,10 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1300
1549
|
/**
|
|
1301
1550
|
* Get a style by index
|
|
1302
1551
|
*/ getStyle(index) {
|
|
1552
|
+
const cached = this._styleObjectCache.get(index);
|
|
1553
|
+
if (cached) return {
|
|
1554
|
+
...cached
|
|
1555
|
+
};
|
|
1303
1556
|
const xf = this._cellXfs[index];
|
|
1304
1557
|
if (!xf) return {};
|
|
1305
1558
|
const font = this._fonts[xf.fontId];
|
|
@@ -1340,13 +1593,16 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1340
1593
|
textRotation: xf.alignment.textRotation
|
|
1341
1594
|
};
|
|
1342
1595
|
}
|
|
1596
|
+
this._styleObjectCache.set(index, {
|
|
1597
|
+
...style
|
|
1598
|
+
});
|
|
1343
1599
|
return style;
|
|
1344
1600
|
}
|
|
1345
1601
|
/**
|
|
1346
1602
|
* Create a style and return its index
|
|
1347
1603
|
* Uses caching to deduplicate identical styles
|
|
1348
1604
|
*/ createStyle(style) {
|
|
1349
|
-
const key =
|
|
1605
|
+
const key = this._getStyleKey(style);
|
|
1350
1606
|
const cached = this._styleCache.get(key);
|
|
1351
1607
|
if (cached !== undefined) {
|
|
1352
1608
|
return cached;
|
|
@@ -1378,8 +1634,20 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1378
1634
|
const index = this._cellXfs.length;
|
|
1379
1635
|
this._cellXfs.push(xf);
|
|
1380
1636
|
this._styleCache.set(key, index);
|
|
1637
|
+
this._styleObjectCache.set(index, {
|
|
1638
|
+
...style
|
|
1639
|
+
});
|
|
1381
1640
|
return index;
|
|
1382
1641
|
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Clone an existing style by index, optionally overriding fields.
|
|
1644
|
+
*/ cloneStyle(index, overrides = {}) {
|
|
1645
|
+
const baseStyle = this.getStyle(index);
|
|
1646
|
+
return this.createStyle({
|
|
1647
|
+
...baseStyle,
|
|
1648
|
+
...overrides
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1383
1651
|
_findOrCreateFont(style) {
|
|
1384
1652
|
const font = {
|
|
1385
1653
|
bold: style.bold || false,
|
|
@@ -1615,6 +1883,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1615
1883
|
this._dirty = false;
|
|
1616
1884
|
// Cache for style deduplication
|
|
1617
1885
|
this._styleCache = new Map();
|
|
1886
|
+
this._styleObjectCache = new Map();
|
|
1618
1887
|
}
|
|
1619
1888
|
}
|
|
1620
1889
|
|
|
@@ -1626,6 +1895,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1626
1895
|
this._columnFields = [];
|
|
1627
1896
|
this._valueFields = [];
|
|
1628
1897
|
this._filterFields = [];
|
|
1898
|
+
this._fieldAssignments = new Map();
|
|
1629
1899
|
this._name = name;
|
|
1630
1900
|
this._cache = cache;
|
|
1631
1901
|
this._targetSheet = targetSheet;
|
|
@@ -1667,11 +1937,13 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1667
1937
|
if (fieldIndex < 0) {
|
|
1668
1938
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1669
1939
|
}
|
|
1670
|
-
|
|
1940
|
+
const assignment = {
|
|
1671
1941
|
fieldName,
|
|
1672
1942
|
fieldIndex,
|
|
1673
1943
|
axis: 'row'
|
|
1674
|
-
}
|
|
1944
|
+
};
|
|
1945
|
+
this._rowFields.push(assignment);
|
|
1946
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1675
1947
|
return this;
|
|
1676
1948
|
}
|
|
1677
1949
|
/**
|
|
@@ -1682,11 +1954,13 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1682
1954
|
if (fieldIndex < 0) {
|
|
1683
1955
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1684
1956
|
}
|
|
1685
|
-
|
|
1957
|
+
const assignment = {
|
|
1686
1958
|
fieldName,
|
|
1687
1959
|
fieldIndex,
|
|
1688
1960
|
axis: 'column'
|
|
1689
|
-
}
|
|
1961
|
+
};
|
|
1962
|
+
this._columnFields.push(assignment);
|
|
1963
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1690
1964
|
return this;
|
|
1691
1965
|
}
|
|
1692
1966
|
/**
|
|
@@ -1700,13 +1974,15 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1700
1974
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1701
1975
|
}
|
|
1702
1976
|
const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
|
|
1703
|
-
|
|
1977
|
+
const assignment = {
|
|
1704
1978
|
fieldName,
|
|
1705
1979
|
fieldIndex,
|
|
1706
1980
|
axis: 'value',
|
|
1707
1981
|
aggregation,
|
|
1708
1982
|
displayName: displayName || defaultName
|
|
1709
|
-
}
|
|
1983
|
+
};
|
|
1984
|
+
this._valueFields.push(assignment);
|
|
1985
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1710
1986
|
return this;
|
|
1711
1987
|
}
|
|
1712
1988
|
/**
|
|
@@ -1717,11 +1993,44 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1717
1993
|
if (fieldIndex < 0) {
|
|
1718
1994
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1719
1995
|
}
|
|
1720
|
-
|
|
1996
|
+
const assignment = {
|
|
1721
1997
|
fieldName,
|
|
1722
1998
|
fieldIndex,
|
|
1723
1999
|
axis: 'filter'
|
|
1724
|
-
}
|
|
2000
|
+
};
|
|
2001
|
+
this._filterFields.push(assignment);
|
|
2002
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
2003
|
+
return this;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Set a sort order for a row/column field
|
|
2007
|
+
*/ sortField(fieldName, order) {
|
|
2008
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
2009
|
+
if (fieldIndex < 0) {
|
|
2010
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
2011
|
+
}
|
|
2012
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
2013
|
+
if (!assignment || assignment.axis !== 'row' && assignment.axis !== 'column') {
|
|
2014
|
+
throw new Error(`Field is not assigned to row or column axis: ${fieldName}`);
|
|
2015
|
+
}
|
|
2016
|
+
assignment.sortOrder = order;
|
|
2017
|
+
return this;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Filter items for a field (include or exclude list)
|
|
2021
|
+
*/ filterField(fieldName, filter) {
|
|
2022
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
2023
|
+
if (fieldIndex < 0) {
|
|
2024
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
2025
|
+
}
|
|
2026
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
2027
|
+
if (!assignment) {
|
|
2028
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
2029
|
+
}
|
|
2030
|
+
if (filter.include && filter.exclude) {
|
|
2031
|
+
throw new Error('Pivot field filter cannot use both include and exclude');
|
|
2032
|
+
}
|
|
2033
|
+
assignment.filter = filter;
|
|
1725
2034
|
return this;
|
|
1726
2035
|
}
|
|
1727
2036
|
/**
|
|
@@ -1886,17 +2195,28 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1886
2195
|
const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
1887
2196
|
const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
1888
2197
|
const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
2198
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
1889
2199
|
if (rowField) {
|
|
1890
2200
|
attrs.axis = 'axisRow';
|
|
1891
2201
|
attrs.showAll = '0';
|
|
2202
|
+
if (assignment?.sortOrder) {
|
|
2203
|
+
attrs.sortType = 'ascending';
|
|
2204
|
+
attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
2205
|
+
}
|
|
1892
2206
|
// Add items for shared values
|
|
1893
2207
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1894
2208
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1895
2209
|
const itemNodes = [];
|
|
2210
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1896
2211
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1897
|
-
|
|
2212
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2213
|
+
const itemAttrs = {
|
|
1898
2214
|
x: String(i)
|
|
1899
|
-
}
|
|
2215
|
+
};
|
|
2216
|
+
if (!shouldInclude) {
|
|
2217
|
+
itemAttrs.h = '1';
|
|
2218
|
+
}
|
|
2219
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1900
2220
|
}
|
|
1901
2221
|
// Add default subtotal item
|
|
1902
2222
|
itemNodes.push(createElement('item', {
|
|
@@ -1909,13 +2229,23 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1909
2229
|
} else if (colField) {
|
|
1910
2230
|
attrs.axis = 'axisCol';
|
|
1911
2231
|
attrs.showAll = '0';
|
|
2232
|
+
if (assignment?.sortOrder) {
|
|
2233
|
+
attrs.sortType = 'ascending';
|
|
2234
|
+
attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
2235
|
+
}
|
|
1912
2236
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1913
2237
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1914
2238
|
const itemNodes = [];
|
|
2239
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1915
2240
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1916
|
-
|
|
2241
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2242
|
+
const itemAttrs = {
|
|
1917
2243
|
x: String(i)
|
|
1918
|
-
}
|
|
2244
|
+
};
|
|
2245
|
+
if (!shouldInclude) {
|
|
2246
|
+
itemAttrs.h = '1';
|
|
2247
|
+
}
|
|
2248
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1919
2249
|
}
|
|
1920
2250
|
itemNodes.push(createElement('item', {
|
|
1921
2251
|
t: 'default'
|
|
@@ -1930,10 +2260,16 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1930
2260
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1931
2261
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1932
2262
|
const itemNodes = [];
|
|
2263
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1933
2264
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1934
|
-
|
|
2265
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2266
|
+
const itemAttrs = {
|
|
1935
2267
|
x: String(i)
|
|
1936
|
-
}
|
|
2268
|
+
};
|
|
2269
|
+
if (!shouldInclude) {
|
|
2270
|
+
itemAttrs.h = '1';
|
|
2271
|
+
}
|
|
2272
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1937
2273
|
}
|
|
1938
2274
|
itemNodes.push(createElement('item', {
|
|
1939
2275
|
t: 'default'
|
|
@@ -1950,6 +2286,31 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1950
2286
|
}
|
|
1951
2287
|
return createElement('pivotField', attrs, children);
|
|
1952
2288
|
}
|
|
2289
|
+
_resolveItemFilter(items, filter) {
|
|
2290
|
+
const allowed = new Set();
|
|
2291
|
+
if (!filter || !filter.include && !filter.exclude) {
|
|
2292
|
+
for(let i = 0; i < items.length; i++){
|
|
2293
|
+
allowed.add(i);
|
|
2294
|
+
}
|
|
2295
|
+
return allowed;
|
|
2296
|
+
}
|
|
2297
|
+
if (filter.include) {
|
|
2298
|
+
for(let i = 0; i < items.length; i++){
|
|
2299
|
+
if (filter.include.includes(items[i])) {
|
|
2300
|
+
allowed.add(i);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return allowed;
|
|
2304
|
+
}
|
|
2305
|
+
if (filter.exclude) {
|
|
2306
|
+
for(let i = 0; i < items.length; i++){
|
|
2307
|
+
if (!filter.exclude.includes(items[i])) {
|
|
2308
|
+
allowed.add(i);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return allowed;
|
|
2313
|
+
}
|
|
1953
2314
|
/**
|
|
1954
2315
|
* Build row items based on unique values in row fields
|
|
1955
2316
|
*/ _buildRowItems() {
|
|
@@ -2100,6 +2461,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2100
2461
|
this._records = [];
|
|
2101
2462
|
this._recordCount = 0;
|
|
2102
2463
|
this._refreshOnLoad = true; // Default to true
|
|
2464
|
+
this._dateGrouping = false;
|
|
2103
2465
|
this._cacheId = cacheId;
|
|
2104
2466
|
this._sourceSheet = sourceSheet;
|
|
2105
2467
|
this._sourceRange = sourceRange;
|
|
@@ -2188,6 +2550,8 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2188
2550
|
}
|
|
2189
2551
|
}
|
|
2190
2552
|
}
|
|
2553
|
+
// Enable date grouping flag if any date field exists
|
|
2554
|
+
this._dateGrouping = this._fields.some((field)=>field.isDate);
|
|
2191
2555
|
// Store records
|
|
2192
2556
|
this._records = data;
|
|
2193
2557
|
}
|
|
@@ -2216,6 +2580,8 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2216
2580
|
v: item
|
|
2217
2581
|
}, []));
|
|
2218
2582
|
}
|
|
2583
|
+
} else if (field.isDate) {
|
|
2584
|
+
sharedItemsAttrs.containsDate = '1';
|
|
2219
2585
|
} else if (field.isNumeric) {
|
|
2220
2586
|
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
2221
2587
|
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
@@ -2246,9 +2612,13 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2246
2612
|
ref: this._sourceRange,
|
|
2247
2613
|
sheet: this._sourceSheet
|
|
2248
2614
|
}, []);
|
|
2249
|
-
const
|
|
2615
|
+
const cacheSourceAttrs = {
|
|
2250
2616
|
type: 'worksheet'
|
|
2251
|
-
}
|
|
2617
|
+
};
|
|
2618
|
+
if (this._dateGrouping) {
|
|
2619
|
+
cacheSourceAttrs.grouping = '1';
|
|
2620
|
+
}
|
|
2621
|
+
const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
|
|
2252
2622
|
worksheetSourceNode
|
|
2253
2623
|
]);
|
|
2254
2624
|
// Build attributes - refreshOnLoad should come early per OOXML schema
|
|
@@ -2394,6 +2764,8 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2394
2764
|
this._pivotTables = [];
|
|
2395
2765
|
this._pivotCaches = [];
|
|
2396
2766
|
this._nextCacheId = 0;
|
|
2767
|
+
// Date serialization handling
|
|
2768
|
+
this._dateHandling = 'jsDate';
|
|
2397
2769
|
this._sharedStrings = new SharedStrings();
|
|
2398
2770
|
this._styles = Styles.createDefault();
|
|
2399
2771
|
}
|
|
@@ -2458,6 +2830,16 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2458
2830
|
return this._styles;
|
|
2459
2831
|
}
|
|
2460
2832
|
/**
|
|
2833
|
+
* Get the workbook date handling strategy.
|
|
2834
|
+
*/ get dateHandling() {
|
|
2835
|
+
return this._dateHandling;
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Set the workbook date handling strategy.
|
|
2839
|
+
*/ set dateHandling(value) {
|
|
2840
|
+
this._dateHandling = value;
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2461
2843
|
* Get a worksheet by name or index
|
|
2462
2844
|
*/ sheet(nameOrIndex) {
|
|
2463
2845
|
let def;
|
|
@@ -3137,4 +3519,4 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3137
3519
|
}
|
|
3138
3520
|
}
|
|
3139
3521
|
|
|
3140
|
-
export { Cell, PivotCache, PivotTable, Range, SharedStrings, Styles, Workbook, Worksheet, parseAddress, parseRange, toAddress, toRange };
|
|
3522
|
+
export { Cell, PivotCache, PivotTable, Range, SharedStrings, Styles, Workbook, Worksheet, colToLetter, isInRange, letterToCol, normalizeRange, parseAddress, parseRange, toAddress, toRange };
|