@shbernal/ts-xlsx 1.0.3 → 1.2.0
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 +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- 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.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- 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 -44
- 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.d.ts +9 -1
- package/dist/core/row.js +0 -58
- 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.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- 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.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- 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 +5 -35
- 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/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 +0 -54
- 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 +1 -88
- package/dist/io/xlsx/read.js +3 -211
- 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 +1 -64
- 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.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- 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 +9 -10
- 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,54 +134,32 @@ export class Worksheet {
|
|
|
260
134
|
}
|
|
261
135
|
return last;
|
|
262
136
|
}
|
|
263
|
-
|
|
137
|
+
get usedRange() {
|
|
138
|
+
const bottom = this.rowCount;
|
|
139
|
+
const right = this.columnCount;
|
|
140
|
+
if (bottom === 0 || right === 0)
|
|
141
|
+
return undefined;
|
|
142
|
+
return new Range(this, 1, 1, bottom, right);
|
|
143
|
+
}
|
|
264
144
|
*columns() {
|
|
265
145
|
for (const index of [...this.#columns.keys()].sort((a, b) => a - b)) {
|
|
266
146
|
yield new Column(this, index);
|
|
267
147
|
}
|
|
268
148
|
}
|
|
269
|
-
/**
|
|
270
|
-
* The rows to serialise, as handles, in ascending row order: the union of rows holding cells and
|
|
271
|
-
* rows holding only metadata (a hidden or grouped row need carry no data). Mirrors how OOXML
|
|
272
|
-
* serialises (`<row>` wrapping `<c>`) and is the writer's row surface.
|
|
273
|
-
*
|
|
274
|
-
* A handle yields its cells only when asked, so a pass that reads nothing but row attributes
|
|
275
|
-
* never assembles a cell array it will not look at.
|
|
276
|
-
*/
|
|
277
149
|
*rows() {
|
|
278
150
|
const numbers = new Set([...this.#rows.keys(), ...this.#rowProperties.keys()]);
|
|
279
151
|
for (const number of [...numbers].sort((a, b) => a - b)) {
|
|
280
152
|
yield new Row(this, number);
|
|
281
153
|
}
|
|
282
154
|
}
|
|
283
|
-
/**
|
|
284
|
-
* Define a table over a range of this sheet. The table's shape invariants (a legal
|
|
285
|
-
* name, at least one column, at least one row) are enforced here; conflicts with the
|
|
286
|
-
* rest of the sheet (e.g. an overlapping merge) are the writer's concern.
|
|
287
|
-
*
|
|
288
|
-
* @throws {AuthoringError} if the name, columns, or geometry are invalid.
|
|
289
|
-
*/
|
|
290
155
|
addTable(options) {
|
|
291
156
|
const table = new Table(options, (row, col, value, style) => {
|
|
292
157
|
const cell = this.#cellAt(row, col);
|
|
293
158
|
cell.value = value;
|
|
294
159
|
if (style !== undefined)
|
|
295
160
|
applyCellStyle(cell, style);
|
|
296
|
-
},
|
|
297
|
-
// Insert one empty grid row at `row`; the splice re-pins this table (growing its data rows) and
|
|
298
|
-
// shifts the totals row and everything below down by one.
|
|
299
|
-
(row) => this.spliceRows(row, 0, []));
|
|
161
|
+
}, (row) => this.spliceRows(row, 0, []));
|
|
300
162
|
this.#tables.push(table);
|
|
301
|
-
// A table's declared range includes its header row, and Excel treats the column metadata and
|
|
302
|
-
// the cells under it as one fact: a header row that is empty in the grid is corruption, and
|
|
303
|
-
// Excel repairs the file on open — discarding the column names entirely. The caller already
|
|
304
|
-
// named the columns once in the table definition, so materialising them here is what makes the
|
|
305
|
-
// obvious API call produce a file that opens.
|
|
306
|
-
//
|
|
307
|
-
// Only *empty* header cells are filled. Reading a workbook re-registers each table through this
|
|
308
|
-
// method after the sheet's cells are loaded, and those cells are authoritative: they may carry
|
|
309
|
-
// rich text, a style, or text that drifted from the column name, none of which a re-declaration
|
|
310
|
-
// may clobber. An empty cell has no such content to lose.
|
|
311
163
|
if (table.headerRow) {
|
|
312
164
|
const { top, left } = table.region;
|
|
313
165
|
table.columns.forEach((column, index) => {
|
|
@@ -317,19 +169,6 @@ export class Worksheet {
|
|
|
317
169
|
this.#cellAt(top, col).value = column.name;
|
|
318
170
|
});
|
|
319
171
|
}
|
|
320
|
-
// Materialize the totals row Excel renders on open, so our files show it immediately rather than a
|
|
321
|
-
// blank strip until the user interacts. A labelled column writes its label string; an aggregate
|
|
322
|
-
// column writes the `SUBTOTAL(code, Table[Column])` formula Excel would compute. Unlike the header
|
|
323
|
-
// row, this is a UX-parity nicety, not a validity fix — Excel opens a declared-but-empty totals row
|
|
324
|
-
// without repair — but matching its on-open rendering is the point.
|
|
325
|
-
//
|
|
326
|
-
// Same round-trip guard as the header row: only *empty* cells are filled. Reading a file
|
|
327
|
-
// re-registers the table after its cells are loaded, so a materialized totals cell — ours, Excel's,
|
|
328
|
-
// or a hand-set override — is authoritative and must survive untouched, keeping the round-trip
|
|
329
|
-
// idempotent. The formula carries no cached result; Excel computes an uncached formula cell on open,
|
|
330
|
-
// so the row shows real values without the library pretending to be a calc engine. A `custom` column
|
|
331
|
-
// writes its stored `totalsRowFormula` verbatim; a `none` column (or a `custom` with no stored
|
|
332
|
-
// formula) has nothing to write (see {@link TOTALS_ROW_SUBTOTAL_CODE}) and stays blank.
|
|
333
172
|
if (table.totalsRow) {
|
|
334
173
|
const { left, bottom } = table.region;
|
|
335
174
|
table.columns.forEach((column, index) => {
|
|
@@ -349,8 +188,6 @@ export class Worksheet {
|
|
|
349
188
|
}
|
|
350
189
|
else if (column.totalsRowFunction === 'custom' &&
|
|
351
190
|
column.totalsRowFormula !== undefined) {
|
|
352
|
-
// A `custom` total is the column's own stored formula, not a SUBTOTAL. Excel stores it
|
|
353
|
-
// without a leading `=`, which is the formula string a cell value expects.
|
|
354
191
|
this.#cellAt(bottom, col).value = { formula: column.totalsRowFormula };
|
|
355
192
|
}
|
|
356
193
|
}
|
|
@@ -358,67 +195,25 @@ export class Worksheet {
|
|
|
358
195
|
}
|
|
359
196
|
return table;
|
|
360
197
|
}
|
|
361
|
-
/** The tables defined on this sheet, in definition order. */
|
|
362
198
|
get tables() {
|
|
363
199
|
return this.#tables;
|
|
364
200
|
}
|
|
365
|
-
/** The table with the given name (case-sensitive, the identifier Excel uses), or `undefined`.
|
|
366
|
-
* A table read back from a file is fully hydrated — its rows can be read and appended to. */
|
|
367
201
|
getTable(name) {
|
|
368
202
|
return this.#tables.find((table) => table.name === name);
|
|
369
203
|
}
|
|
370
|
-
/**
|
|
371
|
-
* Add a pivot table to this (destination) sheet, summarising a source sheet's data. The source is
|
|
372
|
-
* read once, now, so the pivot is a snapshot: later edits to the source do not change it. The
|
|
373
|
-
* supported shape (one summed value field, at least one row and column field) is enforced here.
|
|
374
|
-
*
|
|
375
|
-
* @throws {AuthoringError} if the metric, fields, or source shape are unsupported.
|
|
376
|
-
*/
|
|
377
204
|
addPivotTable(options) {
|
|
378
205
|
const pivot = new PivotTable(options);
|
|
379
206
|
this.#pivotTables.push(pivot);
|
|
380
207
|
return pivot;
|
|
381
208
|
}
|
|
382
|
-
/** The pivot tables hosted on this sheet, in definition order. */
|
|
383
209
|
get pivotTables() {
|
|
384
210
|
return this.#pivotTables;
|
|
385
211
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Pivot tables reconstructed from a loaded package, in the order the reader found them — a
|
|
388
|
-
* read-only inspection view (source range, field roles, value field, aggregation). A pivot
|
|
389
|
-
* authored on this sheet via {@link addPivotTable} does not appear here; a pivot loaded from a
|
|
390
|
-
* file does not appear in {@link pivotTables}. The loaded pivots re-emit verbatim through
|
|
391
|
-
* byte-preservation, so this collection is never itself serialised.
|
|
392
|
-
*/
|
|
393
212
|
get loadedPivotTables() {
|
|
394
213
|
return this.#loadedPivotTables;
|
|
395
214
|
}
|
|
396
|
-
/**
|
|
397
|
-
* Anchor a threaded conversation to a cell — Excel's modern review comment: an opening message, its
|
|
398
|
-
* replies, and whether the discussion was marked resolved. Distinct from a cell's legacy note
|
|
399
|
-
* ({@link Cell.note}), and mutually exclusive with one: Excel refuses to put both on one cell, and a
|
|
400
|
-
* cell carrying both is written back as the conversation alone.
|
|
401
|
-
*
|
|
402
|
-
* Every message supplies its own {@link Comment.id} and {@link Comment.date}, and names its author by
|
|
403
|
-
* {@link Comment.personId} into the workbook registry ({@link Workbook.addPerson}) — the writer has no
|
|
404
|
-
* clock and no id generator, so nothing here is invented and the same workbook always serialises to the
|
|
405
|
-
* same bytes. Every id is normalised to the brace-wrapped upper-case GUID form the format requires, so a
|
|
406
|
-
* `crypto.randomUUID()` is accepted as-is.
|
|
407
|
-
*
|
|
408
|
-
* Message ids must be unique **within this sheet**, because that is the scope in which they mean
|
|
409
|
-
* anything: a reply names its thread by the head's id inside the sheet's own part, and the legacy
|
|
410
|
-
* fallback comment binds its cell by the same id inside the sheet's own comments part. Two sheets reusing
|
|
411
|
-
* one id is therefore harmless and is not rejected — Excel's ids happen to be globally unique, but
|
|
412
|
-
* nothing resolves across a part boundary.
|
|
413
|
-
*
|
|
414
|
-
* @throws {SyntaxError} if the anchor does not resolve to a single cell, if any id is not a GUID, if a
|
|
415
|
-
* message id is already used on this sheet, or if a mention's span is not a whole number the wire can
|
|
416
|
-
* express.
|
|
417
|
-
*/
|
|
418
215
|
addCommentThread(thread) {
|
|
419
216
|
const taken = new Set(this.#commentThreads.flatMap((held) => held.comments.map((comment) => comment.id)));
|
|
420
|
-
// Every message is validated before any of it is stored, so a rejection leaves the sheet untouched
|
|
421
|
-
// rather than half-carrying a conversation whose remaining messages were refused.
|
|
422
217
|
const comments = thread.comments.map((comment) => {
|
|
423
218
|
const id = commentThreadGuid(comment.id, 'a comment id');
|
|
424
219
|
if (taken.has(id)) {
|
|
@@ -445,17 +240,9 @@ export class Worksheet {
|
|
|
445
240
|
});
|
|
446
241
|
this.#commentThreads.push({ ...thread, ref: this.#anchorRef(thread.ref), comments });
|
|
447
242
|
}
|
|
448
|
-
/**
|
|
449
|
-
* The threaded conversations on this sheet — Excel's modern review comments (author, timestamp,
|
|
450
|
-
* replies, resolved state, `@mentions`). Empty for a sheet with none. Distinct from a cell's legacy note
|
|
451
|
-
* ({@link Cell.note}).
|
|
452
|
-
*/
|
|
453
243
|
get commentThreads() {
|
|
454
244
|
return this.#commentThreads;
|
|
455
245
|
}
|
|
456
|
-
// The canonical A1 form of a conversation's anchor. A thread hangs off one cell, and both the writer's
|
|
457
|
-
// fallback comment and {@link commentThreadAt} compare anchors as plain strings, so `$B$2` and `B2` must
|
|
458
|
-
// not be two anchors.
|
|
459
246
|
#anchorRef(reference) {
|
|
460
247
|
const { col, row } = decodeAddress(reference);
|
|
461
248
|
if (col === undefined || row === undefined) {
|
|
@@ -463,20 +250,11 @@ export class Worksheet {
|
|
|
463
250
|
}
|
|
464
251
|
return encodeAddress(col, row);
|
|
465
252
|
}
|
|
466
|
-
/**
|
|
467
|
-
* The conversation anchored to a cell, or `undefined` when that cell carries none. The reference is
|
|
468
|
-
* canonicalized, so an absolute `"$B$2"` finds the same thread as `"B2"`; it names the *anchor* cell,
|
|
469
|
-
* so a cell merely covered by the anchor's merged region is not a match.
|
|
470
|
-
*
|
|
471
|
-
* @throws {SyntaxError} if the reference does not resolve to a single cell.
|
|
472
|
-
*/
|
|
473
253
|
commentThreadAt(reference) {
|
|
474
254
|
const anchor = this.#anchorRef(reference);
|
|
475
255
|
return this.#commentThreads.find((thread) => thread.ref === anchor);
|
|
476
256
|
}
|
|
477
257
|
addImage(imageId, anchor) {
|
|
478
|
-
// Bind the pure anchor geometry to this sheet's per-column/row sizes; a size a column or row does
|
|
479
|
-
// not set defers to the sheet default, then (inside resolveAnchorPoint) to Excel's own default.
|
|
480
258
|
const columnWidth = (col) => this.#columns.get(col + 1)?.width ?? this.properties.defaultColWidth;
|
|
481
259
|
const rowHeight = (row) => this.#rowProperties.get(row + 1)?.height ?? this.properties.defaultRowHeight;
|
|
482
260
|
if ('ext' in anchor) {
|
|
@@ -493,53 +271,28 @@ export class Worksheet {
|
|
|
493
271
|
const twoCell = anchor.editAs !== undefined ? { from, to, editAs: anchor.editAs } : { from, to };
|
|
494
272
|
this.#images.push({ imageId, anchor: twoCell });
|
|
495
273
|
}
|
|
496
|
-
/**
|
|
497
|
-
* Anchor an image with a pre-built model anchor in the model's own units (EMUs). This is the
|
|
498
|
-
* low-level primitive {@link addImage} builds on and the reader uses to re-pin an image parsed from
|
|
499
|
-
* a drawing part without a lossy pixel round-trip.
|
|
500
|
-
*/
|
|
501
274
|
addImageAnchor(imageId, anchor) {
|
|
502
275
|
this.#images.push({ imageId, anchor });
|
|
503
276
|
}
|
|
504
|
-
/** Drop every anchor of the given workbook image from this sheet. The image stays registered on the
|
|
505
|
-
* workbook — another sheet may still show it — so only this sheet's anchors are removed; the writer
|
|
506
|
-
* then omits any media no sheet anchors any longer. */
|
|
507
277
|
removeImage(imageId) {
|
|
508
278
|
const kept = this.#images.filter((image) => image.imageId !== imageId);
|
|
509
279
|
replaceContents(this.#images, kept);
|
|
510
280
|
}
|
|
511
|
-
/** The images anchored to this sheet, in the order they were added. */
|
|
512
281
|
get images() {
|
|
513
282
|
return this.#images;
|
|
514
283
|
}
|
|
515
|
-
/** Set this sheet's background image to a workbook image (the id {@link Workbook.addImage} returned).
|
|
516
|
-
* The picture tiles behind the whole grid; it is not anchored to any cell. Passing a new id replaces
|
|
517
|
-
* the previous background. */
|
|
518
284
|
addBackgroundImage(imageId) {
|
|
519
285
|
this.#backgroundImageId = imageId;
|
|
520
286
|
}
|
|
521
|
-
/** Remove this sheet's background image, if any. The image stays registered on the workbook. */
|
|
522
287
|
removeBackgroundImage() {
|
|
523
288
|
this.#backgroundImageId = undefined;
|
|
524
289
|
}
|
|
525
|
-
/** The workbook image id set as this sheet's background, or `undefined` when it has none. */
|
|
526
290
|
get backgroundImageId() {
|
|
527
291
|
return this.#backgroundImageId;
|
|
528
292
|
}
|
|
529
|
-
/** The worksheet-level references to unmodeled package content preserved for round-tripping. */
|
|
530
293
|
get preservedReferences() {
|
|
531
294
|
return this.#preservedReferences;
|
|
532
295
|
}
|
|
533
|
-
/**
|
|
534
|
-
* Merge a range of cells (`"A1:B2"`). A range that overlaps an already-merged region is
|
|
535
|
-
* rejected — Excel forbids overlapping merges and writes such geometry as a corrupt file.
|
|
536
|
-
* Whole-row/column ranges (`"A:A"`) are unbounded, carry no rectangle, and are not overlap-checked.
|
|
537
|
-
*
|
|
538
|
-
* Any value already sitting in a covered non-anchor cell is discarded, keeping only the top-left
|
|
539
|
-
* anchor's — exactly how Excel collapses a range on merge. Leaving it would emit a populated `<c>`
|
|
540
|
-
* under the `<mergeCell>` ref, the geometry Excel opens with a repair prompt. Covered-cell styles
|
|
541
|
-
* survive (a border spanning the merge is legal), so only the conflicting value is cleared.
|
|
542
|
-
*/
|
|
543
296
|
mergeCells(range) {
|
|
544
297
|
const { top, left, bottom, right } = decodeRange(range);
|
|
545
298
|
if (top !== undefined && left !== undefined && bottom !== undefined && right !== undefined) {
|
|
@@ -549,55 +302,19 @@ export class Worksheet {
|
|
|
549
302
|
throw new AuthoringError(`merged range "${range}" overlaps an existing merged region`);
|
|
550
303
|
}
|
|
551
304
|
this.#mergeRects.push(rect);
|
|
552
|
-
this.#
|
|
305
|
+
clearCoveredValues(this.#rows, rect);
|
|
553
306
|
}
|
|
554
307
|
this.#merges.push(range);
|
|
555
308
|
}
|
|
556
|
-
// Drop any value already sitting in a merge's covered non-anchor cells, keeping only the top-left
|
|
557
|
-
// anchor — the collapse Excel performs on merge. A leftover covered value would serialise as a
|
|
558
|
-
// populated `<c>` under the range's `<mergeCell>` ref, the geometry that trips Excel's repair
|
|
559
|
-
// prompt. Styles are untouched: a border spanning the merged region rides the covered cells.
|
|
560
|
-
#clearCoveredValues(rect) {
|
|
561
|
-
for (let row = rect.top; row <= rect.bottom; row++) {
|
|
562
|
-
const cols = this.#rows.get(row);
|
|
563
|
-
if (cols === undefined)
|
|
564
|
-
continue;
|
|
565
|
-
for (let col = rect.left; col <= rect.right; col++) {
|
|
566
|
-
if (row === rect.top && col === rect.left)
|
|
567
|
-
continue;
|
|
568
|
-
const covered = cols.get(col);
|
|
569
|
-
if (covered !== undefined)
|
|
570
|
-
covered.value = null;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
/** The merged ranges on this sheet, in the order they were added. */
|
|
575
309
|
get merges() {
|
|
576
310
|
return this.#merges;
|
|
577
311
|
}
|
|
578
|
-
/**
|
|
579
|
-
* The sheet's autofilter — its range plus any per-column criteria — or `undefined` when the sheet
|
|
580
|
-
* carries none. Setting one turns on the header-row filter dropdowns Excel draws over the range;
|
|
581
|
-
* the writer emits both the sheet's `<autoFilter>` element and the hidden `_FilterDatabase` defined
|
|
582
|
-
* name Excel derives from it. Setting `undefined` clears the filter.
|
|
583
|
-
*
|
|
584
|
-
* A bare range string is the ergonomic common case — `sheet.autoFilter = 'A1:C10'` for dropdowns
|
|
585
|
-
* with no active criteria; pass an {@link AutoFilter} object to narrow columns. Either way the
|
|
586
|
-
* value is normalised on assignment (range to canonical `A1:C10` form) and the getter returns the
|
|
587
|
-
* structured object. The range must be a bounded rectangle — a whole-row/column reference is not a
|
|
588
|
-
* filterable region and is rejected.
|
|
589
|
-
*/
|
|
590
312
|
get autoFilter() {
|
|
591
313
|
return this.#autoFilter;
|
|
592
314
|
}
|
|
593
315
|
set autoFilter(filter) {
|
|
594
316
|
this.#autoFilter = filter === undefined ? undefined : canonicalizeAutoFilter(filter);
|
|
595
317
|
}
|
|
596
|
-
/**
|
|
597
|
-
* Remove a merged range previously added with {@link mergeCells}, returning whether a merge with
|
|
598
|
-
* that exact range string existed. The covering rectangle is dropped alongside it, so a cell the
|
|
599
|
-
* merge had masked addresses independently again. The inverse of {@link mergeCells}.
|
|
600
|
-
*/
|
|
601
318
|
unmergeCells(range) {
|
|
602
319
|
const index = this.#merges.indexOf(range);
|
|
603
320
|
if (index === -1)
|
|
@@ -611,58 +328,24 @@ export class Worksheet {
|
|
|
611
328
|
}
|
|
612
329
|
return true;
|
|
613
330
|
}
|
|
614
|
-
/**
|
|
615
|
-
* Attach a data validation to a target range (`"B2:B20"`, a whole column `"B2:B1048576"`, or a
|
|
616
|
-
* space-separated `sqref` of several ranges). The rule is stored once against the range, not copied
|
|
617
|
-
* per covered cell, so a whole-column dropdown stays a single entry. A cell inside the range reports
|
|
618
|
-
* the rule through {@link dataValidationAt}.
|
|
619
|
-
*
|
|
620
|
-
* Pass `{extended: true}` to mark a rule that belongs in the 2009 extension form
|
|
621
|
-
* (`<x14:dataValidation>`) — the carrier Excel uses for a list source on another sheet and other
|
|
622
|
-
* shapes the standard element cannot express. The reader sets it for a rule found in that form so a
|
|
623
|
-
* round-trip writes it back there instead of silently corrupting the cross-sheet reference.
|
|
624
|
-
*/
|
|
625
331
|
addDataValidation(sqref, rule, options = {}) {
|
|
626
332
|
this.#dataValidations.add(sqref, rule, options);
|
|
627
333
|
}
|
|
628
|
-
/** The data validations on this sheet, each bound to its target range, in insertion order. */
|
|
629
334
|
get dataValidations() {
|
|
630
335
|
return this.#dataValidations.entries;
|
|
631
336
|
}
|
|
632
|
-
/**
|
|
633
|
-
* Attach a conditional formatting to a target range. `formatting.ref` is an OOXML `sqref` — one
|
|
634
|
-
* range (`"A1:A10"`), a whole column, or several space-separated areas (`"A1:C1 A3:C3"`) sharing one
|
|
635
|
-
* rule set. The block is stored once against the range, defensively copied so the getter never hands
|
|
636
|
-
* back a reference into the caller's object.
|
|
637
|
-
*/
|
|
638
337
|
addConditionalFormatting(formatting) {
|
|
639
338
|
this.#conditionalFormattings.add(formatting);
|
|
640
339
|
}
|
|
641
|
-
/** The conditional formattings on this sheet, each bound to its target range, in insertion order. */
|
|
642
340
|
get conditionalFormattings() {
|
|
643
341
|
return this.#conditionalFormattings.entries;
|
|
644
342
|
}
|
|
645
|
-
/**
|
|
646
|
-
* The validation covering a cell, or `undefined` when none does. The first added rule whose range
|
|
647
|
-
* contains the cell wins, mirroring how a spreadsheet resolves overlapping validations.
|
|
648
|
-
*/
|
|
649
343
|
dataValidationAt(reference) {
|
|
650
344
|
const { col, row } = decodeAddress(reference);
|
|
651
345
|
if (col === undefined || row === undefined)
|
|
652
346
|
return undefined;
|
|
653
347
|
return this.#dataValidations.at(col, row);
|
|
654
348
|
}
|
|
655
|
-
/**
|
|
656
|
-
* Remove `count` rows starting at the 1-based `start`, then insert the given rows in their place.
|
|
657
|
-
* Rows below the edit shift by `inserts.length - count`: a delete pulls the tail up, an insert
|
|
658
|
-
* pushes it down, and doing both at once is a replace. Each inserted row takes either
|
|
659
|
-
* {@link RowInput} shape — a positional array from column A, or a key-addressed object — exactly
|
|
660
|
-
* like {@link addRow}. A `count` larger than the rows present simply clears the tail — it never
|
|
661
|
-
* silently becomes a no-op. Cells carry their full style to the shifted position, and merged ranges
|
|
662
|
-
* shift with the rows they cover.
|
|
663
|
-
*
|
|
664
|
-
* @throws {RangeError} if `start` is not a positive integer or `count` is negative.
|
|
665
|
-
*/
|
|
666
349
|
spliceRows(start, count, ...inserts) {
|
|
667
350
|
if (!Number.isInteger(start) || start < 1) {
|
|
668
351
|
throw new RangeError(`splice start ${start} is out of bounds — rows start at 1`);
|
|
@@ -670,87 +353,26 @@ export class Worksheet {
|
|
|
670
353
|
if (!Number.isInteger(count) || count < 0) {
|
|
671
354
|
throw new RangeError(`splice count ${count} is invalid — it must be a non-negative integer`);
|
|
672
355
|
}
|
|
673
|
-
const inserted = inserts.map((values, i) =>
|
|
356
|
+
const inserted = inserts.map((values, i) => buildRowCells(start + i, values, this.#columns));
|
|
674
357
|
this.#edits.spliceRows(start, count, inserted);
|
|
675
358
|
}
|
|
676
|
-
/**
|
|
677
|
-
* Insert one row of `values` at the 1-based `pos`, shifting the rows at and below it down by one.
|
|
678
|
-
* `values` takes either {@link RowInput} shape (positional array or keyed object), like
|
|
679
|
-
* {@link addRow}. Shorthand for {@link spliceRows}`(pos, 0, values)`.
|
|
680
|
-
*
|
|
681
|
-
* @throws {RangeError} if `pos` is not a positive integer.
|
|
682
|
-
*/
|
|
683
359
|
insertRow(pos, values) {
|
|
684
360
|
this.spliceRows(pos, 0, values);
|
|
685
361
|
}
|
|
686
|
-
/**
|
|
687
|
-
* Append a row of `values` after the last used row, returning the cells it materialised.
|
|
688
|
-
* The append point is {@link rowCount}` + 1`, so the row lands below every row that holds
|
|
689
|
-
* data or its own formatting — never overwriting existing content, unlike {@link insertRow},
|
|
690
|
-
* which shifts and needs a position. Unlike {@link spliceRows}, appending shifts nothing, so
|
|
691
|
-
* it never disturbs merges or the rows above.
|
|
692
|
-
*
|
|
693
|
-
* A row takes either shape: a positional array whose values map to columns from A — a hole in a
|
|
694
|
-
* sparse array (`['a', , 'c']`) leaves that column untouched — or a keyed object whose values
|
|
695
|
-
* land under the columns carrying the matching {@link ColumnProperties.key}.
|
|
696
|
-
*/
|
|
697
362
|
addRow(values) {
|
|
698
363
|
return this.addRows([values])[0] ?? [];
|
|
699
364
|
}
|
|
700
|
-
/**
|
|
701
|
-
* Append several rows after the last used row in one call, returning the cells materialised
|
|
702
|
-
* for each. The rows stack in order — the first lands at {@link rowCount}` + 1`, the next
|
|
703
|
-
* directly below it — so a later row never collides with an earlier one even when both are
|
|
704
|
-
* value-less. Each row is an array or a keyed object independently, so a mixed batch is fine.
|
|
705
|
-
* The bulk form of {@link addRow}.
|
|
706
|
-
*/
|
|
707
365
|
addRows(rows) {
|
|
708
366
|
let number = this.rowCount;
|
|
709
367
|
return rows.map((values) => {
|
|
710
368
|
number += 1;
|
|
711
|
-
return
|
|
369
|
+
return rowPlacements(values, this.#columns).map(([col, value]) => {
|
|
712
370
|
const cell = this.#cellAt(number, col);
|
|
713
371
|
cell.value = value;
|
|
714
372
|
return cell;
|
|
715
373
|
});
|
|
716
374
|
});
|
|
717
375
|
}
|
|
718
|
-
// Resolve a RowInput to the (1-based column, value) placements it names, the one interpretation of
|
|
719
|
-
// row shape that both appending (into the live grid) and splicing (into a detached row) share. A
|
|
720
|
-
// positional array maps each value to its column from A, skipping a hole or an explicit `undefined`
|
|
721
|
-
// so that column is left untouched; a keyed object maps each value under the column carrying the
|
|
722
|
-
// matching key. Array.isArray, not `instanceof Array`: a row built in another realm (a vm context,
|
|
723
|
-
// a browser iframe) is still an array but fails the identity check, and would then be walked as a
|
|
724
|
-
// keyed object — placing nothing.
|
|
725
|
-
#rowPlacements(values) {
|
|
726
|
-
if (Array.isArray(values)) {
|
|
727
|
-
const placements = [];
|
|
728
|
-
values.forEach((value, index) => {
|
|
729
|
-
if (value !== undefined)
|
|
730
|
-
placements.push([index + 1, value]);
|
|
731
|
-
});
|
|
732
|
-
return placements;
|
|
733
|
-
}
|
|
734
|
-
return Object.entries(values).map(([key, value]) => [this.#columnIndexByKey(key), value]);
|
|
735
|
-
}
|
|
736
|
-
// Build the detached cell row an insert introduces: a fresh cell per placement, positioned at
|
|
737
|
-
// `number`, keyed by column. The grid-edit machinery then splices this map into place.
|
|
738
|
-
#buildRowCells(number, values) {
|
|
739
|
-
const row = new Map();
|
|
740
|
-
for (const [col, value] of this.#rowPlacements(values)) {
|
|
741
|
-
const cell = new Cell(number, col);
|
|
742
|
-
cell.value = value;
|
|
743
|
-
row.set(col, cell);
|
|
744
|
-
}
|
|
745
|
-
return row;
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Freeze the top `ySplit` rows and left `xSplit` columns in place; the rest of the sheet scrolls
|
|
749
|
-
* beneath them. `freeze(1)` pins a header row; `freeze(0, 1)` pins the first column. Passing both
|
|
750
|
-
* zero clears the freeze (equivalent to {@link unfreeze}).
|
|
751
|
-
*
|
|
752
|
-
* @throws {RangeError} if either split is a negative or non-integer count.
|
|
753
|
-
*/
|
|
754
376
|
freeze(ySplit = 1, xSplit = 0) {
|
|
755
377
|
if (!Number.isInteger(ySplit) || ySplit < 0 || !Number.isInteger(xSplit) || xSplit < 0) {
|
|
756
378
|
throw new RangeError(`freeze splits must be non-negative integers; got ySplit=${ySplit}, xSplit=${xSplit}`);
|
|
@@ -764,30 +386,12 @@ export class Worksheet {
|
|
|
764
386
|
this.view.ySplit = ySplit;
|
|
765
387
|
this.view.topLeftCell = encodeAddress(xSplit + 1, ySplit + 1);
|
|
766
388
|
}
|
|
767
|
-
/** Clear any frozen split, returning the sheet to a normal (fully scrolling) view. */
|
|
768
389
|
unfreeze() {
|
|
769
390
|
this.view.state = 'normal';
|
|
770
391
|
delete this.view.xSplit;
|
|
771
392
|
delete this.view.ySplit;
|
|
772
393
|
delete this.view.topLeftCell;
|
|
773
394
|
}
|
|
774
|
-
/** The 1-based index of the column carrying `key` (see {@link ColumnProperties.key}). */
|
|
775
|
-
#columnIndexByKey(key) {
|
|
776
|
-
for (const [index, properties] of this.#columns) {
|
|
777
|
-
if (properties.key === key)
|
|
778
|
-
return index;
|
|
779
|
-
}
|
|
780
|
-
throw new AuthoringError(`no column is keyed ${JSON.stringify(key)} — set getColumn(n).key first`);
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Copy the row at the 1-based `start`, `options.count` times (default 1). With `options.insert`
|
|
784
|
-
* (the default) the copies are inserted directly after the source, shifting the rows below — and
|
|
785
|
-
* any merged range there — down by `count`; otherwise the copies overwrite the rows immediately
|
|
786
|
-
* below without shifting. Each copy is a faithful duplicate of the source's values and per-cell
|
|
787
|
-
* styles, and carries no merge of its own, so a range can be merged onto a duplicated row afterwards.
|
|
788
|
-
*
|
|
789
|
-
* @throws {RangeError} if `start` is not a positive integer or `count` is negative.
|
|
790
|
-
*/
|
|
791
395
|
duplicateRow(start, options = {}) {
|
|
792
396
|
const { count = 1, insert = true } = options;
|
|
793
397
|
if (!Number.isInteger(start) || start < 1) {
|
|
@@ -817,15 +421,6 @@ export class Worksheet {
|
|
|
817
421
|
this.#rows.set(start + i, snapshot(start + i));
|
|
818
422
|
}
|
|
819
423
|
}
|
|
820
|
-
/**
|
|
821
|
-
* Remove `count` columns starting at the 1-based `start`, then insert the given columns in their
|
|
822
|
-
* place — the column analog of {@link spliceRows}. Columns to the right shift by
|
|
823
|
-
* `inserts.length - count`, keeping their values and styles, and a merged range lying wholly to
|
|
824
|
-
* the right of the edit re-anchors to its new columns. Each inserted column is an array of values
|
|
825
|
-
* indexed by row (index 0 → row 1); an empty array inserts a blank column.
|
|
826
|
-
*
|
|
827
|
-
* @throws {RangeError} if `start` is not a positive integer or `count` is negative.
|
|
828
|
-
*/
|
|
829
424
|
spliceColumns(start, count, ...inserts) {
|
|
830
425
|
if (!Number.isInteger(start) || start < 1) {
|
|
831
426
|
throw new RangeError(`splice start ${start} is out of bounds — columns start at 1`);
|
|
@@ -835,35 +430,12 @@ export class Worksheet {
|
|
|
835
430
|
}
|
|
836
431
|
this.#edits.spliceColumns(start, count, inserts);
|
|
837
432
|
}
|
|
838
|
-
/**
|
|
839
|
-
* Insert one column of `values` at the 1-based `pos`, shifting the columns at and right of it over
|
|
840
|
-
* by one. `values` is an array of values indexed by row (index 0 → row 1), like
|
|
841
|
-
* {@link addColumn}. Shorthand for {@link spliceColumns}`(pos, 0, values)`.
|
|
842
|
-
*
|
|
843
|
-
* @throws {RangeError} if `pos` is not a positive integer.
|
|
844
|
-
*/
|
|
845
433
|
insertColumn(pos, values) {
|
|
846
434
|
this.spliceColumns(pos, 0, values);
|
|
847
435
|
}
|
|
848
|
-
/**
|
|
849
|
-
* Append a column of `values` after the last used column, returning the cells it materialised.
|
|
850
|
-
* The append point is {@link columnCount}` + 1`, so the column lands right of every column that
|
|
851
|
-
* holds data or its own formatting — never overwriting existing content, unlike {@link insertColumn},
|
|
852
|
-
* which shifts and needs a position. Unlike {@link spliceColumns}, appending shifts nothing, so it
|
|
853
|
-
* never disturbs merges or the columns to its left.
|
|
854
|
-
*
|
|
855
|
-
* `values` is an array indexed by row (index 0 → row 1); a hole or an explicit `undefined` leaves
|
|
856
|
-
* that row untouched, mirroring {@link addRow}'s positional-array shape.
|
|
857
|
-
*/
|
|
858
436
|
addColumn(values) {
|
|
859
437
|
return this.addColumns([values])[0] ?? [];
|
|
860
438
|
}
|
|
861
|
-
/**
|
|
862
|
-
* Append several columns after the last used column in one call, returning the cells materialised
|
|
863
|
-
* for each. The columns stack in order — the first lands at {@link columnCount}` + 1`, the next
|
|
864
|
-
* directly right of it — so a later column never collides with an earlier one even when both are
|
|
865
|
-
* value-less. The bulk form of {@link addColumn}.
|
|
866
|
-
*/
|
|
867
439
|
addColumns(columns) {
|
|
868
440
|
let index = this.columnCount;
|
|
869
441
|
return columns.map((values) => {
|
|
@@ -879,26 +451,12 @@ export class Worksheet {
|
|
|
879
451
|
return cells;
|
|
880
452
|
});
|
|
881
453
|
}
|
|
882
|
-
/**
|
|
883
|
-
* A snapshot of this sheet's value and overlay content (see {@link WorksheetModel}). Reading it and
|
|
884
|
-
* assigning it onto another sheet — `dst.model = src.model` — reproduces the source: merges, cells
|
|
885
|
-
* and their styles, column/row metadata, tables, the autofilter, protection, and the page setup all
|
|
886
|
-
* survive, because the getter emits and the setter consumes exactly the same fields. Identity
|
|
887
|
-
* (`name`, `id`) is not part of the model and is never touched by assignment; nor are attached parts
|
|
888
|
-
* that carry workbook-level identity (images, pivots, byte-preserved charts/drawings) — see
|
|
889
|
-
* {@link WorksheetModel} for that boundary.
|
|
890
|
-
*/
|
|
891
454
|
get model() {
|
|
892
455
|
const model = {};
|
|
893
456
|
for (const facet of WORKSHEET_MODEL_FACETS)
|
|
894
457
|
model[facet.key] = facet.read(this);
|
|
895
|
-
// TypeScript cannot follow an object built key by key; what makes this sound is that the
|
|
896
|
-
// registry is proved exhaustive over `keyof WorksheetModel` — see the type assertion beside it.
|
|
897
458
|
return model;
|
|
898
459
|
}
|
|
899
|
-
// Empty every collection the model round-trips, so a subsequent replay leaves no residue from
|
|
900
|
-
// whatever the sheet held before. Images, pivots, and byte-preserved parts carry workbook-level
|
|
901
|
-
// identity, are excluded from the model, and so are deliberately left untouched here.
|
|
902
460
|
#resetContent() {
|
|
903
461
|
this.#rows.clear();
|
|
904
462
|
this.#columns.clear();
|
|
@@ -909,23 +467,11 @@ export class Worksheet {
|
|
|
909
467
|
this.#conditionalFormattings.clear();
|
|
910
468
|
this.#tables.length = 0;
|
|
911
469
|
}
|
|
912
|
-
// Assigning a model replaces this sheet's content wholesale — the sheet becomes the model, with no
|
|
913
|
-
// residue from whatever it held before. The registry's declaration order is the application order,
|
|
914
|
-
// and it is load-bearing: see WORKSHEET_MODEL_FACETS.
|
|
915
470
|
set model(model) {
|
|
916
471
|
this.#resetContent();
|
|
917
472
|
for (const facet of WORKSHEET_MODEL_FACETS)
|
|
918
473
|
facet.write(this, model);
|
|
919
474
|
}
|
|
920
|
-
/**
|
|
921
|
-
* Protect the sheet, making the per-cell `locked`/`hidden` flags enforceable. Without a
|
|
922
|
-
* password the protection is a soft lock any consumer can lift; with one, the password is
|
|
923
|
-
* salted and hashed on the spot (the plaintext is never retained) so lifting the protection
|
|
924
|
-
* requires re-supplying it. `options` names which operations stay available to a user while
|
|
925
|
-
* the sheet is protected; anything unspecified falls to Excel's default for that operation.
|
|
926
|
-
*
|
|
927
|
-
* Re-protecting replaces any prior protection; {@link unprotect} clears it.
|
|
928
|
-
*/
|
|
929
475
|
protect(password, options = {}) {
|
|
930
476
|
const { spinCount, ...flags } = options;
|
|
931
477
|
const protection = {
|
|
@@ -936,25 +482,12 @@ export class Worksheet {
|
|
|
936
482
|
}
|
|
937
483
|
this.#protection = protection;
|
|
938
484
|
}
|
|
939
|
-
/** Remove any protection previously set by {@link protect}. */
|
|
940
485
|
unprotect() {
|
|
941
486
|
this.#protection = undefined;
|
|
942
487
|
}
|
|
943
|
-
/** The sheet's protection, or `undefined` if the sheet is unprotected. */
|
|
944
488
|
get protection() {
|
|
945
489
|
return this.#protection;
|
|
946
490
|
}
|
|
947
|
-
// Resolve a position to the master (top-left) of the merged region covering it, or to
|
|
948
|
-
// itself when no region does. First covering region wins; overlaps are rejected in
|
|
949
|
-
// `mergeCells`, so at most one region ever applies.
|
|
950
|
-
#masterOf(row, col) {
|
|
951
|
-
for (const rect of this.#mergeRects) {
|
|
952
|
-
if (row >= rect.top && row <= rect.bottom && col >= rect.left && col <= rect.right) {
|
|
953
|
-
return { row: rect.top, col: rect.left };
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
return { row, col };
|
|
957
|
-
}
|
|
958
491
|
#cellAt(row, col) {
|
|
959
492
|
let cols = this.#rows.get(row);
|
|
960
493
|
if (cols === undefined) {
|
|
@@ -968,10 +501,6 @@ export class Worksheet {
|
|
|
968
501
|
}
|
|
969
502
|
return cell;
|
|
970
503
|
}
|
|
971
|
-
/**
|
|
972
|
-
* The codec's channel into this sheet — see `core/internal.ts` for why these are not public
|
|
973
|
-
* methods. Declared last so every private field it closes over is already in scope.
|
|
974
|
-
*/
|
|
975
504
|
[INTERNAL] = {
|
|
976
505
|
evictRow: (number) => {
|
|
977
506
|
this.#rows.delete(number);
|