sheetra 1.0.2 → 1.0.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 +81 -9
- package/dist/index.esm.js +390 -21
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +390 -21
- package/dist/index.js.map +1 -1
- package/dist/types/builders/export-builder.d.ts +33 -1
- package/dist/types/builders/sheet-builder.d.ts +12 -0
- package/dist/types/index.d.ts +63 -1
- package/dist/types/writers/excel-writer.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,15 +13,87 @@ Sheetra is a powerful, zero-dependency library for exporting data to Excel (XLSX
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
|
|
17
|
+
## Example Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { ExportBuilder, StyleBuilder } from 'sheetra';
|
|
21
|
+
|
|
22
|
+
// --- Comprehensive Example: All Features ---
|
|
23
|
+
const users = [
|
|
24
|
+
{ name: 'John Doe', age: 30, email: 'john@example.com', department: 'Engineering' },
|
|
25
|
+
{ name: 'Jane Smith', age: 25, email: 'jane@example.com', department: 'Marketing' },
|
|
26
|
+
];
|
|
27
|
+
const parts = [
|
|
28
|
+
{ part_number: 'P001', part_name: 'Widget', current_stock: 100, price: 29.99 },
|
|
29
|
+
{ part_number: 'P002', part_name: 'Gadget', current_stock: 50, price: 49.99 },
|
|
30
|
+
];
|
|
31
|
+
const salesData = [
|
|
32
|
+
{ product: 'Widget', jan: 1500, feb: 1800, mar: 2100 },
|
|
33
|
+
{ product: 'Gadget', jan: 900, feb: 1100, mar: 1300 },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Styled header
|
|
37
|
+
const headerStyle = StyleBuilder.create()
|
|
38
|
+
.bold()
|
|
39
|
+
.backgroundColor('#4F81BD')
|
|
40
|
+
.color('#FFFFFF')
|
|
41
|
+
.align('center')
|
|
42
|
+
.build();
|
|
43
|
+
|
|
44
|
+
ExportBuilder.create('All Features Demo')
|
|
45
|
+
// Set column widths
|
|
46
|
+
.setColumnWidths([150, 100, 200, 120])
|
|
47
|
+
// Add styled header row
|
|
48
|
+
.addHeaderRow(['Name', 'Age', 'Email', 'Department'], headerStyle)
|
|
49
|
+
// Add data rows
|
|
50
|
+
.addDataRows(users.map(u => [u.name, u.age, u.email, u.department]))
|
|
51
|
+
// Add section
|
|
52
|
+
.addSection({ name: 'Parts', title: 'Parts Inventory' })
|
|
53
|
+
.addHeaderRow(['Part Number', 'Part Name', 'Stock', 'Price'])
|
|
54
|
+
.addDataRows(parts.map(p => [p.part_number, p.part_name, p.current_stock, p.price]))
|
|
55
|
+
// Merge cells for a title
|
|
56
|
+
.addDataRows([['Monthly Sales Report', '', '', '']])
|
|
57
|
+
.mergeCells(7, 0, 7, 3)
|
|
58
|
+
.setAlignment(7, 0, 'center')
|
|
59
|
+
// Alignment demo
|
|
60
|
+
.addHeaderRow(['Left', 'Center', 'Right'])
|
|
61
|
+
.setAlignment(8, 0, 'left')
|
|
62
|
+
.setAlignment(8, 1, 'center')
|
|
63
|
+
.setAlignment(8, 2, 'right')
|
|
64
|
+
.addDataRows([
|
|
65
|
+
['Left Text', 'Center Text', 'Right Text'],
|
|
66
|
+
['Value 1', 'Value 2', 'Value 3'],
|
|
67
|
+
])
|
|
68
|
+
.setRangeAlignment(9, 0, 10, 0, 'left')
|
|
69
|
+
.setRangeAlignment(9, 1, 10, 1, 'center')
|
|
70
|
+
.setRangeAlignment(9, 2, 10, 2, 'right')
|
|
71
|
+
// Auto-size columns
|
|
72
|
+
.autoSizeColumns()
|
|
73
|
+
// Download as XLSX
|
|
74
|
+
.download({ filename: 'all-features-demo.xlsx', format: 'xlsx' });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This example demonstrates:
|
|
78
|
+
- Column widths and auto-sizing
|
|
79
|
+
- Merged cells for titles
|
|
80
|
+
- Alignment (left, center, right, range)
|
|
81
|
+
- Sections and headers
|
|
82
|
+
- Styled headers with StyleBuilder
|
|
83
|
+
- Data rows and multiple tables
|
|
84
|
+
- XLSX export
|
|
85
|
+
|
|
86
|
+
See the [sheetra-demo](./sheetra-demo/src/App.tsx) for a full-featured React demo with all features in action.
|
|
87
|
+
.addDataRows(users.map(u => [u.name, u.age, u.email, u.department]))
|
|
88
|
+
.download({ filename: 'styled-report.xlsx', format: 'xlsx' });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Troubleshooting
|
|
92
|
+
|
|
93
|
+
**Upgrading from previous versions?**
|
|
94
|
+
|
|
95
|
+
If you previously encountered errors with styled exports (e.g., `SyntaxError: Expected ':' after property name in JSON`), upgrade to the latest version. Style and alignment handling is now robust and all edge cases are supported.
|
|
96
|
+
|
|
25
97
|
|
|
26
98
|
---
|
|
27
99
|
|
package/dist/index.esm.js
CHANGED
|
@@ -524,22 +524,129 @@ class ExcelWriter {
|
|
|
524
524
|
const zip = new ZipWriter();
|
|
525
525
|
const sheets = workbook['sheets'];
|
|
526
526
|
const sheetNames = sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`);
|
|
527
|
+
// Collect all styles from sheets
|
|
528
|
+
const styleRegistry = this.collectStyles(sheets);
|
|
527
529
|
// Add required XLSX files
|
|
528
530
|
zip.addFile('[Content_Types].xml', this.generateContentTypes(sheetNames));
|
|
529
531
|
zip.addFile('_rels/.rels', this.generateRels());
|
|
530
532
|
zip.addFile('xl/workbook.xml', this.generateWorkbook(sheetNames));
|
|
531
533
|
zip.addFile('xl/_rels/workbook.xml.rels', this.generateWorkbookRels(sheetNames));
|
|
532
|
-
zip.addFile('xl/styles.xml', this.generateStyles());
|
|
534
|
+
zip.addFile('xl/styles.xml', this.generateStyles(styleRegistry));
|
|
533
535
|
zip.addFile('xl/sharedStrings.xml', this.generateSharedStrings(sheets));
|
|
534
536
|
// Add each worksheet
|
|
535
537
|
sheets.forEach((sheet, index) => {
|
|
536
|
-
zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet));
|
|
538
|
+
zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet, styleRegistry));
|
|
537
539
|
});
|
|
538
540
|
const buffer = zip.generate();
|
|
539
541
|
return new Blob([buffer.buffer], {
|
|
540
542
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
541
543
|
});
|
|
542
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Collect all unique styles from sheets
|
|
547
|
+
*/
|
|
548
|
+
static collectStyles(sheets) {
|
|
549
|
+
const registry = {
|
|
550
|
+
fonts: new Map(),
|
|
551
|
+
fills: new Map(),
|
|
552
|
+
borders: new Map(),
|
|
553
|
+
cellXfs: new Map()
|
|
554
|
+
};
|
|
555
|
+
// Add default font (Calibri 11)
|
|
556
|
+
const defaultFont = this.serializeFont({});
|
|
557
|
+
registry.fonts.set(defaultFont, 0);
|
|
558
|
+
// Add required fills (none and gray125)
|
|
559
|
+
registry.fills.set('none', 0);
|
|
560
|
+
registry.fills.set('gray125', 1);
|
|
561
|
+
// Add default border
|
|
562
|
+
registry.borders.set('none', 0);
|
|
563
|
+
// Add default cellXf (no style)
|
|
564
|
+
registry.cellXfs.set('default', 0);
|
|
565
|
+
// Collect styles from all cells
|
|
566
|
+
sheets.forEach((sheet) => {
|
|
567
|
+
const rows = sheet.getRows();
|
|
568
|
+
rows.forEach((row) => {
|
|
569
|
+
row.getCells().forEach((cell) => {
|
|
570
|
+
const cellData = cell.toData();
|
|
571
|
+
if (cellData.style) {
|
|
572
|
+
this.registerStyle(cellData.style, registry);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
return registry;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Register a style and get its cellXf index
|
|
581
|
+
*/
|
|
582
|
+
static registerStyle(style, registry) {
|
|
583
|
+
var _a, _b, _c, _d;
|
|
584
|
+
// Serialize and register font
|
|
585
|
+
const fontKey = this.serializeFont(style);
|
|
586
|
+
if (!registry.fonts.has(fontKey)) {
|
|
587
|
+
registry.fonts.set(fontKey, registry.fonts.size);
|
|
588
|
+
}
|
|
589
|
+
// Serialize and register fill
|
|
590
|
+
const fillKey = this.serializeFill(style);
|
|
591
|
+
if (fillKey !== 'none' && !registry.fills.has(fillKey)) {
|
|
592
|
+
registry.fills.set(fillKey, registry.fills.size);
|
|
593
|
+
}
|
|
594
|
+
// Serialize and register border
|
|
595
|
+
const borderKey = this.serializeBorder(style);
|
|
596
|
+
if (borderKey !== 'none' && !registry.borders.has(borderKey)) {
|
|
597
|
+
registry.borders.set(borderKey, registry.borders.size);
|
|
598
|
+
}
|
|
599
|
+
// Create cellXf key
|
|
600
|
+
const fontIndex = (_a = registry.fonts.get(fontKey)) !== null && _a !== void 0 ? _a : 0;
|
|
601
|
+
const fillIndex = (_b = registry.fills.get(fillKey)) !== null && _b !== void 0 ? _b : 0;
|
|
602
|
+
const borderIndex = (_c = registry.borders.get(borderKey)) !== null && _c !== void 0 ? _c : 0;
|
|
603
|
+
let alignment = this.serializeAlignment(style);
|
|
604
|
+
if (!alignment)
|
|
605
|
+
alignment = '{}';
|
|
606
|
+
const xfKey = `f${fontIndex}:l${fillIndex}:b${borderIndex}:a${alignment}`;
|
|
607
|
+
if (!registry.cellXfs.has(xfKey)) {
|
|
608
|
+
registry.cellXfs.set(xfKey, registry.cellXfs.size);
|
|
609
|
+
}
|
|
610
|
+
return (_d = registry.cellXfs.get(xfKey)) !== null && _d !== void 0 ? _d : 0;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get style index for a cell style
|
|
614
|
+
*/
|
|
615
|
+
static getStyleIndex(style, registry) {
|
|
616
|
+
if (!style)
|
|
617
|
+
return 0;
|
|
618
|
+
return this.registerStyle(style, registry);
|
|
619
|
+
}
|
|
620
|
+
static serializeFont(style) {
|
|
621
|
+
var _a, _b, _c, _d, _e, _f;
|
|
622
|
+
const bold = style.bold || ((_a = style.font) === null || _a === void 0 ? void 0 : _a.bold);
|
|
623
|
+
const italic = style.italic || ((_b = style.font) === null || _b === void 0 ? void 0 : _b.italic);
|
|
624
|
+
const underline = style.underline || ((_c = style.font) === null || _c === void 0 ? void 0 : _c.underline);
|
|
625
|
+
const color = style.color || ((_d = style.font) === null || _d === void 0 ? void 0 : _d.color);
|
|
626
|
+
const size = style.fontSize || ((_e = style.font) === null || _e === void 0 ? void 0 : _e.size) || 11;
|
|
627
|
+
const name = style.fontFamily || ((_f = style.font) === null || _f === void 0 ? void 0 : _f.name) || 'Calibri';
|
|
628
|
+
return JSON.stringify({ bold, italic, underline, color, size, name });
|
|
629
|
+
}
|
|
630
|
+
static serializeFill(style) {
|
|
631
|
+
var _a;
|
|
632
|
+
const bgColor = style.backgroundColor || ((_a = style.fill) === null || _a === void 0 ? void 0 : _a.fgColor);
|
|
633
|
+
if (!bgColor)
|
|
634
|
+
return 'none';
|
|
635
|
+
return JSON.stringify({ bgColor });
|
|
636
|
+
}
|
|
637
|
+
static serializeBorder(style) {
|
|
638
|
+
if (!style.border && !style.borderAll)
|
|
639
|
+
return 'none';
|
|
640
|
+
return JSON.stringify({ border: style.border, borderAll: style.borderAll });
|
|
641
|
+
}
|
|
642
|
+
static serializeAlignment(style) {
|
|
643
|
+
const h = style.alignment;
|
|
644
|
+
const v = style.verticalAlignment;
|
|
645
|
+
const wrap = style.wrapText;
|
|
646
|
+
if (!h && !v && !wrap)
|
|
647
|
+
return '';
|
|
648
|
+
return JSON.stringify({ h, v, wrap });
|
|
649
|
+
}
|
|
543
650
|
static escapeXml(str) {
|
|
544
651
|
if (typeof str !== 'string')
|
|
545
652
|
return String(str !== null && str !== void 0 ? str : '');
|
|
@@ -593,17 +700,150 @@ class ExcelWriter {
|
|
|
593
700
|
xml += '</Relationships>';
|
|
594
701
|
return xml;
|
|
595
702
|
}
|
|
596
|
-
static generateStyles() {
|
|
703
|
+
static generateStyles(registry) {
|
|
597
704
|
let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
598
705
|
xml += '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
|
|
599
|
-
|
|
600
|
-
xml +=
|
|
601
|
-
|
|
706
|
+
// Generate fonts
|
|
707
|
+
xml += `<fonts count="${registry.fonts.size}">`;
|
|
708
|
+
const fontEntries = Array.from(registry.fonts.entries()).sort((a, b) => a[1] - b[1]);
|
|
709
|
+
for (const [fontKey] of fontEntries) {
|
|
710
|
+
const font = JSON.parse(fontKey);
|
|
711
|
+
xml += '<font>';
|
|
712
|
+
if (font.bold)
|
|
713
|
+
xml += '<b/>';
|
|
714
|
+
if (font.italic)
|
|
715
|
+
xml += '<i/>';
|
|
716
|
+
if (font.underline)
|
|
717
|
+
xml += '<u/>';
|
|
718
|
+
xml += `<sz val="${font.size || 11}"/>`;
|
|
719
|
+
if (font.color) {
|
|
720
|
+
const colorHex = this.colorToARGB(font.color);
|
|
721
|
+
xml += `<color rgb="${colorHex}"/>`;
|
|
722
|
+
}
|
|
723
|
+
xml += `<name val="${font.name || 'Calibri'}"/>`;
|
|
724
|
+
xml += '</font>';
|
|
725
|
+
}
|
|
726
|
+
xml += '</fonts>';
|
|
727
|
+
// Generate fills
|
|
728
|
+
xml += `<fills count="${registry.fills.size}">`;
|
|
729
|
+
const fillEntries = Array.from(registry.fills.entries()).sort((a, b) => a[1] - b[1]);
|
|
730
|
+
for (const [fillKey] of fillEntries) {
|
|
731
|
+
if (fillKey === 'none') {
|
|
732
|
+
xml += '<fill><patternFill patternType="none"/></fill>';
|
|
733
|
+
}
|
|
734
|
+
else if (fillKey === 'gray125') {
|
|
735
|
+
xml += '<fill><patternFill patternType="gray125"/></fill>';
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
const fill = JSON.parse(fillKey);
|
|
739
|
+
const colorHex = this.colorToARGB(fill.bgColor);
|
|
740
|
+
xml += `<fill><patternFill patternType="solid"><fgColor rgb="${colorHex}"/><bgColor indexed="64"/></patternFill></fill>`;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
xml += '</fills>';
|
|
744
|
+
// Generate borders
|
|
745
|
+
xml += `<borders count="${registry.borders.size}">`;
|
|
746
|
+
const borderEntries = Array.from(registry.borders.entries()).sort((a, b) => a[1] - b[1]);
|
|
747
|
+
for (const [borderKey] of borderEntries) {
|
|
748
|
+
if (borderKey === 'none') {
|
|
749
|
+
xml += '<border><left/><right/><top/><bottom/><diagonal/></border>';
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
const borderData = JSON.parse(borderKey);
|
|
753
|
+
xml += '<border>';
|
|
754
|
+
if (borderData.borderAll) {
|
|
755
|
+
const style = borderData.borderAll.style || 'thin';
|
|
756
|
+
const color = borderData.borderAll.color ? this.colorToARGB(borderData.borderAll.color) : 'FF000000';
|
|
757
|
+
xml += `<left style="${style}"><color rgb="${color}"/></left>`;
|
|
758
|
+
xml += `<right style="${style}"><color rgb="${color}"/></right>`;
|
|
759
|
+
xml += `<top style="${style}"><color rgb="${color}"/></top>`;
|
|
760
|
+
xml += `<bottom style="${style}"><color rgb="${color}"/></bottom>`;
|
|
761
|
+
}
|
|
762
|
+
else if (borderData.border) {
|
|
763
|
+
const b = borderData.border;
|
|
764
|
+
xml += this.generateBorderEdge('left', b.left);
|
|
765
|
+
xml += this.generateBorderEdge('right', b.right);
|
|
766
|
+
xml += this.generateBorderEdge('top', b.top);
|
|
767
|
+
xml += this.generateBorderEdge('bottom', b.bottom);
|
|
768
|
+
}
|
|
769
|
+
xml += '<diagonal/></border>';
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
xml += '</borders>';
|
|
773
|
+
// Generate cellStyleXfs (base styles)
|
|
602
774
|
xml += '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>';
|
|
603
|
-
|
|
775
|
+
// Generate cellXfs (cell formats)
|
|
776
|
+
xml += `<cellXfs count="${registry.cellXfs.size}">`;
|
|
777
|
+
const xfEntries = Array.from(registry.cellXfs.entries()).sort((a, b) => a[1] - b[1]);
|
|
778
|
+
for (const [xfKey] of xfEntries) {
|
|
779
|
+
if (xfKey === 'default') {
|
|
780
|
+
xml += '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>';
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
// Parse xf key: f{fontIndex}:l{fillIndex}:b{borderIndex}:a{alignment}
|
|
784
|
+
const parts = xfKey.split(':');
|
|
785
|
+
const fontId = parseInt(parts[0].substring(1)) || 0;
|
|
786
|
+
const fillId = parseInt(parts[1].substring(1)) || 0;
|
|
787
|
+
const borderId = parseInt(parts[2].substring(1)) || 0;
|
|
788
|
+
const alignmentJson = parts[3].substring(1);
|
|
789
|
+
let xf = `<xf numFmtId="0" fontId="${fontId}" fillId="${fillId}" borderId="${borderId}" xfId="0"`;
|
|
790
|
+
if (fontId > 0)
|
|
791
|
+
xf += ' applyFont="1"';
|
|
792
|
+
if (fillId > 0)
|
|
793
|
+
xf += ' applyFill="1"';
|
|
794
|
+
if (borderId > 0)
|
|
795
|
+
xf += ' applyBorder="1"';
|
|
796
|
+
let align = {};
|
|
797
|
+
if (alignmentJson && alignmentJson.trim().startsWith('{')) {
|
|
798
|
+
try {
|
|
799
|
+
align = JSON.parse(alignmentJson);
|
|
800
|
+
}
|
|
801
|
+
catch (e) {
|
|
802
|
+
align = {};
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (Object.keys(align).length > 0) {
|
|
806
|
+
xf += ' applyAlignment="1">';
|
|
807
|
+
xf += '<alignment';
|
|
808
|
+
if (align.h)
|
|
809
|
+
xf += ` horizontal="${align.h}"`;
|
|
810
|
+
if (align.v)
|
|
811
|
+
xf += ` vertical="${align.v === 'middle' ? 'center' : align.v}"`;
|
|
812
|
+
if (align.wrap)
|
|
813
|
+
xf += ' wrapText="1"';
|
|
814
|
+
xf += '/></xf>';
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
xf += '/>';
|
|
818
|
+
}
|
|
819
|
+
xml += xf;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
xml += '</cellXfs>';
|
|
604
823
|
xml += '</styleSheet>';
|
|
605
824
|
return xml;
|
|
606
825
|
}
|
|
826
|
+
static generateBorderEdge(side, edge) {
|
|
827
|
+
if (!edge || !edge.style)
|
|
828
|
+
return `<${side}/>`;
|
|
829
|
+
const color = edge.color ? this.colorToARGB(edge.color) : 'FF000000';
|
|
830
|
+
return `<${side} style="${edge.style}"><color rgb="${color}"/></${side}>`;
|
|
831
|
+
}
|
|
832
|
+
static colorToARGB(color) {
|
|
833
|
+
if (!color)
|
|
834
|
+
return 'FF000000';
|
|
835
|
+
// Remove # if present
|
|
836
|
+
let hex = color.replace('#', '').toUpperCase();
|
|
837
|
+
// Add alpha if not present (6 chars -> 8 chars)
|
|
838
|
+
if (hex.length === 6) {
|
|
839
|
+
hex = 'FF' + hex;
|
|
840
|
+
}
|
|
841
|
+
// Handle 3 char hex
|
|
842
|
+
if (hex.length === 3) {
|
|
843
|
+
hex = 'FF' + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
844
|
+
}
|
|
845
|
+
return hex;
|
|
846
|
+
}
|
|
607
847
|
static generateSharedStrings(sheets) {
|
|
608
848
|
const strings = [];
|
|
609
849
|
sheets.forEach((sheet) => {
|
|
@@ -627,14 +867,32 @@ class ExcelWriter {
|
|
|
627
867
|
xml += '</sst>';
|
|
628
868
|
return xml;
|
|
629
869
|
}
|
|
630
|
-
static generateWorksheet(sheet) {
|
|
870
|
+
static generateWorksheet(sheet, styleRegistry) {
|
|
631
871
|
const rows = sheet.getRows();
|
|
872
|
+
const columns = sheet.getColumns();
|
|
632
873
|
const allStrings = this.collectAllStrings(sheet);
|
|
633
874
|
let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
634
875
|
xml += '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
|
|
635
876
|
// Add sheet dimension
|
|
636
877
|
const dimension = this.getSheetDimension(rows);
|
|
637
878
|
xml += `<dimension ref="${dimension}"/>`;
|
|
879
|
+
// Add column widths if defined
|
|
880
|
+
if (columns.length > 0) {
|
|
881
|
+
xml += '<cols>';
|
|
882
|
+
columns.forEach((col, index) => {
|
|
883
|
+
const colData = col.toData();
|
|
884
|
+
if (colData.width !== undefined && colData.width > 0) {
|
|
885
|
+
// Excel width is in character units, convert from pixels: width / 7
|
|
886
|
+
const excelWidth = colData.width / 7;
|
|
887
|
+
xml += `<col min="${index + 1}" max="${index + 1}" width="${excelWidth}" customWidth="1"`;
|
|
888
|
+
if (colData.hidden) {
|
|
889
|
+
xml += ' hidden="1"';
|
|
890
|
+
}
|
|
891
|
+
xml += '/>';
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
xml += '</cols>';
|
|
895
|
+
}
|
|
638
896
|
xml += '<sheetData>';
|
|
639
897
|
rows.forEach((row, rowIndex) => {
|
|
640
898
|
const cells = row.getCells();
|
|
@@ -643,37 +901,54 @@ class ExcelWriter {
|
|
|
643
901
|
cells.forEach((cell, colIndex) => {
|
|
644
902
|
const cellData = cell.toData();
|
|
645
903
|
const cellRef = this.columnToLetter(colIndex + 1) + (rowIndex + 1);
|
|
904
|
+
const styleIndex = this.getStyleIndex(cellData.style, styleRegistry);
|
|
905
|
+
const styleAttr = styleIndex > 0 ? ` s="${styleIndex}"` : '';
|
|
646
906
|
if (cellData.formula) {
|
|
647
|
-
xml += `<c r="${cellRef}"><f>${this.escapeXml(cellData.formula)}</f></c>`;
|
|
907
|
+
xml += `<c r="${cellRef}"${styleAttr}><f>${this.escapeXml(cellData.formula)}</f></c>`;
|
|
648
908
|
}
|
|
649
909
|
else if (cellData.value !== null && cellData.value !== undefined && cellData.value !== '') {
|
|
650
910
|
if (typeof cellData.value === 'number') {
|
|
651
|
-
xml += `<c r="${cellRef}"><v>${cellData.value}</v></c>`;
|
|
911
|
+
xml += `<c r="${cellRef}"${styleAttr}><v>${cellData.value}</v></c>`;
|
|
652
912
|
}
|
|
653
913
|
else if (typeof cellData.value === 'boolean') {
|
|
654
|
-
xml += `<c r="${cellRef}" t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
|
|
914
|
+
xml += `<c r="${cellRef}"${styleAttr} t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
|
|
655
915
|
}
|
|
656
916
|
else if (cellData.type === 'date') {
|
|
657
917
|
const excelDate = DateFormatter.toExcelDate(cellData.value);
|
|
658
|
-
xml += `<c r="${cellRef}"><v>${excelDate}</v></c>`;
|
|
918
|
+
xml += `<c r="${cellRef}"${styleAttr}><v>${excelDate}</v></c>`;
|
|
659
919
|
}
|
|
660
920
|
else {
|
|
661
921
|
// String value - use shared string index
|
|
662
922
|
const stringIndex = allStrings.indexOf(String(cellData.value));
|
|
663
923
|
if (stringIndex >= 0) {
|
|
664
|
-
xml += `<c r="${cellRef}" t="s"><v>${stringIndex}</v></c>`;
|
|
924
|
+
xml += `<c r="${cellRef}"${styleAttr} t="s"><v>${stringIndex}</v></c>`;
|
|
665
925
|
}
|
|
666
926
|
else {
|
|
667
927
|
// Inline string as fallback
|
|
668
|
-
xml += `<c r="${cellRef}" t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
|
|
928
|
+
xml += `<c r="${cellRef}"${styleAttr} t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
|
|
669
929
|
}
|
|
670
930
|
}
|
|
671
931
|
}
|
|
932
|
+
else if (styleIndex > 0) {
|
|
933
|
+
// Empty cell with style
|
|
934
|
+
xml += `<c r="${cellRef}"${styleAttr}/>`;
|
|
935
|
+
}
|
|
672
936
|
});
|
|
673
937
|
xml += '</row>';
|
|
674
938
|
}
|
|
675
939
|
});
|
|
676
940
|
xml += '</sheetData>';
|
|
941
|
+
// Add merged cells if any
|
|
942
|
+
const sheetData = sheet.toData();
|
|
943
|
+
if (sheetData.mergeCells && sheetData.mergeCells.length > 0) {
|
|
944
|
+
xml += '<mergeCells>';
|
|
945
|
+
sheetData.mergeCells.forEach(merge => {
|
|
946
|
+
const startRef = this.columnToLetter(merge.startCol + 1) + (merge.startRow + 1);
|
|
947
|
+
const endRef = this.columnToLetter(merge.endCol + 1) + (merge.endRow + 1);
|
|
948
|
+
xml += `<mergeCell ref="${startRef}:${endRef}"/>`;
|
|
949
|
+
});
|
|
950
|
+
xml += '</mergeCells>';
|
|
951
|
+
}
|
|
677
952
|
xml += '</worksheet>';
|
|
678
953
|
return xml;
|
|
679
954
|
}
|
|
@@ -967,23 +1242,46 @@ class ExportBuilder {
|
|
|
967
1242
|
});
|
|
968
1243
|
return this;
|
|
969
1244
|
}
|
|
970
|
-
|
|
971
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* Add multiple data rows to the sheet
|
|
1247
|
+
* @param data Array of row data
|
|
1248
|
+
* @param fields Optional array of field names (for object data)
|
|
1249
|
+
* @param styles Optional array of styles per row or per cell
|
|
1250
|
+
*/
|
|
1251
|
+
addDataRows(data, fields, styles) {
|
|
1252
|
+
data.forEach((item, rowIdx) => {
|
|
972
1253
|
const row = this.currentSheet.createRow();
|
|
1254
|
+
let rowStyle = undefined;
|
|
1255
|
+
let cellStyles = undefined;
|
|
1256
|
+
if (styles && styles[rowIdx]) {
|
|
1257
|
+
if (Array.isArray(styles[rowIdx])) {
|
|
1258
|
+
cellStyles = styles[rowIdx];
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
rowStyle = styles[rowIdx];
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
973
1264
|
if (fields && fields.length > 0) {
|
|
974
|
-
fields.forEach(field => {
|
|
1265
|
+
fields.forEach((field, colIdx) => {
|
|
975
1266
|
const value = this.getNestedValue(item, field);
|
|
976
|
-
|
|
1267
|
+
const style = cellStyles ? cellStyles[colIdx] : rowStyle;
|
|
1268
|
+
row.createCell(value, style);
|
|
977
1269
|
});
|
|
978
1270
|
}
|
|
979
1271
|
else if (Array.isArray(item)) {
|
|
980
|
-
item.forEach(value =>
|
|
1272
|
+
item.forEach((value, colIdx) => {
|
|
1273
|
+
const style = cellStyles ? cellStyles[colIdx] : rowStyle;
|
|
1274
|
+
row.createCell(value, style);
|
|
1275
|
+
});
|
|
981
1276
|
}
|
|
982
1277
|
else if (typeof item === 'object') {
|
|
983
|
-
Object.values(item).forEach(value =>
|
|
1278
|
+
Object.values(item).forEach((value, colIdx) => {
|
|
1279
|
+
const style = cellStyles ? cellStyles[colIdx] : rowStyle;
|
|
1280
|
+
row.createCell(value, style);
|
|
1281
|
+
});
|
|
984
1282
|
}
|
|
985
1283
|
else {
|
|
986
|
-
row.createCell(item);
|
|
1284
|
+
row.createCell(item, rowStyle);
|
|
987
1285
|
}
|
|
988
1286
|
});
|
|
989
1287
|
return this;
|
|
@@ -1042,6 +1340,55 @@ class ExportBuilder {
|
|
|
1042
1340
|
});
|
|
1043
1341
|
return this;
|
|
1044
1342
|
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Merge cells in the current worksheet
|
|
1345
|
+
* @param startRow Start row index (0-based)
|
|
1346
|
+
* @param startCol Start column index (0-based)
|
|
1347
|
+
* @param endRow End row index (0-based)
|
|
1348
|
+
* @param endCol End column index (0-based)
|
|
1349
|
+
*/
|
|
1350
|
+
mergeCells(startRow, startCol, endRow, endCol) {
|
|
1351
|
+
this.currentSheet.mergeCells(startRow, startCol, endRow, endCol);
|
|
1352
|
+
return this;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Set alignment for a specific cell
|
|
1356
|
+
* @param row Row index (0-based)
|
|
1357
|
+
* @param col Column index (0-based)
|
|
1358
|
+
* @param horizontal Horizontal alignment
|
|
1359
|
+
* @param vertical Vertical alignment (optional)
|
|
1360
|
+
*/
|
|
1361
|
+
setAlignment(row, col, horizontal, vertical) {
|
|
1362
|
+
const rowObj = this.currentSheet.getRow(row);
|
|
1363
|
+
if (rowObj) {
|
|
1364
|
+
const cells = rowObj.getCells();
|
|
1365
|
+
if (cells[col]) {
|
|
1366
|
+
const style = StyleBuilder.create().align(horizontal);
|
|
1367
|
+
if (vertical) {
|
|
1368
|
+
style.verticalAlign(vertical);
|
|
1369
|
+
}
|
|
1370
|
+
cells[col].setStyle(style.build());
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return this;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Set alignment for a range of cells
|
|
1377
|
+
* @param startRow Start row index (0-based)
|
|
1378
|
+
* @param startCol Start column index (0-based)
|
|
1379
|
+
* @param endRow End row index (0-based)
|
|
1380
|
+
* @param endCol End column index (0-based)
|
|
1381
|
+
* @param horizontal Horizontal alignment
|
|
1382
|
+
* @param vertical Vertical alignment (optional)
|
|
1383
|
+
*/
|
|
1384
|
+
setRangeAlignment(startRow, startCol, endRow, endCol, horizontal, vertical) {
|
|
1385
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1386
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1387
|
+
this.setAlignment(r, c, horizontal, vertical);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return this;
|
|
1391
|
+
}
|
|
1045
1392
|
autoSizeColumns() {
|
|
1046
1393
|
const rows = this.currentSheet.getRows();
|
|
1047
1394
|
const maxLengths = [];
|
|
@@ -1166,6 +1513,28 @@ class SheetBuilder {
|
|
|
1166
1513
|
});
|
|
1167
1514
|
return this;
|
|
1168
1515
|
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Set the width of a specific column
|
|
1518
|
+
* @param colIndex Column index
|
|
1519
|
+
* @param width Width to set
|
|
1520
|
+
*/
|
|
1521
|
+
setColumnWidth(colIndex, width) {
|
|
1522
|
+
const column = this.getOrCreateColumn(colIndex);
|
|
1523
|
+
column.setWidth(width);
|
|
1524
|
+
return this;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Set the height of a specific row
|
|
1528
|
+
* @param rowIndex Row index
|
|
1529
|
+
* @param height Height to set
|
|
1530
|
+
*/
|
|
1531
|
+
setRowHeight(rowIndex, height) {
|
|
1532
|
+
const row = this.worksheet.getRow(rowIndex);
|
|
1533
|
+
if (row) {
|
|
1534
|
+
row.setHeight(height);
|
|
1535
|
+
}
|
|
1536
|
+
return this;
|
|
1537
|
+
}
|
|
1169
1538
|
/**
|
|
1170
1539
|
* Add a title row
|
|
1171
1540
|
* @param title Title text
|