@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/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 { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
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
- const value = cell?.value ?? null;
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
- return { minRow, maxRow, minCol, maxCol };
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 rowNode = createElement('row', { r: String(rowIdx + 1) }, cellNodes);
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[] = [sheetDataNode];
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) {