@niicojs/excel 0.3.1 → 0.3.3
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 +20 -20
- package/README.md +585 -585
- package/dist/index.cjs +1004 -438
- package/dist/index.d.cts +53 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1005 -439
- package/package.json +3 -3
- package/src/cell.ts +20 -6
- package/src/index.ts +45 -45
- package/src/pivot-cache.ts +501 -300
- package/src/pivot-table.ts +680 -684
- package/src/range.ts +154 -154
- package/src/shared-strings.ts +185 -178
- package/src/styles.ts +819 -819
- package/src/table.ts +386 -386
- package/src/types.ts +54 -36
- package/src/utils/address.ts +121 -121
- package/src/utils/format.ts +356 -0
- package/src/utils/xml.ts +153 -140
- package/src/utils/zip.ts +29 -5
- package/src/workbook.ts +1412 -1390
- package/src/worksheet.ts +85 -84
package/src/table.ts
CHANGED
|
@@ -1,386 +1,386 @@
|
|
|
1
|
-
import type { TableConfig, TableStyleConfig, TableTotalFunction, RangeAddress } from './types';
|
|
2
|
-
import type { Worksheet } from './worksheet';
|
|
3
|
-
import { parseRange, toAddress, toRange } from './utils/address';
|
|
4
|
-
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Maps table total function names to SUBTOTAL function numbers
|
|
8
|
-
* SUBTOTAL uses 101-111 for functions that ignore hidden values
|
|
9
|
-
*/
|
|
10
|
-
const TOTAL_FUNCTION_NUMBERS: Record<TableTotalFunction, number> = {
|
|
11
|
-
average: 101,
|
|
12
|
-
count: 102,
|
|
13
|
-
countNums: 103,
|
|
14
|
-
max: 104,
|
|
15
|
-
min: 105,
|
|
16
|
-
stdDev: 107,
|
|
17
|
-
sum: 109,
|
|
18
|
-
var: 110,
|
|
19
|
-
none: 0,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Maps table total function names to XML attribute values
|
|
24
|
-
*/
|
|
25
|
-
const TOTAL_FUNCTION_NAMES: Record<TableTotalFunction, string> = {
|
|
26
|
-
average: 'average',
|
|
27
|
-
count: 'count',
|
|
28
|
-
countNums: 'countNums',
|
|
29
|
-
max: 'max',
|
|
30
|
-
min: 'min',
|
|
31
|
-
stdDev: 'stdDev',
|
|
32
|
-
sum: 'sum',
|
|
33
|
-
var: 'var',
|
|
34
|
-
none: 'none',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Represents an Excel Table (ListObject) with auto-filter, banded styling, and total row.
|
|
39
|
-
*/
|
|
40
|
-
export class Table {
|
|
41
|
-
private _name: string;
|
|
42
|
-
private _displayName: string;
|
|
43
|
-
private _worksheet: Worksheet;
|
|
44
|
-
private _range: RangeAddress;
|
|
45
|
-
private _baseRange: RangeAddress;
|
|
46
|
-
private _totalRow: boolean;
|
|
47
|
-
private _autoFilter: boolean;
|
|
48
|
-
private _style: TableStyleConfig;
|
|
49
|
-
private _columns: TableColumn[] = [];
|
|
50
|
-
private _id: number;
|
|
51
|
-
private _dirty = true;
|
|
52
|
-
private _headerRow: boolean;
|
|
53
|
-
|
|
54
|
-
constructor(worksheet: Worksheet, config: TableConfig, tableId: number) {
|
|
55
|
-
this._worksheet = worksheet;
|
|
56
|
-
this._name = config.name;
|
|
57
|
-
this._displayName = config.name;
|
|
58
|
-
this._range = parseRange(config.range);
|
|
59
|
-
this._baseRange = { start: { ...this._range.start }, end: { ...this._range.end } };
|
|
60
|
-
this._totalRow = config.totalRow === true; // Default false
|
|
61
|
-
this._autoFilter = true; // Tables have auto-filter by default
|
|
62
|
-
this._headerRow = config.headerRow !== false;
|
|
63
|
-
this._id = tableId;
|
|
64
|
-
|
|
65
|
-
// Expand range to include total row if enabled
|
|
66
|
-
if (this._totalRow) {
|
|
67
|
-
this._range.end.row++;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Set default style
|
|
71
|
-
this._style = {
|
|
72
|
-
name: config.style?.name ?? 'TableStyleMedium2',
|
|
73
|
-
showRowStripes: config.style?.showRowStripes !== false, // Default true
|
|
74
|
-
showColumnStripes: config.style?.showColumnStripes === true, // Default false
|
|
75
|
-
showFirstColumn: config.style?.showFirstColumn === true, // Default false
|
|
76
|
-
showLastColumn: config.style?.showLastColumn === true, // Default false
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Extract column names from worksheet headers
|
|
80
|
-
this._extractColumns();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get the table name
|
|
85
|
-
*/
|
|
86
|
-
get name(): string {
|
|
87
|
-
return this._name;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get the table display name
|
|
92
|
-
*/
|
|
93
|
-
get displayName(): string {
|
|
94
|
-
return this._displayName;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Get the table ID
|
|
99
|
-
*/
|
|
100
|
-
get id(): number {
|
|
101
|
-
return this._id;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Get the worksheet this table belongs to
|
|
106
|
-
*/
|
|
107
|
-
get worksheet(): Worksheet {
|
|
108
|
-
return this._worksheet;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Get the table range address string
|
|
113
|
-
*/
|
|
114
|
-
get range(): string {
|
|
115
|
-
return toRange(this._range);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get the base range excluding total row
|
|
120
|
-
*/
|
|
121
|
-
get baseRange(): string {
|
|
122
|
-
return toRange(this._baseRange);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Get the table range as RangeAddress
|
|
127
|
-
*/
|
|
128
|
-
get rangeAddress(): RangeAddress {
|
|
129
|
-
return { ...this._range };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Get column names
|
|
134
|
-
*/
|
|
135
|
-
get columns(): string[] {
|
|
136
|
-
return this._columns.map((c) => c.name);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Check if table has a total row
|
|
141
|
-
*/
|
|
142
|
-
get hasTotalRow(): boolean {
|
|
143
|
-
return this._totalRow;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Check if table has a header row
|
|
148
|
-
*/
|
|
149
|
-
get hasHeaderRow(): boolean {
|
|
150
|
-
return this._headerRow;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Check if table has auto-filter enabled
|
|
155
|
-
*/
|
|
156
|
-
get hasAutoFilter(): boolean {
|
|
157
|
-
return this._autoFilter;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get the current style configuration
|
|
162
|
-
*/
|
|
163
|
-
get style(): TableStyleConfig {
|
|
164
|
-
return { ...this._style };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Check if the table has been modified
|
|
169
|
-
*/
|
|
170
|
-
get dirty(): boolean {
|
|
171
|
-
return this._dirty;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Set a total function for a column
|
|
176
|
-
* @param columnName - Name of the column (header text)
|
|
177
|
-
* @param fn - Aggregation function to use
|
|
178
|
-
* @returns this for method chaining
|
|
179
|
-
*/
|
|
180
|
-
setTotalFunction(columnName: string, fn: TableTotalFunction): this {
|
|
181
|
-
if (!this._totalRow) {
|
|
182
|
-
throw new Error('Cannot set total function: table does not have a total row enabled');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const column = this._columns.find((c) => c.name === columnName);
|
|
186
|
-
if (!column) {
|
|
187
|
-
throw new Error(`Column not found: ${columnName}`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
column.totalFunction = fn;
|
|
191
|
-
this._dirty = true;
|
|
192
|
-
|
|
193
|
-
// Write the formula to the total row cell
|
|
194
|
-
this._writeTotalRowFormula(column);
|
|
195
|
-
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Get total function for a column if set
|
|
201
|
-
*/
|
|
202
|
-
getTotalFunction(columnName: string): TableTotalFunction | undefined {
|
|
203
|
-
const column = this._columns.find((c) => c.name === columnName);
|
|
204
|
-
return column?.totalFunction;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Enable or disable auto-filter
|
|
209
|
-
* @param enabled - Whether auto-filter should be enabled
|
|
210
|
-
* @returns this for method chaining
|
|
211
|
-
*/
|
|
212
|
-
setAutoFilter(enabled: boolean): this {
|
|
213
|
-
this._autoFilter = enabled;
|
|
214
|
-
this._dirty = true;
|
|
215
|
-
return this;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Update table style configuration
|
|
220
|
-
* @param style - Style options to apply
|
|
221
|
-
* @returns this for method chaining
|
|
222
|
-
*/
|
|
223
|
-
setStyle(style: Partial<TableStyleConfig>): this {
|
|
224
|
-
if (style.name !== undefined) this._style.name = style.name;
|
|
225
|
-
if (style.showRowStripes !== undefined) this._style.showRowStripes = style.showRowStripes;
|
|
226
|
-
if (style.showColumnStripes !== undefined) this._style.showColumnStripes = style.showColumnStripes;
|
|
227
|
-
if (style.showFirstColumn !== undefined) this._style.showFirstColumn = style.showFirstColumn;
|
|
228
|
-
if (style.showLastColumn !== undefined) this._style.showLastColumn = style.showLastColumn;
|
|
229
|
-
this._dirty = true;
|
|
230
|
-
return this;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Enable or disable the total row
|
|
235
|
-
* @param enabled - Whether total row should be shown
|
|
236
|
-
* @returns this for method chaining
|
|
237
|
-
*/
|
|
238
|
-
setTotalRow(enabled: boolean): this {
|
|
239
|
-
if (this._totalRow === enabled) return this;
|
|
240
|
-
|
|
241
|
-
this._totalRow = enabled;
|
|
242
|
-
this._dirty = true;
|
|
243
|
-
|
|
244
|
-
if (enabled) {
|
|
245
|
-
this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
|
|
246
|
-
this._range.end.row++;
|
|
247
|
-
} else {
|
|
248
|
-
this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
|
|
249
|
-
for (const col of this._columns) {
|
|
250
|
-
col.totalFunction = undefined;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return this;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Extract column names from the header row of the worksheet
|
|
259
|
-
*/
|
|
260
|
-
private _extractColumns(): void {
|
|
261
|
-
const headerRow = this._range.start.row;
|
|
262
|
-
const startCol = this._range.start.col;
|
|
263
|
-
const endCol = this._range.end.col;
|
|
264
|
-
|
|
265
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
266
|
-
const cell = this._headerRow ? this._worksheet.getCellIfExists(headerRow, col) : undefined;
|
|
267
|
-
const value = cell?.value;
|
|
268
|
-
const name = value != null ? String(value) : `Column${col - startCol + 1}`;
|
|
269
|
-
|
|
270
|
-
this._columns.push({
|
|
271
|
-
id: col - startCol + 1,
|
|
272
|
-
name,
|
|
273
|
-
colIndex: col,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Write the SUBTOTAL formula to a total row cell
|
|
280
|
-
*/
|
|
281
|
-
private _writeTotalRowFormula(column: TableColumn): void {
|
|
282
|
-
if (!this._totalRow || !column.totalFunction || column.totalFunction === 'none') {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const totalRowIndex = this._range.end.row;
|
|
287
|
-
const cell = this._worksheet.cell(totalRowIndex, column.colIndex);
|
|
288
|
-
|
|
289
|
-
// Generate SUBTOTAL formula with structured reference
|
|
290
|
-
const funcNum = TOTAL_FUNCTION_NUMBERS[column.totalFunction];
|
|
291
|
-
// Use structured reference: SUBTOTAL(109,[ColumnName])
|
|
292
|
-
const formula = `SUBTOTAL(${funcNum},[${column.name}])`;
|
|
293
|
-
cell.formula = formula;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Get the auto-filter range (excludes total row if present)
|
|
298
|
-
*/
|
|
299
|
-
private _getAutoFilterRange(): string {
|
|
300
|
-
const start = toAddress(this._range.start.row, this._range.start.col);
|
|
301
|
-
|
|
302
|
-
// Auto-filter excludes the total row
|
|
303
|
-
let endRow = this._range.end.row;
|
|
304
|
-
if (this._totalRow) {
|
|
305
|
-
endRow--;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const end = toAddress(endRow, this._range.end.col);
|
|
309
|
-
return `${start}:${end}`;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Generate the table definition XML
|
|
314
|
-
*/
|
|
315
|
-
toXml(): string {
|
|
316
|
-
const children: XmlNode[] = [];
|
|
317
|
-
|
|
318
|
-
// Auto-filter element
|
|
319
|
-
if (this._autoFilter) {
|
|
320
|
-
const autoFilterRef = this._getAutoFilterRange();
|
|
321
|
-
children.push(createElement('autoFilter', { ref: autoFilterRef }, []));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Table columns
|
|
325
|
-
const columnNodes: XmlNode[] = this._columns.map((col) => {
|
|
326
|
-
const attrs: Record<string, string> = {
|
|
327
|
-
id: String(col.id),
|
|
328
|
-
name: col.name,
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
// Add total function if specified
|
|
332
|
-
if (this._totalRow && col.totalFunction && col.totalFunction !== 'none') {
|
|
333
|
-
attrs.totalsRowFunction = TOTAL_FUNCTION_NAMES[col.totalFunction];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return createElement('tableColumn', attrs, []);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
children.push(createElement('tableColumns', { count: String(columnNodes.length) }, columnNodes));
|
|
340
|
-
|
|
341
|
-
// Table style info
|
|
342
|
-
const styleAttrs: Record<string, string> = {
|
|
343
|
-
name: this._style.name || 'TableStyleMedium2',
|
|
344
|
-
showFirstColumn: this._style.showFirstColumn ? '1' : '0',
|
|
345
|
-
showLastColumn: this._style.showLastColumn ? '1' : '0',
|
|
346
|
-
showRowStripes: this._style.showRowStripes !== false ? '1' : '0',
|
|
347
|
-
showColumnStripes: this._style.showColumnStripes ? '1' : '0',
|
|
348
|
-
};
|
|
349
|
-
children.push(createElement('tableStyleInfo', styleAttrs, []));
|
|
350
|
-
|
|
351
|
-
// Build table attributes
|
|
352
|
-
const tableRef = toRange(this._range);
|
|
353
|
-
const tableAttrs: Record<string, string> = {
|
|
354
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
355
|
-
id: String(this._id),
|
|
356
|
-
name: this._name,
|
|
357
|
-
displayName: this._displayName,
|
|
358
|
-
ref: tableRef,
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
if (!this._headerRow) {
|
|
362
|
-
tableAttrs.headerRowCount = '0';
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (this._totalRow) {
|
|
366
|
-
tableAttrs.totalsRowCount = '1';
|
|
367
|
-
} else {
|
|
368
|
-
tableAttrs.totalsRowShown = '0';
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Build complete table node
|
|
372
|
-
const tableNode = createElement('table', tableAttrs, children);
|
|
373
|
-
|
|
374
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([tableNode])}`;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Internal column representation
|
|
380
|
-
*/
|
|
381
|
-
interface TableColumn {
|
|
382
|
-
id: number;
|
|
383
|
-
name: string;
|
|
384
|
-
colIndex: number;
|
|
385
|
-
totalFunction?: TableTotalFunction;
|
|
386
|
-
}
|
|
1
|
+
import type { TableConfig, TableStyleConfig, TableTotalFunction, RangeAddress } from './types';
|
|
2
|
+
import type { Worksheet } from './worksheet';
|
|
3
|
+
import { parseRange, toAddress, toRange } from './utils/address';
|
|
4
|
+
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maps table total function names to SUBTOTAL function numbers
|
|
8
|
+
* SUBTOTAL uses 101-111 for functions that ignore hidden values
|
|
9
|
+
*/
|
|
10
|
+
const TOTAL_FUNCTION_NUMBERS: Record<TableTotalFunction, number> = {
|
|
11
|
+
average: 101,
|
|
12
|
+
count: 102,
|
|
13
|
+
countNums: 103,
|
|
14
|
+
max: 104,
|
|
15
|
+
min: 105,
|
|
16
|
+
stdDev: 107,
|
|
17
|
+
sum: 109,
|
|
18
|
+
var: 110,
|
|
19
|
+
none: 0,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Maps table total function names to XML attribute values
|
|
24
|
+
*/
|
|
25
|
+
const TOTAL_FUNCTION_NAMES: Record<TableTotalFunction, string> = {
|
|
26
|
+
average: 'average',
|
|
27
|
+
count: 'count',
|
|
28
|
+
countNums: 'countNums',
|
|
29
|
+
max: 'max',
|
|
30
|
+
min: 'min',
|
|
31
|
+
stdDev: 'stdDev',
|
|
32
|
+
sum: 'sum',
|
|
33
|
+
var: 'var',
|
|
34
|
+
none: 'none',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Represents an Excel Table (ListObject) with auto-filter, banded styling, and total row.
|
|
39
|
+
*/
|
|
40
|
+
export class Table {
|
|
41
|
+
private _name: string;
|
|
42
|
+
private _displayName: string;
|
|
43
|
+
private _worksheet: Worksheet;
|
|
44
|
+
private _range: RangeAddress;
|
|
45
|
+
private _baseRange: RangeAddress;
|
|
46
|
+
private _totalRow: boolean;
|
|
47
|
+
private _autoFilter: boolean;
|
|
48
|
+
private _style: TableStyleConfig;
|
|
49
|
+
private _columns: TableColumn[] = [];
|
|
50
|
+
private _id: number;
|
|
51
|
+
private _dirty = true;
|
|
52
|
+
private _headerRow: boolean;
|
|
53
|
+
|
|
54
|
+
constructor(worksheet: Worksheet, config: TableConfig, tableId: number) {
|
|
55
|
+
this._worksheet = worksheet;
|
|
56
|
+
this._name = config.name;
|
|
57
|
+
this._displayName = config.name;
|
|
58
|
+
this._range = parseRange(config.range);
|
|
59
|
+
this._baseRange = { start: { ...this._range.start }, end: { ...this._range.end } };
|
|
60
|
+
this._totalRow = config.totalRow === true; // Default false
|
|
61
|
+
this._autoFilter = true; // Tables have auto-filter by default
|
|
62
|
+
this._headerRow = config.headerRow !== false;
|
|
63
|
+
this._id = tableId;
|
|
64
|
+
|
|
65
|
+
// Expand range to include total row if enabled
|
|
66
|
+
if (this._totalRow) {
|
|
67
|
+
this._range.end.row++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Set default style
|
|
71
|
+
this._style = {
|
|
72
|
+
name: config.style?.name ?? 'TableStyleMedium2',
|
|
73
|
+
showRowStripes: config.style?.showRowStripes !== false, // Default true
|
|
74
|
+
showColumnStripes: config.style?.showColumnStripes === true, // Default false
|
|
75
|
+
showFirstColumn: config.style?.showFirstColumn === true, // Default false
|
|
76
|
+
showLastColumn: config.style?.showLastColumn === true, // Default false
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Extract column names from worksheet headers
|
|
80
|
+
this._extractColumns();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the table name
|
|
85
|
+
*/
|
|
86
|
+
get name(): string {
|
|
87
|
+
return this._name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the table display name
|
|
92
|
+
*/
|
|
93
|
+
get displayName(): string {
|
|
94
|
+
return this._displayName;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the table ID
|
|
99
|
+
*/
|
|
100
|
+
get id(): number {
|
|
101
|
+
return this._id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the worksheet this table belongs to
|
|
106
|
+
*/
|
|
107
|
+
get worksheet(): Worksheet {
|
|
108
|
+
return this._worksheet;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the table range address string
|
|
113
|
+
*/
|
|
114
|
+
get range(): string {
|
|
115
|
+
return toRange(this._range);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the base range excluding total row
|
|
120
|
+
*/
|
|
121
|
+
get baseRange(): string {
|
|
122
|
+
return toRange(this._baseRange);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the table range as RangeAddress
|
|
127
|
+
*/
|
|
128
|
+
get rangeAddress(): RangeAddress {
|
|
129
|
+
return { ...this._range };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get column names
|
|
134
|
+
*/
|
|
135
|
+
get columns(): string[] {
|
|
136
|
+
return this._columns.map((c) => c.name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if table has a total row
|
|
141
|
+
*/
|
|
142
|
+
get hasTotalRow(): boolean {
|
|
143
|
+
return this._totalRow;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if table has a header row
|
|
148
|
+
*/
|
|
149
|
+
get hasHeaderRow(): boolean {
|
|
150
|
+
return this._headerRow;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if table has auto-filter enabled
|
|
155
|
+
*/
|
|
156
|
+
get hasAutoFilter(): boolean {
|
|
157
|
+
return this._autoFilter;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get the current style configuration
|
|
162
|
+
*/
|
|
163
|
+
get style(): TableStyleConfig {
|
|
164
|
+
return { ...this._style };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if the table has been modified
|
|
169
|
+
*/
|
|
170
|
+
get dirty(): boolean {
|
|
171
|
+
return this._dirty;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Set a total function for a column
|
|
176
|
+
* @param columnName - Name of the column (header text)
|
|
177
|
+
* @param fn - Aggregation function to use
|
|
178
|
+
* @returns this for method chaining
|
|
179
|
+
*/
|
|
180
|
+
setTotalFunction(columnName: string, fn: TableTotalFunction): this {
|
|
181
|
+
if (!this._totalRow) {
|
|
182
|
+
throw new Error('Cannot set total function: table does not have a total row enabled');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const column = this._columns.find((c) => c.name === columnName);
|
|
186
|
+
if (!column) {
|
|
187
|
+
throw new Error(`Column not found: ${columnName}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
column.totalFunction = fn;
|
|
191
|
+
this._dirty = true;
|
|
192
|
+
|
|
193
|
+
// Write the formula to the total row cell
|
|
194
|
+
this._writeTotalRowFormula(column);
|
|
195
|
+
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get total function for a column if set
|
|
201
|
+
*/
|
|
202
|
+
getTotalFunction(columnName: string): TableTotalFunction | undefined {
|
|
203
|
+
const column = this._columns.find((c) => c.name === columnName);
|
|
204
|
+
return column?.totalFunction;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Enable or disable auto-filter
|
|
209
|
+
* @param enabled - Whether auto-filter should be enabled
|
|
210
|
+
* @returns this for method chaining
|
|
211
|
+
*/
|
|
212
|
+
setAutoFilter(enabled: boolean): this {
|
|
213
|
+
this._autoFilter = enabled;
|
|
214
|
+
this._dirty = true;
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Update table style configuration
|
|
220
|
+
* @param style - Style options to apply
|
|
221
|
+
* @returns this for method chaining
|
|
222
|
+
*/
|
|
223
|
+
setStyle(style: Partial<TableStyleConfig>): this {
|
|
224
|
+
if (style.name !== undefined) this._style.name = style.name;
|
|
225
|
+
if (style.showRowStripes !== undefined) this._style.showRowStripes = style.showRowStripes;
|
|
226
|
+
if (style.showColumnStripes !== undefined) this._style.showColumnStripes = style.showColumnStripes;
|
|
227
|
+
if (style.showFirstColumn !== undefined) this._style.showFirstColumn = style.showFirstColumn;
|
|
228
|
+
if (style.showLastColumn !== undefined) this._style.showLastColumn = style.showLastColumn;
|
|
229
|
+
this._dirty = true;
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Enable or disable the total row
|
|
235
|
+
* @param enabled - Whether total row should be shown
|
|
236
|
+
* @returns this for method chaining
|
|
237
|
+
*/
|
|
238
|
+
setTotalRow(enabled: boolean): this {
|
|
239
|
+
if (this._totalRow === enabled) return this;
|
|
240
|
+
|
|
241
|
+
this._totalRow = enabled;
|
|
242
|
+
this._dirty = true;
|
|
243
|
+
|
|
244
|
+
if (enabled) {
|
|
245
|
+
this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
|
|
246
|
+
this._range.end.row++;
|
|
247
|
+
} else {
|
|
248
|
+
this._range = { start: { ...this._baseRange.start }, end: { ...this._baseRange.end } };
|
|
249
|
+
for (const col of this._columns) {
|
|
250
|
+
col.totalFunction = undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract column names from the header row of the worksheet
|
|
259
|
+
*/
|
|
260
|
+
private _extractColumns(): void {
|
|
261
|
+
const headerRow = this._range.start.row;
|
|
262
|
+
const startCol = this._range.start.col;
|
|
263
|
+
const endCol = this._range.end.col;
|
|
264
|
+
|
|
265
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
266
|
+
const cell = this._headerRow ? this._worksheet.getCellIfExists(headerRow, col) : undefined;
|
|
267
|
+
const value = cell?.value;
|
|
268
|
+
const name = value != null ? String(value) : `Column${col - startCol + 1}`;
|
|
269
|
+
|
|
270
|
+
this._columns.push({
|
|
271
|
+
id: col - startCol + 1,
|
|
272
|
+
name,
|
|
273
|
+
colIndex: col,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Write the SUBTOTAL formula to a total row cell
|
|
280
|
+
*/
|
|
281
|
+
private _writeTotalRowFormula(column: TableColumn): void {
|
|
282
|
+
if (!this._totalRow || !column.totalFunction || column.totalFunction === 'none') {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const totalRowIndex = this._range.end.row;
|
|
287
|
+
const cell = this._worksheet.cell(totalRowIndex, column.colIndex);
|
|
288
|
+
|
|
289
|
+
// Generate SUBTOTAL formula with structured reference
|
|
290
|
+
const funcNum = TOTAL_FUNCTION_NUMBERS[column.totalFunction];
|
|
291
|
+
// Use structured reference: SUBTOTAL(109,[ColumnName])
|
|
292
|
+
const formula = `SUBTOTAL(${funcNum},[${column.name}])`;
|
|
293
|
+
cell.formula = formula;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get the auto-filter range (excludes total row if present)
|
|
298
|
+
*/
|
|
299
|
+
private _getAutoFilterRange(): string {
|
|
300
|
+
const start = toAddress(this._range.start.row, this._range.start.col);
|
|
301
|
+
|
|
302
|
+
// Auto-filter excludes the total row
|
|
303
|
+
let endRow = this._range.end.row;
|
|
304
|
+
if (this._totalRow) {
|
|
305
|
+
endRow--;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const end = toAddress(endRow, this._range.end.col);
|
|
309
|
+
return `${start}:${end}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Generate the table definition XML
|
|
314
|
+
*/
|
|
315
|
+
toXml(): string {
|
|
316
|
+
const children: XmlNode[] = [];
|
|
317
|
+
|
|
318
|
+
// Auto-filter element
|
|
319
|
+
if (this._autoFilter) {
|
|
320
|
+
const autoFilterRef = this._getAutoFilterRange();
|
|
321
|
+
children.push(createElement('autoFilter', { ref: autoFilterRef }, []));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Table columns
|
|
325
|
+
const columnNodes: XmlNode[] = this._columns.map((col) => {
|
|
326
|
+
const attrs: Record<string, string> = {
|
|
327
|
+
id: String(col.id),
|
|
328
|
+
name: col.name,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Add total function if specified
|
|
332
|
+
if (this._totalRow && col.totalFunction && col.totalFunction !== 'none') {
|
|
333
|
+
attrs.totalsRowFunction = TOTAL_FUNCTION_NAMES[col.totalFunction];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return createElement('tableColumn', attrs, []);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
children.push(createElement('tableColumns', { count: String(columnNodes.length) }, columnNodes));
|
|
340
|
+
|
|
341
|
+
// Table style info
|
|
342
|
+
const styleAttrs: Record<string, string> = {
|
|
343
|
+
name: this._style.name || 'TableStyleMedium2',
|
|
344
|
+
showFirstColumn: this._style.showFirstColumn ? '1' : '0',
|
|
345
|
+
showLastColumn: this._style.showLastColumn ? '1' : '0',
|
|
346
|
+
showRowStripes: this._style.showRowStripes !== false ? '1' : '0',
|
|
347
|
+
showColumnStripes: this._style.showColumnStripes ? '1' : '0',
|
|
348
|
+
};
|
|
349
|
+
children.push(createElement('tableStyleInfo', styleAttrs, []));
|
|
350
|
+
|
|
351
|
+
// Build table attributes
|
|
352
|
+
const tableRef = toRange(this._range);
|
|
353
|
+
const tableAttrs: Record<string, string> = {
|
|
354
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
355
|
+
id: String(this._id),
|
|
356
|
+
name: this._name,
|
|
357
|
+
displayName: this._displayName,
|
|
358
|
+
ref: tableRef,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
if (!this._headerRow) {
|
|
362
|
+
tableAttrs.headerRowCount = '0';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this._totalRow) {
|
|
366
|
+
tableAttrs.totalsRowCount = '1';
|
|
367
|
+
} else {
|
|
368
|
+
tableAttrs.totalsRowShown = '0';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Build complete table node
|
|
372
|
+
const tableNode = createElement('table', tableAttrs, children);
|
|
373
|
+
|
|
374
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([tableNode])}`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Internal column representation
|
|
380
|
+
*/
|
|
381
|
+
interface TableColumn {
|
|
382
|
+
id: number;
|
|
383
|
+
name: string;
|
|
384
|
+
colIndex: number;
|
|
385
|
+
totalFunction?: TableTotalFunction;
|
|
386
|
+
}
|