@shbernal/ts-xlsx 1.0.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/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,1025 @@
|
|
|
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
|
+
import { AuthoringError } from "../errors.js";
|
|
9
|
+
import { decodeAddress, decodeRange, encodeAddress } from "./address.js";
|
|
10
|
+
import { canonicalizeAutoFilter } from "./autofilter.js";
|
|
11
|
+
import { applyCellStyle, Cell, copyCellContent } from "./cell.js";
|
|
12
|
+
import { Column } from "./column.js";
|
|
13
|
+
import { commentThreadGuid, commentThreadOffset } from "./comment-thread.js";
|
|
14
|
+
import { ConditionalFormattingOverlay } from "./conditional-formatting-overlay.js";
|
|
15
|
+
import { replaceContents } from "./containers.js";
|
|
16
|
+
import { DataValidationOverlay } from "./data-validation-overlay.js";
|
|
17
|
+
import { GridEdits } from "./grid-edits.js";
|
|
18
|
+
import { PX_TO_EMU, resolveAnchorPoint, } from "./image.js";
|
|
19
|
+
import { INTERNAL } from "./internal.js";
|
|
20
|
+
import { rectsOverlap } from "./merge.js";
|
|
21
|
+
import { PivotTable } from "./pivot-table.js";
|
|
22
|
+
import { deriveCredential, } from "./protection.js";
|
|
23
|
+
import { Range, rangeFrom } from "./range.js";
|
|
24
|
+
import { Row } from "./row.js";
|
|
25
|
+
import { Table, TOTALS_ROW_SUBTOTAL_CODE } from "./table.js";
|
|
26
|
+
import { WORKSHEET_MODEL_FACETS } from "./worksheet-model.js";
|
|
27
|
+
export class Worksheet {
|
|
28
|
+
name;
|
|
29
|
+
/** 1-based workbook-assigned id, stable for the sheet's lifetime. */
|
|
30
|
+
id;
|
|
31
|
+
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
|
+
tabColor;
|
|
38
|
+
/** Sheet-level format defaults. Mutate in place: `sheet.properties.defaultRowHeight = 20`. */
|
|
39
|
+
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
|
+
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
|
+
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
|
+
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
|
+
printOptions = {};
|
|
62
|
+
/** Print margins. Mutate in place: `sheet.pageMargins.left = 0.5`. Empty means unset. */
|
|
63
|
+
pageMargins = {};
|
|
64
|
+
/** Page header/footer text. Mutate in place: `sheet.headerFooter.oddHeader = '&C&"..."'`. */
|
|
65
|
+
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
|
+
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
|
+
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
|
+
#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
|
+
#columns = new Map();
|
|
85
|
+
#rowProperties = new Map();
|
|
86
|
+
// Tables, merged ranges, and anchored images are sheet-level overlays on the grid, not cell storage.
|
|
87
|
+
#tables = [];
|
|
88
|
+
#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
|
+
#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
|
+
#commentThreads = [];
|
|
99
|
+
#merges = [];
|
|
100
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
#dataValidations = new DataValidationOverlay();
|
|
117
|
+
#conditionalFormattings = new ConditionalFormattingOverlay();
|
|
118
|
+
// Sheet-level protection is a single overlay switch, absent until `protect` is called.
|
|
119
|
+
#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
|
+
#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
|
+
#edits;
|
|
131
|
+
constructor(name, id, state = 'visible') {
|
|
132
|
+
this.name = name;
|
|
133
|
+
this.id = id;
|
|
134
|
+
this.state = state;
|
|
135
|
+
this.#edits = new GridEdits({
|
|
136
|
+
rows: this.#rows,
|
|
137
|
+
rowProperties: this.#rowProperties,
|
|
138
|
+
columns: this.#columns,
|
|
139
|
+
merges: this.#merges,
|
|
140
|
+
mergeRects: this.#mergeRects,
|
|
141
|
+
tables: this.#tables,
|
|
142
|
+
images: this.#images,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
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
|
+
getCell(reference) {
|
|
159
|
+
const { col, row } = decodeAddress(reference);
|
|
160
|
+
if (col === undefined || row === undefined) {
|
|
161
|
+
throw new SyntaxError(`"${reference}" is not a single-cell reference — it omits a column or row`);
|
|
162
|
+
}
|
|
163
|
+
const master = this.#masterOf(row, col);
|
|
164
|
+
return this.#cellAt(master.row, master.col);
|
|
165
|
+
}
|
|
166
|
+
/** Whether a cell has been materialised at the given 1-based position. */
|
|
167
|
+
hasCell(row, col) {
|
|
168
|
+
return this.#rows.get(row)?.has(col) ?? false;
|
|
169
|
+
}
|
|
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
|
+
getColumn(index) {
|
|
179
|
+
return new Column(this, index);
|
|
180
|
+
}
|
|
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
|
+
getRow(number) {
|
|
190
|
+
return new Row(this, number);
|
|
191
|
+
}
|
|
192
|
+
getRange(referenceOrTop, left, bottom, right) {
|
|
193
|
+
if (typeof referenceOrTop === 'string')
|
|
194
|
+
return rangeFrom(this, referenceOrTop);
|
|
195
|
+
return new Range(this, referenceOrTop, left ?? 0, bottom ?? 0, right ?? 0);
|
|
196
|
+
}
|
|
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
|
+
get rowCount() {
|
|
204
|
+
let last = 0;
|
|
205
|
+
for (const [number, cols] of this.#rows) {
|
|
206
|
+
if (number > last && this.#rowHasContent(cols))
|
|
207
|
+
last = number;
|
|
208
|
+
}
|
|
209
|
+
for (const number of this.#rowProperties.keys()) {
|
|
210
|
+
if (number > last)
|
|
211
|
+
last = number;
|
|
212
|
+
}
|
|
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
|
+
for (const rect of this.#mergeRects) {
|
|
216
|
+
if (rect.bottom > last)
|
|
217
|
+
last = rect.bottom;
|
|
218
|
+
}
|
|
219
|
+
return last;
|
|
220
|
+
}
|
|
221
|
+
/** The number of rows that hold at least one non-empty cell, ignoring gaps and formatting-only rows. */
|
|
222
|
+
get actualRowCount() {
|
|
223
|
+
let count = 0;
|
|
224
|
+
for (const cols of this.#rows.values()) {
|
|
225
|
+
if (this.#rowHasContent(cols))
|
|
226
|
+
count++;
|
|
227
|
+
}
|
|
228
|
+
return count;
|
|
229
|
+
}
|
|
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
|
+
#rowHasContent(cols) {
|
|
234
|
+
for (const cell of cols.values()) {
|
|
235
|
+
if (cell.value !== null)
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
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
|
+
get columnCount() {
|
|
246
|
+
let last = 0;
|
|
247
|
+
for (const cols of this.#rows.values()) {
|
|
248
|
+
for (const [col, cell] of cols) {
|
|
249
|
+
if (cell.value !== null && col > last)
|
|
250
|
+
last = col;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const index of this.#columns.keys()) {
|
|
254
|
+
if (index > last)
|
|
255
|
+
last = index;
|
|
256
|
+
}
|
|
257
|
+
for (const rect of this.#mergeRects) {
|
|
258
|
+
if (rect.right > last)
|
|
259
|
+
last = rect.right;
|
|
260
|
+
}
|
|
261
|
+
return last;
|
|
262
|
+
}
|
|
263
|
+
/** The columns carrying format properties, as handles, in ascending index order. */
|
|
264
|
+
*columns() {
|
|
265
|
+
for (const index of [...this.#columns.keys()].sort((a, b) => a - b)) {
|
|
266
|
+
yield new Column(this, index);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
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
|
+
*rows() {
|
|
278
|
+
const numbers = new Set([...this.#rows.keys(), ...this.#rowProperties.keys()]);
|
|
279
|
+
for (const number of [...numbers].sort((a, b) => a - b)) {
|
|
280
|
+
yield new Row(this, number);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
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 {@link AuthoringError} if the name, columns, or geometry are invalid.
|
|
289
|
+
*/
|
|
290
|
+
addTable(options) {
|
|
291
|
+
const table = new Table(options, (row, col, value, style) => {
|
|
292
|
+
const cell = this.#cellAt(row, col);
|
|
293
|
+
cell.value = value;
|
|
294
|
+
if (style !== undefined)
|
|
295
|
+
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, []));
|
|
300
|
+
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
|
+
if (table.headerRow) {
|
|
312
|
+
const { top, left } = table.region;
|
|
313
|
+
table.columns.forEach((column, index) => {
|
|
314
|
+
const col = left + index;
|
|
315
|
+
if (this.hasCell(top, col) && this.#cellAt(top, col).value != null)
|
|
316
|
+
return;
|
|
317
|
+
this.#cellAt(top, col).value = column.name;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
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
|
+
if (table.totalsRow) {
|
|
334
|
+
const { left, bottom } = table.region;
|
|
335
|
+
table.columns.forEach((column, index) => {
|
|
336
|
+
const col = left + index;
|
|
337
|
+
if (this.hasCell(bottom, col) && this.#cellAt(bottom, col).value != null)
|
|
338
|
+
return;
|
|
339
|
+
if (column.totalsRowLabel !== undefined) {
|
|
340
|
+
this.#cellAt(bottom, col).value = column.totalsRowLabel;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (column.totalsRowFunction !== undefined) {
|
|
344
|
+
const code = TOTALS_ROW_SUBTOTAL_CODE[column.totalsRowFunction];
|
|
345
|
+
if (code !== undefined) {
|
|
346
|
+
this.#cellAt(bottom, col).value = {
|
|
347
|
+
formula: `SUBTOTAL(${code},${table.name}[${column.name}])`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
else if (column.totalsRowFunction === 'custom' &&
|
|
351
|
+
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
|
+
this.#cellAt(bottom, col).value = { formula: column.totalsRowFormula };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return table;
|
|
360
|
+
}
|
|
361
|
+
/** The tables defined on this sheet, in definition order. */
|
|
362
|
+
get tables() {
|
|
363
|
+
return this.#tables;
|
|
364
|
+
}
|
|
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
|
+
getTable(name) {
|
|
368
|
+
return this.#tables.find((table) => table.name === name);
|
|
369
|
+
}
|
|
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 {@link AuthoringError} if the metric, fields, or source shape are unsupported.
|
|
376
|
+
*/
|
|
377
|
+
addPivotTable(options) {
|
|
378
|
+
const pivot = new PivotTable(options);
|
|
379
|
+
this.#pivotTables.push(pivot);
|
|
380
|
+
return pivot;
|
|
381
|
+
}
|
|
382
|
+
/** The pivot tables hosted on this sheet, in definition order. */
|
|
383
|
+
get pivotTables() {
|
|
384
|
+
return this.#pivotTables;
|
|
385
|
+
}
|
|
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
|
+
get loadedPivotTables() {
|
|
394
|
+
return this.#loadedPivotTables;
|
|
395
|
+
}
|
|
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
|
+
addCommentThread(thread) {
|
|
419
|
+
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
|
+
const comments = thread.comments.map((comment) => {
|
|
423
|
+
const id = commentThreadGuid(comment.id, 'a comment id');
|
|
424
|
+
if (taken.has(id)) {
|
|
425
|
+
throw new SyntaxError(`a comment id must be unique within a sheet, but "${id}" is already used on "${this.name}" — ` +
|
|
426
|
+
'a reply and the legacy fallback comment both find their thread by it');
|
|
427
|
+
}
|
|
428
|
+
taken.add(id);
|
|
429
|
+
return {
|
|
430
|
+
...comment,
|
|
431
|
+
id,
|
|
432
|
+
...(comment.personId !== undefined
|
|
433
|
+
? { personId: commentThreadGuid(comment.personId, "a comment's author id") }
|
|
434
|
+
: {}),
|
|
435
|
+
mentions: comment.mentions.map((mention) => ({
|
|
436
|
+
...mention,
|
|
437
|
+
personId: commentThreadGuid(mention.personId, "a mention's person id"),
|
|
438
|
+
startIndex: commentThreadOffset(mention.startIndex, "a mention's startIndex"),
|
|
439
|
+
length: commentThreadOffset(mention.length, "a mention's length"),
|
|
440
|
+
...(mention.mentionId !== undefined
|
|
441
|
+
? { mentionId: commentThreadGuid(mention.mentionId, 'a mention id') }
|
|
442
|
+
: {}),
|
|
443
|
+
})),
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
this.#commentThreads.push({ ...thread, ref: this.#anchorRef(thread.ref), comments });
|
|
447
|
+
}
|
|
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
|
+
get commentThreads() {
|
|
454
|
+
return this.#commentThreads;
|
|
455
|
+
}
|
|
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
|
+
#anchorRef(reference) {
|
|
460
|
+
const { col, row } = decodeAddress(reference);
|
|
461
|
+
if (col === undefined || row === undefined) {
|
|
462
|
+
throw new SyntaxError(`"${reference}" is not a single-cell reference — it omits a column or row`);
|
|
463
|
+
}
|
|
464
|
+
return encodeAddress(col, row);
|
|
465
|
+
}
|
|
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
|
+
commentThreadAt(reference) {
|
|
474
|
+
const anchor = this.#anchorRef(reference);
|
|
475
|
+
return this.#commentThreads.find((thread) => thread.ref === anchor);
|
|
476
|
+
}
|
|
477
|
+
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
|
+
const columnWidth = (col) => this.#columns.get(col + 1)?.width ?? this.properties.defaultColWidth;
|
|
481
|
+
const rowHeight = (row) => this.#rowProperties.get(row + 1)?.height ?? this.properties.defaultRowHeight;
|
|
482
|
+
if ('ext' in anchor) {
|
|
483
|
+
const ext = {
|
|
484
|
+
cx: Math.round(anchor.ext.width * PX_TO_EMU),
|
|
485
|
+
cy: Math.round(anchor.ext.height * PX_TO_EMU),
|
|
486
|
+
};
|
|
487
|
+
const from = resolveAnchorPoint(anchor.tl, columnWidth, rowHeight);
|
|
488
|
+
this.#images.push({ imageId, anchor: { from, ext } });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const from = resolveAnchorPoint(anchor.tl, columnWidth, rowHeight);
|
|
492
|
+
const to = resolveAnchorPoint(anchor.br, columnWidth, rowHeight);
|
|
493
|
+
const twoCell = anchor.editAs !== undefined ? { from, to, editAs: anchor.editAs } : { from, to };
|
|
494
|
+
this.#images.push({ imageId, anchor: twoCell });
|
|
495
|
+
}
|
|
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
|
+
addImageAnchor(imageId, anchor) {
|
|
502
|
+
this.#images.push({ imageId, anchor });
|
|
503
|
+
}
|
|
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
|
+
removeImage(imageId) {
|
|
508
|
+
const kept = this.#images.filter((image) => image.imageId !== imageId);
|
|
509
|
+
replaceContents(this.#images, kept);
|
|
510
|
+
}
|
|
511
|
+
/** The images anchored to this sheet, in the order they were added. */
|
|
512
|
+
get images() {
|
|
513
|
+
return this.#images;
|
|
514
|
+
}
|
|
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
|
+
addBackgroundImage(imageId) {
|
|
519
|
+
this.#backgroundImageId = imageId;
|
|
520
|
+
}
|
|
521
|
+
/** Remove this sheet's background image, if any. The image stays registered on the workbook. */
|
|
522
|
+
removeBackgroundImage() {
|
|
523
|
+
this.#backgroundImageId = undefined;
|
|
524
|
+
}
|
|
525
|
+
/** The workbook image id set as this sheet's background, or `undefined` when it has none. */
|
|
526
|
+
get backgroundImageId() {
|
|
527
|
+
return this.#backgroundImageId;
|
|
528
|
+
}
|
|
529
|
+
/** The worksheet-level references to unmodeled package content preserved for round-tripping. */
|
|
530
|
+
get preservedReferences() {
|
|
531
|
+
return this.#preservedReferences;
|
|
532
|
+
}
|
|
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
|
+
mergeCells(range) {
|
|
544
|
+
const { top, left, bottom, right } = decodeRange(range);
|
|
545
|
+
if (top !== undefined && left !== undefined && bottom !== undefined && right !== undefined) {
|
|
546
|
+
const rect = { top, left, bottom, right };
|
|
547
|
+
const clash = this.#mergeRects.find((existing) => rectsOverlap(existing, rect));
|
|
548
|
+
if (clash) {
|
|
549
|
+
throw new AuthoringError(`merged range "${range}" overlaps an existing merged region`);
|
|
550
|
+
}
|
|
551
|
+
this.#mergeRects.push(rect);
|
|
552
|
+
this.#clearCoveredValues(rect);
|
|
553
|
+
}
|
|
554
|
+
this.#merges.push(range);
|
|
555
|
+
}
|
|
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
|
+
get merges() {
|
|
576
|
+
return this.#merges;
|
|
577
|
+
}
|
|
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
|
+
get autoFilter() {
|
|
591
|
+
return this.#autoFilter;
|
|
592
|
+
}
|
|
593
|
+
set autoFilter(filter) {
|
|
594
|
+
this.#autoFilter = filter === undefined ? undefined : canonicalizeAutoFilter(filter);
|
|
595
|
+
}
|
|
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
|
+
unmergeCells(range) {
|
|
602
|
+
const index = this.#merges.indexOf(range);
|
|
603
|
+
if (index === -1)
|
|
604
|
+
return false;
|
|
605
|
+
this.#merges.splice(index, 1);
|
|
606
|
+
const { top, left, bottom, right } = decodeRange(range);
|
|
607
|
+
if (top !== undefined && left !== undefined && bottom !== undefined && right !== undefined) {
|
|
608
|
+
const rectIndex = this.#mergeRects.findIndex((r) => r.top === top && r.left === left && r.bottom === bottom && r.right === right);
|
|
609
|
+
if (rectIndex !== -1)
|
|
610
|
+
this.#mergeRects.splice(rectIndex, 1);
|
|
611
|
+
}
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
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
|
+
addDataValidation(sqref, rule, options = {}) {
|
|
626
|
+
this.#dataValidations.add(sqref, rule, options);
|
|
627
|
+
}
|
|
628
|
+
/** The data validations on this sheet, each bound to its target range, in insertion order. */
|
|
629
|
+
get dataValidations() {
|
|
630
|
+
return this.#dataValidations.entries;
|
|
631
|
+
}
|
|
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
|
+
addConditionalFormatting(formatting) {
|
|
639
|
+
this.#conditionalFormattings.add(formatting);
|
|
640
|
+
}
|
|
641
|
+
/** The conditional formattings on this sheet, each bound to its target range, in insertion order. */
|
|
642
|
+
get conditionalFormattings() {
|
|
643
|
+
return this.#conditionalFormattings.entries;
|
|
644
|
+
}
|
|
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
|
+
dataValidationAt(reference) {
|
|
650
|
+
const { col, row } = decodeAddress(reference);
|
|
651
|
+
if (col === undefined || row === undefined)
|
|
652
|
+
return undefined;
|
|
653
|
+
return this.#dataValidations.at(col, row);
|
|
654
|
+
}
|
|
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
|
+
spliceRows(start, count, ...inserts) {
|
|
667
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
668
|
+
throw new RangeError(`splice start ${start} is out of bounds — rows start at 1`);
|
|
669
|
+
}
|
|
670
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
671
|
+
throw new RangeError(`splice count ${count} is invalid — it must be a non-negative integer`);
|
|
672
|
+
}
|
|
673
|
+
const inserted = inserts.map((values, i) => this.#buildRowCells(start + i, values));
|
|
674
|
+
this.#edits.spliceRows(start, count, inserted);
|
|
675
|
+
}
|
|
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
|
+
insertRow(pos, values) {
|
|
684
|
+
this.spliceRows(pos, 0, values);
|
|
685
|
+
}
|
|
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
|
+
addRow(values) {
|
|
698
|
+
return this.addRows([values])[0] ?? [];
|
|
699
|
+
}
|
|
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
|
+
addRows(rows) {
|
|
708
|
+
let number = this.rowCount;
|
|
709
|
+
return rows.map((values) => {
|
|
710
|
+
number += 1;
|
|
711
|
+
return this.#rowPlacements(values).map(([col, value]) => {
|
|
712
|
+
const cell = this.#cellAt(number, col);
|
|
713
|
+
cell.value = value;
|
|
714
|
+
return cell;
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
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
|
+
freeze(ySplit = 1, xSplit = 0) {
|
|
755
|
+
if (!Number.isInteger(ySplit) || ySplit < 0 || !Number.isInteger(xSplit) || xSplit < 0) {
|
|
756
|
+
throw new RangeError(`freeze splits must be non-negative integers; got ySplit=${ySplit}, xSplit=${xSplit}`);
|
|
757
|
+
}
|
|
758
|
+
if (ySplit === 0 && xSplit === 0) {
|
|
759
|
+
this.unfreeze();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
this.view.state = 'frozen';
|
|
763
|
+
this.view.xSplit = xSplit;
|
|
764
|
+
this.view.ySplit = ySplit;
|
|
765
|
+
this.view.topLeftCell = encodeAddress(xSplit + 1, ySplit + 1);
|
|
766
|
+
}
|
|
767
|
+
/** Clear any frozen split, returning the sheet to a normal (fully scrolling) view. */
|
|
768
|
+
unfreeze() {
|
|
769
|
+
this.view.state = 'normal';
|
|
770
|
+
delete this.view.xSplit;
|
|
771
|
+
delete this.view.ySplit;
|
|
772
|
+
delete this.view.topLeftCell;
|
|
773
|
+
}
|
|
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
|
+
duplicateRow(start, options = {}) {
|
|
792
|
+
const { count = 1, insert = true } = options;
|
|
793
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
794
|
+
throw new RangeError(`duplicate start ${start} is out of bounds — rows start at 1`);
|
|
795
|
+
}
|
|
796
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
797
|
+
throw new RangeError(`duplicate count ${count} is invalid — it must be a non-negative integer`);
|
|
798
|
+
}
|
|
799
|
+
const source = this.#rows.get(start);
|
|
800
|
+
const snapshot = (destRow) => {
|
|
801
|
+
const row = new Map();
|
|
802
|
+
if (source) {
|
|
803
|
+
for (const [col, cell] of source) {
|
|
804
|
+
const copy = new Cell(destRow, col);
|
|
805
|
+
copyCellContent(cell, copy);
|
|
806
|
+
row.set(col, copy);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return row;
|
|
810
|
+
};
|
|
811
|
+
if (insert) {
|
|
812
|
+
const copies = Array.from({ length: count }, () => snapshot(start));
|
|
813
|
+
this.#edits.spliceRows(start + 1, 0, copies);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
for (let i = 1; i <= count; i++)
|
|
817
|
+
this.#rows.set(start + i, snapshot(start + i));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
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
|
+
spliceColumns(start, count, ...inserts) {
|
|
830
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
831
|
+
throw new RangeError(`splice start ${start} is out of bounds — columns start at 1`);
|
|
832
|
+
}
|
|
833
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
834
|
+
throw new RangeError(`splice count ${count} is invalid — it must be a non-negative integer`);
|
|
835
|
+
}
|
|
836
|
+
this.#edits.spliceColumns(start, count, inserts);
|
|
837
|
+
}
|
|
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
|
+
insertColumn(pos, values) {
|
|
846
|
+
this.spliceColumns(pos, 0, values);
|
|
847
|
+
}
|
|
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
|
+
addColumn(values) {
|
|
859
|
+
return this.addColumns([values])[0] ?? [];
|
|
860
|
+
}
|
|
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
|
+
addColumns(columns) {
|
|
868
|
+
let index = this.columnCount;
|
|
869
|
+
return columns.map((values) => {
|
|
870
|
+
index += 1;
|
|
871
|
+
const cells = [];
|
|
872
|
+
values.forEach((value, i) => {
|
|
873
|
+
if (value === undefined)
|
|
874
|
+
return;
|
|
875
|
+
const cell = this.#cellAt(i + 1, index);
|
|
876
|
+
cell.value = value;
|
|
877
|
+
cells.push(cell);
|
|
878
|
+
});
|
|
879
|
+
return cells;
|
|
880
|
+
});
|
|
881
|
+
}
|
|
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
|
+
get model() {
|
|
892
|
+
const model = {};
|
|
893
|
+
for (const facet of WORKSHEET_MODEL_FACETS)
|
|
894
|
+
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
|
+
return model;
|
|
898
|
+
}
|
|
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
|
+
#resetContent() {
|
|
903
|
+
this.#rows.clear();
|
|
904
|
+
this.#columns.clear();
|
|
905
|
+
this.#rowProperties.clear();
|
|
906
|
+
this.#merges.length = 0;
|
|
907
|
+
this.#mergeRects.length = 0;
|
|
908
|
+
this.#dataValidations.clear();
|
|
909
|
+
this.#conditionalFormattings.clear();
|
|
910
|
+
this.#tables.length = 0;
|
|
911
|
+
}
|
|
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
|
+
set model(model) {
|
|
916
|
+
this.#resetContent();
|
|
917
|
+
for (const facet of WORKSHEET_MODEL_FACETS)
|
|
918
|
+
facet.write(this, model);
|
|
919
|
+
}
|
|
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
|
+
protect(password, options = {}) {
|
|
930
|
+
const { spinCount, ...flags } = options;
|
|
931
|
+
const protection = {
|
|
932
|
+
flags,
|
|
933
|
+
};
|
|
934
|
+
if (password !== undefined && password !== '') {
|
|
935
|
+
protection.credential = deriveCredential(password, spinCount);
|
|
936
|
+
}
|
|
937
|
+
this.#protection = protection;
|
|
938
|
+
}
|
|
939
|
+
/** Remove any protection previously set by {@link protect}. */
|
|
940
|
+
unprotect() {
|
|
941
|
+
this.#protection = undefined;
|
|
942
|
+
}
|
|
943
|
+
/** The sheet's protection, or `undefined` if the sheet is unprotected. */
|
|
944
|
+
get protection() {
|
|
945
|
+
return this.#protection;
|
|
946
|
+
}
|
|
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
|
+
#cellAt(row, col) {
|
|
959
|
+
let cols = this.#rows.get(row);
|
|
960
|
+
if (cols === undefined) {
|
|
961
|
+
cols = new Map();
|
|
962
|
+
this.#rows.set(row, cols);
|
|
963
|
+
}
|
|
964
|
+
let cell = cols.get(col);
|
|
965
|
+
if (cell === undefined) {
|
|
966
|
+
cell = new Cell(row, col);
|
|
967
|
+
cols.set(col, cell);
|
|
968
|
+
}
|
|
969
|
+
return cell;
|
|
970
|
+
}
|
|
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
|
+
[INTERNAL] = {
|
|
976
|
+
evictRow: (number) => {
|
|
977
|
+
this.#rows.delete(number);
|
|
978
|
+
this.#rowProperties.delete(number);
|
|
979
|
+
},
|
|
980
|
+
addLoadedPivotTable: (pivot) => {
|
|
981
|
+
this.#loadedPivotTables.push(pivot);
|
|
982
|
+
},
|
|
983
|
+
restoreCommentThreads: (threads) => {
|
|
984
|
+
replaceContents(this.#commentThreads, threads);
|
|
985
|
+
},
|
|
986
|
+
addPreservedReference: (reference) => {
|
|
987
|
+
this.#preservedReferences.push(reference);
|
|
988
|
+
},
|
|
989
|
+
restoreProtection: (protection) => {
|
|
990
|
+
this.#protection = protection;
|
|
991
|
+
},
|
|
992
|
+
cellAt: (row, col) => this.#cellAt(row, col),
|
|
993
|
+
rowPropertiesOf: (number) => this.#rowProperties.get(number),
|
|
994
|
+
ensureRowProperties: (number) => {
|
|
995
|
+
let properties = this.#rowProperties.get(number);
|
|
996
|
+
if (properties === undefined) {
|
|
997
|
+
properties = {};
|
|
998
|
+
this.#rowProperties.set(number, properties);
|
|
999
|
+
}
|
|
1000
|
+
return properties;
|
|
1001
|
+
},
|
|
1002
|
+
rowCells: (number) => {
|
|
1003
|
+
const cols = this.#rows.get(number);
|
|
1004
|
+
return cols ? [...cols].sort(([a], [b]) => a - b).map(([, cell]) => cell) : [];
|
|
1005
|
+
},
|
|
1006
|
+
columnPropertiesOf: (index) => this.#columns.get(index),
|
|
1007
|
+
ensureColumnProperties: (index) => {
|
|
1008
|
+
let properties = this.#columns.get(index);
|
|
1009
|
+
if (properties === undefined) {
|
|
1010
|
+
properties = {};
|
|
1011
|
+
this.#columns.set(index, properties);
|
|
1012
|
+
}
|
|
1013
|
+
return properties;
|
|
1014
|
+
},
|
|
1015
|
+
columnCells: (index) => {
|
|
1016
|
+
const cells = [];
|
|
1017
|
+
for (const number of [...this.#rows.keys()].sort((a, b) => a - b)) {
|
|
1018
|
+
const cell = this.#rows.get(number)?.get(index);
|
|
1019
|
+
if (cell !== undefined)
|
|
1020
|
+
cells.push(cell);
|
|
1021
|
+
}
|
|
1022
|
+
return cells;
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
}
|