@shbernal/ts-xlsx 1.1.0 → 1.3.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/README.md +26 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.js +0 -131
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.js +0 -60
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.js +0 -45
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -48
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.js +0 -66
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.js +0 -40
- package/dist/core/theme.js +0 -85
- package/dist/core/value.js +0 -94
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -1
- package/dist/core/workbook.js +0 -380
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +9 -0
- package/dist/core/worksheet.js +7 -506
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.js +0 -6
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +0 -18
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/opc/zip-mtime.d.ts +5 -0
- package/dist/io/opc/zip-mtime.js +1 -0
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +2 -55
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +7 -88
- package/dist/io/xlsx/read.js +36 -213
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +4 -66
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.d.ts +1 -1
- package/dist/io/xlsx/workbook-xml.js +6 -96
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +2 -137
- package/dist/io/xlsx/write.d.ts +5 -1
- package/dist/io/xlsx/write.js +5 -137
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +8 -9
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
package/dist/core/worksheet.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// A worksheet: a sparsely-populated grid of cells addressed by A1 reference.
|
|
2
|
-
//
|
|
3
|
-
// Storage is sparse by construction — a spreadsheet is mostly empty, so cells
|
|
4
|
-
// materialise on first access and only occupied positions cost memory. Column and
|
|
5
|
-
// row metadata (widths, heights, visibility, outline grouping) are stored apart from
|
|
6
|
-
// the cell grid, because a column or row can carry formatting while holding no cells.
|
|
7
|
-
// Merges and views layer on in later slices.
|
|
8
1
|
import { AuthoringError } from '../errors.js';
|
|
9
2
|
import { decodeAddress, decodeRange, encodeAddress } from './address.js';
|
|
10
3
|
import { canonicalizeAutoFilter } from './autofilter.js';
|
|
@@ -17,116 +10,44 @@ import { DataValidationOverlay } from './data-validation-overlay.js';
|
|
|
17
10
|
import { GridEdits } from './grid-edits.js';
|
|
18
11
|
import { PX_TO_EMU, resolveAnchorPoint, } from './image.js';
|
|
19
12
|
import { INTERNAL } from './internal.js';
|
|
20
|
-
import { rectsOverlap } from './merge.js';
|
|
13
|
+
import { clearCoveredValues, masterOf, rectsOverlap } from './merge.js';
|
|
21
14
|
import { PivotTable } from './pivot-table.js';
|
|
22
15
|
import { deriveCredential, } from './protection.js';
|
|
23
16
|
import { Range, rangeFrom } from './range.js';
|
|
24
17
|
import { Row } from './row.js';
|
|
18
|
+
import { buildRowCells, rowPlacements } from './row-input.js';
|
|
25
19
|
import { Table, TOTALS_ROW_SUBTOTAL_CODE } from './table.js';
|
|
26
20
|
import { WORKSHEET_MODEL_FACETS } from './worksheet-model.js';
|
|
27
21
|
export class Worksheet {
|
|
28
22
|
name;
|
|
29
|
-
/** 1-based workbook-assigned id, stable for the sheet's lifetime. */
|
|
30
23
|
id;
|
|
31
24
|
state;
|
|
32
|
-
/**
|
|
33
|
-
* Colour of the sheet's tab, as an ARGB/theme {@link Color}. `undefined` leaves the tab its
|
|
34
|
-
* default colour; the writer emits no `<tabColor>` for an uncoloured sheet, so a round-trip
|
|
35
|
-
* never fabricates one.
|
|
36
|
-
*/
|
|
37
25
|
tabColor;
|
|
38
|
-
/** Sheet-level format defaults. Mutate in place: `sheet.properties.defaultRowHeight = 20`. */
|
|
39
26
|
properties = {};
|
|
40
|
-
/**
|
|
41
|
-
* Outline summary-position flags. Mutate in place: `sheet.outline.summaryBelow = false`. Empty
|
|
42
|
-
* means unset — the writer emits no `<outlinePr>` and a round-trip never fabricates one.
|
|
43
|
-
*/
|
|
44
27
|
outline = {};
|
|
45
|
-
/**
|
|
46
|
-
* The sheet's frozen-pane view. Empty (a normal view) emits no `<pane>`. Use {@link freeze} and
|
|
47
|
-
* {@link unfreeze} for the common cases, or mutate in place for finer control.
|
|
48
|
-
*/
|
|
49
28
|
view = {};
|
|
50
|
-
/**
|
|
51
|
-
* Print-scaling and orientation. Mutate in place: `sheet.pageSetup.fitToPage = true`. Empty means
|
|
52
|
-
* unset — the writer emits neither `<pageSetUpPr>` nor `<pageSetup>` and a round-trip never
|
|
53
|
-
* fabricates them.
|
|
54
|
-
*/
|
|
55
29
|
pageSetup = {};
|
|
56
|
-
/**
|
|
57
|
-
* Print-toggle flags (`<printOptions>`): centring, and whether headings/gridlines print. Mutate in
|
|
58
|
-
* place: `sheet.printOptions.gridLines = true`. Empty means unset — the writer emits no element and
|
|
59
|
-
* a round-trip never fabricates one.
|
|
60
|
-
*/
|
|
61
30
|
printOptions = {};
|
|
62
|
-
/** Print margins. Mutate in place: `sheet.pageMargins.left = 0.5`. Empty means unset. */
|
|
63
31
|
pageMargins = {};
|
|
64
|
-
/** Page header/footer text. Mutate in place: `sheet.headerFooter.oddHeader = '&C&"..."'`. */
|
|
65
32
|
headerFooter = {};
|
|
66
|
-
/**
|
|
67
|
-
* Manual horizontal page breaks (`<rowBreaks>`): each break's `id` is a row the print layout splits
|
|
68
|
-
* before. Mutate in place: `sheet.rowBreaks.push({id: 3})`. Empty means no manual row breaks and the
|
|
69
|
-
* writer emits no `<rowBreaks>` element.
|
|
70
|
-
*/
|
|
71
33
|
rowBreaks = [];
|
|
72
|
-
/**
|
|
73
|
-
* Manual vertical page breaks (`<colBreaks>`): each break's `id` is a column the print layout splits
|
|
74
|
-
* before. Mutate in place: `sheet.columnBreaks.push({id: 3})`. Empty means no manual column breaks and
|
|
75
|
-
* the writer emits no `<colBreaks>` element.
|
|
76
|
-
*/
|
|
77
34
|
columnBreaks = [];
|
|
78
|
-
// Row-major sparse storage: row index → (column index → cell). Keeping rows as the
|
|
79
|
-
// outer key makes whole-row iteration cheap and mirrors how OOXML serializes
|
|
80
|
-
// (`<row>` wrapping `<c>`).
|
|
81
35
|
#rows = new Map();
|
|
82
|
-
// Column and row metadata live apart from the grid so an empty-but-formatted line
|
|
83
|
-
// (a hidden column, a tall header row with no data yet) costs no phantom cells.
|
|
84
36
|
#columns = new Map();
|
|
85
37
|
#rowProperties = new Map();
|
|
86
|
-
// Tables, merged ranges, and anchored images are sheet-level overlays on the grid, not cell storage.
|
|
87
38
|
#tables = [];
|
|
88
39
|
#pivotTables = [];
|
|
89
|
-
// Pivot tables reconstructed from a loaded package (see io/xlsx/pivot-read.ts) — a read-only,
|
|
90
|
-
// inspection-only view distinct from #pivotTables. A loaded pivot round-trips by byte-preservation
|
|
91
|
-
// (#preservedReferences), which stays its sole emission authority; this collection is never emitted,
|
|
92
|
-
// so exposing it cannot double-emit. Empty for a sheet authored from scratch.
|
|
93
40
|
#loadedPivotTables = [];
|
|
94
|
-
// The sheet's threaded conversations (see io/xlsx/threaded-comments.ts) — unlike #loadedPivotTables, the
|
|
95
|
-
// authority the writer serialises from: both the sheet's threadedComment part and the legacy fallback
|
|
96
|
-
// comment that binds each cell to its conversation are derived from this list. Empty for a sheet with no
|
|
97
|
-
// threaded comments.
|
|
98
41
|
#commentThreads = [];
|
|
99
42
|
#merges = [];
|
|
100
43
|
#images = [];
|
|
101
|
-
// A sheet background is a single workbook image tiled behind the grid — distinct from an anchored
|
|
102
|
-
// drawing (it has no anchor and rides its own worksheet relationship, not a drawing part).
|
|
103
44
|
#backgroundImageId;
|
|
104
|
-
// Worksheet-level references to package content the model does not interpret (a vector-shape
|
|
105
|
-
// drawing, a header/footer image), captured verbatim on read so a round-trip re-emits them rather
|
|
106
|
-
// than dropping them. Empty for a sheet authored from scratch.
|
|
107
45
|
#preservedReferences = [];
|
|
108
|
-
// Decoded rectangles parallel to #merges, kept so that addressing a covered cell can
|
|
109
|
-
// resolve to its region's master without re-parsing the range string on every access, and
|
|
110
|
-
// so that a new merge can be checked for overlap against the existing ones. Only fully-bounded
|
|
111
|
-
// merges (a real cell block) get a rect; an unbounded whole-row/column merge is still declared
|
|
112
|
-
// but participates in neither slave resolution nor overlap checking.
|
|
113
46
|
#mergeRects = [];
|
|
114
|
-
// Data validations and conditional formattings are sheet-level overlays keyed by range, each owning
|
|
115
|
-
// its own storage/cloning/lookup — see DataValidationOverlay and ConditionalFormattingOverlay.
|
|
116
47
|
#dataValidations = new DataValidationOverlay();
|
|
117
48
|
#conditionalFormattings = new ConditionalFormattingOverlay();
|
|
118
|
-
// Sheet-level protection is a single overlay switch, absent until `protect` is called.
|
|
119
49
|
#protection;
|
|
120
|
-
// The sheet's autofilter (range plus any per-column criteria), absent until one is set. A single
|
|
121
|
-
// sheet-level overlay, distinct from a table's own autofilter; stored canonically so the
|
|
122
|
-
// `<autoFilter>` element and the derived `_FilterDatabase` defined name always agree.
|
|
123
50
|
#autoFilter;
|
|
124
|
-
// Structural-edit machinery (row/column splices), sharing this sheet's storage by reference. The
|
|
125
|
-
// public spliceRows/spliceColumns/duplicateRow build the cells an insert introduces, then delegate
|
|
126
|
-
// the shift arithmetic here. Wired in the constructor body, not a field initializer, so it never
|
|
127
|
-
// depends on this field's declaration position relative to the storage fields above — a class field
|
|
128
|
-
// initializer only sees an earlier-declared field's initial value, so if this stayed a field
|
|
129
|
-
// initializer, reordering the class body could silently hand GridEdits a still-undefined map.
|
|
130
51
|
#edits;
|
|
131
52
|
constructor(name, id, state = 'visible') {
|
|
132
53
|
this.name = name;
|
|
@@ -142,50 +63,20 @@ export class Worksheet {
|
|
|
142
63
|
images: this.#images,
|
|
143
64
|
});
|
|
144
65
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Get the cell at an A1 reference, creating it on first access. The reference must
|
|
147
|
-
* name both a column and a row (`"B3"`); a whole-row or whole-column reference is
|
|
148
|
-
* not a cell and is rejected.
|
|
149
|
-
*
|
|
150
|
-
* Addressing a cell covered by a merged region resolves to that region's master
|
|
151
|
-
* (top-left) cell, mirroring how a spreadsheet treats the merge as one cell: a value
|
|
152
|
-
* or style written through a covered address lands on the master, and reading a
|
|
153
|
-
* covered address returns the master's. Only the master ever holds an independent
|
|
154
|
-
* value, so the serialized sheet stays well-formed (no stray value on a covered cell).
|
|
155
|
-
*
|
|
156
|
-
* @throws {SyntaxError} if the reference does not resolve to a single cell.
|
|
157
|
-
*/
|
|
158
66
|
getCell(reference) {
|
|
159
67
|
const { col, row } = decodeAddress(reference);
|
|
160
68
|
if (col === undefined || row === undefined) {
|
|
161
69
|
throw new SyntaxError(`"${reference}" is not a single-cell reference — it omits a column or row`);
|
|
162
70
|
}
|
|
163
|
-
const master = this.#
|
|
71
|
+
const master = masterOf(this.#mergeRects, row, col);
|
|
164
72
|
return this.#cellAt(master.row, master.col);
|
|
165
73
|
}
|
|
166
|
-
/** Whether a cell has been materialised at the given 1-based position. */
|
|
167
74
|
hasCell(row, col) {
|
|
168
75
|
return this.#rows.get(row)?.has(col) ?? false;
|
|
169
76
|
}
|
|
170
|
-
/**
|
|
171
|
-
* A handle on a 1-based column: its formatting, its cells, and its values. Cheap and stateless —
|
|
172
|
-
* it creates neither cells nor a format record, so asking about a column costs nothing and does
|
|
173
|
-
* not extend the used range. Writing through it (`getColumn(2).width = 12`) is what materialises
|
|
174
|
-
* the record.
|
|
175
|
-
*
|
|
176
|
-
* @throws {RangeError} if the index is not a positive integer.
|
|
177
|
-
*/
|
|
178
77
|
getColumn(index) {
|
|
179
78
|
return new Column(this, index);
|
|
180
79
|
}
|
|
181
|
-
/**
|
|
182
|
-
* A handle on a 1-based row: its formatting, its cells, and its values. Cheap and stateless — it
|
|
183
|
-
* creates neither cells nor a format record, so asking about a row costs nothing and does not
|
|
184
|
-
* extend the used range. Writing through it (`getRow(3).height = 20`) is what materialises the
|
|
185
|
-
* record.
|
|
186
|
-
*
|
|
187
|
-
* @throws {RangeError} if the number is not a positive integer.
|
|
188
|
-
*/
|
|
189
80
|
getRow(number) {
|
|
190
81
|
return new Row(this, number);
|
|
191
82
|
}
|
|
@@ -194,12 +85,6 @@ export class Worksheet {
|
|
|
194
85
|
return rangeFrom(this, referenceOrTop);
|
|
195
86
|
return new Range(this, referenceOrTop, left ?? 0, bottom ?? 0, right ?? 0);
|
|
196
87
|
}
|
|
197
|
-
/**
|
|
198
|
-
* The 1-based index of the last row carrying anything — data or its own formatting —
|
|
199
|
-
* or 0 for an empty sheet. Spans gaps: a value in row 5 makes this 5 even if rows 2–4
|
|
200
|
-
* are empty. This is the used-range extent, not a populated-row tally (see
|
|
201
|
-
* {@link actualRowCount}).
|
|
202
|
-
*/
|
|
203
88
|
get rowCount() {
|
|
204
89
|
let last = 0;
|
|
205
90
|
for (const [number, cols] of this.#rows) {
|
|
@@ -210,15 +95,12 @@ export class Worksheet {
|
|
|
210
95
|
if (number > last)
|
|
211
96
|
last = number;
|
|
212
97
|
}
|
|
213
|
-
// A merged region occupies its whole rectangle even where the covered cells are empty, so a merge
|
|
214
|
-
// extending past the last populated row still belongs to the used range.
|
|
215
98
|
for (const rect of this.#mergeRects) {
|
|
216
99
|
if (rect.bottom > last)
|
|
217
100
|
last = rect.bottom;
|
|
218
101
|
}
|
|
219
102
|
return last;
|
|
220
103
|
}
|
|
221
|
-
/** The number of rows that hold at least one non-empty cell, ignoring gaps and formatting-only rows. */
|
|
222
104
|
get actualRowCount() {
|
|
223
105
|
let count = 0;
|
|
224
106
|
for (const cols of this.#rows.values()) {
|
|
@@ -227,9 +109,6 @@ export class Worksheet {
|
|
|
227
109
|
}
|
|
228
110
|
return count;
|
|
229
111
|
}
|
|
230
|
-
// Whether any cell materialised in a row holds a value — the used-range test {@link rowCount} and
|
|
231
|
-
// {@link actualRowCount} share. Short-circuits on the first non-empty cell rather than allocating the
|
|
232
|
-
// row's values into a throwaway array to scan them.
|
|
233
112
|
#rowHasContent(cols) {
|
|
234
113
|
for (const cell of cols.values()) {
|
|
235
114
|
if (cell.value !== null)
|
|
@@ -237,11 +116,6 @@ export class Worksheet {
|
|
|
237
116
|
}
|
|
238
117
|
return false;
|
|
239
118
|
}
|
|
240
|
-
/**
|
|
241
|
-
* The 1-based index of the last column carrying anything — a non-empty cell or its own format
|
|
242
|
-
* properties — or 0 for an empty sheet. The used-range width, mirroring {@link rowCount} for the
|
|
243
|
-
* other axis: a value in column E makes this 5 even if columns B–D are empty.
|
|
244
|
-
*/
|
|
245
119
|
get columnCount() {
|
|
246
120
|
let last = 0;
|
|
247
121
|
for (const cols of this.#rows.values()) {
|
|
@@ -260,27 +134,6 @@ export class Worksheet {
|
|
|
260
134
|
}
|
|
261
135
|
return last;
|
|
262
136
|
}
|
|
263
|
-
/**
|
|
264
|
-
* The sheet's used range as one handle — `A1` through the last row and column that carry
|
|
265
|
-
* anything — or `undefined` when there is no rectangle to name.
|
|
266
|
-
*
|
|
267
|
-
* This is {@link rowCount} and {@link columnCount} said once, so a caller stops reassembling
|
|
268
|
-
* `A1:${numberToColumn(sheet.columnCount)}${sheet.rowCount}` by hand. That is what an
|
|
269
|
-
* {@link autoFilter} covering the whole sheet wants — `sheet.autoFilter = sheet.usedRange.address`
|
|
270
|
-
* — and Excel writes exactly that ref for a filter it applies itself. A header-only ref filters
|
|
271
|
-
* nothing, which is the bug this exists to make hard to write.
|
|
272
|
-
*
|
|
273
|
-
* It inherits both counts' definition of *used*, so it spans gaps (a value in `E5` and nothing
|
|
274
|
-
* else still gives `A1:E5`) and includes a line carrying only its own formatting — a set column
|
|
275
|
-
* width, an outline level, a merge reaching past the last value. `undefined` therefore means
|
|
276
|
-
* strictly "no rectangle": an empty sheet, or one carrying only row formatting and no columns at
|
|
277
|
-
* all (or the reverse), where an axis has no extent to bound the other against.
|
|
278
|
-
*
|
|
279
|
-
* Not the same thing as the `<dimension>` a written package records. That is the *tight* box —
|
|
280
|
-
* top-left at the first used cell, formatting-only rows excluded — because Excel writes it to
|
|
281
|
-
* describe where the data is, not what the grid spans. This handle is anchored at `A1`, because
|
|
282
|
-
* a caller asking for the used range means the block to read, style or filter.
|
|
283
|
-
*/
|
|
284
137
|
get usedRange() {
|
|
285
138
|
const bottom = this.rowCount;
|
|
286
139
|
const right = this.columnCount;
|
|
@@ -288,54 +141,25 @@ export class Worksheet {
|
|
|
288
141
|
return undefined;
|
|
289
142
|
return new Range(this, 1, 1, bottom, right);
|
|
290
143
|
}
|
|
291
|
-
/** The columns carrying format properties, as handles, in ascending index order. */
|
|
292
144
|
*columns() {
|
|
293
145
|
for (const index of [...this.#columns.keys()].sort((a, b) => a - b)) {
|
|
294
146
|
yield new Column(this, index);
|
|
295
147
|
}
|
|
296
148
|
}
|
|
297
|
-
/**
|
|
298
|
-
* The rows to serialise, as handles, in ascending row order: the union of rows holding cells and
|
|
299
|
-
* rows holding only metadata (a hidden or grouped row need carry no data). Mirrors how OOXML
|
|
300
|
-
* serialises (`<row>` wrapping `<c>`) and is the writer's row surface.
|
|
301
|
-
*
|
|
302
|
-
* A handle yields its cells only when asked, so a pass that reads nothing but row attributes
|
|
303
|
-
* never assembles a cell array it will not look at.
|
|
304
|
-
*/
|
|
305
149
|
*rows() {
|
|
306
150
|
const numbers = new Set([...this.#rows.keys(), ...this.#rowProperties.keys()]);
|
|
307
151
|
for (const number of [...numbers].sort((a, b) => a - b)) {
|
|
308
152
|
yield new Row(this, number);
|
|
309
153
|
}
|
|
310
154
|
}
|
|
311
|
-
/**
|
|
312
|
-
* Define a table over a range of this sheet. The table's shape invariants (a legal
|
|
313
|
-
* name, at least one column, at least one row) are enforced here; conflicts with the
|
|
314
|
-
* rest of the sheet (e.g. an overlapping merge) are the writer's concern.
|
|
315
|
-
*
|
|
316
|
-
* @throws {AuthoringError} if the name, columns, or geometry are invalid.
|
|
317
|
-
*/
|
|
318
155
|
addTable(options) {
|
|
319
156
|
const table = new Table(options, (row, col, value, style) => {
|
|
320
157
|
const cell = this.#cellAt(row, col);
|
|
321
158
|
cell.value = value;
|
|
322
159
|
if (style !== undefined)
|
|
323
160
|
applyCellStyle(cell, style);
|
|
324
|
-
},
|
|
325
|
-
// Insert one empty grid row at `row`; the splice re-pins this table (growing its data rows) and
|
|
326
|
-
// shifts the totals row and everything below down by one.
|
|
327
|
-
(row) => this.spliceRows(row, 0, []));
|
|
161
|
+
}, (row) => this.spliceRows(row, 0, []));
|
|
328
162
|
this.#tables.push(table);
|
|
329
|
-
// A table's declared range includes its header row, and Excel treats the column metadata and
|
|
330
|
-
// the cells under it as one fact: a header row that is empty in the grid is corruption, and
|
|
331
|
-
// Excel repairs the file on open — discarding the column names entirely. The caller already
|
|
332
|
-
// named the columns once in the table definition, so materialising them here is what makes the
|
|
333
|
-
// obvious API call produce a file that opens.
|
|
334
|
-
//
|
|
335
|
-
// Only *empty* header cells are filled. Reading a workbook re-registers each table through this
|
|
336
|
-
// method after the sheet's cells are loaded, and those cells are authoritative: they may carry
|
|
337
|
-
// rich text, a style, or text that drifted from the column name, none of which a re-declaration
|
|
338
|
-
// may clobber. An empty cell has no such content to lose.
|
|
339
163
|
if (table.headerRow) {
|
|
340
164
|
const { top, left } = table.region;
|
|
341
165
|
table.columns.forEach((column, index) => {
|
|
@@ -345,19 +169,6 @@ export class Worksheet {
|
|
|
345
169
|
this.#cellAt(top, col).value = column.name;
|
|
346
170
|
});
|
|
347
171
|
}
|
|
348
|
-
// Materialize the totals row Excel renders on open, so our files show it immediately rather than a
|
|
349
|
-
// blank strip until the user interacts. A labelled column writes its label string; an aggregate
|
|
350
|
-
// column writes the `SUBTOTAL(code, Table[Column])` formula Excel would compute. Unlike the header
|
|
351
|
-
// row, this is a UX-parity nicety, not a validity fix — Excel opens a declared-but-empty totals row
|
|
352
|
-
// without repair — but matching its on-open rendering is the point.
|
|
353
|
-
//
|
|
354
|
-
// Same round-trip guard as the header row: only *empty* cells are filled. Reading a file
|
|
355
|
-
// re-registers the table after its cells are loaded, so a materialized totals cell — ours, Excel's,
|
|
356
|
-
// or a hand-set override — is authoritative and must survive untouched, keeping the round-trip
|
|
357
|
-
// idempotent. The formula carries no cached result; Excel computes an uncached formula cell on open,
|
|
358
|
-
// so the row shows real values without the library pretending to be a calc engine. A `custom` column
|
|
359
|
-
// writes its stored `totalsRowFormula` verbatim; a `none` column (or a `custom` with no stored
|
|
360
|
-
// formula) has nothing to write (see {@link TOTALS_ROW_SUBTOTAL_CODE}) and stays blank.
|
|
361
172
|
if (table.totalsRow) {
|
|
362
173
|
const { left, bottom } = table.region;
|
|
363
174
|
table.columns.forEach((column, index) => {
|
|
@@ -377,8 +188,6 @@ export class Worksheet {
|
|
|
377
188
|
}
|
|
378
189
|
else if (column.totalsRowFunction === 'custom' &&
|
|
379
190
|
column.totalsRowFormula !== undefined) {
|
|
380
|
-
// A `custom` total is the column's own stored formula, not a SUBTOTAL. Excel stores it
|
|
381
|
-
// without a leading `=`, which is the formula string a cell value expects.
|
|
382
191
|
this.#cellAt(bottom, col).value = { formula: column.totalsRowFormula };
|
|
383
192
|
}
|
|
384
193
|
}
|
|
@@ -386,67 +195,25 @@ export class Worksheet {
|
|
|
386
195
|
}
|
|
387
196
|
return table;
|
|
388
197
|
}
|
|
389
|
-
/** The tables defined on this sheet, in definition order. */
|
|
390
198
|
get tables() {
|
|
391
199
|
return this.#tables;
|
|
392
200
|
}
|
|
393
|
-
/** The table with the given name (case-sensitive, the identifier Excel uses), or `undefined`.
|
|
394
|
-
* A table read back from a file is fully hydrated — its rows can be read and appended to. */
|
|
395
201
|
getTable(name) {
|
|
396
202
|
return this.#tables.find((table) => table.name === name);
|
|
397
203
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Add a pivot table to this (destination) sheet, summarising a source sheet's data. The source is
|
|
400
|
-
* read once, now, so the pivot is a snapshot: later edits to the source do not change it. The
|
|
401
|
-
* supported shape (one summed value field, at least one row and column field) is enforced here.
|
|
402
|
-
*
|
|
403
|
-
* @throws {AuthoringError} if the metric, fields, or source shape are unsupported.
|
|
404
|
-
*/
|
|
405
204
|
addPivotTable(options) {
|
|
406
205
|
const pivot = new PivotTable(options);
|
|
407
206
|
this.#pivotTables.push(pivot);
|
|
408
207
|
return pivot;
|
|
409
208
|
}
|
|
410
|
-
/** The pivot tables hosted on this sheet, in definition order. */
|
|
411
209
|
get pivotTables() {
|
|
412
210
|
return this.#pivotTables;
|
|
413
211
|
}
|
|
414
|
-
/**
|
|
415
|
-
* Pivot tables reconstructed from a loaded package, in the order the reader found them — a
|
|
416
|
-
* read-only inspection view (source range, field roles, value field, aggregation). A pivot
|
|
417
|
-
* authored on this sheet via {@link addPivotTable} does not appear here; a pivot loaded from a
|
|
418
|
-
* file does not appear in {@link pivotTables}. The loaded pivots re-emit verbatim through
|
|
419
|
-
* byte-preservation, so this collection is never itself serialised.
|
|
420
|
-
*/
|
|
421
212
|
get loadedPivotTables() {
|
|
422
213
|
return this.#loadedPivotTables;
|
|
423
214
|
}
|
|
424
|
-
/**
|
|
425
|
-
* Anchor a threaded conversation to a cell — Excel's modern review comment: an opening message, its
|
|
426
|
-
* replies, and whether the discussion was marked resolved. Distinct from a cell's legacy note
|
|
427
|
-
* ({@link Cell.note}), and mutually exclusive with one: Excel refuses to put both on one cell, and a
|
|
428
|
-
* cell carrying both is written back as the conversation alone.
|
|
429
|
-
*
|
|
430
|
-
* Every message supplies its own {@link Comment.id} and {@link Comment.date}, and names its author by
|
|
431
|
-
* {@link Comment.personId} into the workbook registry ({@link Workbook.addPerson}) — the writer has no
|
|
432
|
-
* clock and no id generator, so nothing here is invented and the same workbook always serialises to the
|
|
433
|
-
* same bytes. Every id is normalised to the brace-wrapped upper-case GUID form the format requires, so a
|
|
434
|
-
* `crypto.randomUUID()` is accepted as-is.
|
|
435
|
-
*
|
|
436
|
-
* Message ids must be unique **within this sheet**, because that is the scope in which they mean
|
|
437
|
-
* anything: a reply names its thread by the head's id inside the sheet's own part, and the legacy
|
|
438
|
-
* fallback comment binds its cell by the same id inside the sheet's own comments part. Two sheets reusing
|
|
439
|
-
* one id is therefore harmless and is not rejected — Excel's ids happen to be globally unique, but
|
|
440
|
-
* nothing resolves across a part boundary.
|
|
441
|
-
*
|
|
442
|
-
* @throws {SyntaxError} if the anchor does not resolve to a single cell, if any id is not a GUID, if a
|
|
443
|
-
* message id is already used on this sheet, or if a mention's span is not a whole number the wire can
|
|
444
|
-
* express.
|
|
445
|
-
*/
|
|
446
215
|
addCommentThread(thread) {
|
|
447
216
|
const taken = new Set(this.#commentThreads.flatMap((held) => held.comments.map((comment) => comment.id)));
|
|
448
|
-
// Every message is validated before any of it is stored, so a rejection leaves the sheet untouched
|
|
449
|
-
// rather than half-carrying a conversation whose remaining messages were refused.
|
|
450
217
|
const comments = thread.comments.map((comment) => {
|
|
451
218
|
const id = commentThreadGuid(comment.id, 'a comment id');
|
|
452
219
|
if (taken.has(id)) {
|
|
@@ -473,17 +240,9 @@ export class Worksheet {
|
|
|
473
240
|
});
|
|
474
241
|
this.#commentThreads.push({ ...thread, ref: this.#anchorRef(thread.ref), comments });
|
|
475
242
|
}
|
|
476
|
-
/**
|
|
477
|
-
* The threaded conversations on this sheet — Excel's modern review comments (author, timestamp,
|
|
478
|
-
* replies, resolved state, `@mentions`). Empty for a sheet with none. Distinct from a cell's legacy note
|
|
479
|
-
* ({@link Cell.note}).
|
|
480
|
-
*/
|
|
481
243
|
get commentThreads() {
|
|
482
244
|
return this.#commentThreads;
|
|
483
245
|
}
|
|
484
|
-
// The canonical A1 form of a conversation's anchor. A thread hangs off one cell, and both the writer's
|
|
485
|
-
// fallback comment and {@link commentThreadAt} compare anchors as plain strings, so `$B$2` and `B2` must
|
|
486
|
-
// not be two anchors.
|
|
487
246
|
#anchorRef(reference) {
|
|
488
247
|
const { col, row } = decodeAddress(reference);
|
|
489
248
|
if (col === undefined || row === undefined) {
|
|
@@ -491,20 +250,11 @@ export class Worksheet {
|
|
|
491
250
|
}
|
|
492
251
|
return encodeAddress(col, row);
|
|
493
252
|
}
|
|
494
|
-
/**
|
|
495
|
-
* The conversation anchored to a cell, or `undefined` when that cell carries none. The reference is
|
|
496
|
-
* canonicalized, so an absolute `"$B$2"` finds the same thread as `"B2"`; it names the *anchor* cell,
|
|
497
|
-
* so a cell merely covered by the anchor's merged region is not a match.
|
|
498
|
-
*
|
|
499
|
-
* @throws {SyntaxError} if the reference does not resolve to a single cell.
|
|
500
|
-
*/
|
|
501
253
|
commentThreadAt(reference) {
|
|
502
254
|
const anchor = this.#anchorRef(reference);
|
|
503
255
|
return this.#commentThreads.find((thread) => thread.ref === anchor);
|
|
504
256
|
}
|
|
505
257
|
addImage(imageId, anchor) {
|
|
506
|
-
// Bind the pure anchor geometry to this sheet's per-column/row sizes; a size a column or row does
|
|
507
|
-
// not set defers to the sheet default, then (inside resolveAnchorPoint) to Excel's own default.
|
|
508
258
|
const columnWidth = (col) => this.#columns.get(col + 1)?.width ?? this.properties.defaultColWidth;
|
|
509
259
|
const rowHeight = (row) => this.#rowProperties.get(row + 1)?.height ?? this.properties.defaultRowHeight;
|
|
510
260
|
if ('ext' in anchor) {
|
|
@@ -521,53 +271,28 @@ export class Worksheet {
|
|
|
521
271
|
const twoCell = anchor.editAs !== undefined ? { from, to, editAs: anchor.editAs } : { from, to };
|
|
522
272
|
this.#images.push({ imageId, anchor: twoCell });
|
|
523
273
|
}
|
|
524
|
-
/**
|
|
525
|
-
* Anchor an image with a pre-built model anchor in the model's own units (EMUs). This is the
|
|
526
|
-
* low-level primitive {@link addImage} builds on and the reader uses to re-pin an image parsed from
|
|
527
|
-
* a drawing part without a lossy pixel round-trip.
|
|
528
|
-
*/
|
|
529
274
|
addImageAnchor(imageId, anchor) {
|
|
530
275
|
this.#images.push({ imageId, anchor });
|
|
531
276
|
}
|
|
532
|
-
/** Drop every anchor of the given workbook image from this sheet. The image stays registered on the
|
|
533
|
-
* workbook — another sheet may still show it — so only this sheet's anchors are removed; the writer
|
|
534
|
-
* then omits any media no sheet anchors any longer. */
|
|
535
277
|
removeImage(imageId) {
|
|
536
278
|
const kept = this.#images.filter((image) => image.imageId !== imageId);
|
|
537
279
|
replaceContents(this.#images, kept);
|
|
538
280
|
}
|
|
539
|
-
/** The images anchored to this sheet, in the order they were added. */
|
|
540
281
|
get images() {
|
|
541
282
|
return this.#images;
|
|
542
283
|
}
|
|
543
|
-
/** Set this sheet's background image to a workbook image (the id {@link Workbook.addImage} returned).
|
|
544
|
-
* The picture tiles behind the whole grid; it is not anchored to any cell. Passing a new id replaces
|
|
545
|
-
* the previous background. */
|
|
546
284
|
addBackgroundImage(imageId) {
|
|
547
285
|
this.#backgroundImageId = imageId;
|
|
548
286
|
}
|
|
549
|
-
/** Remove this sheet's background image, if any. The image stays registered on the workbook. */
|
|
550
287
|
removeBackgroundImage() {
|
|
551
288
|
this.#backgroundImageId = undefined;
|
|
552
289
|
}
|
|
553
|
-
/** The workbook image id set as this sheet's background, or `undefined` when it has none. */
|
|
554
290
|
get backgroundImageId() {
|
|
555
291
|
return this.#backgroundImageId;
|
|
556
292
|
}
|
|
557
|
-
/** The worksheet-level references to unmodeled package content preserved for round-tripping. */
|
|
558
293
|
get preservedReferences() {
|
|
559
294
|
return this.#preservedReferences;
|
|
560
295
|
}
|
|
561
|
-
/**
|
|
562
|
-
* Merge a range of cells (`"A1:B2"`). A range that overlaps an already-merged region is
|
|
563
|
-
* rejected — Excel forbids overlapping merges and writes such geometry as a corrupt file.
|
|
564
|
-
* Whole-row/column ranges (`"A:A"`) are unbounded, carry no rectangle, and are not overlap-checked.
|
|
565
|
-
*
|
|
566
|
-
* Any value already sitting in a covered non-anchor cell is discarded, keeping only the top-left
|
|
567
|
-
* anchor's — exactly how Excel collapses a range on merge. Leaving it would emit a populated `<c>`
|
|
568
|
-
* under the `<mergeCell>` ref, the geometry Excel opens with a repair prompt. Covered-cell styles
|
|
569
|
-
* survive (a border spanning the merge is legal), so only the conflicting value is cleared.
|
|
570
|
-
*/
|
|
571
296
|
mergeCells(range) {
|
|
572
297
|
const { top, left, bottom, right } = decodeRange(range);
|
|
573
298
|
if (top !== undefined && left !== undefined && bottom !== undefined && right !== undefined) {
|
|
@@ -577,55 +302,19 @@ export class Worksheet {
|
|
|
577
302
|
throw new AuthoringError(`merged range "${range}" overlaps an existing merged region`);
|
|
578
303
|
}
|
|
579
304
|
this.#mergeRects.push(rect);
|
|
580
|
-
this.#
|
|
305
|
+
clearCoveredValues(this.#rows, rect);
|
|
581
306
|
}
|
|
582
307
|
this.#merges.push(range);
|
|
583
308
|
}
|
|
584
|
-
// Drop any value already sitting in a merge's covered non-anchor cells, keeping only the top-left
|
|
585
|
-
// anchor — the collapse Excel performs on merge. A leftover covered value would serialise as a
|
|
586
|
-
// populated `<c>` under the range's `<mergeCell>` ref, the geometry that trips Excel's repair
|
|
587
|
-
// prompt. Styles are untouched: a border spanning the merged region rides the covered cells.
|
|
588
|
-
#clearCoveredValues(rect) {
|
|
589
|
-
for (let row = rect.top; row <= rect.bottom; row++) {
|
|
590
|
-
const cols = this.#rows.get(row);
|
|
591
|
-
if (cols === undefined)
|
|
592
|
-
continue;
|
|
593
|
-
for (let col = rect.left; col <= rect.right; col++) {
|
|
594
|
-
if (row === rect.top && col === rect.left)
|
|
595
|
-
continue;
|
|
596
|
-
const covered = cols.get(col);
|
|
597
|
-
if (covered !== undefined)
|
|
598
|
-
covered.value = null;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
/** The merged ranges on this sheet, in the order they were added. */
|
|
603
309
|
get merges() {
|
|
604
310
|
return this.#merges;
|
|
605
311
|
}
|
|
606
|
-
/**
|
|
607
|
-
* The sheet's autofilter — its range plus any per-column criteria — or `undefined` when the sheet
|
|
608
|
-
* carries none. Setting one turns on the header-row filter dropdowns Excel draws over the range;
|
|
609
|
-
* the writer emits both the sheet's `<autoFilter>` element and the hidden `_FilterDatabase` defined
|
|
610
|
-
* name Excel derives from it. Setting `undefined` clears the filter.
|
|
611
|
-
*
|
|
612
|
-
* A bare range string is the ergonomic common case — `sheet.autoFilter = 'A1:C10'` for dropdowns
|
|
613
|
-
* with no active criteria; pass an {@link AutoFilter} object to narrow columns. Either way the
|
|
614
|
-
* value is normalised on assignment (range to canonical `A1:C10` form) and the getter returns the
|
|
615
|
-
* structured object. The range must be a bounded rectangle — a whole-row/column reference is not a
|
|
616
|
-
* filterable region and is rejected.
|
|
617
|
-
*/
|
|
618
312
|
get autoFilter() {
|
|
619
313
|
return this.#autoFilter;
|
|
620
314
|
}
|
|
621
315
|
set autoFilter(filter) {
|
|
622
316
|
this.#autoFilter = filter === undefined ? undefined : canonicalizeAutoFilter(filter);
|
|
623
317
|
}
|
|
624
|
-
/**
|
|
625
|
-
* Remove a merged range previously added with {@link mergeCells}, returning whether a merge with
|
|
626
|
-
* that exact range string existed. The covering rectangle is dropped alongside it, so a cell the
|
|
627
|
-
* merge had masked addresses independently again. The inverse of {@link mergeCells}.
|
|
628
|
-
*/
|
|
629
318
|
unmergeCells(range) {
|
|
630
319
|
const index = this.#merges.indexOf(range);
|
|
631
320
|
if (index === -1)
|
|
@@ -639,58 +328,24 @@ export class Worksheet {
|
|
|
639
328
|
}
|
|
640
329
|
return true;
|
|
641
330
|
}
|
|
642
|
-
/**
|
|
643
|
-
* Attach a data validation to a target range (`"B2:B20"`, a whole column `"B2:B1048576"`, or a
|
|
644
|
-
* space-separated `sqref` of several ranges). The rule is stored once against the range, not copied
|
|
645
|
-
* per covered cell, so a whole-column dropdown stays a single entry. A cell inside the range reports
|
|
646
|
-
* the rule through {@link dataValidationAt}.
|
|
647
|
-
*
|
|
648
|
-
* Pass `{extended: true}` to mark a rule that belongs in the 2009 extension form
|
|
649
|
-
* (`<x14:dataValidation>`) — the carrier Excel uses for a list source on another sheet and other
|
|
650
|
-
* shapes the standard element cannot express. The reader sets it for a rule found in that form so a
|
|
651
|
-
* round-trip writes it back there instead of silently corrupting the cross-sheet reference.
|
|
652
|
-
*/
|
|
653
331
|
addDataValidation(sqref, rule, options = {}) {
|
|
654
332
|
this.#dataValidations.add(sqref, rule, options);
|
|
655
333
|
}
|
|
656
|
-
/** The data validations on this sheet, each bound to its target range, in insertion order. */
|
|
657
334
|
get dataValidations() {
|
|
658
335
|
return this.#dataValidations.entries;
|
|
659
336
|
}
|
|
660
|
-
/**
|
|
661
|
-
* Attach a conditional formatting to a target range. `formatting.ref` is an OOXML `sqref` — one
|
|
662
|
-
* range (`"A1:A10"`), a whole column, or several space-separated areas (`"A1:C1 A3:C3"`) sharing one
|
|
663
|
-
* rule set. The block is stored once against the range, defensively copied so the getter never hands
|
|
664
|
-
* back a reference into the caller's object.
|
|
665
|
-
*/
|
|
666
337
|
addConditionalFormatting(formatting) {
|
|
667
338
|
this.#conditionalFormattings.add(formatting);
|
|
668
339
|
}
|
|
669
|
-
/** The conditional formattings on this sheet, each bound to its target range, in insertion order. */
|
|
670
340
|
get conditionalFormattings() {
|
|
671
341
|
return this.#conditionalFormattings.entries;
|
|
672
342
|
}
|
|
673
|
-
/**
|
|
674
|
-
* The validation covering a cell, or `undefined` when none does. The first added rule whose range
|
|
675
|
-
* contains the cell wins, mirroring how a spreadsheet resolves overlapping validations.
|
|
676
|
-
*/
|
|
677
343
|
dataValidationAt(reference) {
|
|
678
344
|
const { col, row } = decodeAddress(reference);
|
|
679
345
|
if (col === undefined || row === undefined)
|
|
680
346
|
return undefined;
|
|
681
347
|
return this.#dataValidations.at(col, row);
|
|
682
348
|
}
|
|
683
|
-
/**
|
|
684
|
-
* Remove `count` rows starting at the 1-based `start`, then insert the given rows in their place.
|
|
685
|
-
* Rows below the edit shift by `inserts.length - count`: a delete pulls the tail up, an insert
|
|
686
|
-
* pushes it down, and doing both at once is a replace. Each inserted row takes either
|
|
687
|
-
* {@link RowInput} shape — a positional array from column A, or a key-addressed object — exactly
|
|
688
|
-
* like {@link addRow}. A `count` larger than the rows present simply clears the tail — it never
|
|
689
|
-
* silently becomes a no-op. Cells carry their full style to the shifted position, and merged ranges
|
|
690
|
-
* shift with the rows they cover.
|
|
691
|
-
*
|
|
692
|
-
* @throws {RangeError} if `start` is not a positive integer or `count` is negative.
|
|
693
|
-
*/
|
|
694
349
|
spliceRows(start, count, ...inserts) {
|
|
695
350
|
if (!Number.isInteger(start) || start < 1) {
|
|
696
351
|
throw new RangeError(`splice start ${start} is out of bounds — rows start at 1`);
|
|
@@ -698,87 +353,26 @@ export class Worksheet {
|
|
|
698
353
|
if (!Number.isInteger(count) || count < 0) {
|
|
699
354
|
throw new RangeError(`splice count ${count} is invalid — it must be a non-negative integer`);
|
|
700
355
|
}
|
|
701
|
-
const inserted = inserts.map((values, i) =>
|
|
356
|
+
const inserted = inserts.map((values, i) => buildRowCells(start + i, values, this.#columns));
|
|
702
357
|
this.#edits.spliceRows(start, count, inserted);
|
|
703
358
|
}
|
|
704
|
-
/**
|
|
705
|
-
* Insert one row of `values` at the 1-based `pos`, shifting the rows at and below it down by one.
|
|
706
|
-
* `values` takes either {@link RowInput} shape (positional array or keyed object), like
|
|
707
|
-
* {@link addRow}. Shorthand for {@link spliceRows}`(pos, 0, values)`.
|
|
708
|
-
*
|
|
709
|
-
* @throws {RangeError} if `pos` is not a positive integer.
|
|
710
|
-
*/
|
|
711
359
|
insertRow(pos, values) {
|
|
712
360
|
this.spliceRows(pos, 0, values);
|
|
713
361
|
}
|
|
714
|
-
/**
|
|
715
|
-
* Append a row of `values` after the last used row, returning the cells it materialised.
|
|
716
|
-
* The append point is {@link rowCount}` + 1`, so the row lands below every row that holds
|
|
717
|
-
* data or its own formatting — never overwriting existing content, unlike {@link insertRow},
|
|
718
|
-
* which shifts and needs a position. Unlike {@link spliceRows}, appending shifts nothing, so
|
|
719
|
-
* it never disturbs merges or the rows above.
|
|
720
|
-
*
|
|
721
|
-
* A row takes either shape: a positional array whose values map to columns from A — a hole in a
|
|
722
|
-
* sparse array (`['a', , 'c']`) leaves that column untouched — or a keyed object whose values
|
|
723
|
-
* land under the columns carrying the matching {@link ColumnProperties.key}.
|
|
724
|
-
*/
|
|
725
362
|
addRow(values) {
|
|
726
363
|
return this.addRows([values])[0] ?? [];
|
|
727
364
|
}
|
|
728
|
-
/**
|
|
729
|
-
* Append several rows after the last used row in one call, returning the cells materialised
|
|
730
|
-
* for each. The rows stack in order — the first lands at {@link rowCount}` + 1`, the next
|
|
731
|
-
* directly below it — so a later row never collides with an earlier one even when both are
|
|
732
|
-
* value-less. Each row is an array or a keyed object independently, so a mixed batch is fine.
|
|
733
|
-
* The bulk form of {@link addRow}.
|
|
734
|
-
*/
|
|
735
365
|
addRows(rows) {
|
|
736
366
|
let number = this.rowCount;
|
|
737
367
|
return rows.map((values) => {
|
|
738
368
|
number += 1;
|
|
739
|
-
return
|
|
369
|
+
return rowPlacements(values, this.#columns).map(([col, value]) => {
|
|
740
370
|
const cell = this.#cellAt(number, col);
|
|
741
371
|
cell.value = value;
|
|
742
372
|
return cell;
|
|
743
373
|
});
|
|
744
374
|
});
|
|
745
375
|
}
|
|
746
|
-
// Resolve a RowInput to the (1-based column, value) placements it names, the one interpretation of
|
|
747
|
-
// row shape that both appending (into the live grid) and splicing (into a detached row) share. A
|
|
748
|
-
// positional array maps each value to its column from A, skipping a hole or an explicit `undefined`
|
|
749
|
-
// so that column is left untouched; a keyed object maps each value under the column carrying the
|
|
750
|
-
// matching key. Array.isArray, not `instanceof Array`: a row built in another realm (a vm context,
|
|
751
|
-
// a browser iframe) is still an array but fails the identity check, and would then be walked as a
|
|
752
|
-
// keyed object — placing nothing.
|
|
753
|
-
#rowPlacements(values) {
|
|
754
|
-
if (Array.isArray(values)) {
|
|
755
|
-
const placements = [];
|
|
756
|
-
values.forEach((value, index) => {
|
|
757
|
-
if (value !== undefined)
|
|
758
|
-
placements.push([index + 1, value]);
|
|
759
|
-
});
|
|
760
|
-
return placements;
|
|
761
|
-
}
|
|
762
|
-
return Object.entries(values).map(([key, value]) => [this.#columnIndexByKey(key), value]);
|
|
763
|
-
}
|
|
764
|
-
// Build the detached cell row an insert introduces: a fresh cell per placement, positioned at
|
|
765
|
-
// `number`, keyed by column. The grid-edit machinery then splices this map into place.
|
|
766
|
-
#buildRowCells(number, values) {
|
|
767
|
-
const row = new Map();
|
|
768
|
-
for (const [col, value] of this.#rowPlacements(values)) {
|
|
769
|
-
const cell = new Cell(number, col);
|
|
770
|
-
cell.value = value;
|
|
771
|
-
row.set(col, cell);
|
|
772
|
-
}
|
|
773
|
-
return row;
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Freeze the top `ySplit` rows and left `xSplit` columns in place; the rest of the sheet scrolls
|
|
777
|
-
* beneath them. `freeze(1)` pins a header row; `freeze(0, 1)` pins the first column. Passing both
|
|
778
|
-
* zero clears the freeze (equivalent to {@link unfreeze}).
|
|
779
|
-
*
|
|
780
|
-
* @throws {RangeError} if either split is a negative or non-integer count.
|
|
781
|
-
*/
|
|
782
376
|
freeze(ySplit = 1, xSplit = 0) {
|
|
783
377
|
if (!Number.isInteger(ySplit) || ySplit < 0 || !Number.isInteger(xSplit) || xSplit < 0) {
|
|
784
378
|
throw new RangeError(`freeze splits must be non-negative integers; got ySplit=${ySplit}, xSplit=${xSplit}`);
|
|
@@ -792,30 +386,12 @@ export class Worksheet {
|
|
|
792
386
|
this.view.ySplit = ySplit;
|
|
793
387
|
this.view.topLeftCell = encodeAddress(xSplit + 1, ySplit + 1);
|
|
794
388
|
}
|
|
795
|
-
/** Clear any frozen split, returning the sheet to a normal (fully scrolling) view. */
|
|
796
389
|
unfreeze() {
|
|
797
390
|
this.view.state = 'normal';
|
|
798
391
|
delete this.view.xSplit;
|
|
799
392
|
delete this.view.ySplit;
|
|
800
393
|
delete this.view.topLeftCell;
|
|
801
394
|
}
|
|
802
|
-
/** The 1-based index of the column carrying `key` (see {@link ColumnProperties.key}). */
|
|
803
|
-
#columnIndexByKey(key) {
|
|
804
|
-
for (const [index, properties] of this.#columns) {
|
|
805
|
-
if (properties.key === key)
|
|
806
|
-
return index;
|
|
807
|
-
}
|
|
808
|
-
throw new AuthoringError(`no column is keyed ${JSON.stringify(key)} — set getColumn(n).key first`);
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Copy the row at the 1-based `start`, `options.count` times (default 1). With `options.insert`
|
|
812
|
-
* (the default) the copies are inserted directly after the source, shifting the rows below — and
|
|
813
|
-
* any merged range there — down by `count`; otherwise the copies overwrite the rows immediately
|
|
814
|
-
* below without shifting. Each copy is a faithful duplicate of the source's values and per-cell
|
|
815
|
-
* styles, and carries no merge of its own, so a range can be merged onto a duplicated row afterwards.
|
|
816
|
-
*
|
|
817
|
-
* @throws {RangeError} if `start` is not a positive integer or `count` is negative.
|
|
818
|
-
*/
|
|
819
395
|
duplicateRow(start, options = {}) {
|
|
820
396
|
const { count = 1, insert = true } = options;
|
|
821
397
|
if (!Number.isInteger(start) || start < 1) {
|
|
@@ -845,15 +421,6 @@ export class Worksheet {
|
|
|
845
421
|
this.#rows.set(start + i, snapshot(start + i));
|
|
846
422
|
}
|
|
847
423
|
}
|
|
848
|
-
/**
|
|
849
|
-
* Remove `count` columns starting at the 1-based `start`, then insert the given columns in their
|
|
850
|
-
* place — the column analog of {@link spliceRows}. Columns to the right shift by
|
|
851
|
-
* `inserts.length - count`, keeping their values and styles, and a merged range lying wholly to
|
|
852
|
-
* the right of the edit re-anchors to its new columns. Each inserted column is an array of values
|
|
853
|
-
* indexed by row (index 0 → row 1); an empty array inserts a blank column.
|
|
854
|
-
*
|
|
855
|
-
* @throws {RangeError} if `start` is not a positive integer or `count` is negative.
|
|
856
|
-
*/
|
|
857
424
|
spliceColumns(start, count, ...inserts) {
|
|
858
425
|
if (!Number.isInteger(start) || start < 1) {
|
|
859
426
|
throw new RangeError(`splice start ${start} is out of bounds — columns start at 1`);
|
|
@@ -863,35 +430,12 @@ export class Worksheet {
|
|
|
863
430
|
}
|
|
864
431
|
this.#edits.spliceColumns(start, count, inserts);
|
|
865
432
|
}
|
|
866
|
-
/**
|
|
867
|
-
* Insert one column of `values` at the 1-based `pos`, shifting the columns at and right of it over
|
|
868
|
-
* by one. `values` is an array of values indexed by row (index 0 → row 1), like
|
|
869
|
-
* {@link addColumn}. Shorthand for {@link spliceColumns}`(pos, 0, values)`.
|
|
870
|
-
*
|
|
871
|
-
* @throws {RangeError} if `pos` is not a positive integer.
|
|
872
|
-
*/
|
|
873
433
|
insertColumn(pos, values) {
|
|
874
434
|
this.spliceColumns(pos, 0, values);
|
|
875
435
|
}
|
|
876
|
-
/**
|
|
877
|
-
* Append a column of `values` after the last used column, returning the cells it materialised.
|
|
878
|
-
* The append point is {@link columnCount}` + 1`, so the column lands right of every column that
|
|
879
|
-
* holds data or its own formatting — never overwriting existing content, unlike {@link insertColumn},
|
|
880
|
-
* which shifts and needs a position. Unlike {@link spliceColumns}, appending shifts nothing, so it
|
|
881
|
-
* never disturbs merges or the columns to its left.
|
|
882
|
-
*
|
|
883
|
-
* `values` is an array indexed by row (index 0 → row 1); a hole or an explicit `undefined` leaves
|
|
884
|
-
* that row untouched, mirroring {@link addRow}'s positional-array shape.
|
|
885
|
-
*/
|
|
886
436
|
addColumn(values) {
|
|
887
437
|
return this.addColumns([values])[0] ?? [];
|
|
888
438
|
}
|
|
889
|
-
/**
|
|
890
|
-
* Append several columns after the last used column in one call, returning the cells materialised
|
|
891
|
-
* for each. The columns stack in order — the first lands at {@link columnCount}` + 1`, the next
|
|
892
|
-
* directly right of it — so a later column never collides with an earlier one even when both are
|
|
893
|
-
* value-less. The bulk form of {@link addColumn}.
|
|
894
|
-
*/
|
|
895
439
|
addColumns(columns) {
|
|
896
440
|
let index = this.columnCount;
|
|
897
441
|
return columns.map((values) => {
|
|
@@ -907,26 +451,12 @@ export class Worksheet {
|
|
|
907
451
|
return cells;
|
|
908
452
|
});
|
|
909
453
|
}
|
|
910
|
-
/**
|
|
911
|
-
* A snapshot of this sheet's value and overlay content (see {@link WorksheetModel}). Reading it and
|
|
912
|
-
* assigning it onto another sheet — `dst.model = src.model` — reproduces the source: merges, cells
|
|
913
|
-
* and their styles, column/row metadata, tables, the autofilter, protection, and the page setup all
|
|
914
|
-
* survive, because the getter emits and the setter consumes exactly the same fields. Identity
|
|
915
|
-
* (`name`, `id`) is not part of the model and is never touched by assignment; nor are attached parts
|
|
916
|
-
* that carry workbook-level identity (images, pivots, byte-preserved charts/drawings) — see
|
|
917
|
-
* {@link WorksheetModel} for that boundary.
|
|
918
|
-
*/
|
|
919
454
|
get model() {
|
|
920
455
|
const model = {};
|
|
921
456
|
for (const facet of WORKSHEET_MODEL_FACETS)
|
|
922
457
|
model[facet.key] = facet.read(this);
|
|
923
|
-
// TypeScript cannot follow an object built key by key; what makes this sound is that the
|
|
924
|
-
// registry is proved exhaustive over `keyof WorksheetModel` — see the type assertion beside it.
|
|
925
458
|
return model;
|
|
926
459
|
}
|
|
927
|
-
// Empty every collection the model round-trips, so a subsequent replay leaves no residue from
|
|
928
|
-
// whatever the sheet held before. Images, pivots, and byte-preserved parts carry workbook-level
|
|
929
|
-
// identity, are excluded from the model, and so are deliberately left untouched here.
|
|
930
460
|
#resetContent() {
|
|
931
461
|
this.#rows.clear();
|
|
932
462
|
this.#columns.clear();
|
|
@@ -937,23 +467,11 @@ export class Worksheet {
|
|
|
937
467
|
this.#conditionalFormattings.clear();
|
|
938
468
|
this.#tables.length = 0;
|
|
939
469
|
}
|
|
940
|
-
// Assigning a model replaces this sheet's content wholesale — the sheet becomes the model, with no
|
|
941
|
-
// residue from whatever it held before. The registry's declaration order is the application order,
|
|
942
|
-
// and it is load-bearing: see WORKSHEET_MODEL_FACETS.
|
|
943
470
|
set model(model) {
|
|
944
471
|
this.#resetContent();
|
|
945
472
|
for (const facet of WORKSHEET_MODEL_FACETS)
|
|
946
473
|
facet.write(this, model);
|
|
947
474
|
}
|
|
948
|
-
/**
|
|
949
|
-
* Protect the sheet, making the per-cell `locked`/`hidden` flags enforceable. Without a
|
|
950
|
-
* password the protection is a soft lock any consumer can lift; with one, the password is
|
|
951
|
-
* salted and hashed on the spot (the plaintext is never retained) so lifting the protection
|
|
952
|
-
* requires re-supplying it. `options` names which operations stay available to a user while
|
|
953
|
-
* the sheet is protected; anything unspecified falls to Excel's default for that operation.
|
|
954
|
-
*
|
|
955
|
-
* Re-protecting replaces any prior protection; {@link unprotect} clears it.
|
|
956
|
-
*/
|
|
957
475
|
protect(password, options = {}) {
|
|
958
476
|
const { spinCount, ...flags } = options;
|
|
959
477
|
const protection = {
|
|
@@ -964,25 +482,12 @@ export class Worksheet {
|
|
|
964
482
|
}
|
|
965
483
|
this.#protection = protection;
|
|
966
484
|
}
|
|
967
|
-
/** Remove any protection previously set by {@link protect}. */
|
|
968
485
|
unprotect() {
|
|
969
486
|
this.#protection = undefined;
|
|
970
487
|
}
|
|
971
|
-
/** The sheet's protection, or `undefined` if the sheet is unprotected. */
|
|
972
488
|
get protection() {
|
|
973
489
|
return this.#protection;
|
|
974
490
|
}
|
|
975
|
-
// Resolve a position to the master (top-left) of the merged region covering it, or to
|
|
976
|
-
// itself when no region does. First covering region wins; overlaps are rejected in
|
|
977
|
-
// `mergeCells`, so at most one region ever applies.
|
|
978
|
-
#masterOf(row, col) {
|
|
979
|
-
for (const rect of this.#mergeRects) {
|
|
980
|
-
if (row >= rect.top && row <= rect.bottom && col >= rect.left && col <= rect.right) {
|
|
981
|
-
return { row: rect.top, col: rect.left };
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
return { row, col };
|
|
985
|
-
}
|
|
986
491
|
#cellAt(row, col) {
|
|
987
492
|
let cols = this.#rows.get(row);
|
|
988
493
|
if (cols === undefined) {
|
|
@@ -996,10 +501,6 @@ export class Worksheet {
|
|
|
996
501
|
}
|
|
997
502
|
return cell;
|
|
998
503
|
}
|
|
999
|
-
/**
|
|
1000
|
-
* The codec's channel into this sheet — see `core/internal.ts` for why these are not public
|
|
1001
|
-
* methods. Declared last so every private field it closes over is already in scope.
|
|
1002
|
-
*/
|
|
1003
504
|
[INTERNAL] = {
|
|
1004
505
|
evictRow: (number) => {
|
|
1005
506
|
this.#rows.delete(number);
|