@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/src/worksheet.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { CellData, RangeAddress, SheetToJsonConfig, CellValue } from './types';
|
|
1
|
+
import type { CellData, RangeAddress, SheetToJsonConfig, CellValue, DateHandling } from './types';
|
|
2
2
|
import type { Workbook } from './workbook';
|
|
3
3
|
import { Cell, parseCellRef } from './cell';
|
|
4
4
|
import { Range } from './range';
|
|
5
|
-
import { parseRange, toAddress, parseAddress } from './utils/address';
|
|
5
|
+
import { parseRange, toAddress, parseAddress, letterToCol } from './utils/address';
|
|
6
6
|
import {
|
|
7
7
|
parseXml,
|
|
8
8
|
findElement,
|
|
@@ -25,6 +25,11 @@ export class Worksheet {
|
|
|
25
25
|
private _dirty = false;
|
|
26
26
|
private _mergedCells: Set<string> = new Set();
|
|
27
27
|
private _sheetData: XmlNode[] = [];
|
|
28
|
+
private _columnWidths: Map<number, number> = new Map();
|
|
29
|
+
private _rowHeights: Map<number, number> = new Map();
|
|
30
|
+
private _frozenPane: { row: number; col: number } | null = null;
|
|
31
|
+
private _dataBoundsCache: { minRow: number; maxRow: number; minCol: number; maxCol: number } | null = null;
|
|
32
|
+
private _boundsDirty = true;
|
|
28
33
|
|
|
29
34
|
constructor(workbook: Workbook, name: string) {
|
|
30
35
|
this._workbook = workbook;
|
|
@@ -63,6 +68,24 @@ export class Worksheet {
|
|
|
63
68
|
|
|
64
69
|
const worksheetChildren = getChildren(worksheet, 'worksheet');
|
|
65
70
|
|
|
71
|
+
// Parse sheet views (freeze panes)
|
|
72
|
+
const sheetViews = findElement(worksheetChildren, 'sheetViews');
|
|
73
|
+
if (sheetViews) {
|
|
74
|
+
const viewChildren = getChildren(sheetViews, 'sheetViews');
|
|
75
|
+
const sheetView = findElement(viewChildren, 'sheetView');
|
|
76
|
+
if (sheetView) {
|
|
77
|
+
const sheetViewChildren = getChildren(sheetView, 'sheetView');
|
|
78
|
+
const pane = findElement(sheetViewChildren, 'pane');
|
|
79
|
+
if (pane && getAttr(pane, 'state') === 'frozen') {
|
|
80
|
+
const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);
|
|
81
|
+
const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);
|
|
82
|
+
if (xSplit > 0 || ySplit > 0) {
|
|
83
|
+
this._frozenPane = { row: ySplit, col: xSplit };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
// Parse sheet data (cells)
|
|
67
90
|
const sheetData = findElement(worksheetChildren, 'sheetData');
|
|
68
91
|
if (sheetData) {
|
|
@@ -70,6 +93,24 @@ export class Worksheet {
|
|
|
70
93
|
this._parseSheetData(this._sheetData);
|
|
71
94
|
}
|
|
72
95
|
|
|
96
|
+
// Parse column widths
|
|
97
|
+
const cols = findElement(worksheetChildren, 'cols');
|
|
98
|
+
if (cols) {
|
|
99
|
+
const colChildren = getChildren(cols, 'cols');
|
|
100
|
+
for (const col of colChildren) {
|
|
101
|
+
if (!('col' in col)) continue;
|
|
102
|
+
const min = parseInt(getAttr(col, 'min') || '0', 10);
|
|
103
|
+
const max = parseInt(getAttr(col, 'max') || '0', 10);
|
|
104
|
+
const width = parseFloat(getAttr(col, 'width') || '0');
|
|
105
|
+
if (!Number.isFinite(width) || width <= 0) continue;
|
|
106
|
+
if (min > 0 && max > 0) {
|
|
107
|
+
for (let idx = min; idx <= max; idx++) {
|
|
108
|
+
this._columnWidths.set(idx - 1, width);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
73
114
|
// Parse merged cells
|
|
74
115
|
const mergeCells = findElement(worksheetChildren, 'mergeCells');
|
|
75
116
|
if (mergeCells) {
|
|
@@ -92,6 +133,12 @@ export class Worksheet {
|
|
|
92
133
|
for (const rowNode of rows) {
|
|
93
134
|
if (!('row' in rowNode)) continue;
|
|
94
135
|
|
|
136
|
+
const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;
|
|
137
|
+
const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');
|
|
138
|
+
if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {
|
|
139
|
+
this._rowHeights.set(rowIndex, rowHeight);
|
|
140
|
+
}
|
|
141
|
+
|
|
95
142
|
const rowChildren = getChildren(rowNode, 'row');
|
|
96
143
|
for (const cellNode of rowChildren) {
|
|
97
144
|
if (!('c' in cellNode)) continue;
|
|
@@ -105,6 +152,8 @@ export class Worksheet {
|
|
|
105
152
|
this._cells.set(ref, cell);
|
|
106
153
|
}
|
|
107
154
|
}
|
|
155
|
+
|
|
156
|
+
this._boundsDirty = true;
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
/**
|
|
@@ -205,11 +254,21 @@ export class Worksheet {
|
|
|
205
254
|
if (!cell) {
|
|
206
255
|
cell = new Cell(this, row, c);
|
|
207
256
|
this._cells.set(address, cell);
|
|
257
|
+
this._boundsDirty = true;
|
|
208
258
|
}
|
|
209
259
|
|
|
210
260
|
return cell;
|
|
211
261
|
}
|
|
212
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Get an existing cell without creating it.
|
|
265
|
+
*/
|
|
266
|
+
getCellIfExists(rowOrAddress: number | string, col?: number): Cell | undefined {
|
|
267
|
+
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
268
|
+
const address = toAddress(row, c);
|
|
269
|
+
return this._cells.get(address);
|
|
270
|
+
}
|
|
271
|
+
|
|
213
272
|
/**
|
|
214
273
|
* Get a range of cells
|
|
215
274
|
*/
|
|
@@ -280,6 +339,75 @@ export class Worksheet {
|
|
|
280
339
|
return this._cells;
|
|
281
340
|
}
|
|
282
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Set a column width (0-based index or column letter)
|
|
344
|
+
*/
|
|
345
|
+
setColumnWidth(col: number | string, width: number): void {
|
|
346
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
347
|
+
throw new Error('Column width must be a positive number');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
351
|
+
if (colIndex < 0) {
|
|
352
|
+
throw new Error(`Invalid column: ${col}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this._columnWidths.set(colIndex, width);
|
|
356
|
+
this._dirty = true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get a column width if set
|
|
361
|
+
*/
|
|
362
|
+
getColumnWidth(col: number | string): number | undefined {
|
|
363
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
364
|
+
return this._columnWidths.get(colIndex);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Set a row height (0-based index)
|
|
369
|
+
*/
|
|
370
|
+
setRowHeight(row: number, height: number): void {
|
|
371
|
+
if (!Number.isFinite(height) || height <= 0) {
|
|
372
|
+
throw new Error('Row height must be a positive number');
|
|
373
|
+
}
|
|
374
|
+
if (row < 0) {
|
|
375
|
+
throw new Error('Row index must be >= 0');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this._rowHeights.set(row, height);
|
|
379
|
+
this._dirty = true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get a row height if set
|
|
384
|
+
*/
|
|
385
|
+
getRowHeight(row: number): number | undefined {
|
|
386
|
+
return this._rowHeights.get(row);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Freeze panes at a given row/column split (counts from top-left)
|
|
391
|
+
*/
|
|
392
|
+
freezePane(rowSplit: number, colSplit: number): void {
|
|
393
|
+
if (rowSplit < 0 || colSplit < 0) {
|
|
394
|
+
throw new Error('Freeze pane splits must be >= 0');
|
|
395
|
+
}
|
|
396
|
+
if (rowSplit === 0 && colSplit === 0) {
|
|
397
|
+
this._frozenPane = null;
|
|
398
|
+
} else {
|
|
399
|
+
this._frozenPane = { row: rowSplit, col: colSplit };
|
|
400
|
+
}
|
|
401
|
+
this._dirty = true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get current frozen pane configuration
|
|
406
|
+
*/
|
|
407
|
+
getFrozenPane(): { row: number; col: number } | null {
|
|
408
|
+
return this._frozenPane ? { ...this._frozenPane } : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
283
411
|
/**
|
|
284
412
|
* Convert sheet data to an array of JSON objects.
|
|
285
413
|
*
|
|
@@ -299,7 +427,15 @@ export class Worksheet {
|
|
|
299
427
|
* ```
|
|
300
428
|
*/
|
|
301
429
|
toJson<T = Record<string, CellValue>>(config: SheetToJsonConfig = {}): T[] {
|
|
302
|
-
const {
|
|
430
|
+
const {
|
|
431
|
+
fields,
|
|
432
|
+
startRow = 0,
|
|
433
|
+
startCol = 0,
|
|
434
|
+
endRow,
|
|
435
|
+
endCol,
|
|
436
|
+
stopOnEmptyRow = true,
|
|
437
|
+
dateHandling = this._workbook.dateHandling,
|
|
438
|
+
} = config;
|
|
303
439
|
|
|
304
440
|
// Get the bounds of data in the sheet
|
|
305
441
|
const bounds = this._getDataBounds();
|
|
@@ -339,7 +475,11 @@ export class Worksheet {
|
|
|
339
475
|
for (let colOffset = 0; colOffset < fieldNames.length; colOffset++) {
|
|
340
476
|
const col = startCol + colOffset;
|
|
341
477
|
const cell = this._cells.get(toAddress(row, col));
|
|
342
|
-
|
|
478
|
+
let value = cell?.value ?? null;
|
|
479
|
+
|
|
480
|
+
if (value instanceof Date) {
|
|
481
|
+
value = this._serializeDate(value, dateHandling, cell);
|
|
482
|
+
}
|
|
343
483
|
|
|
344
484
|
if (value !== null) {
|
|
345
485
|
hasData = true;
|
|
@@ -362,11 +502,29 @@ export class Worksheet {
|
|
|
362
502
|
return result;
|
|
363
503
|
}
|
|
364
504
|
|
|
505
|
+
private _serializeDate(value: Date, dateHandling: DateHandling, cell?: Cell | null): CellValue | number | string {
|
|
506
|
+
if (dateHandling === 'excelSerial') {
|
|
507
|
+
return cell?._jsDateToExcel(value) ?? value;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (dateHandling === 'isoString') {
|
|
511
|
+
return value.toISOString();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
|
|
365
517
|
/**
|
|
366
518
|
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
367
519
|
*/
|
|
368
520
|
private _getDataBounds(): { minRow: number; maxRow: number; minCol: number; maxCol: number } | null {
|
|
521
|
+
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
522
|
+
return this._dataBoundsCache;
|
|
523
|
+
}
|
|
524
|
+
|
|
369
525
|
if (this._cells.size === 0) {
|
|
526
|
+
this._dataBoundsCache = null;
|
|
527
|
+
this._boundsDirty = false;
|
|
370
528
|
return null;
|
|
371
529
|
}
|
|
372
530
|
|
|
@@ -385,10 +543,14 @@ export class Worksheet {
|
|
|
385
543
|
}
|
|
386
544
|
|
|
387
545
|
if (minRow === Infinity) {
|
|
546
|
+
this._dataBoundsCache = null;
|
|
547
|
+
this._boundsDirty = false;
|
|
388
548
|
return null;
|
|
389
549
|
}
|
|
390
550
|
|
|
391
|
-
|
|
551
|
+
this._dataBoundsCache = { minRow, maxRow, minCol, maxCol };
|
|
552
|
+
this._boundsDirty = false;
|
|
553
|
+
return this._dataBoundsCache;
|
|
392
554
|
}
|
|
393
555
|
|
|
394
556
|
/**
|
|
@@ -405,6 +567,12 @@ export class Worksheet {
|
|
|
405
567
|
rowMap.get(row)!.push(cell);
|
|
406
568
|
}
|
|
407
569
|
|
|
570
|
+
for (const rowIdx of this._rowHeights.keys()) {
|
|
571
|
+
if (!rowMap.has(rowIdx)) {
|
|
572
|
+
rowMap.set(rowIdx, []);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
408
576
|
// Sort rows and cells
|
|
409
577
|
const sortedRows = Array.from(rowMap.entries()).sort((a, b) => a[0] - b[0]);
|
|
410
578
|
|
|
@@ -418,14 +586,74 @@ export class Worksheet {
|
|
|
418
586
|
cellNodes.push(cellNode);
|
|
419
587
|
}
|
|
420
588
|
|
|
421
|
-
const
|
|
589
|
+
const rowAttrs: Record<string, string> = { r: String(rowIdx + 1) };
|
|
590
|
+
const rowHeight = this._rowHeights.get(rowIdx);
|
|
591
|
+
if (rowHeight !== undefined) {
|
|
592
|
+
rowAttrs.ht = String(rowHeight);
|
|
593
|
+
rowAttrs.customHeight = '1';
|
|
594
|
+
}
|
|
595
|
+
const rowNode = createElement('row', rowAttrs, cellNodes);
|
|
422
596
|
rowNodes.push(rowNode);
|
|
423
597
|
}
|
|
424
598
|
|
|
425
599
|
const sheetDataNode = createElement('sheetData', {}, rowNodes);
|
|
426
600
|
|
|
427
601
|
// Build worksheet structure
|
|
428
|
-
const worksheetChildren: XmlNode[] = [
|
|
602
|
+
const worksheetChildren: XmlNode[] = [];
|
|
603
|
+
|
|
604
|
+
// Sheet views (freeze panes)
|
|
605
|
+
if (this._frozenPane) {
|
|
606
|
+
const paneAttrs: Record<string, string> = { state: 'frozen' };
|
|
607
|
+
const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
|
|
608
|
+
paneAttrs.topLeftCell = topLeftCell;
|
|
609
|
+
if (this._frozenPane.col > 0) {
|
|
610
|
+
paneAttrs.xSplit = String(this._frozenPane.col);
|
|
611
|
+
}
|
|
612
|
+
if (this._frozenPane.row > 0) {
|
|
613
|
+
paneAttrs.ySplit = String(this._frozenPane.row);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
let activePane = 'bottomRight';
|
|
617
|
+
if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
|
|
618
|
+
activePane = 'bottomLeft';
|
|
619
|
+
} else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
|
|
620
|
+
activePane = 'topRight';
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
paneAttrs.activePane = activePane;
|
|
624
|
+
const paneNode = createElement('pane', paneAttrs, []);
|
|
625
|
+
const selectionNode = createElement(
|
|
626
|
+
'selection',
|
|
627
|
+
{ pane: activePane, activeCell: topLeftCell, sqref: topLeftCell },
|
|
628
|
+
[],
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const sheetViewNode = createElement('sheetView', { workbookViewId: '0' }, [paneNode, selectionNode]);
|
|
632
|
+
worksheetChildren.push(createElement('sheetViews', {}, [sheetViewNode]));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Column widths
|
|
636
|
+
if (this._columnWidths.size > 0) {
|
|
637
|
+
const colNodes: XmlNode[] = [];
|
|
638
|
+
const entries = Array.from(this._columnWidths.entries()).sort((a, b) => a[0] - b[0]);
|
|
639
|
+
for (const [colIndex, width] of entries) {
|
|
640
|
+
colNodes.push(
|
|
641
|
+
createElement(
|
|
642
|
+
'col',
|
|
643
|
+
{
|
|
644
|
+
min: String(colIndex + 1),
|
|
645
|
+
max: String(colIndex + 1),
|
|
646
|
+
width: String(width),
|
|
647
|
+
customWidth: '1',
|
|
648
|
+
},
|
|
649
|
+
[],
|
|
650
|
+
),
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
worksheetChildren.push(createElement('cols', {}, colNodes));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
worksheetChildren.push(sheetDataNode);
|
|
429
657
|
|
|
430
658
|
// Add merged cells if any
|
|
431
659
|
if (this._mergedCells.size > 0) {
|