@nexterajs/ui 0.0.1
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 +202 -0
- package/NOTICE +16 -0
- package/README.md +64 -0
- package/fesm2022/nexterajs-ui-animations.mjs +184 -0
- package/fesm2022/nexterajs-ui-animations.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-core.mjs +326 -0
- package/fesm2022/nexterajs-ui-core.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs +953 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-excel.mjs +779 -0
- package/fesm2022/nexterajs-ui-excel.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-file-uploader.mjs +125 -0
- package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-forms.mjs +1113 -0
- package/fesm2022/nexterajs-ui-forms.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-grid.mjs +4145 -0
- package/fesm2022/nexterajs-ui-grid.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-intl.mjs +377 -0
- package/fesm2022/nexterajs-ui-intl.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-pivot.mjs +4739 -0
- package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-styling.mjs +59 -0
- package/fesm2022/nexterajs-ui-styling.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs +202 -0
- package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-validation.mjs +269 -0
- package/fesm2022/nexterajs-ui-validation.mjs.map +1 -0
- package/fesm2022/nexterajs-ui.mjs +23 -0
- package/fesm2022/nexterajs-ui.mjs.map +1 -0
- package/package.json +100 -0
- package/styles/_button-directives.scss +39 -0
- package/styles/_dropdown.scss +113 -0
- package/styles/_editor.scss +146 -0
- package/styles/_mixins.scss +2 -0
- package/styles/_tokens.scss +77 -0
- package/styles/_widget.scss +58 -0
- package/styles/index.scss +83 -0
- package/types/nexterajs-ui-animations.d.ts +22 -0
- package/types/nexterajs-ui-core.d.ts +328 -0
- package/types/nexterajs-ui-dropdowns.d.ts +334 -0
- package/types/nexterajs-ui-excel.d.ts +159 -0
- package/types/nexterajs-ui-file-uploader.d.ts +47 -0
- package/types/nexterajs-ui-forms.d.ts +296 -0
- package/types/nexterajs-ui-grid.d.ts +1485 -0
- package/types/nexterajs-ui-intl.d.ts +125 -0
- package/types/nexterajs-ui-pivot.d.ts +1470 -0
- package/types/nexterajs-ui-styling.d.ts +40 -0
- package/types/nexterajs-ui-tree-view.d.ts +69 -0
- package/types/nexterajs-ui-validation.d.ts +102 -0
- package/types/nexterajs-ui.d.ts +12 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public value types for the in-repo .xlsx writer. These mirror the narrow slice of the exceljs
|
|
3
|
+
* `Cell` shape the app actually uses (verified across every call site), so existing code migrates by
|
|
4
|
+
* import path only — see {@link NxeWorkbook} (aliased as `Workbook`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal, zero-dependency ZIP support for the in-repo .xlsx writer/reader — replacing the
|
|
9
|
+
* `jszip`/`pako` machinery that used to arrive transitively via `exceljs`.
|
|
10
|
+
*
|
|
11
|
+
* The WRITE half ({@link writeZip}) emits a STORE-only (uncompressed) archive — valid for `.xlsx`,
|
|
12
|
+
* which Excel reads happily — so no DEFLATE compressor is needed. CRC-32 is still required per entry
|
|
13
|
+
* even under STORE, so it is computed here.
|
|
14
|
+
*
|
|
15
|
+
* The READ half ({@link readCentralDirectory} / {@link readZipEntryBytes}) parses a real-world
|
|
16
|
+
* archive via its central directory (the authoritative size source — local headers may carry zeroed
|
|
17
|
+
* sizes when a data descriptor is used) and inflates DEFLATE entries with the browser-native
|
|
18
|
+
* {@link DecompressionStream} (`deflate-raw`); STORE entries are copied. No external lib.
|
|
19
|
+
*/
|
|
20
|
+
const SIG_LOCAL = 0x04034b50;
|
|
21
|
+
const SIG_CENTRAL = 0x02014b50;
|
|
22
|
+
const SIG_EOCD = 0x06054b50;
|
|
23
|
+
/** A fixed DOS date/time (1980-01-01 00:00:00) so writer output is byte-reproducible. */
|
|
24
|
+
const DOS_DATE = 0x0021;
|
|
25
|
+
const DOS_TIME = 0x0000;
|
|
26
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// CRC-32 (lazy 256-entry table, standard 0xEDB88320 polynomial)
|
|
28
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
let CRC_TABLE = null;
|
|
30
|
+
function crcTable() {
|
|
31
|
+
if (CRC_TABLE) {
|
|
32
|
+
return CRC_TABLE;
|
|
33
|
+
}
|
|
34
|
+
const t = new Uint32Array(256);
|
|
35
|
+
for (let n = 0; n < 256; n++) {
|
|
36
|
+
let c = n;
|
|
37
|
+
for (let k = 0; k < 8; k++) {
|
|
38
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
39
|
+
}
|
|
40
|
+
t[n] = c >>> 0;
|
|
41
|
+
}
|
|
42
|
+
return (CRC_TABLE = t);
|
|
43
|
+
}
|
|
44
|
+
/** CRC-32 of a byte array (as required by the ZIP format, including STORE entries). */
|
|
45
|
+
function crc32(bytes) {
|
|
46
|
+
const t = crcTable();
|
|
47
|
+
let c = 0xffffffff;
|
|
48
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
49
|
+
c = t[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
50
|
+
}
|
|
51
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build a STORE (uncompressed) ZIP archive from the given parts. Layout: every local file header +
|
|
55
|
+
* data, then the central directory, then the End-Of-Central-Directory record. The first two bytes
|
|
56
|
+
* are `PK` (0x50 0x4B). Returns the whole archive as one {@link Uint8Array}.
|
|
57
|
+
*/
|
|
58
|
+
function writeZip(parts) {
|
|
59
|
+
const enc = new TextEncoder();
|
|
60
|
+
const entries = parts.map((p) => {
|
|
61
|
+
const nameBytes = enc.encode(p.name);
|
|
62
|
+
return { nameBytes, data: p.bytes, crc: crc32(p.bytes) };
|
|
63
|
+
});
|
|
64
|
+
let localSize = 0;
|
|
65
|
+
let centralSize = 0;
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
localSize += 30 + e.nameBytes.length + e.data.length;
|
|
68
|
+
centralSize += 46 + e.nameBytes.length;
|
|
69
|
+
}
|
|
70
|
+
const total = localSize + centralSize + 22;
|
|
71
|
+
const out = new Uint8Array(total);
|
|
72
|
+
const view = new DataView(out.buffer);
|
|
73
|
+
let offset = 0;
|
|
74
|
+
const localOffsets = [];
|
|
75
|
+
// Local file headers + data.
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
localOffsets.push(offset);
|
|
78
|
+
view.setUint32(offset, SIG_LOCAL, true);
|
|
79
|
+
view.setUint16(offset + 4, 20, true); // version needed
|
|
80
|
+
view.setUint16(offset + 6, 0, true); // general-purpose flag
|
|
81
|
+
view.setUint16(offset + 8, 0, true); // method 0 = STORE
|
|
82
|
+
view.setUint16(offset + 10, DOS_TIME, true);
|
|
83
|
+
view.setUint16(offset + 12, DOS_DATE, true);
|
|
84
|
+
view.setUint32(offset + 14, e.crc, true);
|
|
85
|
+
view.setUint32(offset + 18, e.data.length, true); // compressed size
|
|
86
|
+
view.setUint32(offset + 22, e.data.length, true); // uncompressed size
|
|
87
|
+
view.setUint16(offset + 26, e.nameBytes.length, true);
|
|
88
|
+
view.setUint16(offset + 28, 0, true); // extra length
|
|
89
|
+
offset += 30;
|
|
90
|
+
out.set(e.nameBytes, offset);
|
|
91
|
+
offset += e.nameBytes.length;
|
|
92
|
+
out.set(e.data, offset);
|
|
93
|
+
offset += e.data.length;
|
|
94
|
+
}
|
|
95
|
+
// Central directory.
|
|
96
|
+
const centralStart = offset;
|
|
97
|
+
entries.forEach((e, i) => {
|
|
98
|
+
view.setUint32(offset, SIG_CENTRAL, true);
|
|
99
|
+
view.setUint16(offset + 4, 20, true); // version made by
|
|
100
|
+
view.setUint16(offset + 6, 20, true); // version needed
|
|
101
|
+
view.setUint16(offset + 8, 0, true); // flag
|
|
102
|
+
view.setUint16(offset + 10, 0, true); // method STORE
|
|
103
|
+
view.setUint16(offset + 12, DOS_TIME, true);
|
|
104
|
+
view.setUint16(offset + 14, DOS_DATE, true);
|
|
105
|
+
view.setUint32(offset + 16, e.crc, true);
|
|
106
|
+
view.setUint32(offset + 20, e.data.length, true); // compressed size
|
|
107
|
+
view.setUint32(offset + 24, e.data.length, true); // uncompressed size
|
|
108
|
+
view.setUint16(offset + 28, e.nameBytes.length, true);
|
|
109
|
+
view.setUint16(offset + 30, 0, true); // extra length
|
|
110
|
+
view.setUint16(offset + 32, 0, true); // comment length
|
|
111
|
+
view.setUint16(offset + 34, 0, true); // disk number start
|
|
112
|
+
view.setUint16(offset + 36, 0, true); // internal attrs
|
|
113
|
+
view.setUint32(offset + 38, 0, true); // external attrs
|
|
114
|
+
view.setUint32(offset + 42, localOffsets[i], true); // local header offset
|
|
115
|
+
offset += 46;
|
|
116
|
+
out.set(e.nameBytes, offset);
|
|
117
|
+
offset += e.nameBytes.length;
|
|
118
|
+
});
|
|
119
|
+
// End of central directory.
|
|
120
|
+
view.setUint32(offset, SIG_EOCD, true);
|
|
121
|
+
view.setUint16(offset + 4, 0, true); // disk number
|
|
122
|
+
view.setUint16(offset + 6, 0, true); // disk with central dir
|
|
123
|
+
view.setUint16(offset + 8, entries.length, true); // entries this disk
|
|
124
|
+
view.setUint16(offset + 10, entries.length, true); // total entries
|
|
125
|
+
view.setUint32(offset + 12, centralSize, true);
|
|
126
|
+
view.setUint32(offset + 16, centralStart, true);
|
|
127
|
+
view.setUint16(offset + 20, 0, true); // comment length
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Parse the central directory of a ZIP buffer into a name → {@link ZipEntry} map. Scans backwards
|
|
132
|
+
* for the EOCD record, then walks each central-directory header. Throws on a ZIP64 archive (never
|
|
133
|
+
* produced by Excel for the small workbooks this app handles).
|
|
134
|
+
*/
|
|
135
|
+
function readCentralDirectory(buf) {
|
|
136
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
137
|
+
const dec = new TextDecoder('utf-8');
|
|
138
|
+
// Locate EOCD (signature within the last 22 + up to 65535 comment bytes).
|
|
139
|
+
let eocd = -1;
|
|
140
|
+
const minPos = Math.max(0, buf.length - 22 - 65535);
|
|
141
|
+
for (let i = buf.length - 22; i >= minPos; i--) {
|
|
142
|
+
if (view.getUint32(i, true) === SIG_EOCD) {
|
|
143
|
+
eocd = i;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (eocd < 0) {
|
|
148
|
+
throw new Error('Not a valid .xlsx file (no ZIP end-of-central-directory record).');
|
|
149
|
+
}
|
|
150
|
+
const entryCount = view.getUint16(eocd + 10, true);
|
|
151
|
+
const cdOffset = view.getUint32(eocd + 16, true);
|
|
152
|
+
if (entryCount === 0xffff || cdOffset === 0xffffffff) {
|
|
153
|
+
throw new Error('Unsupported ZIP (ZIP64 archives are not supported).');
|
|
154
|
+
}
|
|
155
|
+
const map = new Map();
|
|
156
|
+
let p = cdOffset;
|
|
157
|
+
for (let i = 0; i < entryCount; i++) {
|
|
158
|
+
if (view.getUint32(p, true) !== SIG_CENTRAL) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const method = view.getUint16(p + 10, true);
|
|
162
|
+
const compSize = view.getUint32(p + 20, true);
|
|
163
|
+
const uncompSize = view.getUint32(p + 24, true);
|
|
164
|
+
const nameLen = view.getUint16(p + 28, true);
|
|
165
|
+
const extraLen = view.getUint16(p + 30, true);
|
|
166
|
+
const commentLen = view.getUint16(p + 32, true);
|
|
167
|
+
const localOffset = view.getUint32(p + 42, true);
|
|
168
|
+
const name = dec.decode(buf.subarray(p + 46, p + 46 + nameLen));
|
|
169
|
+
map.set(name, { name, method, compSize, uncompSize, localOffset });
|
|
170
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
171
|
+
}
|
|
172
|
+
return map;
|
|
173
|
+
}
|
|
174
|
+
/** Inflate a raw-DEFLATE byte array using the browser-native DecompressionStream. */
|
|
175
|
+
async function inflateRaw(bytes) {
|
|
176
|
+
// Copy into a fresh ArrayBuffer-backed view so it satisfies BlobPart (a subarray may be SAB-typed).
|
|
177
|
+
const part = new Uint8Array(bytes);
|
|
178
|
+
const stream = new Blob([part]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
|
|
179
|
+
const buf = await new Response(stream).arrayBuffer();
|
|
180
|
+
return new Uint8Array(buf);
|
|
181
|
+
}
|
|
182
|
+
/** Resolve where an entry's file data begins, reading the LOCAL header's own name/extra lengths. */
|
|
183
|
+
function localDataOffset(buf, view, localOffset) {
|
|
184
|
+
if (view.getUint32(localOffset, true) !== SIG_LOCAL) {
|
|
185
|
+
throw new Error('Corrupt ZIP (bad local file header).');
|
|
186
|
+
}
|
|
187
|
+
const nameLen = view.getUint16(localOffset + 26, true);
|
|
188
|
+
const extraLen = view.getUint16(localOffset + 28, true);
|
|
189
|
+
return localOffset + 30 + nameLen + extraLen;
|
|
190
|
+
}
|
|
191
|
+
/** Read (and inflate, if DEFLATE) one archive member's raw bytes. */
|
|
192
|
+
async function readZipEntryBytes(buf, entry) {
|
|
193
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
194
|
+
const start = localDataOffset(buf, view, entry.localOffset);
|
|
195
|
+
const comp = buf.subarray(start, start + entry.compSize);
|
|
196
|
+
if (entry.method === 0) {
|
|
197
|
+
return comp.slice();
|
|
198
|
+
}
|
|
199
|
+
if (entry.method === 8) {
|
|
200
|
+
return inflateRaw(comp);
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`Unsupported ZIP compression method ${entry.method}.`);
|
|
203
|
+
}
|
|
204
|
+
/** Read one archive member and decode it as UTF-8 text (for the OOXML parts). */
|
|
205
|
+
async function readZipEntryText(buf, entry) {
|
|
206
|
+
return new TextDecoder('utf-8').decode(await readZipEntryBytes(buf, entry));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const MAIN_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
|
210
|
+
const REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
|
|
211
|
+
const PKG_REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
|
212
|
+
const CT_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
|
213
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
214
|
+
// Small helpers (column letters, escaping, Excel serial dates)
|
|
215
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
/** 1 → A, 27 → AA. */
|
|
217
|
+
function colLetter(n) {
|
|
218
|
+
let s = '';
|
|
219
|
+
while (n > 0) {
|
|
220
|
+
const r = (n - 1) % 26;
|
|
221
|
+
s = String.fromCharCode(65 + r) + s;
|
|
222
|
+
n = Math.floor((n - 1) / 26);
|
|
223
|
+
}
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
226
|
+
/** 1-based (row, col) → A1 reference. */
|
|
227
|
+
function cellRef(row, col) {
|
|
228
|
+
return colLetter(col) + row;
|
|
229
|
+
}
|
|
230
|
+
/** Strip XML-1.0-forbidden control chars, then escape `& < >` for element text. */
|
|
231
|
+
function escapeText(s) {
|
|
232
|
+
return s
|
|
233
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
|
|
234
|
+
.replace(/&/g, '&')
|
|
235
|
+
.replace(/</g, '<')
|
|
236
|
+
.replace(/>/g, '>');
|
|
237
|
+
}
|
|
238
|
+
/** Escape for attribute values (adds `"`). */
|
|
239
|
+
function escapeAttr(s) {
|
|
240
|
+
return escapeText(s).replace(/"/g, '"');
|
|
241
|
+
}
|
|
242
|
+
/** `xml:space="preserve"` when leading/trailing whitespace would otherwise be collapsed. */
|
|
243
|
+
function preserve(s) {
|
|
244
|
+
return /^\s|\s$/.test(s) ? ' xml:space="preserve"' : '';
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Excel serial date — days since 1899-12-30 (epoch absorbs the 1900 leap-year bug for all dates
|
|
248
|
+
* ≥ 1900-03-01, the only range these reports produce). Built from LOCAL calendar components so the
|
|
249
|
+
* printed `dd mmm yyyy` matches the day the grid showed, regardless of timezone.
|
|
250
|
+
*/
|
|
251
|
+
function excelSerial(d) {
|
|
252
|
+
const epoch = Date.UTC(1899, 11, 30);
|
|
253
|
+
const ms = Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
|
|
254
|
+
return (ms - epoch) / 86400000;
|
|
255
|
+
}
|
|
256
|
+
function alignmentXml(a) {
|
|
257
|
+
const h = a.horizontal ? ` horizontal="${a.horizontal}"` : '';
|
|
258
|
+
const vMap = { top: 'top', middle: 'center', bottom: 'bottom' };
|
|
259
|
+
const v = a.vertical ? ` vertical="${vMap[a.vertical]}"` : '';
|
|
260
|
+
const i = a.indent != null && a.indent > 0 ? ` indent="${a.indent}"` : '';
|
|
261
|
+
return h || v || i ? `<alignment${h}${v}${i}/>` : '';
|
|
262
|
+
}
|
|
263
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
264
|
+
// Style registry — de-duped numFmts / fonts / fills / cellXfs
|
|
265
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
266
|
+
class StyleRegistry {
|
|
267
|
+
numFmts = [];
|
|
268
|
+
numFmtIndex = new Map();
|
|
269
|
+
fonts = ['<font><sz val="11"/><name val="Calibri Light"/></font>'];
|
|
270
|
+
fontIndex = new Map([['Calibri Light|false|11', 0]]);
|
|
271
|
+
fills = [
|
|
272
|
+
'<fill><patternFill patternType="none"/></fill>',
|
|
273
|
+
'<fill><patternFill patternType="gray125"/></fill>',
|
|
274
|
+
];
|
|
275
|
+
fillIndex = new Map();
|
|
276
|
+
xfs = ['<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'];
|
|
277
|
+
xfIndex = new Map();
|
|
278
|
+
numFmtId(code) {
|
|
279
|
+
let id = this.numFmtIndex.get(code);
|
|
280
|
+
if (id === undefined) {
|
|
281
|
+
id = 164 + this.numFmts.length;
|
|
282
|
+
this.numFmts.push(code);
|
|
283
|
+
this.numFmtIndex.set(code, id);
|
|
284
|
+
}
|
|
285
|
+
return id;
|
|
286
|
+
}
|
|
287
|
+
fontId(font) {
|
|
288
|
+
const name = font.name || 'Calibri Light';
|
|
289
|
+
const bold = !!font.bold;
|
|
290
|
+
const size = font.size || 11;
|
|
291
|
+
const key = `${name}|${bold}|${size}`;
|
|
292
|
+
let id = this.fontIndex.get(key);
|
|
293
|
+
if (id === undefined) {
|
|
294
|
+
id = this.fonts.length;
|
|
295
|
+
this.fonts.push(`<font>${bold ? '<b/>' : ''}<sz val="${size}"/><name val="${escapeAttr(name)}"/></font>`);
|
|
296
|
+
this.fontIndex.set(key, id);
|
|
297
|
+
}
|
|
298
|
+
return id;
|
|
299
|
+
}
|
|
300
|
+
fillId(fill) {
|
|
301
|
+
const argb = fill.fgColor.argb;
|
|
302
|
+
let id = this.fillIndex.get(argb);
|
|
303
|
+
if (id === undefined) {
|
|
304
|
+
id = this.fills.length;
|
|
305
|
+
this.fills.push(`<fill><patternFill patternType="solid"><fgColor rgb="${escapeAttr(argb)}"/></patternFill></fill>`);
|
|
306
|
+
this.fillIndex.set(argb, id);
|
|
307
|
+
}
|
|
308
|
+
return id;
|
|
309
|
+
}
|
|
310
|
+
/** Resolve the 0-based cellXfs index for a cell's facets; 0 means "all default" (omit `s`). */
|
|
311
|
+
cellStyle(cell) {
|
|
312
|
+
const numFmtId = cell.numFmt ? this.numFmtId(cell.numFmt) : 0;
|
|
313
|
+
const fontId = cell.font ? this.fontId(cell.font) : 0;
|
|
314
|
+
const fillId = cell.fill ? this.fillId(cell.fill) : 0;
|
|
315
|
+
const align = cell.alignment ? alignmentXml(cell.alignment) : '';
|
|
316
|
+
if (numFmtId === 0 && fontId === 0 && fillId === 0 && align === '') {
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
const key = `${numFmtId}|${fontId}|${fillId}|${align}`;
|
|
320
|
+
let s = this.xfIndex.get(key);
|
|
321
|
+
if (s === undefined) {
|
|
322
|
+
s = this.xfs.length;
|
|
323
|
+
const flags = (numFmtId !== 0 ? ' applyNumberFormat="1"' : '') +
|
|
324
|
+
(fontId !== 0 ? ' applyFont="1"' : '') +
|
|
325
|
+
(fillId !== 0 ? ' applyFill="1"' : '') +
|
|
326
|
+
(align !== '' ? ' applyAlignment="1"' : '');
|
|
327
|
+
this.xfs.push(`<xf numFmtId="${numFmtId}" fontId="${fontId}" fillId="${fillId}" borderId="0" xfId="0"${flags}>${align}</xf>`);
|
|
328
|
+
this.xfIndex.set(key, s);
|
|
329
|
+
}
|
|
330
|
+
return s;
|
|
331
|
+
}
|
|
332
|
+
toXml() {
|
|
333
|
+
const numFmtsXml = this.numFmts.length
|
|
334
|
+
? `<numFmts count="${this.numFmts.length}">${this.numFmts
|
|
335
|
+
.map((code, i) => `<numFmt numFmtId="${164 + i}" formatCode="${escapeAttr(code)}"/>`)
|
|
336
|
+
.join('')}</numFmts>`
|
|
337
|
+
: '';
|
|
338
|
+
return ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
339
|
+
`<styleSheet xmlns="${MAIN_NS}">` +
|
|
340
|
+
numFmtsXml +
|
|
341
|
+
`<fonts count="${this.fonts.length}">${this.fonts.join('')}</fonts>` +
|
|
342
|
+
`<fills count="${this.fills.length}">${this.fills.join('')}</fills>` +
|
|
343
|
+
'<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>' +
|
|
344
|
+
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>' +
|
|
345
|
+
`<cellXfs count="${this.xfs.length}">${this.xfs.join('')}</cellXfs>` +
|
|
346
|
+
'</styleSheet>');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
350
|
+
// Object model
|
|
351
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
352
|
+
class NxeCell {
|
|
353
|
+
value = null;
|
|
354
|
+
numFmt;
|
|
355
|
+
/** Default MUST be undefined (never a shared `{}`) — `formatExcel` reads `cell.font?.name`. */
|
|
356
|
+
font;
|
|
357
|
+
fill;
|
|
358
|
+
alignment;
|
|
359
|
+
}
|
|
360
|
+
class NxeRow {
|
|
361
|
+
/** 1-based column → cell (only accessed columns exist). */
|
|
362
|
+
cells = new Map();
|
|
363
|
+
_font;
|
|
364
|
+
/**
|
|
365
|
+
* exceljs parity: setting `row.font` overwrites every existing cell's font and seeds cells
|
|
366
|
+
* created later in this row. Each cell gets its own copy — `formatExcel`-style passes mutate
|
|
367
|
+
* `cell.font` in place, so cells must not alias one object.
|
|
368
|
+
*/
|
|
369
|
+
get font() {
|
|
370
|
+
return this._font;
|
|
371
|
+
}
|
|
372
|
+
set font(f) {
|
|
373
|
+
this._font = f;
|
|
374
|
+
this.cells.forEach((cell) => {
|
|
375
|
+
cell.font = f ? { ...f } : undefined;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
getCell(col) {
|
|
379
|
+
let c = this.cells.get(col);
|
|
380
|
+
if (!c) {
|
|
381
|
+
c = new NxeCell();
|
|
382
|
+
if (this._font) {
|
|
383
|
+
c.font = { ...this._font };
|
|
384
|
+
}
|
|
385
|
+
this.cells.set(col, c);
|
|
386
|
+
}
|
|
387
|
+
return c;
|
|
388
|
+
}
|
|
389
|
+
eachCell(cb) {
|
|
390
|
+
for (const col of [...this.cells.keys()].sort((a, b) => a - b)) {
|
|
391
|
+
cb(this.cells.get(col), col);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Column facet (exceljs `Column` slice) — only `width` (Excel character units) is supported. */
|
|
396
|
+
class NxeColumn {
|
|
397
|
+
number;
|
|
398
|
+
width;
|
|
399
|
+
constructor(number) {
|
|
400
|
+
this.number = number;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
class NxeWorksheet {
|
|
404
|
+
name;
|
|
405
|
+
autoFilter;
|
|
406
|
+
rows = new Map();
|
|
407
|
+
maxRow = 0;
|
|
408
|
+
merges = [];
|
|
409
|
+
/** 1-based column → column facet (only accessed columns exist). */
|
|
410
|
+
colMap = new Map();
|
|
411
|
+
constructor(name) {
|
|
412
|
+
this.name = name;
|
|
413
|
+
}
|
|
414
|
+
/** Highest populated row (0 when empty) — `getCell` auto-extends past the `addRow` cursor. */
|
|
415
|
+
get rowCount() {
|
|
416
|
+
return this.maxRow;
|
|
417
|
+
}
|
|
418
|
+
rowAt(r) {
|
|
419
|
+
let row = this.rows.get(r);
|
|
420
|
+
if (!row) {
|
|
421
|
+
row = new NxeRow();
|
|
422
|
+
this.rows.set(r, row);
|
|
423
|
+
}
|
|
424
|
+
if (r > this.maxRow) {
|
|
425
|
+
this.maxRow = r;
|
|
426
|
+
}
|
|
427
|
+
return row;
|
|
428
|
+
}
|
|
429
|
+
addRow(values) {
|
|
430
|
+
const row = this.rowAt(this.maxRow + 1);
|
|
431
|
+
values.forEach((v, i) => {
|
|
432
|
+
row.getCell(i + 1).value = v;
|
|
433
|
+
});
|
|
434
|
+
return row;
|
|
435
|
+
}
|
|
436
|
+
/** 1-based (row, col); auto-extends the sheet (the pivot exporter writes past the cursor). */
|
|
437
|
+
getCell(row, col) {
|
|
438
|
+
return this.rowAt(row).getCell(col);
|
|
439
|
+
}
|
|
440
|
+
/** Lazily-created column facet, 1-based (exceljs `getColumn`); may point past the used range. */
|
|
441
|
+
getColumn(n) {
|
|
442
|
+
let c = this.colMap.get(n);
|
|
443
|
+
if (!c) {
|
|
444
|
+
c = new NxeColumn(n);
|
|
445
|
+
this.colMap.set(n, c);
|
|
446
|
+
}
|
|
447
|
+
return c;
|
|
448
|
+
}
|
|
449
|
+
/** exceljs parity: one entry per column touched so far (via `addRow`/`getCell`/`getColumn`). */
|
|
450
|
+
get columns() {
|
|
451
|
+
let max = 0;
|
|
452
|
+
this.eachRow((row) => row.eachCell((_c, col) => (max = Math.max(max, col))));
|
|
453
|
+
for (const n of this.colMap.keys()) {
|
|
454
|
+
max = Math.max(max, n);
|
|
455
|
+
}
|
|
456
|
+
const out = [];
|
|
457
|
+
for (let n = 1; n <= max; n++) {
|
|
458
|
+
out.push(this.getColumn(n));
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
mergeCells(r1, c1, r2, c2) {
|
|
463
|
+
this.merges.push(`${cellRef(r1, c1)}:${cellRef(r2, c2)}`);
|
|
464
|
+
this.getCell(r1, c1); // ensure the anchor cell renders
|
|
465
|
+
}
|
|
466
|
+
eachRow(cb) {
|
|
467
|
+
for (const r of [...this.rows.keys()].sort((a, b) => a - b)) {
|
|
468
|
+
cb(this.rows.get(r), r);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/** @internal Serialize this sheet to its worksheet XML, registering styles as it goes. */
|
|
472
|
+
toXml(styles) {
|
|
473
|
+
const rowsXml = [];
|
|
474
|
+
this.eachRow((row, r) => {
|
|
475
|
+
const cellsXml = [];
|
|
476
|
+
row.eachCell((cell, c) => {
|
|
477
|
+
const xml = buildCellXml(cell, r, c, styles);
|
|
478
|
+
if (xml) {
|
|
479
|
+
cellsXml.push(xml);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
if (cellsXml.length) {
|
|
483
|
+
rowsXml.push(`<row r="${r}">${cellsXml.join('')}</row>`);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
const sheetData = rowsXml.length ? `<sheetData>${rowsXml.join('')}</sheetData>` : '<sheetData/>';
|
|
487
|
+
const af = this.autoFilter;
|
|
488
|
+
const autoFilterXml = af
|
|
489
|
+
? `<autoFilter ref="${cellRef(af.from.row, af.from.column)}:${cellRef(af.to.row, af.to.column)}"/>`
|
|
490
|
+
: '';
|
|
491
|
+
const mergeXml = this.merges.length
|
|
492
|
+
? `<mergeCells count="${this.merges.length}">${this.merges.map((m) => `<mergeCell ref="${m}"/>`).join('')}</mergeCells>`
|
|
493
|
+
: '';
|
|
494
|
+
const widthCols = [...this.colMap.values()]
|
|
495
|
+
.filter((c) => c.width != null)
|
|
496
|
+
.sort((a, b) => a.number - b.number);
|
|
497
|
+
// Schema requires ≥1 <col> child — never emit an empty <cols/>.
|
|
498
|
+
const colsXml = widthCols.length
|
|
499
|
+
? `<cols>${widthCols.map((c) => `<col min="${c.number}" max="${c.number}" width="${c.width}" customWidth="1"/>`).join('')}</cols>`
|
|
500
|
+
: '';
|
|
501
|
+
// Schema order: cols → sheetData → autoFilter → mergeCells (regardless of call order).
|
|
502
|
+
return ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
503
|
+
`<worksheet xmlns="${MAIN_NS}" xmlns:r="${REL_NS}">` +
|
|
504
|
+
colsXml +
|
|
505
|
+
sheetData +
|
|
506
|
+
autoFilterXml +
|
|
507
|
+
mergeXml +
|
|
508
|
+
'</worksheet>');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function buildCellXml(cell, r, c, styles) {
|
|
512
|
+
const ref = cellRef(r, c);
|
|
513
|
+
const s = styles.cellStyle(cell);
|
|
514
|
+
const sAttr = s !== 0 ? ` s="${s}"` : '';
|
|
515
|
+
const v = cell.value;
|
|
516
|
+
if (typeof v === 'number' && isFinite(v)) {
|
|
517
|
+
return `<c r="${ref}"${sAttr}><v>${v}</v></c>`;
|
|
518
|
+
}
|
|
519
|
+
if (v instanceof Date && !isNaN(v.getTime())) {
|
|
520
|
+
return `<c r="${ref}"${sAttr}><v>${excelSerial(v)}</v></c>`;
|
|
521
|
+
}
|
|
522
|
+
if (v !== null && v !== undefined && v !== '') {
|
|
523
|
+
const text = String(v);
|
|
524
|
+
return `<c r="${ref}"${sAttr} t="inlineStr"><is><t${preserve(text)}>${escapeText(text)}</t></is></c>`;
|
|
525
|
+
}
|
|
526
|
+
// Empty value: emit only if it carries a non-default style; otherwise omit entirely.
|
|
527
|
+
return s !== 0 ? `<c r="${ref}"${sAttr}/>` : '';
|
|
528
|
+
}
|
|
529
|
+
class NxeWorkbook {
|
|
530
|
+
worksheets = [];
|
|
531
|
+
/** Mirrors `workbook.xlsx.writeBuffer()` — resolves to the packed `.xlsx` bytes. */
|
|
532
|
+
xlsx = { writeBuffer: () => Promise.resolve(this.serialize()) };
|
|
533
|
+
addWorksheet(name) {
|
|
534
|
+
const ws = new NxeWorksheet(name || `Sheet${this.worksheets.length + 1}`);
|
|
535
|
+
this.worksheets.push(ws);
|
|
536
|
+
return ws;
|
|
537
|
+
}
|
|
538
|
+
eachSheet(cb) {
|
|
539
|
+
this.worksheets.forEach((ws, i) => cb(ws, i + 1));
|
|
540
|
+
}
|
|
541
|
+
/** Build the full OOXML package and pack it as a STORE ZIP. */
|
|
542
|
+
serialize() {
|
|
543
|
+
const styles = new StyleRegistry();
|
|
544
|
+
const sheetXmls = this.worksheets.map((ws) => ws.toXml(styles)); // registers styles
|
|
545
|
+
const stylesXml = styles.toXml();
|
|
546
|
+
const n = this.worksheets.length;
|
|
547
|
+
const overrides = this.worksheets
|
|
548
|
+
.map((_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`)
|
|
549
|
+
.join('');
|
|
550
|
+
const contentTypes = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
551
|
+
`<Types xmlns="${CT_NS}">` +
|
|
552
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
553
|
+
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
554
|
+
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
|
|
555
|
+
'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' +
|
|
556
|
+
overrides +
|
|
557
|
+
'</Types>';
|
|
558
|
+
const rootRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
559
|
+
`<Relationships xmlns="${PKG_REL_NS}">` +
|
|
560
|
+
`<Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/>` +
|
|
561
|
+
'</Relationships>';
|
|
562
|
+
const sheetsXml = this.worksheets
|
|
563
|
+
.map((ws, i) => `<sheet name="${escapeAttr(ws.name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`)
|
|
564
|
+
.join('');
|
|
565
|
+
const workbookXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
566
|
+
`<workbook xmlns="${MAIN_NS}" xmlns:r="${REL_NS}"><sheets>${sheetsXml}</sheets></workbook>`;
|
|
567
|
+
const wsRels = this.worksheets
|
|
568
|
+
.map((_, i) => `<Relationship Id="rId${i + 1}" Type="${REL_NS}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`)
|
|
569
|
+
.join('');
|
|
570
|
+
const workbookRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
571
|
+
`<Relationships xmlns="${PKG_REL_NS}">` +
|
|
572
|
+
wsRels +
|
|
573
|
+
`<Relationship Id="rId${n + 1}" Type="${REL_NS}/styles" Target="styles.xml"/>` +
|
|
574
|
+
'</Relationships>';
|
|
575
|
+
const enc = new TextEncoder();
|
|
576
|
+
const part = (name, xml) => ({ name, bytes: enc.encode(xml) });
|
|
577
|
+
const parts = [
|
|
578
|
+
part('[Content_Types].xml', contentTypes),
|
|
579
|
+
part('_rels/.rels', rootRels),
|
|
580
|
+
part('xl/workbook.xml', workbookXml),
|
|
581
|
+
part('xl/_rels/workbook.xml.rels', workbookRels),
|
|
582
|
+
part('xl/styles.xml', stylesXml),
|
|
583
|
+
...sheetXmls.map((xml, i) => part(`xl/worksheets/sheet${i + 1}.xml`, xml)),
|
|
584
|
+
];
|
|
585
|
+
return writeZip(parts);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Zero-dependency, in-repo `.xlsx` reader — replaces SheetJS (`xlsx`), which was used in exactly one
|
|
591
|
+
* place to read an uploaded workbook into a positional array-of-arrays. Modern `.xlsx` only (legacy
|
|
592
|
+
* binary `.xls` is intentionally unsupported — guard with {@link isXlsxBytes} and reject upstream).
|
|
593
|
+
*
|
|
594
|
+
* Pipeline: {@link readCentralDirectory}/{@link readZipEntryText} (from {@link './nxe-zip'}) unzip the
|
|
595
|
+
* needed OOXML parts, then the browser-native `DOMParser` reads `xl/workbook.xml` → first sheet,
|
|
596
|
+
* `xl/_rels/workbook.xml.rels` → its path, optional `xl/sharedStrings.xml` → the string table, and
|
|
597
|
+
* the sheet XML → cells. Output mirrors SheetJS `sheet_to_json(ws, { header: 1 })`: one array per
|
|
598
|
+
* row, values placed at their column index (gaps filled with `''`), numbers kept as real numbers.
|
|
599
|
+
*/
|
|
600
|
+
/** True when the bytes begin with the ZIP local-file-header magic (`PK\x03\x04`) — i.e. an `.xlsx`. */
|
|
601
|
+
function isXlsxBytes(buf) {
|
|
602
|
+
return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04;
|
|
603
|
+
}
|
|
604
|
+
function parseXml(text) {
|
|
605
|
+
const doc = new DOMParser().parseFromString(text, 'application/xml');
|
|
606
|
+
if (doc.getElementsByTagName('parsererror').length) {
|
|
607
|
+
throw new Error('Malformed .xlsx (could not parse XML).');
|
|
608
|
+
}
|
|
609
|
+
return doc;
|
|
610
|
+
}
|
|
611
|
+
/** Read a namespaced attribute by its local name (e.g. the `r:id` on a `<sheet>`). */
|
|
612
|
+
function attrByLocalName(el, local) {
|
|
613
|
+
const direct = el.getAttribute(local) ?? el.getAttribute('r:' + local);
|
|
614
|
+
if (direct != null) {
|
|
615
|
+
return direct;
|
|
616
|
+
}
|
|
617
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
618
|
+
if (el.attributes[i].localName === local) {
|
|
619
|
+
return el.attributes[i].value;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
/** A1 reference → 0-based { col, row }. */
|
|
625
|
+
function refToColRow(ref) {
|
|
626
|
+
const m = /^([A-Z]+)(\d+)$/.exec(ref);
|
|
627
|
+
if (!m) {
|
|
628
|
+
return { col: 0, row: 0 };
|
|
629
|
+
}
|
|
630
|
+
let col = 0;
|
|
631
|
+
for (const ch of m[1]) {
|
|
632
|
+
col = col * 26 + (ch.charCodeAt(0) - 64);
|
|
633
|
+
}
|
|
634
|
+
return { col: col - 1, row: Number(m[2]) - 1 };
|
|
635
|
+
}
|
|
636
|
+
/** Concatenate every descendant `<t>` text of a node (covers rich-text runs in shared strings). */
|
|
637
|
+
function collectText(node) {
|
|
638
|
+
const ts = node.getElementsByTagName('t');
|
|
639
|
+
if (!ts.length) {
|
|
640
|
+
return '';
|
|
641
|
+
}
|
|
642
|
+
let out = '';
|
|
643
|
+
for (let i = 0; i < ts.length; i++) {
|
|
644
|
+
out += ts[i].textContent ?? '';
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
function resolveFirstSheetPath(dir, workbookXml, relsXml) {
|
|
649
|
+
const wb = parseXml(workbookXml);
|
|
650
|
+
const sheets = wb.getElementsByTagName('sheet');
|
|
651
|
+
const fallback = 'xl/worksheets/sheet1.xml';
|
|
652
|
+
if (!sheets.length) {
|
|
653
|
+
return fallback;
|
|
654
|
+
}
|
|
655
|
+
const rid = attrByLocalName(sheets[0], 'id');
|
|
656
|
+
if (rid && relsXml) {
|
|
657
|
+
const rels = parseXml(relsXml);
|
|
658
|
+
const relationships = rels.getElementsByTagName('Relationship');
|
|
659
|
+
for (let i = 0; i < relationships.length; i++) {
|
|
660
|
+
if (relationships[i].getAttribute('Id') === rid) {
|
|
661
|
+
let target = relationships[i].getAttribute('Target') || '';
|
|
662
|
+
target = target.replace(/^\//, '').replace(/^xl\//, '');
|
|
663
|
+
const full = 'xl/' + target.replace(/^\.\.\//, '');
|
|
664
|
+
return dir.has(full) ? full : fallback;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return dir.has(fallback) ? fallback : [...dir.keys()].find((k) => /^xl\/worksheets\/.+\.xml$/.test(k)) || fallback;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Read the FIRST worksheet of an `.xlsx` into a dense, positional array-of-arrays (matching SheetJS
|
|
672
|
+
* `sheet_to_json(ws, { header: 1 })`). Numeric cells are returned as numbers; string cells as
|
|
673
|
+
* strings; missing leading/intermediate cells as `''`.
|
|
674
|
+
*/
|
|
675
|
+
async function readXlsxFirstSheet(input) {
|
|
676
|
+
const buf = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
677
|
+
if (!isXlsxBytes(buf)) {
|
|
678
|
+
throw new Error('Not an .xlsx file.');
|
|
679
|
+
}
|
|
680
|
+
const dir = readCentralDirectory(buf);
|
|
681
|
+
const get = async (name) => {
|
|
682
|
+
const entry = dir.get(name);
|
|
683
|
+
return entry ? readZipEntryText(buf, entry) : null;
|
|
684
|
+
};
|
|
685
|
+
const workbookXml = await get('xl/workbook.xml');
|
|
686
|
+
if (!workbookXml) {
|
|
687
|
+
throw new Error('Invalid .xlsx (missing xl/workbook.xml).');
|
|
688
|
+
}
|
|
689
|
+
const relsXml = await get('xl/_rels/workbook.xml.rels');
|
|
690
|
+
const sheetPath = resolveFirstSheetPath(dir, workbookXml, relsXml);
|
|
691
|
+
// Shared strings (optional).
|
|
692
|
+
const sharedStrings = [];
|
|
693
|
+
const sharedXml = await get('xl/sharedStrings.xml');
|
|
694
|
+
if (sharedXml) {
|
|
695
|
+
const si = parseXml(sharedXml).getElementsByTagName('si');
|
|
696
|
+
for (let i = 0; i < si.length; i++) {
|
|
697
|
+
sharedStrings.push(collectText(si[i]));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const sheetXml = await get(sheetPath);
|
|
701
|
+
if (!sheetXml) {
|
|
702
|
+
throw new Error('Invalid .xlsx (missing worksheet part).');
|
|
703
|
+
}
|
|
704
|
+
const result = [];
|
|
705
|
+
const rows = parseXml(sheetXml).getElementsByTagName('row');
|
|
706
|
+
let runningRow = 0;
|
|
707
|
+
for (let i = 0; i < rows.length; i++) {
|
|
708
|
+
const rowEl = rows[i];
|
|
709
|
+
const rAttr = rowEl.getAttribute('r');
|
|
710
|
+
const rowIdx = rAttr ? Number(rAttr) - 1 : runningRow;
|
|
711
|
+
runningRow = rowIdx + 1;
|
|
712
|
+
const out = [];
|
|
713
|
+
const cells = rowEl.getElementsByTagName('c');
|
|
714
|
+
let runningCol = 0;
|
|
715
|
+
for (let j = 0; j < cells.length; j++) {
|
|
716
|
+
const cell = cells[j];
|
|
717
|
+
const ref = cell.getAttribute('r');
|
|
718
|
+
const colIdx = ref ? refToColRow(ref).col : runningCol;
|
|
719
|
+
runningCol = colIdx + 1;
|
|
720
|
+
const t = cell.getAttribute('t');
|
|
721
|
+
let value = '';
|
|
722
|
+
if (t === 's') {
|
|
723
|
+
const v = cell.getElementsByTagName('v')[0];
|
|
724
|
+
value = v ? sharedStrings[Number(v.textContent)] ?? '' : '';
|
|
725
|
+
}
|
|
726
|
+
else if (t === 'inlineStr') {
|
|
727
|
+
const is = cell.getElementsByTagName('is')[0];
|
|
728
|
+
value = is ? collectText(is) : '';
|
|
729
|
+
}
|
|
730
|
+
else if (t === 'str') {
|
|
731
|
+
const v = cell.getElementsByTagName('v')[0];
|
|
732
|
+
value = v ? v.textContent ?? '' : '';
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// number / boolean / default
|
|
736
|
+
const v = cell.getElementsByTagName('v')[0];
|
|
737
|
+
value = v && v.textContent !== '' ? Number(v.textContent) : '';
|
|
738
|
+
}
|
|
739
|
+
for (let k = out.length; k < colIdx; k++) {
|
|
740
|
+
out[k] = '';
|
|
741
|
+
}
|
|
742
|
+
out[colIdx] = value;
|
|
743
|
+
}
|
|
744
|
+
result[rowIdx] = out;
|
|
745
|
+
}
|
|
746
|
+
// Fill any skipped rows so the array is dense (positional), like SheetJS header:1.
|
|
747
|
+
for (let i = 0; i < result.length; i++) {
|
|
748
|
+
if (!result[i]) {
|
|
749
|
+
result[i] = [];
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Tiny in-repo replacement for `file-saver-es`'s `saveAs`. Triggers a browser download of a Blob by
|
|
757
|
+
* clicking a synthetic anchor with an object URL, then revoking it. Callers keep building their own
|
|
758
|
+
* Blob (so the MIME type is preserved).
|
|
759
|
+
*/
|
|
760
|
+
function nxeSaveAs(blob, fileName) {
|
|
761
|
+
const url = URL.createObjectURL(blob);
|
|
762
|
+
const a = document.createElement('a');
|
|
763
|
+
a.href = url;
|
|
764
|
+
a.download = fileName;
|
|
765
|
+
a.style.display = 'none';
|
|
766
|
+
document.body.appendChild(a);
|
|
767
|
+
a.click();
|
|
768
|
+
document.body.removeChild(a);
|
|
769
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// @nexterajs/ui/excel — public API.
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Generated bundle index. Do not edit.
|
|
776
|
+
*/
|
|
777
|
+
|
|
778
|
+
export { NxeCell, NxeColumn, NxeRow, NxeWorkbook, NxeWorksheet, NxeWorkbook as Workbook, colLetter, isXlsxBytes, nxeSaveAs, readXlsxFirstSheet };
|
|
779
|
+
//# sourceMappingURL=nexterajs-ui-excel.mjs.map
|