@pixerate/editor 0.24.0 → 0.26.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 +101 -1
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent/index.cjs +175 -11
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +18 -4
- package/dist/agent/index.d.ts +18 -4
- package/dist/agent/index.js +168 -4
- package/dist/agent/index.js.map +1 -1
- package/dist/{arrangeShortcuts-B8O3f8-P.d.cts → arrange-BICutYPk.d.cts} +243 -29
- package/dist/{arrangeShortcuts-sLlD2rSX.d.ts → arrange-BICutYPk.d.ts} +243 -29
- package/dist/builtins-CdPcLwwF.d.cts +176 -0
- package/dist/builtins-Chj6zv_O.d.ts +176 -0
- package/dist/canvas/index.cjs +8588 -1857
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +1847 -9
- package/dist/canvas/index.d.ts +1847 -9
- package/dist/canvas/index.js +8577 -1846
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-3CC7OPUQ.cjs +831 -0
- package/dist/chunk-3CC7OPUQ.cjs.map +1 -0
- package/dist/chunk-4BVP64JJ.cjs +1406 -0
- package/dist/chunk-4BVP64JJ.cjs.map +1 -0
- package/dist/chunk-BVP3TSLH.cjs +69 -0
- package/dist/chunk-BVP3TSLH.cjs.map +1 -0
- package/dist/{chunk-OLZJXL4R.cjs → chunk-CS6NZWWA.cjs} +59 -25
- package/dist/chunk-CS6NZWWA.cjs.map +1 -0
- package/dist/{chunk-MCSJFWAO.cjs → chunk-EWCOFSCQ.cjs} +685 -100
- package/dist/chunk-EWCOFSCQ.cjs.map +1 -0
- package/dist/chunk-HDYMLS3X.cjs +409 -0
- package/dist/chunk-HDYMLS3X.cjs.map +1 -0
- package/dist/chunk-HRMEDT2L.js +1406 -0
- package/dist/chunk-HRMEDT2L.js.map +1 -0
- package/dist/{chunk-D27YFC46.cjs → chunk-IFHKKVXH.cjs} +382 -63
- package/dist/chunk-IFHKKVXH.cjs.map +1 -0
- package/dist/{chunk-PGNZKFQS.js → chunk-JOIREKL7.js} +137 -58
- package/dist/chunk-JOIREKL7.js.map +1 -0
- package/dist/chunk-KRGXUQ2Y.js +1696 -0
- package/dist/chunk-KRGXUQ2Y.js.map +1 -0
- package/dist/{chunk-3LV6RRPH.js → chunk-MHEPLBFB.js} +2 -2
- package/dist/chunk-MR2764X2.js +831 -0
- package/dist/chunk-MR2764X2.js.map +1 -0
- package/dist/{chunk-XNORY7M6.cjs → chunk-NFVBT3UZ.cjs} +166 -87
- package/dist/chunk-NFVBT3UZ.cjs.map +1 -0
- package/dist/chunk-NQF32I6Z.cjs +6478 -0
- package/dist/chunk-NQF32I6Z.cjs.map +1 -0
- package/dist/chunk-O5RYH2PG.js +69 -0
- package/dist/chunk-O5RYH2PG.js.map +1 -0
- package/dist/{chunk-ITBSKI26.js → chunk-Q2OHWD2A.js} +1064 -360
- package/dist/chunk-Q2OHWD2A.js.map +1 -0
- package/dist/{chunk-FODNLX5N.js → chunk-QJSOZFCZ.js} +38 -4
- package/dist/chunk-QJSOZFCZ.js.map +1 -0
- package/dist/{chunk-SML64VJG.cjs → chunk-RRREAOVM.cjs} +4 -4
- package/dist/{chunk-SML64VJG.cjs.map → chunk-RRREAOVM.cjs.map} +1 -1
- package/dist/{chunk-5S3Z4PPF.js → chunk-SWE3NPOU.js} +347 -28
- package/dist/chunk-SWE3NPOU.js.map +1 -0
- package/dist/chunk-TXXNBDQX.cjs +1696 -0
- package/dist/chunk-TXXNBDQX.cjs.map +1 -0
- package/dist/chunk-UBCFIABU.js +409 -0
- package/dist/chunk-UBCFIABU.js.map +1 -0
- package/dist/chunk-X7HXL2SB.js +6478 -0
- package/dist/chunk-X7HXL2SB.js.map +1 -0
- package/dist/{chunk-SBCZ6RN5.cjs → chunk-YOMYSFJU.cjs} +1163 -459
- package/dist/chunk-YOMYSFJU.cjs.map +1 -0
- package/dist/{chunk-3CN2WD3X.js → chunk-ZRNHGAQT.js} +661 -76
- package/dist/chunk-ZRNHGAQT.js.map +1 -0
- package/dist/commands/index.cjs +425 -4
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +39 -77
- package/dist/commands/index.d.ts +39 -77
- package/dist/commands/index.js +424 -3
- package/dist/commands/index.js.map +1 -1
- package/dist/compare/index.cjs +3 -3
- package/dist/compare/index.d.cts +1 -1
- package/dist/compare/index.d.ts +1 -1
- package/dist/compare/index.js +2 -2
- package/dist/controller-CxXGOEmy.d.cts +2038 -0
- package/dist/controller-CxXGOEmy.d.ts +2038 -0
- package/dist/{documents-BQjzJNe2.d.ts → documents-CHT1U5Vs.d.ts} +10 -0
- package/dist/{documents-KRTPhvst.d.cts → documents-Gp7wb0Me.d.cts} +10 -0
- package/dist/export/index.cjs +36 -0
- package/dist/export/index.cjs.map +1 -0
- package/dist/export/index.d.cts +243 -0
- package/dist/export/index.d.ts +243 -0
- package/dist/export/index.js +36 -0
- package/dist/export/index.js.map +1 -0
- package/dist/extensions/index.cjs +101 -6
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +18 -5
- package/dist/extensions/index.d.ts +18 -5
- package/dist/extensions/index.js +101 -6
- package/dist/generation/index.cjs +1013 -69
- package/dist/generation/index.cjs.map +1 -1
- package/dist/generation/index.d.cts +474 -13
- package/dist/generation/index.d.ts +474 -13
- package/dist/generation/index.js +976 -32
- package/dist/generation/index.js.map +1 -1
- package/dist/image-editor/index.cjs +3385 -1275
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +693 -128
- package/dist/image-editor/index.d.ts +693 -128
- package/dist/image-editor/index.js +3325 -1215
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{index-fJrlkJCd.d.ts → index-B273Zllk.d.ts} +500 -7
- package/dist/{index-BhJBSm2d.d.cts → index-jcJwmBYt.d.cts} +500 -7
- package/dist/index.cjs +1603 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -27
- package/dist/index.d.ts +154 -27
- package/dist/index.js +1584 -26
- package/dist/index.js.map +1 -1
- package/dist/mention-text-CDnCFd3c.d.cts +53 -0
- package/dist/mention-text-CDnCFd3c.d.ts +53 -0
- package/dist/model-C1zApQM8.d.cts +219 -0
- package/dist/model-C1zApQM8.d.ts +219 -0
- package/dist/persistence/index.d.cts +3 -120
- package/dist/persistence/index.d.ts +3 -120
- package/dist/registry-CED5C0lA.d.cts +204 -0
- package/dist/registry-CED5C0lA.d.ts +204 -0
- package/dist/{registry-GJI4gToU.d.cts → registry-CEgwXED1.d.cts} +12 -1
- package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.ts} +12 -1
- package/dist/{registry-D32LpIc4.d.cts → registry-D0GADKc7.d.cts} +1 -1
- package/dist/{registry-evKTP4ls.d.ts → registry-DhVUZioj.d.ts} +1 -1
- package/dist/serializers/index.cjs +12 -4
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +41 -13
- package/dist/serializers/index.d.ts +41 -13
- package/dist/serializers/index.js +12 -4
- package/dist/spreadsheet/index.cjs +157 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +534 -11
- package/dist/spreadsheet/index.d.ts +534 -11
- package/dist/spreadsheet/index.js +157 -3
- package/dist/storage-BpFfspHm.d.cts +120 -0
- package/dist/storage-BpFfspHm.d.ts +120 -0
- package/dist/tokens/index.cjs +115 -0
- package/dist/tokens/index.cjs.map +1 -0
- package/dist/tokens/index.d.cts +212 -0
- package/dist/tokens/index.d.ts +212 -0
- package/dist/tokens/index.js +115 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/{track-changes-BK5sVf4_.d.cts → track-changes-D_VAPELi.d.cts} +95 -11
- package/dist/{track-changes-CDjCrwKe.d.ts → track-changes-Db_fGEvx.d.ts} +95 -11
- package/package.json +15 -1
- package/dist/chunk-3CN2WD3X.js.map +0 -1
- package/dist/chunk-5S3Z4PPF.js.map +0 -1
- package/dist/chunk-D27YFC46.cjs.map +0 -1
- package/dist/chunk-FODNLX5N.js.map +0 -1
- package/dist/chunk-ITBSKI26.js.map +0 -1
- package/dist/chunk-MA2F5KHF.cjs +0 -3223
- package/dist/chunk-MA2F5KHF.cjs.map +0 -1
- package/dist/chunk-MCSJFWAO.cjs.map +0 -1
- package/dist/chunk-OLZJXL4R.cjs.map +0 -1
- package/dist/chunk-PGNZKFQS.js.map +0 -1
- package/dist/chunk-SBCZ6RN5.cjs.map +0 -1
- package/dist/chunk-U5KWZU4X.js +0 -3223
- package/dist/chunk-U5KWZU4X.js.map +0 -1
- package/dist/chunk-XNORY7M6.cjs.map +0 -1
- package/dist/controller-BhLLAoi8.d.cts +0 -1000
- package/dist/controller-BhLLAoi8.d.ts +0 -1000
- package/dist/types-DX4Mh4SL.d.cts +0 -217
- package/dist/types-DX4Mh4SL.d.ts +0 -217
- /package/dist/{chunk-3LV6RRPH.js.map → chunk-MHEPLBFB.js.map} +0 -0
package/dist/chunk-U5KWZU4X.js
DELETED
|
@@ -1,3223 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseCsv,
|
|
3
|
-
parseTsv,
|
|
4
|
-
serializeCsv,
|
|
5
|
-
serializeTsv
|
|
6
|
-
} from "./chunk-664LQYFJ.js";
|
|
7
|
-
import {
|
|
8
|
-
FormulaEvaluator,
|
|
9
|
-
NumberFormat,
|
|
10
|
-
WHOLE_COLUMN_END_ROW,
|
|
11
|
-
colNameToIndex,
|
|
12
|
-
compareValues,
|
|
13
|
-
describeFormulaError,
|
|
14
|
-
formatTextFunction,
|
|
15
|
-
getFormulaArgumentSpecIndex,
|
|
16
|
-
getFormulaFunction,
|
|
17
|
-
getFormulaSignature,
|
|
18
|
-
getFormulaSignatureParts,
|
|
19
|
-
indexToColName,
|
|
20
|
-
isBlank,
|
|
21
|
-
isFormulaError,
|
|
22
|
-
numericValue,
|
|
23
|
-
parseCellInput,
|
|
24
|
-
parseFormula,
|
|
25
|
-
parseIsoDate,
|
|
26
|
-
searchFormulaFunctions,
|
|
27
|
-
sortCompareValues,
|
|
28
|
-
toText,
|
|
29
|
-
tokenizeFormula
|
|
30
|
-
} from "./chunk-3CN2WD3X.js";
|
|
31
|
-
|
|
32
|
-
// src/spreadsheet/formula/dependency-graph.ts
|
|
33
|
-
var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
|
|
34
|
-
function extractCellDependencies(node) {
|
|
35
|
-
const deps = [];
|
|
36
|
-
const visit = (curr) => {
|
|
37
|
-
if (curr.type === "CellRef") {
|
|
38
|
-
deps.push({ col: curr.col, row: curr.row });
|
|
39
|
-
} else if (curr.type === "SpillRef") {
|
|
40
|
-
deps.push({ col: curr.col, row: curr.row });
|
|
41
|
-
} else if (curr.type === "Range" && curr.endRow >= WHOLE_COLUMN_END_ROW) {
|
|
42
|
-
const startC = colNameToIndex(curr.startCol);
|
|
43
|
-
const endC = colNameToIndex(curr.endCol);
|
|
44
|
-
for (let c = Math.min(startC, endC); c <= Math.max(startC, endC); c++) {
|
|
45
|
-
deps.push({ col: indexToColName(c), row: 0 });
|
|
46
|
-
}
|
|
47
|
-
} else if (curr.type === "Range") {
|
|
48
|
-
const startC = colNameToIndex(curr.startCol);
|
|
49
|
-
const endC = colNameToIndex(curr.endCol);
|
|
50
|
-
const minC = Math.min(startC, endC);
|
|
51
|
-
const maxC = Math.max(startC, endC);
|
|
52
|
-
const minR = Math.min(curr.startRow, curr.endRow);
|
|
53
|
-
const maxR = Math.max(curr.startRow, curr.endRow);
|
|
54
|
-
for (let c = minC; c <= maxC; c++) {
|
|
55
|
-
for (let r = minR; r <= maxR; r++) {
|
|
56
|
-
deps.push({ col: indexToColName(c), row: r });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
} else if (curr.type === "BinaryOp") {
|
|
60
|
-
visit(curr.left);
|
|
61
|
-
visit(curr.right);
|
|
62
|
-
} else if (curr.type === "UnaryOp") {
|
|
63
|
-
visit(curr.argument);
|
|
64
|
-
} else if (curr.type === "FunctionCall") {
|
|
65
|
-
curr.args.forEach(visit);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
visit(node);
|
|
69
|
-
return deps;
|
|
70
|
-
}
|
|
71
|
-
var DependencyGraph = class {
|
|
72
|
-
// Map of cellKey (e.g. "B2") -> Set of cellKeys that this cell depends ON
|
|
73
|
-
dependencies = /* @__PURE__ */ new Map();
|
|
74
|
-
// Map of cellKey (e.g. "A1") -> Set of cellKeys that depend ON this cell (dependents)
|
|
75
|
-
dependents = /* @__PURE__ */ new Map();
|
|
76
|
-
// Dynamic-array edges, set by the controller after evaluating an anchor:
|
|
77
|
-
// spilled cells depend on their anchor, and a blocked (#SPILL!) anchor
|
|
78
|
-
// depends on the cells blocking it. Kept apart from formula dependencies,
|
|
79
|
-
// so re-entering a formula doesn't drop them.
|
|
80
|
-
spillDependencies = /* @__PURE__ */ new Map();
|
|
81
|
-
spillDependents = /* @__PURE__ */ new Map();
|
|
82
|
-
// anchor -> the edges it registered (so they can be replaced).
|
|
83
|
-
spillEdges = /* @__PURE__ */ new Map();
|
|
84
|
-
setCellFormula(cellKey, formulaStr) {
|
|
85
|
-
const key = cellKey.toUpperCase();
|
|
86
|
-
const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
|
|
87
|
-
for (const dep of oldDeps) {
|
|
88
|
-
this.dependents.get(dep)?.delete(key);
|
|
89
|
-
}
|
|
90
|
-
this.dependencies.delete(key);
|
|
91
|
-
if (!formulaStr || !formulaStr.startsWith("=")) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
const ast = parseFormula(formulaStr);
|
|
96
|
-
const deps = extractCellDependencies(ast);
|
|
97
|
-
const newDeps = /* @__PURE__ */ new Set();
|
|
98
|
-
for (const d of deps) {
|
|
99
|
-
const depKey = d.row === 0 ? columnDependencyKey(d.col) : `${d.col}${d.row}`.toUpperCase();
|
|
100
|
-
newDeps.add(depKey);
|
|
101
|
-
if (!this.dependents.has(depKey)) {
|
|
102
|
-
this.dependents.set(depKey, /* @__PURE__ */ new Set());
|
|
103
|
-
}
|
|
104
|
-
this.dependents.get(depKey).add(key);
|
|
105
|
-
}
|
|
106
|
-
this.dependencies.set(key, newDeps);
|
|
107
|
-
} catch {
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/** Removes every registered formula and dependency. */
|
|
111
|
-
clear() {
|
|
112
|
-
this.dependencies.clear();
|
|
113
|
-
this.dependents.clear();
|
|
114
|
-
this.spillDependencies.clear();
|
|
115
|
-
this.spillDependents.clear();
|
|
116
|
-
this.spillEdges.clear();
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Records the dynamic-array edges of the anchor `anchorKey`, replacing its
|
|
120
|
-
* previous ones: every key in `spilledKeys` (cells holding its spilled
|
|
121
|
-
* values) depends on the anchor, and the anchor depends on every key in
|
|
122
|
-
* `blockingKeys` (cells that stop it spilling). Pass empty lists to remove
|
|
123
|
-
* the anchor's edges.
|
|
124
|
-
*/
|
|
125
|
-
setSpillEdges(anchorKey, spilledKeys, blockingKeys = []) {
|
|
126
|
-
const anchor = anchorKey.toUpperCase();
|
|
127
|
-
for (const [from, to] of this.spillEdges.get(anchor) ?? []) {
|
|
128
|
-
this.spillDependencies.get(from)?.delete(to);
|
|
129
|
-
this.spillDependents.get(to)?.delete(from);
|
|
130
|
-
}
|
|
131
|
-
const edges = [];
|
|
132
|
-
for (const key of spilledKeys) edges.push([key.toUpperCase(), anchor]);
|
|
133
|
-
for (const key of blockingKeys) edges.push([anchor, key.toUpperCase()]);
|
|
134
|
-
for (const [from, to] of edges) {
|
|
135
|
-
if (!this.spillDependencies.has(from)) this.spillDependencies.set(from, /* @__PURE__ */ new Set());
|
|
136
|
-
this.spillDependencies.get(from).add(to);
|
|
137
|
-
if (!this.spillDependents.has(to)) this.spillDependents.set(to, /* @__PURE__ */ new Set());
|
|
138
|
-
this.spillDependents.get(to).add(from);
|
|
139
|
-
}
|
|
140
|
-
if (edges.length > 0) this.spillEdges.set(anchor, edges);
|
|
141
|
-
else this.spillEdges.delete(anchor);
|
|
142
|
-
}
|
|
143
|
-
/** The spilled cells registered for `anchorKey` by `setSpillEdges`. */
|
|
144
|
-
getSpilledKeys(anchorKey) {
|
|
145
|
-
const anchor = anchorKey.toUpperCase();
|
|
146
|
-
return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);
|
|
147
|
-
}
|
|
148
|
-
/** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */
|
|
149
|
-
getDependencies(cellKey) {
|
|
150
|
-
const key = cellKey.toUpperCase();
|
|
151
|
-
return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));
|
|
152
|
-
}
|
|
153
|
-
/** Cells depending on `cellKey`, through formulas or spills. */
|
|
154
|
-
getDependents(cellKey) {
|
|
155
|
-
const key = cellKey.toUpperCase();
|
|
156
|
-
return unionOf(this.dependents.get(key), this.spillDependents.get(key));
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Detects if there is a cycle containing cellKey.
|
|
160
|
-
*/
|
|
161
|
-
hasCycle(cellKey) {
|
|
162
|
-
const key = cellKey.toUpperCase();
|
|
163
|
-
const visited = /* @__PURE__ */ new Set();
|
|
164
|
-
const recStack = /* @__PURE__ */ new Set();
|
|
165
|
-
const dfs = (node) => {
|
|
166
|
-
visited.add(node);
|
|
167
|
-
recStack.add(node);
|
|
168
|
-
const deps = this.getDependencies(node);
|
|
169
|
-
for (const neighbor of deps) {
|
|
170
|
-
if (!visited.has(neighbor)) {
|
|
171
|
-
if (dfs(neighbor)) return true;
|
|
172
|
-
} else if (recStack.has(neighbor)) {
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
recStack.delete(node);
|
|
177
|
-
return false;
|
|
178
|
-
};
|
|
179
|
-
return dfs(key);
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Returns cells affected by updating cellKey in topological order.
|
|
183
|
-
*/
|
|
184
|
-
getEvaluationOrder(startCellKey) {
|
|
185
|
-
const start = startCellKey.toUpperCase();
|
|
186
|
-
const visited = /* @__PURE__ */ new Set();
|
|
187
|
-
const recStack = /* @__PURE__ */ new Set();
|
|
188
|
-
const order = [];
|
|
189
|
-
let cycleDetected = false;
|
|
190
|
-
const affected = /* @__PURE__ */ new Set();
|
|
191
|
-
const collectAffected = (node) => {
|
|
192
|
-
affected.add(node);
|
|
193
|
-
const directDependents = this.dependents.get(node) || /* @__PURE__ */ new Set();
|
|
194
|
-
for (const dep of directDependents) {
|
|
195
|
-
if (!affected.has(dep)) {
|
|
196
|
-
collectAffected(dep);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
collectAffected(start);
|
|
201
|
-
const visit = (node) => {
|
|
202
|
-
if (recStack.has(node)) {
|
|
203
|
-
cycleDetected = true;
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (visited.has(node)) return;
|
|
207
|
-
recStack.add(node);
|
|
208
|
-
visited.add(node);
|
|
209
|
-
const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
|
|
210
|
-
for (const neighbor of deps) {
|
|
211
|
-
if (affected.has(neighbor)) {
|
|
212
|
-
visit(neighbor);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
recStack.delete(node);
|
|
216
|
-
order.push(node);
|
|
217
|
-
};
|
|
218
|
-
for (const node of affected) {
|
|
219
|
-
if (!visited.has(node)) {
|
|
220
|
-
visit(node);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return { order, hasCycle: cycleDetected };
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Plans a recalculation over `cellKeys` (and, with `includeDependents`, every
|
|
227
|
-
* cell that transitively depends on them). Returns the keys in evaluation
|
|
228
|
-
* order (dependencies first) and the set of keys that are part of a cycle,
|
|
229
|
-
* which must be marked `#CYCLE!` instead of evaluated. Every member of a
|
|
230
|
-
* cycle is reported, not just the cell that closed it.
|
|
231
|
-
*/
|
|
232
|
-
getEvaluationPlan(cellKeys, options = {}) {
|
|
233
|
-
const nodes = /* @__PURE__ */ new Set();
|
|
234
|
-
const queue = [];
|
|
235
|
-
for (const key of cellKeys) {
|
|
236
|
-
const upper = key.toUpperCase();
|
|
237
|
-
if (!nodes.has(upper)) {
|
|
238
|
-
nodes.add(upper);
|
|
239
|
-
queue.push(upper);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (options.includeDependents) {
|
|
243
|
-
while (queue.length > 0) {
|
|
244
|
-
const node = queue.pop();
|
|
245
|
-
const direct = [
|
|
246
|
-
...this.dependents.get(node) ?? [],
|
|
247
|
-
...this.spillDependents.get(node) ?? [],
|
|
248
|
-
...this.dependents.get(columnDependencyKey(node.replace(/\d+$/, ""))) ?? []
|
|
249
|
-
];
|
|
250
|
-
for (const dep of direct) {
|
|
251
|
-
if (!nodes.has(dep)) {
|
|
252
|
-
nodes.add(dep);
|
|
253
|
-
queue.push(dep);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
const neighbors = (node) => {
|
|
259
|
-
const result = [];
|
|
260
|
-
for (const dep of this.getDependencies(node)) {
|
|
261
|
-
if (dep.endsWith(":")) {
|
|
262
|
-
const col = dep.slice(0, -1);
|
|
263
|
-
for (const candidate of nodes) {
|
|
264
|
-
if (candidate.replace(/\d+$/, "") === col) result.push(candidate);
|
|
265
|
-
}
|
|
266
|
-
} else if (nodes.has(dep)) {
|
|
267
|
-
result.push(dep);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return result;
|
|
271
|
-
};
|
|
272
|
-
const order = [];
|
|
273
|
-
const cyclic = /* @__PURE__ */ new Set();
|
|
274
|
-
const index = /* @__PURE__ */ new Map();
|
|
275
|
-
const low = /* @__PURE__ */ new Map();
|
|
276
|
-
const stack = [];
|
|
277
|
-
const onStack = /* @__PURE__ */ new Set();
|
|
278
|
-
let counter = 0;
|
|
279
|
-
for (const root of nodes) {
|
|
280
|
-
if (index.has(root)) continue;
|
|
281
|
-
const work = [];
|
|
282
|
-
const enter = (node) => {
|
|
283
|
-
index.set(node, counter);
|
|
284
|
-
low.set(node, counter);
|
|
285
|
-
counter++;
|
|
286
|
-
stack.push(node);
|
|
287
|
-
onStack.add(node);
|
|
288
|
-
work.push({ node, edges: neighbors(node), next: 0 });
|
|
289
|
-
};
|
|
290
|
-
enter(root);
|
|
291
|
-
while (work.length > 0) {
|
|
292
|
-
const frame = work[work.length - 1];
|
|
293
|
-
if (frame.next < frame.edges.length) {
|
|
294
|
-
const target = frame.edges[frame.next++];
|
|
295
|
-
if (!index.has(target)) {
|
|
296
|
-
enter(target);
|
|
297
|
-
} else if (onStack.has(target)) {
|
|
298
|
-
low.set(frame.node, Math.min(low.get(frame.node), index.get(target)));
|
|
299
|
-
}
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
work.pop();
|
|
303
|
-
if (work.length > 0) {
|
|
304
|
-
const parent = work[work.length - 1].node;
|
|
305
|
-
low.set(parent, Math.min(low.get(parent), low.get(frame.node)));
|
|
306
|
-
}
|
|
307
|
-
if (low.get(frame.node) === index.get(frame.node)) {
|
|
308
|
-
const component = [];
|
|
309
|
-
let member;
|
|
310
|
-
do {
|
|
311
|
-
member = stack.pop();
|
|
312
|
-
onStack.delete(member);
|
|
313
|
-
component.push(member);
|
|
314
|
-
} while (member !== frame.node);
|
|
315
|
-
const isCycle = component.length > 1 || frame.edges.includes(frame.node);
|
|
316
|
-
for (const key of component) {
|
|
317
|
-
order.push(key);
|
|
318
|
-
if (isCycle) cyclic.add(key);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return { order, cyclic };
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
function unionOf(a, b) {
|
|
327
|
-
if (!b || b.size === 0) return Array.from(a ?? []);
|
|
328
|
-
if (!a || a.size === 0) return Array.from(b);
|
|
329
|
-
return Array.from(/* @__PURE__ */ new Set([...a, ...b]));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// src/spreadsheet/formula/references.ts
|
|
333
|
-
var CELL_PART = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
|
|
334
|
-
var COLUMN_PART = /^(\$?)([A-Za-z]+)$/;
|
|
335
|
-
var REF_CHAR = /[A-Za-z0-9_$.:]/;
|
|
336
|
-
function parsePart(text) {
|
|
337
|
-
const cell = CELL_PART.exec(text);
|
|
338
|
-
if (cell) {
|
|
339
|
-
return {
|
|
340
|
-
colAbs: cell[1],
|
|
341
|
-
col: colNameToIndex(cell[2]),
|
|
342
|
-
rowAbs: cell[3],
|
|
343
|
-
row: parseInt(cell[4], 10) - 1
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
const column = COLUMN_PART.exec(text);
|
|
347
|
-
if (column) {
|
|
348
|
-
return { colAbs: column[1], col: colNameToIndex(column[2]), rowAbs: "", row: null };
|
|
349
|
-
}
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
function formatPart(part) {
|
|
353
|
-
const col = `${part.colAbs}${indexToColName(part.col)}`;
|
|
354
|
-
return part.row === null ? col : `${col}${part.rowAbs}${part.row + 1}`;
|
|
355
|
-
}
|
|
356
|
-
function shiftCoordinate(value, change) {
|
|
357
|
-
if (change.type === "insert") {
|
|
358
|
-
return value >= change.index ? value + 1 : value;
|
|
359
|
-
}
|
|
360
|
-
if (value === change.index) return null;
|
|
361
|
-
return value > change.index ? value - 1 : value;
|
|
362
|
-
}
|
|
363
|
-
function shiftSpan(lo, hi, change) {
|
|
364
|
-
if (change.type === "insert") {
|
|
365
|
-
return [lo >= change.index ? lo + 1 : lo, hi >= change.index ? hi + 1 : hi];
|
|
366
|
-
}
|
|
367
|
-
if (lo === change.index && hi === change.index) return null;
|
|
368
|
-
return [lo > change.index ? lo - 1 : lo, hi >= change.index ? hi - 1 : hi];
|
|
369
|
-
}
|
|
370
|
-
function rewriteReference(text, change) {
|
|
371
|
-
const [startText, endText] = text.split(":");
|
|
372
|
-
const start = parsePart(startText);
|
|
373
|
-
if (!start) return text;
|
|
374
|
-
const key = change.axis === "row" ? "row" : "col";
|
|
375
|
-
if (endText === void 0) {
|
|
376
|
-
if (start[key] === null) return text;
|
|
377
|
-
const shifted = shiftCoordinate(start[key], change);
|
|
378
|
-
if (shifted === null) return "#REF!";
|
|
379
|
-
return formatPart({ ...start, [key]: shifted });
|
|
380
|
-
}
|
|
381
|
-
const end = parsePart(endText);
|
|
382
|
-
if (!end) return text;
|
|
383
|
-
if (start[key] === null || end[key] === null) return text;
|
|
384
|
-
const startValue = start[key];
|
|
385
|
-
const endValue = end[key];
|
|
386
|
-
const span = shiftSpan(Math.min(startValue, endValue), Math.max(startValue, endValue), change);
|
|
387
|
-
if (span === null) return "#REF!";
|
|
388
|
-
const [lo, hi] = span;
|
|
389
|
-
const newStart = { ...start, [key]: startValue <= endValue ? lo : hi };
|
|
390
|
-
const newEnd = { ...end, [key]: startValue <= endValue ? hi : lo };
|
|
391
|
-
return `${formatPart(newStart)}:${formatPart(newEnd)}`;
|
|
392
|
-
}
|
|
393
|
-
function shiftFormulaReferences(formula, change) {
|
|
394
|
-
return mapFormulaReferences(formula, (text) => rewriteReference(text, change));
|
|
395
|
-
}
|
|
396
|
-
function mapFormulaReferences(formula, rewrite) {
|
|
397
|
-
if (!formula.startsWith("=")) return formula;
|
|
398
|
-
let tokens;
|
|
399
|
-
try {
|
|
400
|
-
tokens = tokenizeFormula(formula);
|
|
401
|
-
} catch {
|
|
402
|
-
return formula;
|
|
403
|
-
}
|
|
404
|
-
let result = "";
|
|
405
|
-
let cursor = 0;
|
|
406
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
407
|
-
const token = tokens[i];
|
|
408
|
-
if (token.type !== "CELL_REF" && token.type !== "RANGE" && token.type !== "SPILL_REF") continue;
|
|
409
|
-
if (tokens[i + 1]?.type === "LPAREN") continue;
|
|
410
|
-
let end = token.position;
|
|
411
|
-
while (end < formula.length && REF_CHAR.test(formula[end])) end++;
|
|
412
|
-
const original = formula.slice(token.position, end);
|
|
413
|
-
if (token.type === "SPILL_REF") {
|
|
414
|
-
const rewritten = rewrite(original);
|
|
415
|
-
result += formula.slice(cursor, token.position) + (rewritten === "#REF!" ? rewritten : `${rewritten}#`);
|
|
416
|
-
cursor = end + 1;
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
result += formula.slice(cursor, token.position) + rewrite(original);
|
|
420
|
-
cursor = end;
|
|
421
|
-
}
|
|
422
|
-
return result + formula.slice(cursor);
|
|
423
|
-
}
|
|
424
|
-
function offsetFormulaReferences(formula, rowOffset, colOffset) {
|
|
425
|
-
if (rowOffset === 0 && colOffset === 0) return formula;
|
|
426
|
-
return mapFormulaReferences(formula, (text) => {
|
|
427
|
-
const parts = text.split(":").map(parsePart);
|
|
428
|
-
if (parts.some((p) => p === null)) return text;
|
|
429
|
-
const moved = [];
|
|
430
|
-
for (const part of parts) {
|
|
431
|
-
const col = part.colAbs ? part.col : part.col + colOffset;
|
|
432
|
-
const row = part.row === null || part.rowAbs ? part.row : part.row + rowOffset;
|
|
433
|
-
if (col < 0 || row !== null && row < 0) return "#REF!";
|
|
434
|
-
moved.push({ ...part, col, row });
|
|
435
|
-
}
|
|
436
|
-
return moved.map(formatPart).join(":");
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
var MAX_REMAP_RANGE_CELLS = 1e5;
|
|
440
|
-
function remapFormulaReferences(formula, moveCell) {
|
|
441
|
-
return mapFormulaReferences(formula, (text) => {
|
|
442
|
-
const parts = text.split(":").map(parsePart);
|
|
443
|
-
if (parts.some((p) => p === null)) return text;
|
|
444
|
-
const [start, end] = parts;
|
|
445
|
-
if (start.row === null || end && end.row === null) return text;
|
|
446
|
-
if (!end) {
|
|
447
|
-
const target = moveCell({ row: start.row, col: start.col });
|
|
448
|
-
return target ? formatPart({ ...start, row: target.row, col: target.col }) : text;
|
|
449
|
-
}
|
|
450
|
-
const lo = { row: Math.min(start.row, end.row), col: Math.min(start.col, end.col) };
|
|
451
|
-
const hi = { row: Math.max(start.row, end.row), col: Math.max(start.col, end.col) };
|
|
452
|
-
const area = (hi.row - lo.row + 1) * (hi.col - lo.col + 1);
|
|
453
|
-
if (area > MAX_REMAP_RANGE_CELLS) return text;
|
|
454
|
-
let minR = Infinity;
|
|
455
|
-
let maxR = -Infinity;
|
|
456
|
-
let minC = Infinity;
|
|
457
|
-
let maxC = -Infinity;
|
|
458
|
-
let movedAny = false;
|
|
459
|
-
const targets = /* @__PURE__ */ new Set();
|
|
460
|
-
for (let r = lo.row; r <= hi.row; r++) {
|
|
461
|
-
for (let c = lo.col; c <= hi.col; c++) {
|
|
462
|
-
const moved = moveCell({ row: r, col: c });
|
|
463
|
-
if (moved) movedAny = true;
|
|
464
|
-
const t = moved ?? { row: r, col: c };
|
|
465
|
-
targets.add(`${t.row}:${t.col}`);
|
|
466
|
-
minR = Math.min(minR, t.row);
|
|
467
|
-
maxR = Math.max(maxR, t.row);
|
|
468
|
-
minC = Math.min(minC, t.col);
|
|
469
|
-
maxC = Math.max(maxC, t.col);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (!movedAny || targets.size !== area || (maxR - minR + 1) * (maxC - minC + 1) !== area) {
|
|
473
|
-
return text;
|
|
474
|
-
}
|
|
475
|
-
return `${formatPart({ ...start, row: minR, col: minC })}:${formatPart({ ...end, row: maxR, col: maxC })}`;
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// src/spreadsheet/formula/caret.ts
|
|
480
|
-
var EMPTY = { prefix: null, call: null };
|
|
481
|
-
function getFormulaCaretContext(input, caret = input.length) {
|
|
482
|
-
const leading = input.length - input.trimStart().length;
|
|
483
|
-
if (input[leading] !== "=") return EMPTY;
|
|
484
|
-
const end = Math.max(0, Math.min(caret, input.length));
|
|
485
|
-
if (end <= leading) return EMPTY;
|
|
486
|
-
const stack = [];
|
|
487
|
-
let quote2 = null;
|
|
488
|
-
let inProperty = false;
|
|
489
|
-
for (let i = leading + 1; i < end; i++) {
|
|
490
|
-
const char = input[i];
|
|
491
|
-
if (quote2) {
|
|
492
|
-
if (char === "\\") i++;
|
|
493
|
-
else if (char === quote2) quote2 = null;
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
if (inProperty) {
|
|
497
|
-
if (char === "]") inProperty = false;
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
if (char === '"' || char === "'") {
|
|
501
|
-
quote2 = char;
|
|
502
|
-
} else if (char === "[") {
|
|
503
|
-
inProperty = true;
|
|
504
|
-
} else if (char === "(") {
|
|
505
|
-
const name = /([A-Za-z_][A-Za-z0-9_.]*)\s*$/.exec(input.slice(leading + 1, i));
|
|
506
|
-
const isFunction = name && !/^\$?[A-Za-z]+\$?\d+$/.test(name[1]) && !/[:$]$/.test(input.slice(0, i - name[0].length));
|
|
507
|
-
stack.push({ name: isFunction ? name[1].toUpperCase() : null, argIndex: 0, openParen: i });
|
|
508
|
-
} else if (char === ")") {
|
|
509
|
-
stack.pop();
|
|
510
|
-
} else if (char === "," && stack.length > 0) {
|
|
511
|
-
stack[stack.length - 1].argIndex++;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (quote2 || inProperty) {
|
|
515
|
-
return { prefix: null, call: innermostCall(stack) };
|
|
516
|
-
}
|
|
517
|
-
let prefix = null;
|
|
518
|
-
const before = input.slice(leading + 1, end);
|
|
519
|
-
const word = /[A-Za-z_][A-Za-z0-9_.]*$/.exec(before);
|
|
520
|
-
if (word) {
|
|
521
|
-
const start = leading + 1 + word.index;
|
|
522
|
-
const preceding = input[start - 1] ?? "";
|
|
523
|
-
if (!/[:$0-9.]/.test(preceding)) {
|
|
524
|
-
prefix = { text: word[0], start, end };
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return { prefix, call: innermostCall(stack) };
|
|
528
|
-
}
|
|
529
|
-
function innermostCall(stack) {
|
|
530
|
-
for (let i = stack.length - 1; i >= 0; i--) {
|
|
531
|
-
const frame = stack[i];
|
|
532
|
-
if (frame.name) return { name: frame.name, argIndex: frame.argIndex, openParen: frame.openParen };
|
|
533
|
-
}
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
var toSuggestion = (def) => ({
|
|
537
|
-
name: def.name,
|
|
538
|
-
signature: getFormulaSignature(def),
|
|
539
|
-
description: def.description,
|
|
540
|
-
category: def.category
|
|
541
|
-
});
|
|
542
|
-
function getFormulaHints(input, caret = input.length, options = {}) {
|
|
543
|
-
const context = getFormulaCaretContext(input, caret);
|
|
544
|
-
let suggestions = [];
|
|
545
|
-
if (context.prefix) {
|
|
546
|
-
const query = context.prefix.text.toUpperCase();
|
|
547
|
-
suggestions = searchFormulaFunctions(query, { limit: void 0 }).filter((def) => def.name.startsWith(query)).slice(0, options.limit ?? 8).map(toSuggestion);
|
|
548
|
-
const followedByParen = input.slice(context.prefix.end).trimStart().startsWith("(");
|
|
549
|
-
if (followedByParen || suggestions.length === 1 && suggestions[0].name === query) suggestions = [];
|
|
550
|
-
}
|
|
551
|
-
let activeCall = null;
|
|
552
|
-
if (context.call) {
|
|
553
|
-
const def = getFormulaFunction(context.call.name);
|
|
554
|
-
if (def) {
|
|
555
|
-
const specIndex = getFormulaArgumentSpecIndex(def, context.call.argIndex);
|
|
556
|
-
activeCall = {
|
|
557
|
-
name: def.name,
|
|
558
|
-
signature: getFormulaSignature(def),
|
|
559
|
-
description: def.description,
|
|
560
|
-
parts: getFormulaSignatureParts(def, context.call.argIndex),
|
|
561
|
-
argIndex: context.call.argIndex,
|
|
562
|
-
argument: specIndex >= 0 ? def.args[specIndex] : null
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return { context, suggestions, activeCall };
|
|
567
|
-
}
|
|
568
|
-
function applyFormulaSuggestion(input, caret, name) {
|
|
569
|
-
const context = getFormulaCaretContext(input, caret);
|
|
570
|
-
const start = context.prefix?.start ?? caret;
|
|
571
|
-
const end = context.prefix?.end ?? caret;
|
|
572
|
-
const hasParen = input[end] === "(";
|
|
573
|
-
const insert = hasParen ? name.toUpperCase() : `${name.toUpperCase()}(`;
|
|
574
|
-
const value = input.slice(0, start) + insert + input.slice(end);
|
|
575
|
-
return { value, caret: start + insert.length + (hasParen ? 1 : 0) };
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// src/spreadsheet/numfmt/presets.ts
|
|
579
|
-
var NUMBER_FORMAT_PRESETS = [
|
|
580
|
-
{ id: "general", label: "General", code: "General" },
|
|
581
|
-
{ id: "number", label: "Number (0.00)", code: "0.00" },
|
|
582
|
-
{ id: "currency", label: "Currency ($1,234.50)", code: '"$"#,##0.00' },
|
|
583
|
-
{ id: "percent", label: "Percent (12.5%)", code: "0.0%" },
|
|
584
|
-
{ id: "date", label: "Date (3/14/2026)", code: "m/d/yyyy" },
|
|
585
|
-
{ id: "time", label: "Time (1:30 PM)", code: "h:mm AM/PM" }
|
|
586
|
-
];
|
|
587
|
-
function numberFormatForCellFormat(format) {
|
|
588
|
-
switch (format) {
|
|
589
|
-
case "currency":
|
|
590
|
-
return '"$"#,##0.00';
|
|
591
|
-
case "percentage":
|
|
592
|
-
return "0%";
|
|
593
|
-
case "date":
|
|
594
|
-
return "m/d/yyyy";
|
|
595
|
-
default:
|
|
596
|
-
return void 0;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
function cellFormatForNumberFormat(code) {
|
|
600
|
-
switch (NumberFormat.parseCached(code).kind()) {
|
|
601
|
-
case "number":
|
|
602
|
-
case "fraction":
|
|
603
|
-
case "scientific":
|
|
604
|
-
case "special":
|
|
605
|
-
return "number";
|
|
606
|
-
case "currency":
|
|
607
|
-
case "accounting":
|
|
608
|
-
return "currency";
|
|
609
|
-
case "percentage":
|
|
610
|
-
return "percentage";
|
|
611
|
-
case "date":
|
|
612
|
-
return "date";
|
|
613
|
-
case "text":
|
|
614
|
-
return "text";
|
|
615
|
-
default:
|
|
616
|
-
return void 0;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
var isGeneralNumberFormat = (code) => code === void 0 || NumberFormat.parseCached(code).isGeneral();
|
|
620
|
-
|
|
621
|
-
// src/spreadsheet/formula/format.ts
|
|
622
|
-
function formatCellForDisplay(cell) {
|
|
623
|
-
if (!cell) return { text: "", numeric: false };
|
|
624
|
-
if (cell.error) return { text: cell.error, numeric: false };
|
|
625
|
-
const { value } = cell;
|
|
626
|
-
const code = cell.numberFormat ?? (typeof value === "number" ? numberFormatForCellFormat(cell.format) : void 0);
|
|
627
|
-
if (code !== void 0 && !isGeneralNumberFormat(code) && !isBlank(value) && typeof value !== "boolean") {
|
|
628
|
-
return NumberFormat.parseCached(code).format(value);
|
|
629
|
-
}
|
|
630
|
-
return { text: toText(value), numeric: typeof value === "number" };
|
|
631
|
-
}
|
|
632
|
-
function formatCellDisplay(cell) {
|
|
633
|
-
return formatCellForDisplay(cell).text;
|
|
634
|
-
}
|
|
635
|
-
function formatValueWithCode(value, format) {
|
|
636
|
-
const result = formatTextFunction(value, format);
|
|
637
|
-
return "text" in result ? result.text : null;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// src/spreadsheet/datasource/index.ts
|
|
641
|
-
var DataSourceBinder = class {
|
|
642
|
-
dataSources = /* @__PURE__ */ new Map();
|
|
643
|
-
register(dataSource) {
|
|
644
|
-
this.dataSources.set(dataSource.id, dataSource);
|
|
645
|
-
}
|
|
646
|
-
unregister(dataSourceId) {
|
|
647
|
-
this.dataSources.delete(dataSourceId);
|
|
648
|
-
}
|
|
649
|
-
getDataSource(id) {
|
|
650
|
-
return this.dataSources.get(id);
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Generates or reconciles spreadsheet rows from a primary data source.
|
|
654
|
-
*/
|
|
655
|
-
syncRows(currentRows, dataSourceId) {
|
|
656
|
-
const ds = this.dataSources.get(dataSourceId);
|
|
657
|
-
if (!ds) return currentRows;
|
|
658
|
-
const records = ds.getRecords();
|
|
659
|
-
const existingRecordRowMap = /* @__PURE__ */ new Map();
|
|
660
|
-
const freeformRows = [];
|
|
661
|
-
for (const row of currentRows) {
|
|
662
|
-
if (row.type === "record" && row.recordId) {
|
|
663
|
-
existingRecordRowMap.set(row.recordId, row);
|
|
664
|
-
} else {
|
|
665
|
-
freeformRows.push(row);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
const nextRows = [];
|
|
669
|
-
let rowIndex = 0;
|
|
670
|
-
for (const record of records) {
|
|
671
|
-
const existing = existingRecordRowMap.get(record.id);
|
|
672
|
-
nextRows.push({
|
|
673
|
-
...existing?.hidden ? { hidden: true } : {},
|
|
674
|
-
id: existing?.id || `row_${record.id}`,
|
|
675
|
-
index: rowIndex++,
|
|
676
|
-
height: existing?.height || 32,
|
|
677
|
-
recordId: record.id,
|
|
678
|
-
type: "record"
|
|
679
|
-
});
|
|
680
|
-
existingRecordRowMap.delete(record.id);
|
|
681
|
-
}
|
|
682
|
-
for (const freeform of freeformRows) {
|
|
683
|
-
nextRows.push({
|
|
684
|
-
...freeform,
|
|
685
|
-
index: rowIndex++
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
return nextRows;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Resolves a record's bound field value.
|
|
692
|
-
*/
|
|
693
|
-
resolveBoundValue(dataSourceId, recordId, field) {
|
|
694
|
-
const ds = this.dataSources.get(dataSourceId);
|
|
695
|
-
if (!ds) return void 0;
|
|
696
|
-
const records = ds.getRecords();
|
|
697
|
-
const record = records.find((r) => r.id === recordId);
|
|
698
|
-
return record ? record[field] : void 0;
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* Dispatches an update to the underlying data source.
|
|
702
|
-
*/
|
|
703
|
-
async commitBoundValue(dataSourceId, recordId, field, value) {
|
|
704
|
-
const ds = this.dataSources.get(dataSourceId);
|
|
705
|
-
if (!ds) return;
|
|
706
|
-
await ds.updateRecord(recordId, field, value);
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
// src/spreadsheet/table.ts
|
|
711
|
-
function normalizeRange(range) {
|
|
712
|
-
return {
|
|
713
|
-
startRow: Math.min(range.startRow, range.endRow),
|
|
714
|
-
startCol: Math.min(range.startCol, range.endCol),
|
|
715
|
-
endRow: Math.max(range.startRow, range.endRow),
|
|
716
|
-
endCol: Math.max(range.startCol, range.endCol)
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
function rangeContains(range, row, col) {
|
|
720
|
-
const r = normalizeRange(range);
|
|
721
|
-
return row >= r.startRow && row <= r.endRow && col >= r.startCol && col <= r.endCol;
|
|
722
|
-
}
|
|
723
|
-
function rangesIntersect(a, b) {
|
|
724
|
-
const x = normalizeRange(a);
|
|
725
|
-
const y = normalizeRange(b);
|
|
726
|
-
return x.startRow <= y.endRow && y.startRow <= x.endRow && x.startCol <= y.endCol && y.startCol <= x.endCol;
|
|
727
|
-
}
|
|
728
|
-
function rangeContainsRange(outer, inner) {
|
|
729
|
-
const o = normalizeRange(outer);
|
|
730
|
-
const i = normalizeRange(inner);
|
|
731
|
-
return i.startRow >= o.startRow && i.endRow <= o.endRow && i.startCol >= o.startCol && i.endCol <= o.endCol;
|
|
732
|
-
}
|
|
733
|
-
function isSingleCellRange(range) {
|
|
734
|
-
const r = normalizeRange(range);
|
|
735
|
-
return r.startRow === r.endRow && r.startCol === r.endCol;
|
|
736
|
-
}
|
|
737
|
-
var NUMBER_PATTERN = /^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i;
|
|
738
|
-
var ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
739
|
-
function isBlankValue(value) {
|
|
740
|
-
return value === null || value === void 0 || typeof value === "string" && value.trim() === "";
|
|
741
|
-
}
|
|
742
|
-
function toNumber(value) {
|
|
743
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
744
|
-
if (typeof value === "string") {
|
|
745
|
-
const trimmed = value.trim();
|
|
746
|
-
if (NUMBER_PATTERN.test(trimmed)) return parseFloat(trimmed);
|
|
747
|
-
}
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
var collator = typeof Intl !== "undefined" ? new Intl.Collator(void 0, { numeric: true, sensitivity: "base" }) : null;
|
|
751
|
-
function compareText(a, b) {
|
|
752
|
-
return collator ? collator.compare(a, b) : a < b ? -1 : a > b ? 1 : 0;
|
|
753
|
-
}
|
|
754
|
-
function compareCellValues(a, b, direction = "asc") {
|
|
755
|
-
const aBlank = isBlankValue(a);
|
|
756
|
-
const bBlank = isBlankValue(b);
|
|
757
|
-
if (aBlank || bBlank) return aBlank === bBlank ? 0 : aBlank ? 1 : -1;
|
|
758
|
-
let result;
|
|
759
|
-
if (isPlainText(a) && isPlainText(b) && !(parseIsoDate(a) && parseIsoDate(b))) {
|
|
760
|
-
result = compareText(a, b);
|
|
761
|
-
} else {
|
|
762
|
-
result = sortCompareValues(a, b);
|
|
763
|
-
}
|
|
764
|
-
if (result === 0) return 0;
|
|
765
|
-
return direction === "desc" ? -Math.sign(result) : Math.sign(result);
|
|
766
|
-
}
|
|
767
|
-
var isPlainText = (value) => typeof value === "string" && !isFormulaError(value);
|
|
768
|
-
function filterValue(value) {
|
|
769
|
-
if (isBlankValue(value) || isFormulaError(value)) return null;
|
|
770
|
-
if (typeof value === "number") return Number.isFinite(value) ? { value, kind: "number" } : null;
|
|
771
|
-
if (typeof value === "boolean") return { value, kind: "boolean" };
|
|
772
|
-
const num = numericValue(value);
|
|
773
|
-
if (num !== null) return { value: num, kind: "number" };
|
|
774
|
-
const text = String(value);
|
|
775
|
-
return { value: text, kind: parseIsoDate(text) ? "number" : "text" };
|
|
776
|
-
}
|
|
777
|
-
function filterOperand(operand) {
|
|
778
|
-
if (typeof operand !== "string") return filterValue(operand);
|
|
779
|
-
if (operand.trim() === "") return null;
|
|
780
|
-
const parsed = parseCellInput(operand.trim());
|
|
781
|
-
if (parsed.type === "number" || parsed.type === "date" || parsed.type === "boolean") return filterValue(parsed.value);
|
|
782
|
-
return { value: operand, kind: "text" };
|
|
783
|
-
}
|
|
784
|
-
function compareForFilter(value, operand) {
|
|
785
|
-
const a = filterValue(value);
|
|
786
|
-
const b = filterOperand(operand);
|
|
787
|
-
if (!a || !b || a.kind !== b.kind) return null;
|
|
788
|
-
return compareValues(a.value, b.value);
|
|
789
|
-
}
|
|
790
|
-
function looselyEquals(value, operand) {
|
|
791
|
-
const c = compareForFilter(value, operand);
|
|
792
|
-
if (c !== null) return c === 0;
|
|
793
|
-
return String(value ?? "").trim().toLowerCase() === String(operand ?? "").trim().toLowerCase();
|
|
794
|
-
}
|
|
795
|
-
function matchesFilterRule(value, rule) {
|
|
796
|
-
const text = String(value ?? "").toLowerCase();
|
|
797
|
-
switch (rule.operator) {
|
|
798
|
-
case "equals":
|
|
799
|
-
return looselyEquals(value, rule.value);
|
|
800
|
-
case "notEquals":
|
|
801
|
-
return !looselyEquals(value, rule.value);
|
|
802
|
-
case "contains":
|
|
803
|
-
return text.includes(String(rule.value ?? "").toLowerCase());
|
|
804
|
-
case "notContains":
|
|
805
|
-
return !text.includes(String(rule.value ?? "").toLowerCase());
|
|
806
|
-
case "greaterThan": {
|
|
807
|
-
const c = compareForFilter(value, rule.value);
|
|
808
|
-
return c !== null && c > 0;
|
|
809
|
-
}
|
|
810
|
-
case "greaterThanOrEqual": {
|
|
811
|
-
const c = compareForFilter(value, rule.value);
|
|
812
|
-
return c !== null && c >= 0;
|
|
813
|
-
}
|
|
814
|
-
case "lessThan": {
|
|
815
|
-
const c = compareForFilter(value, rule.value);
|
|
816
|
-
return c !== null && c < 0;
|
|
817
|
-
}
|
|
818
|
-
case "lessThanOrEqual": {
|
|
819
|
-
const c = compareForFilter(value, rule.value);
|
|
820
|
-
return c !== null && c <= 0;
|
|
821
|
-
}
|
|
822
|
-
case "between": {
|
|
823
|
-
const lo = compareForFilter(value, rule.value);
|
|
824
|
-
const hi = compareForFilter(value, rule.value2);
|
|
825
|
-
return lo !== null && hi !== null && lo >= 0 && hi <= 0;
|
|
826
|
-
}
|
|
827
|
-
case "isBlank":
|
|
828
|
-
return isBlankValue(value);
|
|
829
|
-
case "isNotBlank":
|
|
830
|
-
return !isBlankValue(value);
|
|
831
|
-
case "oneOf":
|
|
832
|
-
return (rule.values ?? []).some((candidate) => String(candidate) === String(value ?? ""));
|
|
833
|
-
default:
|
|
834
|
-
return true;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
function aggregateValues(values, aggregate) {
|
|
838
|
-
if (aggregate === "count") return values.filter((v) => !isBlankValue(v)).length;
|
|
839
|
-
const numbers = [];
|
|
840
|
-
for (const v of values) {
|
|
841
|
-
const n = toNumber(v);
|
|
842
|
-
if (n !== null) numbers.push(n);
|
|
843
|
-
}
|
|
844
|
-
if (numbers.length === 0) return aggregate === "sum" ? 0 : null;
|
|
845
|
-
switch (aggregate) {
|
|
846
|
-
case "sum":
|
|
847
|
-
return roundFloat(numbers.reduce((s, n) => s + n, 0));
|
|
848
|
-
case "average":
|
|
849
|
-
return roundFloat(numbers.reduce((s, n) => s + n, 0) / numbers.length);
|
|
850
|
-
case "min":
|
|
851
|
-
return numbers.reduce((m, n) => n < m ? n : m, numbers[0]);
|
|
852
|
-
case "max":
|
|
853
|
-
return numbers.reduce((m, n) => n > m ? n : m, numbers[0]);
|
|
854
|
-
default:
|
|
855
|
-
return null;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
function roundFloat(value) {
|
|
859
|
-
return Number.isInteger(value) ? value : parseFloat(value.toPrecision(12));
|
|
860
|
-
}
|
|
861
|
-
var DAY_MS = 864e5;
|
|
862
|
-
var TRAILING_NUMBER = /^(.*?)(\d+)(\D*)$/;
|
|
863
|
-
function classifyFillValue(raw) {
|
|
864
|
-
if (raw.trim() === "") return { kind: "blank" };
|
|
865
|
-
const num = toNumber(raw);
|
|
866
|
-
if (num !== null) return { kind: "number", value: num };
|
|
867
|
-
const typed = parseCellInput(raw);
|
|
868
|
-
const date = ISO_DATE_PATTERN.exec(typed.type === "date" ? String(typed.value) : raw.trim());
|
|
869
|
-
if (date) {
|
|
870
|
-
const [year, month, day] = [Number(date[1]), Number(date[2]), Number(date[3])];
|
|
871
|
-
const time = Date.UTC(year, month - 1, day);
|
|
872
|
-
const check = new Date(time);
|
|
873
|
-
if (check.getUTCMonth() === month - 1 && check.getUTCDate() === day) {
|
|
874
|
-
return { kind: "date", time, year, month, day };
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
const textNumber = TRAILING_NUMBER.exec(raw);
|
|
878
|
-
if (textNumber && textNumber[1] !== "") {
|
|
879
|
-
return {
|
|
880
|
-
kind: "textNumber",
|
|
881
|
-
prefix: textNumber[1],
|
|
882
|
-
value: parseInt(textNumber[2], 10),
|
|
883
|
-
width: textNumber[2].length,
|
|
884
|
-
suffix: textNumber[3]
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
return { kind: "text", text: raw };
|
|
888
|
-
}
|
|
889
|
-
var pad = (n, width) => String(Math.abs(n)).padStart(width, "0");
|
|
890
|
-
function formatDate(time) {
|
|
891
|
-
const d = new Date(time);
|
|
892
|
-
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1, 2)}-${pad(d.getUTCDate(), 2)}`;
|
|
893
|
-
}
|
|
894
|
-
function constantStep(values) {
|
|
895
|
-
const step = values[1] - values[0];
|
|
896
|
-
for (let i = 2; i < values.length; i++) {
|
|
897
|
-
if (Math.abs(values[i] - values[i - 1] - step) > 1e-9) return null;
|
|
898
|
-
}
|
|
899
|
-
return step;
|
|
900
|
-
}
|
|
901
|
-
function linearTrend(values, x) {
|
|
902
|
-
const n = values.length;
|
|
903
|
-
const meanX = (n - 1) / 2;
|
|
904
|
-
const meanY = values.reduce((s, v) => s + v, 0) / n;
|
|
905
|
-
let num = 0;
|
|
906
|
-
let den = 0;
|
|
907
|
-
values.forEach((v, i) => {
|
|
908
|
-
num += (i - meanX) * (v - meanY);
|
|
909
|
-
den += (i - meanX) ** 2;
|
|
910
|
-
});
|
|
911
|
-
const slope = den === 0 ? 0 : num / den;
|
|
912
|
-
return meanY + slope * (x - meanX);
|
|
913
|
-
}
|
|
914
|
-
function fillSeries(source, count) {
|
|
915
|
-
if (count <= 0) return [];
|
|
916
|
-
if (source.length === 0) return Array(count).fill("");
|
|
917
|
-
const repeat = () => Array.from({ length: count }, (_, i) => source[i % source.length]);
|
|
918
|
-
const parsed = source.map(classifyFillValue);
|
|
919
|
-
const kind = parsed[0].kind;
|
|
920
|
-
if (!parsed.every((p) => p.kind === kind)) return repeat();
|
|
921
|
-
const n = source.length;
|
|
922
|
-
if (kind === "number") {
|
|
923
|
-
if (n === 1) return repeat();
|
|
924
|
-
const values = parsed.map((p) => p.value);
|
|
925
|
-
return Array.from({ length: count }, (_, i) => String(roundFloat(linearTrend(values, n + i))));
|
|
926
|
-
}
|
|
927
|
-
if (kind === "date") {
|
|
928
|
-
const dates = parsed;
|
|
929
|
-
if (n === 1) {
|
|
930
|
-
return Array.from({ length: count }, (_, i) => formatDate(dates[0].time + (i + 1) * DAY_MS));
|
|
931
|
-
}
|
|
932
|
-
const monthIndex = dates.map((d) => d.year * 12 + (d.month - 1));
|
|
933
|
-
const monthStep = constantStep(monthIndex);
|
|
934
|
-
if (monthStep !== null && monthStep !== 0 && dates.every((d) => d.day === dates[0].day)) {
|
|
935
|
-
const last = monthIndex[n - 1];
|
|
936
|
-
return Array.from({ length: count }, (_, i) => {
|
|
937
|
-
const m = last + monthStep * (i + 1);
|
|
938
|
-
const year = Math.floor(m / 12);
|
|
939
|
-
const month = m - year * 12;
|
|
940
|
-
const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
941
|
-
return formatDate(Date.UTC(year, month, Math.min(dates[0].day, daysInMonth)));
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
const dayStep = constantStep(dates.map((d) => Math.round(d.time / DAY_MS)));
|
|
945
|
-
if (dayStep === null) return repeat();
|
|
946
|
-
const lastTime = dates[n - 1].time;
|
|
947
|
-
return Array.from({ length: count }, (_, i) => formatDate(lastTime + dayStep * (i + 1) * DAY_MS));
|
|
948
|
-
}
|
|
949
|
-
if (kind === "textNumber") {
|
|
950
|
-
const items = parsed;
|
|
951
|
-
const { prefix, suffix } = items[0];
|
|
952
|
-
if (!items.every((t) => t.prefix === prefix && t.suffix === suffix)) return repeat();
|
|
953
|
-
const step = n === 1 ? 1 : constantStep(items.map((t) => t.value));
|
|
954
|
-
if (step === null) return repeat();
|
|
955
|
-
const last = items[n - 1];
|
|
956
|
-
return Array.from({ length: count }, (_, i) => {
|
|
957
|
-
const value = last.value + step * (i + 1);
|
|
958
|
-
const digits = value < 0 ? `-${pad(value, last.width)}` : pad(value, last.width);
|
|
959
|
-
return `${prefix}${digits}${suffix}`;
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
return repeat();
|
|
963
|
-
}
|
|
964
|
-
function computeFillTarget(source, pointer) {
|
|
965
|
-
const s = normalizeRange(source);
|
|
966
|
-
const rowOverflow = pointer.row > s.endRow ? pointer.row - s.endRow : pointer.row < s.startRow ? s.startRow - pointer.row : 0;
|
|
967
|
-
const colOverflow = pointer.col > s.endCol ? pointer.col - s.endCol : pointer.col < s.startCol ? s.startCol - pointer.col : 0;
|
|
968
|
-
if (rowOverflow === 0 && colOverflow === 0) return null;
|
|
969
|
-
if (rowOverflow >= colOverflow) {
|
|
970
|
-
return pointer.row > s.endRow ? { ...s, endRow: pointer.row } : { ...s, startRow: pointer.row };
|
|
971
|
-
}
|
|
972
|
-
return pointer.col > s.endCol ? { ...s, endCol: pointer.col } : { ...s, startCol: pointer.col };
|
|
973
|
-
}
|
|
974
|
-
function getFillDirection(source, target) {
|
|
975
|
-
const s = normalizeRange(source);
|
|
976
|
-
const t = normalizeRange(target);
|
|
977
|
-
const sameCols = s.startCol === t.startCol && s.endCol === t.endCol;
|
|
978
|
-
const sameRows = s.startRow === t.startRow && s.endRow === t.endRow;
|
|
979
|
-
if (sameCols && t.startRow === s.startRow && t.endRow > s.endRow) return "down";
|
|
980
|
-
if (sameCols && t.endRow === s.endRow && t.startRow < s.startRow) return "up";
|
|
981
|
-
if (sameRows && t.startCol === s.startCol && t.endCol > s.endCol) return "right";
|
|
982
|
-
if (sameRows && t.endCol === s.endCol && t.startCol < s.startCol) return "left";
|
|
983
|
-
return null;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// src/spreadsheet/clipboard.ts
|
|
987
|
-
var SPREADSHEET_CLIPBOARD_MIME = "application/x.pixerate.spreadsheet+json";
|
|
988
|
-
var displayText = (value) => value === null || value === void 0 ? "" : String(value);
|
|
989
|
-
function escapeHtml(text) {
|
|
990
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
991
|
-
}
|
|
992
|
-
function serializeHtmlTable(values) {
|
|
993
|
-
const body = values.map(
|
|
994
|
-
(row) => `<tr>${row.map((v) => `<td>${escapeHtml(displayText(v)).replace(/\r?\n/g, "<br>")}</td>`).join("")}</tr>`
|
|
995
|
-
).join("");
|
|
996
|
-
return `<table><tbody>${body}</tbody></table>`;
|
|
997
|
-
}
|
|
998
|
-
var NAMED_ENTITIES = {
|
|
999
|
-
amp: "&",
|
|
1000
|
-
lt: "<",
|
|
1001
|
-
gt: ">",
|
|
1002
|
-
quot: '"',
|
|
1003
|
-
apos: "'",
|
|
1004
|
-
nbsp: " "
|
|
1005
|
-
};
|
|
1006
|
-
function decodeEntities(text) {
|
|
1007
|
-
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
|
|
1008
|
-
if (entity[0] === "#") {
|
|
1009
|
-
const code = entity[1] === "x" || entity[1] === "X" ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
|
|
1010
|
-
return Number.isFinite(code) ? String.fromCodePoint(code) : match;
|
|
1011
|
-
}
|
|
1012
|
-
return NAMED_ENTITIES[entity.toLowerCase()] ?? match;
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
var LINE_BREAK = "\uE000";
|
|
1016
|
-
function cellHtmlToText(html) {
|
|
1017
|
-
const text = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<(style|script)[\s\S]*?<\/\1\s*>/gi, "").replace(/<br\s*\/?>/gi, LINE_BREAK).replace(/<\/(p|div)\s*>/gi, LINE_BREAK).replace(/<[^>]*>/g, "").replace(/\s+/g, " ");
|
|
1018
|
-
return decodeEntities(text).replace(/ /g, " ").split(LINE_BREAK).map((line) => line.trim()).join("\n").replace(/^\n+|\n+$/g, "");
|
|
1019
|
-
}
|
|
1020
|
-
function parseHtmlTable(html) {
|
|
1021
|
-
const tableMatch = /<table[\s>][\s\S]*?<\/table\s*>/i.exec(html);
|
|
1022
|
-
if (!tableMatch) return null;
|
|
1023
|
-
const table = tableMatch[0];
|
|
1024
|
-
const rows = [];
|
|
1025
|
-
const occupied = /* @__PURE__ */ new Map();
|
|
1026
|
-
const rowPattern = /<tr[\s>]([\s\S]*?)<\/tr\s*>/gi;
|
|
1027
|
-
const cellPattern = /<(td|th)(\s[^>]*)?>([\s\S]*?)<\/\1\s*>/gi;
|
|
1028
|
-
let rowMatch;
|
|
1029
|
-
let rowIndex = 0;
|
|
1030
|
-
while (rowMatch = rowPattern.exec(table)) {
|
|
1031
|
-
const row = [];
|
|
1032
|
-
const taken = occupied.get(rowIndex) ?? /* @__PURE__ */ new Set();
|
|
1033
|
-
let col = 0;
|
|
1034
|
-
let cellMatch;
|
|
1035
|
-
cellPattern.lastIndex = 0;
|
|
1036
|
-
while (cellMatch = cellPattern.exec(rowMatch[1])) {
|
|
1037
|
-
while (taken.has(col)) row[col++] = "";
|
|
1038
|
-
const attrs = cellMatch[2] ?? "";
|
|
1039
|
-
const colspan = Math.max(1, parseInt(/colspan\s*=\s*["']?(\d+)/i.exec(attrs)?.[1] ?? "1", 10));
|
|
1040
|
-
const rowspan = Math.max(1, parseInt(/rowspan\s*=\s*["']?(\d+)/i.exec(attrs)?.[1] ?? "1", 10));
|
|
1041
|
-
row[col] = cellHtmlToText(cellMatch[3]);
|
|
1042
|
-
for (let c = col; c < col + colspan; c++) {
|
|
1043
|
-
if (c > col) row[c] = "";
|
|
1044
|
-
for (let r = rowIndex + 1; r < rowIndex + rowspan; r++) {
|
|
1045
|
-
if (!occupied.has(r)) occupied.set(r, /* @__PURE__ */ new Set());
|
|
1046
|
-
occupied.get(r).add(c);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
col += colspan;
|
|
1050
|
-
}
|
|
1051
|
-
while (taken.has(col)) row[col++] = "";
|
|
1052
|
-
rows.push(Array.from(row, (v) => v ?? ""));
|
|
1053
|
-
rowIndex++;
|
|
1054
|
-
}
|
|
1055
|
-
return rows.length > 0 ? rows : null;
|
|
1056
|
-
}
|
|
1057
|
-
function createSpreadsheetClipboardData(cells, origin) {
|
|
1058
|
-
const texts = cells.map((row) => row.map((cell) => cell.text ?? displayText(cell.value)));
|
|
1059
|
-
const payload = {
|
|
1060
|
-
type: "pixerate-spreadsheet",
|
|
1061
|
-
version: 1,
|
|
1062
|
-
origin,
|
|
1063
|
-
cells: cells.map((row) => row.map(({ raw, value }) => ({ raw, value })))
|
|
1064
|
-
};
|
|
1065
|
-
return {
|
|
1066
|
-
"text/plain": serializeTsv(texts),
|
|
1067
|
-
"text/html": serializeHtmlTable(texts),
|
|
1068
|
-
[SPREADSHEET_CLIPBOARD_MIME]: JSON.stringify(payload)
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
function readFormat(source, format) {
|
|
1072
|
-
try {
|
|
1073
|
-
if (typeof source.getData === "function") {
|
|
1074
|
-
return source.getData(format) ?? "";
|
|
1075
|
-
}
|
|
1076
|
-
return source[format] ?? "";
|
|
1077
|
-
} catch {
|
|
1078
|
-
return "";
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
function parsePayload(json) {
|
|
1082
|
-
try {
|
|
1083
|
-
const payload = JSON.parse(json);
|
|
1084
|
-
if (payload?.type !== "pixerate-spreadsheet" || !Array.isArray(payload.cells) || !payload.cells.every((row) => Array.isArray(row)) || typeof payload.origin?.row !== "number" || typeof payload.origin?.col !== "number") {
|
|
1085
|
-
return null;
|
|
1086
|
-
}
|
|
1087
|
-
return payload;
|
|
1088
|
-
} catch {
|
|
1089
|
-
return null;
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
function readSpreadsheetClipboard(source) {
|
|
1093
|
-
const payload = parsePayload(readFormat(source, SPREADSHEET_CLIPBOARD_MIME));
|
|
1094
|
-
if (payload) {
|
|
1095
|
-
return {
|
|
1096
|
-
format: "internal",
|
|
1097
|
-
origin: payload.origin,
|
|
1098
|
-
rows: payload.cells.map((row) => row.map((cell) => typeof cell?.raw === "string" ? cell.raw : displayText(cell?.value)))
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
const html = readFormat(source, "text/html");
|
|
1102
|
-
if (html) {
|
|
1103
|
-
const rows = parseHtmlTable(html);
|
|
1104
|
-
if (rows) return { format: "html", rows };
|
|
1105
|
-
}
|
|
1106
|
-
const text = readFormat(source, "text/plain");
|
|
1107
|
-
if (text) {
|
|
1108
|
-
const rows = parseTsv(text);
|
|
1109
|
-
if (rows.length > 0) return { format: "text", rows };
|
|
1110
|
-
}
|
|
1111
|
-
return null;
|
|
1112
|
-
}
|
|
1113
|
-
function rebaseClipboardRows(rows, origin, target) {
|
|
1114
|
-
const rowOffset = target.row - origin.row;
|
|
1115
|
-
const colOffset = target.col - origin.col;
|
|
1116
|
-
return rows.map(
|
|
1117
|
-
(row) => row.map((raw) => raw.startsWith("=") ? offsetFormulaReferences(raw, rowOffset, colOffset) : raw)
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// src/spreadsheet/view.ts
|
|
1122
|
-
var DEFAULT_ROW_HEIGHT = 32;
|
|
1123
|
-
var DEFAULT_COLUMN_WIDTH = 130;
|
|
1124
|
-
var clampCount = (value, max) => Math.max(0, Math.min(Math.floor(value ?? 0), max));
|
|
1125
|
-
function getTableSections(doc) {
|
|
1126
|
-
const total = doc.rows.length;
|
|
1127
|
-
const headerRows = clampCount(doc.table?.headerRowCount, total);
|
|
1128
|
-
const footerRows = clampCount(doc.table?.footerRowCount, total - headerRows);
|
|
1129
|
-
return { headerRows, footerRows };
|
|
1130
|
-
}
|
|
1131
|
-
function getRowSection(doc, rowIndex) {
|
|
1132
|
-
const { headerRows, footerRows } = getTableSections(doc);
|
|
1133
|
-
if (rowIndex < headerRows) return "header";
|
|
1134
|
-
if (rowIndex >= doc.rows.length - footerRows) return "footer";
|
|
1135
|
-
return "body";
|
|
1136
|
-
}
|
|
1137
|
-
function rowPassesFilters(doc, rowIndex, getValue) {
|
|
1138
|
-
const filter = doc.table?.filter;
|
|
1139
|
-
if (!filter?.enabled || filter.rules.length === 0) return true;
|
|
1140
|
-
if (getRowSection(doc, rowIndex) !== "body") return true;
|
|
1141
|
-
return filter.rules.every((rule) => {
|
|
1142
|
-
const colIndex = doc.columns.findIndex((c) => c.id === rule.colId);
|
|
1143
|
-
if (colIndex === -1) return true;
|
|
1144
|
-
return matchesFilterRule(getValue(rowIndex, colIndex), rule);
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
function computeRowGroups(doc, getValue) {
|
|
1148
|
-
const grouping = doc.table?.grouping;
|
|
1149
|
-
if (!grouping) return [];
|
|
1150
|
-
const groupCol = doc.columns.findIndex((c) => c.id === grouping.colId);
|
|
1151
|
-
if (groupCol === -1) return [];
|
|
1152
|
-
const { headerRows, footerRows } = getTableSections(doc);
|
|
1153
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
1154
|
-
for (let r = headerRows; r < doc.rows.length - footerRows; r++) {
|
|
1155
|
-
if (doc.rows[r].hidden || !rowPassesFilters(doc, r, getValue)) continue;
|
|
1156
|
-
const value = getValue(r, groupCol);
|
|
1157
|
-
const key = isBlankValue(value) ? "" : String(value).trim();
|
|
1158
|
-
if (!byKey.has(key)) byKey.set(key, []);
|
|
1159
|
-
byKey.get(key).push(r);
|
|
1160
|
-
}
|
|
1161
|
-
const collapsed = new Set(grouping.collapsed);
|
|
1162
|
-
return Array.from(byKey.keys()).sort((a, b) => compareCellValues(a, b)).map((key) => {
|
|
1163
|
-
const rowIndices = byKey.get(key);
|
|
1164
|
-
const summaries = {};
|
|
1165
|
-
for (const summary of grouping.summaries) {
|
|
1166
|
-
const colIndex = doc.columns.findIndex((c) => c.id === summary.colId);
|
|
1167
|
-
if (colIndex === -1) continue;
|
|
1168
|
-
summaries[summary.colId] = aggregateValues(
|
|
1169
|
-
rowIndices.map((r) => getValue(r, colIndex)),
|
|
1170
|
-
summary.aggregate
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
return { key, label: key === "" ? "(blank)" : key, rowIndices, collapsed: collapsed.has(key), summaries };
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
function buildSpreadsheetView(doc, getValue) {
|
|
1177
|
-
const { headerRows, footerRows } = getTableSections(doc);
|
|
1178
|
-
const frozenRows = clampCount(doc.table?.frozenRowCount, doc.rows.length);
|
|
1179
|
-
const frozenCols = clampCount(doc.table?.frozenColumnCount, doc.columns.length);
|
|
1180
|
-
const headerCols = clampCount(doc.table?.headerColumnCount, doc.columns.length);
|
|
1181
|
-
const columns = [];
|
|
1182
|
-
let left = 0;
|
|
1183
|
-
let hiddenColumnCount = 0;
|
|
1184
|
-
doc.columns.forEach((column, index) => {
|
|
1185
|
-
if (column.hidden) {
|
|
1186
|
-
hiddenColumnCount++;
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
const frozen = index < frozenCols;
|
|
1190
|
-
columns.push({ column, index, header: index < headerCols, frozen, stickyLeft: frozen ? left : 0 });
|
|
1191
|
-
if (frozen) left += column.width || DEFAULT_COLUMN_WIDTH;
|
|
1192
|
-
});
|
|
1193
|
-
const rows = [];
|
|
1194
|
-
let hiddenRowCount = 0;
|
|
1195
|
-
let filteredOutCount = 0;
|
|
1196
|
-
const pushRow = (index, section, groupKey) => {
|
|
1197
|
-
const row = doc.rows[index];
|
|
1198
|
-
rows.push({ kind: "row", row, index, section, frozen: false, stickyTop: 0, groupKey });
|
|
1199
|
-
};
|
|
1200
|
-
for (let r = 0; r < headerRows; r++) {
|
|
1201
|
-
if (doc.rows[r].hidden) hiddenRowCount++;
|
|
1202
|
-
else pushRow(r, "header");
|
|
1203
|
-
}
|
|
1204
|
-
const bodyEnd = doc.rows.length - footerRows;
|
|
1205
|
-
const groups = computeRowGroups(doc, getValue);
|
|
1206
|
-
for (let r = headerRows; r < bodyEnd; r++) {
|
|
1207
|
-
if (doc.rows[r].hidden) hiddenRowCount++;
|
|
1208
|
-
else if (!rowPassesFilters(doc, r, getValue)) filteredOutCount++;
|
|
1209
|
-
}
|
|
1210
|
-
const grouping = doc.table?.grouping;
|
|
1211
|
-
if (grouping && doc.columns.some((c) => c.id === grouping.colId)) {
|
|
1212
|
-
for (const group of groups) {
|
|
1213
|
-
rows.push({ kind: "group", group });
|
|
1214
|
-
if (!group.collapsed) group.rowIndices.forEach((r) => pushRow(r, "body", group.key));
|
|
1215
|
-
}
|
|
1216
|
-
} else {
|
|
1217
|
-
for (let r = headerRows; r < bodyEnd; r++) {
|
|
1218
|
-
if (!doc.rows[r].hidden && rowPassesFilters(doc, r, getValue)) pushRow(r, "body");
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
for (let r = bodyEnd; r < doc.rows.length; r++) {
|
|
1222
|
-
if (doc.rows[r].hidden) hiddenRowCount++;
|
|
1223
|
-
else pushRow(r, "footer");
|
|
1224
|
-
}
|
|
1225
|
-
let top = 0;
|
|
1226
|
-
for (const viewRow of rows) {
|
|
1227
|
-
if (viewRow.kind !== "row" || viewRow.index >= frozenRows) break;
|
|
1228
|
-
viewRow.frozen = true;
|
|
1229
|
-
viewRow.stickyTop = top;
|
|
1230
|
-
top += viewRow.row.height || DEFAULT_ROW_HEIGHT;
|
|
1231
|
-
}
|
|
1232
|
-
const rowOrder = rows.flatMap((r) => r.kind === "row" ? [r.index] : []);
|
|
1233
|
-
const columnOrder = columns.map((c) => c.index);
|
|
1234
|
-
const rowPos = new Map(rowOrder.map((index, pos) => [index, pos]));
|
|
1235
|
-
const colPos = new Map(columnOrder.map((index, pos) => [index, pos]));
|
|
1236
|
-
const spans = /* @__PURE__ */ new Map();
|
|
1237
|
-
const contiguousRun = (indices, positions) => {
|
|
1238
|
-
const visible = indices.filter((i) => positions.has(i)).sort((a, b) => positions.get(a) - positions.get(b));
|
|
1239
|
-
if (visible.length === 0) return null;
|
|
1240
|
-
let run = 1;
|
|
1241
|
-
while (run < visible.length && positions.get(visible[run]) === positions.get(visible[0]) + run) run++;
|
|
1242
|
-
return { first: visible[0], run };
|
|
1243
|
-
};
|
|
1244
|
-
for (const raw of doc.table?.merges ?? []) {
|
|
1245
|
-
const merge = normalizeRange(raw);
|
|
1246
|
-
const rowIdx = [];
|
|
1247
|
-
const colIdx = [];
|
|
1248
|
-
for (let r = merge.startRow; r <= merge.endRow; r++) rowIdx.push(r);
|
|
1249
|
-
for (let c = merge.startCol; c <= merge.endCol; c++) colIdx.push(c);
|
|
1250
|
-
const rowRun = contiguousRun(rowIdx, rowPos);
|
|
1251
|
-
const colRun = contiguousRun(colIdx, colPos);
|
|
1252
|
-
for (const r of rowIdx) for (const c of colIdx) spans.set(`${r}:${c}`, null);
|
|
1253
|
-
if (rowRun && colRun) {
|
|
1254
|
-
spans.set(`${rowRun.first}:${colRun.first}`, {
|
|
1255
|
-
rowSpan: rowRun.run,
|
|
1256
|
-
colSpan: colRun.run,
|
|
1257
|
-
source: { row: merge.startRow, col: merge.startCol },
|
|
1258
|
-
merge
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
return {
|
|
1263
|
-
rows,
|
|
1264
|
-
columns,
|
|
1265
|
-
groups,
|
|
1266
|
-
rowOrder,
|
|
1267
|
-
columnOrder,
|
|
1268
|
-
filteredOutCount,
|
|
1269
|
-
hiddenRowCount,
|
|
1270
|
-
hiddenColumnCount,
|
|
1271
|
-
getCellSpan(row, col) {
|
|
1272
|
-
const span = spans.get(`${row}:${col}`);
|
|
1273
|
-
if (span === void 0) return { rowSpan: 1, colSpan: 1, source: { row, col } };
|
|
1274
|
-
return span;
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// src/spreadsheet/controller.ts
|
|
1280
|
-
var nextIdSequence = 0;
|
|
1281
|
-
var DEFAULT_MAX_HISTORY_BYTES = 64 * 1024 * 1024;
|
|
1282
|
-
var MAX_SPILL_PASSES = 25;
|
|
1283
|
-
var spillCellId = (rowId, colId) => `${rowId}\0${colId}`;
|
|
1284
|
-
var SpreadsheetController = class {
|
|
1285
|
-
document;
|
|
1286
|
-
activeCell = null;
|
|
1287
|
-
selectedRange = null;
|
|
1288
|
-
editingCell = null;
|
|
1289
|
-
draftValue = "";
|
|
1290
|
-
binder = new DataSourceBinder();
|
|
1291
|
-
depGraph = new DependencyGraph();
|
|
1292
|
-
evaluator;
|
|
1293
|
-
primaryDataSourceId;
|
|
1294
|
-
onDocumentChange;
|
|
1295
|
-
onSelectionChange;
|
|
1296
|
-
onCellCommit;
|
|
1297
|
-
// History stack for undo/redo
|
|
1298
|
-
undoStack = [];
|
|
1299
|
-
redoStack = [];
|
|
1300
|
-
isApplyingHistory = false;
|
|
1301
|
-
maxHistoryBytes;
|
|
1302
|
-
// Set while importing TSV: history and recalculation are done once at the end.
|
|
1303
|
-
batching = false;
|
|
1304
|
-
pendingCommits = [];
|
|
1305
|
-
// Row being evaluated, so [property] references resolve per row.
|
|
1306
|
-
evaluatingRowIndex = null;
|
|
1307
|
-
// Cell being evaluated, for implicit intersection (`=@A:A`).
|
|
1308
|
-
evaluatingCell = null;
|
|
1309
|
-
// Dynamic arrays. Spilled values are derived state: they live here, not in
|
|
1310
|
-
// the document, and every recalculation rebuilds them.
|
|
1311
|
-
spills = /* @__PURE__ */ new Map();
|
|
1312
|
-
// anchor key -> spill
|
|
1313
|
-
spilledBy = /* @__PURE__ */ new Map();
|
|
1314
|
-
// spilled cell key -> anchor key
|
|
1315
|
-
spillCells = /* @__PURE__ */ new Map();
|
|
1316
|
-
// spillCellId -> displayed cell
|
|
1317
|
-
// Cells whose spill ownership changed during the current pass.
|
|
1318
|
-
spillChanges = null;
|
|
1319
|
-
constructor(options = {}) {
|
|
1320
|
-
const defaultCols = [
|
|
1321
|
-
{ id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
|
|
1322
|
-
{ id: "col_b", key: "B", title: "B", width: 120, type: "freeform" },
|
|
1323
|
-
{ id: "col_c", key: "C", title: "C", width: 120, type: "freeform" },
|
|
1324
|
-
{ id: "col_d", key: "D", title: "D", width: 120, type: "freeform" }
|
|
1325
|
-
];
|
|
1326
|
-
const defaultRows = options.primaryDataSourceId ? [] : Array.from({ length: 10 }, (_, i) => ({
|
|
1327
|
-
id: `row_${i + 1}`,
|
|
1328
|
-
index: i,
|
|
1329
|
-
height: 32,
|
|
1330
|
-
type: "freeform"
|
|
1331
|
-
}));
|
|
1332
|
-
this.document = {
|
|
1333
|
-
id: options.document?.id || `sheet_${Date.now()}`,
|
|
1334
|
-
name: options.document?.name || "Sheet 1",
|
|
1335
|
-
columns: options.document?.columns || defaultCols,
|
|
1336
|
-
rows: options.document?.rows || defaultRows,
|
|
1337
|
-
cells: options.document?.cells || {},
|
|
1338
|
-
createdAt: options.document?.createdAt || Date.now(),
|
|
1339
|
-
updatedAt: options.document?.updatedAt || Date.now(),
|
|
1340
|
-
...options.document?.table ? { table: options.document.table } : {}
|
|
1341
|
-
};
|
|
1342
|
-
this.primaryDataSourceId = options.primaryDataSourceId;
|
|
1343
|
-
this.maxHistoryBytes = Math.max(0, options.maxHistoryBytes ?? DEFAULT_MAX_HISTORY_BYTES);
|
|
1344
|
-
this.onDocumentChange = options.onDocumentChange;
|
|
1345
|
-
this.onSelectionChange = options.onSelectionChange;
|
|
1346
|
-
this.onCellCommit = options.onCellCommit;
|
|
1347
|
-
if (options.dataSources) {
|
|
1348
|
-
options.dataSources.forEach((ds) => this.binder.register(ds));
|
|
1349
|
-
}
|
|
1350
|
-
this.evaluator = new FormulaEvaluator({
|
|
1351
|
-
resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
|
|
1352
|
-
resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
|
|
1353
|
-
resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),
|
|
1354
|
-
resolveSpill: (colStr, rowNum) => this.resolveSpillByCoord(colStr, rowNum),
|
|
1355
|
-
getFormulaCell: () => this.evaluatingCell
|
|
1356
|
-
});
|
|
1357
|
-
this.syncWithDataSource();
|
|
1358
|
-
this.rederiveTypedValues();
|
|
1359
|
-
this.recalculateAll();
|
|
1360
|
-
}
|
|
1361
|
-
loadDocument(document) {
|
|
1362
|
-
this.document = JSON.parse(JSON.stringify(document));
|
|
1363
|
-
this.activeCell = null;
|
|
1364
|
-
this.selectedRange = null;
|
|
1365
|
-
this.editingCell = null;
|
|
1366
|
-
this.draftValue = "";
|
|
1367
|
-
this.undoStack = [];
|
|
1368
|
-
this.redoStack = [];
|
|
1369
|
-
this.syncWithDataSource();
|
|
1370
|
-
this.rederiveTypedValues();
|
|
1371
|
-
this.recalculateAll();
|
|
1372
|
-
this.notifyChange();
|
|
1373
|
-
}
|
|
1374
|
-
/**
|
|
1375
|
-
* Re-parses the value of every stored typed (non-formula) cell from its
|
|
1376
|
-
* `raw` input, so documents saved before typed input was parsed (whose
|
|
1377
|
-
* values are text such as `"120"`) compare and sort as numbers. Cells of
|
|
1378
|
-
* bound columns in record rows keep their value: it belongs to the data
|
|
1379
|
-
* source.
|
|
1380
|
-
*/
|
|
1381
|
-
rederiveTypedValues() {
|
|
1382
|
-
for (const row of this.document.rows) {
|
|
1383
|
-
const cells = this.document.cells[row.id];
|
|
1384
|
-
if (!cells) continue;
|
|
1385
|
-
for (const col of this.document.columns) {
|
|
1386
|
-
const cell = cells[col.id];
|
|
1387
|
-
if (!cell || typeof cell.raw !== "string" || cell.raw.startsWith("=")) continue;
|
|
1388
|
-
if (col.type === "bound" && col.binding && row.recordId) continue;
|
|
1389
|
-
const typed = this.typedCell(cell.raw, cell.format, col, cell);
|
|
1390
|
-
cell.value = typed.value;
|
|
1391
|
-
if (typed.numberFormat) cell.numberFormat = typed.numberFormat;
|
|
1392
|
-
else delete cell.numberFormat;
|
|
1393
|
-
if (typed.format) cell.format = typed.format;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
/**
|
|
1398
|
-
* Value of typed (non-formula) input: a number, boolean, ISO date, error
|
|
1399
|
-
* or text (see `parseCellInput`), its automatic number format, and the
|
|
1400
|
-
* `CellFormat` to use: the automatic one (currency, percentage, date,
|
|
1401
|
-
* number) when `format` is unset or `text` (the default of new columns).
|
|
1402
|
-
*/
|
|
1403
|
-
typedCell(raw, format, col, existing) {
|
|
1404
|
-
const parsed = parseCellInput(raw);
|
|
1405
|
-
const fixed = this.fixedNumberFormat(col, existing);
|
|
1406
|
-
const numberFormat = fixed ?? parsed.numberFormat;
|
|
1407
|
-
return {
|
|
1408
|
-
value: parsed.value,
|
|
1409
|
-
...numberFormat ? { numberFormat } : {},
|
|
1410
|
-
...existing?.explicitNumberFormat ? { explicitNumberFormat: true } : {},
|
|
1411
|
-
format: fixed === void 0 && parsed.cellFormat && (!format || format === "text") ? parsed.cellFormat : format
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
/**
|
|
1415
|
-
* The number format a cell keeps whatever is typed into it: its explicit
|
|
1416
|
-
* one, else its column's. Undefined lets typed input choose one.
|
|
1417
|
-
*/
|
|
1418
|
-
fixedNumberFormat(col, existing) {
|
|
1419
|
-
if (existing?.explicitNumberFormat && existing.numberFormat !== void 0) return existing.numberFormat;
|
|
1420
|
-
return col?.numberFormat && !isGeneralNumberFormat(col.numberFormat) ? col.numberFormat : void 0;
|
|
1421
|
-
}
|
|
1422
|
-
/** `numberFormat` / `explicitNumberFormat` fields for a formula, bound or spilled cell. */
|
|
1423
|
-
fixedNumberFormatFields(col, existing) {
|
|
1424
|
-
const numberFormat = this.fixedNumberFormat(col, existing);
|
|
1425
|
-
if (numberFormat === void 0) return {};
|
|
1426
|
-
return existing?.explicitNumberFormat ? { numberFormat, explicitNumberFormat: true } : { numberFormat };
|
|
1427
|
-
}
|
|
1428
|
-
/**
|
|
1429
|
-
* Typed input for a bound field, coerced by the field's declared type:
|
|
1430
|
-
* number, boolean and date fields get a number, boolean or ISO date when
|
|
1431
|
-
* the input parses as one; string and enum fields (and unknown fields)
|
|
1432
|
-
* keep the text exactly as typed.
|
|
1433
|
-
*/
|
|
1434
|
-
boundInputValue(dataSourceId, field, raw) {
|
|
1435
|
-
if (raw.startsWith("=")) return raw;
|
|
1436
|
-
const type = this.binder.getDataSource(dataSourceId)?.getFields().find((f) => f.key === field)?.type;
|
|
1437
|
-
if (type !== "number" && type !== "boolean" && type !== "date") return raw;
|
|
1438
|
-
const parsed = parseCellInput(raw);
|
|
1439
|
-
if (type === "number" && parsed.type === "number") return parsed.value;
|
|
1440
|
-
if (type === "boolean" && parsed.type === "boolean") return parsed.value;
|
|
1441
|
-
if (type === "date" && parsed.type === "date") return parsed.value;
|
|
1442
|
-
return raw;
|
|
1443
|
-
}
|
|
1444
|
-
registerDataSource(ds) {
|
|
1445
|
-
this.binder.register(ds);
|
|
1446
|
-
this.syncWithDataSource();
|
|
1447
|
-
this.recalculateAll();
|
|
1448
|
-
}
|
|
1449
|
-
setPrimaryDataSource(dataSourceId) {
|
|
1450
|
-
this.primaryDataSourceId = dataSourceId;
|
|
1451
|
-
this.syncWithDataSource();
|
|
1452
|
-
this.recalculateAll();
|
|
1453
|
-
}
|
|
1454
|
-
syncWithDataSource() {
|
|
1455
|
-
if (!this.primaryDataSourceId) return;
|
|
1456
|
-
this.document.rows = this.binder.syncRows(this.document.rows, this.primaryDataSourceId);
|
|
1457
|
-
this.recomputeRowIndices();
|
|
1458
|
-
}
|
|
1459
|
-
recomputeRowIndices() {
|
|
1460
|
-
this.document.rows.forEach((r, idx) => {
|
|
1461
|
-
r.index = idx;
|
|
1462
|
-
});
|
|
1463
|
-
this.document.columns.forEach((c, idx) => {
|
|
1464
|
-
c.key = indexToColName(idx);
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
getColumn(colIdOrKey) {
|
|
1468
|
-
return this.document.columns.find(
|
|
1469
|
-
(c) => c.id === colIdOrKey || c.key.toUpperCase() === colIdOrKey.toUpperCase()
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
getRow(rowIdOrIndex) {
|
|
1473
|
-
if (typeof rowIdOrIndex === "number") {
|
|
1474
|
-
return this.document.rows[rowIdOrIndex];
|
|
1475
|
-
}
|
|
1476
|
-
return this.document.rows.find((r) => r.id === rowIdOrIndex);
|
|
1477
|
-
}
|
|
1478
|
-
getCell(rowId, colId) {
|
|
1479
|
-
const existing = this.document.cells[rowId]?.[colId];
|
|
1480
|
-
if (existing && existing.raw !== "") return existing;
|
|
1481
|
-
if (this.spillCells.size > 0) {
|
|
1482
|
-
const spilled = this.spillCells.get(spillCellId(rowId, colId));
|
|
1483
|
-
if (spilled) return spilled;
|
|
1484
|
-
}
|
|
1485
|
-
if (existing && !existing.explicitNumberFormat) return existing;
|
|
1486
|
-
const col = this.document.columns.find((c) => c.id === colId);
|
|
1487
|
-
const row = this.document.rows.find((r) => r.id === rowId);
|
|
1488
|
-
const derived = this.getDerivedCell(row, col);
|
|
1489
|
-
if (!existing) return derived;
|
|
1490
|
-
if (derived.raw === "") return existing;
|
|
1491
|
-
return { ...derived, numberFormat: existing.numberFormat, explicitNumberFormat: true };
|
|
1492
|
-
}
|
|
1493
|
-
/** Value of a cell with no stored data: bound record value, column formula, or blank. */
|
|
1494
|
-
getDerivedCell(row, col) {
|
|
1495
|
-
if (col && row && col.type === "bound" && col.binding && row.recordId) {
|
|
1496
|
-
const boundVal = this.binder.resolveBoundValue(
|
|
1497
|
-
col.binding.dataSource,
|
|
1498
|
-
row.recordId,
|
|
1499
|
-
col.binding.field
|
|
1500
|
-
);
|
|
1501
|
-
return {
|
|
1502
|
-
raw: boundVal !== void 0 ? String(boundVal) : "",
|
|
1503
|
-
value: boundVal ?? "",
|
|
1504
|
-
format: col.format,
|
|
1505
|
-
...this.fixedNumberFormatFields(col, void 0)
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
if (col && col.type === "formula" && col.formula) {
|
|
1509
|
-
return {
|
|
1510
|
-
raw: col.formula,
|
|
1511
|
-
value: "",
|
|
1512
|
-
format: col.format,
|
|
1513
|
-
...this.fixedNumberFormatFields(col, void 0)
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
return { raw: "", value: "", format: col?.format, ...this.fixedNumberFormatFields(col, void 0) };
|
|
1517
|
-
}
|
|
1518
|
-
setCellValue(rowId, colId, raw) {
|
|
1519
|
-
this.recordSnapshot();
|
|
1520
|
-
if (!this.document.cells[rowId]) {
|
|
1521
|
-
this.document.cells[rowId] = {};
|
|
1522
|
-
}
|
|
1523
|
-
const col = this.document.columns.find((c) => c.id === colId);
|
|
1524
|
-
const row = this.document.rows.find((r) => r.id === rowId);
|
|
1525
|
-
const isFormula = raw.startsWith("=");
|
|
1526
|
-
const existing = this.document.cells[rowId][colId];
|
|
1527
|
-
let cell;
|
|
1528
|
-
if (col?.type === "bound" && col.binding && row?.recordId) {
|
|
1529
|
-
const value = this.boundInputValue(col.binding.dataSource, col.binding.field, raw);
|
|
1530
|
-
this.binder.commitBoundValue(col.binding.dataSource, row.recordId, col.binding.field, value);
|
|
1531
|
-
cell = { raw, value, format: col.format, ...this.fixedNumberFormatFields(col, existing) };
|
|
1532
|
-
} else if (isFormula) {
|
|
1533
|
-
cell = { raw, value: raw, format: col?.format, ...this.fixedNumberFormatFields(col, existing) };
|
|
1534
|
-
} else {
|
|
1535
|
-
cell = { raw, ...this.typedCell(raw, col?.format, col, existing) };
|
|
1536
|
-
}
|
|
1537
|
-
const cellKey = this.getCellKey(rowId, colId);
|
|
1538
|
-
this.depGraph.setCellFormula(cellKey, isFormula ? raw : null);
|
|
1539
|
-
this.document.cells[rowId][colId] = cell;
|
|
1540
|
-
this.document.updatedAt = Date.now();
|
|
1541
|
-
if (this.batching) {
|
|
1542
|
-
this.pendingCommits.push([rowId, colId, raw]);
|
|
1543
|
-
return;
|
|
1544
|
-
}
|
|
1545
|
-
this.recalculateCell(rowId, colId);
|
|
1546
|
-
this.notifyChange();
|
|
1547
|
-
const evaluatedVal = this.document.cells[rowId][colId]?.value;
|
|
1548
|
-
this.onCellCommit?.(rowId, colId, raw, evaluatedVal);
|
|
1549
|
-
}
|
|
1550
|
-
getCellKey(rowId, colId) {
|
|
1551
|
-
const row = this.document.rows.find((r) => r.id === rowId);
|
|
1552
|
-
const col = this.document.columns.find((c) => c.id === colId);
|
|
1553
|
-
if (!row || !col) return `${colId}_${rowId}`;
|
|
1554
|
-
return `${col.key}${row.index + 1}`.toUpperCase();
|
|
1555
|
-
}
|
|
1556
|
-
/**
|
|
1557
|
-
* Recalculates a cell and everything that depends on it, in dependency
|
|
1558
|
-
* order. Every cell in a reference cycle is marked `#CYCLE!`.
|
|
1559
|
-
*/
|
|
1560
|
-
recalculateCell(rowId, colId) {
|
|
1561
|
-
const cellKey = this.getCellKey(rowId, colId);
|
|
1562
|
-
const anchor = this.spilledBy.get(cellKey);
|
|
1563
|
-
this.recalculateKeys(anchor ? [cellKey, anchor] : [cellKey], true);
|
|
1564
|
-
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Recalculates `keys` (and, with `includeDependents`, their dependents) in
|
|
1567
|
-
* dependency order. Spill ranges are only known after evaluating their
|
|
1568
|
-
* anchors, so when a pass changes which cells hold spilled values (and so
|
|
1569
|
-
* the dependency graph), the dependents of those cells are recalculated in
|
|
1570
|
-
* another pass, until nothing changes.
|
|
1571
|
-
*/
|
|
1572
|
-
recalculateKeys(keys, includeDependents) {
|
|
1573
|
-
let pending = Array.from(keys);
|
|
1574
|
-
let withDependents = includeDependents;
|
|
1575
|
-
for (let pass = 0; pass < MAX_SPILL_PASSES && pending.length > 0; pass++) {
|
|
1576
|
-
const changed = /* @__PURE__ */ new Set();
|
|
1577
|
-
this.spillChanges = changed;
|
|
1578
|
-
try {
|
|
1579
|
-
this.runEvaluationPlan(this.depGraph.getEvaluationPlan(pending, { includeDependents: withDependents }));
|
|
1580
|
-
} finally {
|
|
1581
|
-
this.spillChanges = null;
|
|
1582
|
-
}
|
|
1583
|
-
pending = Array.from(changed);
|
|
1584
|
-
withDependents = true;
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
/**
|
|
1588
|
-
* Rebuilds the dependency graph from scratch and recalculates every formula
|
|
1589
|
-
* in dependency order.
|
|
1590
|
-
*/
|
|
1591
|
-
recalculateAll() {
|
|
1592
|
-
this.depGraph.clear();
|
|
1593
|
-
this.spills.clear();
|
|
1594
|
-
this.spilledBy.clear();
|
|
1595
|
-
this.spillCells.clear();
|
|
1596
|
-
const formulaKeys = [];
|
|
1597
|
-
for (const row of this.document.rows) {
|
|
1598
|
-
for (const col of this.document.columns) {
|
|
1599
|
-
const cell = this.document.cells[row.id]?.[col.id] ?? this.getDerivedCell(row, col);
|
|
1600
|
-
if (cell.raw.startsWith("=")) {
|
|
1601
|
-
const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
|
|
1602
|
-
this.depGraph.setCellFormula(cellKey, cell.raw);
|
|
1603
|
-
formulaKeys.push(cellKey);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
this.recalculateKeys(formulaKeys, false);
|
|
1608
|
-
}
|
|
1609
|
-
runEvaluationPlan(plan) {
|
|
1610
|
-
for (const key of plan.order) {
|
|
1611
|
-
if (plan.cyclic.has(key)) {
|
|
1612
|
-
this.markCycle(key);
|
|
1613
|
-
} else {
|
|
1614
|
-
this.evalSingleCellByKey(key);
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
locateCellKey(cellKey) {
|
|
1619
|
-
const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
|
|
1620
|
-
if (!match) return null;
|
|
1621
|
-
const col = this.document.columns.find((c) => c.key.toUpperCase() === match[1].toUpperCase());
|
|
1622
|
-
const row = this.document.rows[parseInt(match[2], 10) - 1];
|
|
1623
|
-
return col && row ? { row, col } : null;
|
|
1624
|
-
}
|
|
1625
|
-
markCycle(cellKey) {
|
|
1626
|
-
const located = this.locateCellKey(cellKey);
|
|
1627
|
-
if (!located) return;
|
|
1628
|
-
const { row, col } = located;
|
|
1629
|
-
const cell = this.getCell(row.id, col.id);
|
|
1630
|
-
if (!cell.raw.startsWith("=")) return;
|
|
1631
|
-
if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
|
|
1632
|
-
const deps = this.depGraph.getDependencies(cellKey);
|
|
1633
|
-
const selfReference = deps.includes(cellKey) || deps.includes(`${col.key.toUpperCase()}:`);
|
|
1634
|
-
const spilledKeys = this.depGraph.getSpilledKeys(cellKey);
|
|
1635
|
-
const ownSpill = deps.find((dep) => spilledKeys.includes(dep));
|
|
1636
|
-
const errorMessage = selfReference ? "This formula can't reference its own cell." : ownSpill ? `This formula refers to ${ownSpill}, which holds its own spilled results (a circular reference).` : "This formula is part of a circular reference: it depends on its own result through other cells.";
|
|
1637
|
-
this.clearSpill(cellKey.toUpperCase(), true);
|
|
1638
|
-
this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!", errorMessage };
|
|
1639
|
-
}
|
|
1640
|
-
evalSingleCellByKey(cellKey) {
|
|
1641
|
-
const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
|
|
1642
|
-
if (!match) return;
|
|
1643
|
-
const colName = match[1].toUpperCase();
|
|
1644
|
-
const rowNum = parseInt(match[2], 10);
|
|
1645
|
-
const col = this.document.columns.find((c) => c.key.toUpperCase() === colName);
|
|
1646
|
-
const row = this.document.rows[rowNum - 1];
|
|
1647
|
-
if (!col || !row) return;
|
|
1648
|
-
const cell = this.getCell(row.id, col.id);
|
|
1649
|
-
if (!cell.raw.startsWith("=")) {
|
|
1650
|
-
if (cell.error === "#CYCLE!") {
|
|
1651
|
-
cell.error = void 0;
|
|
1652
|
-
cell.errorMessage = void 0;
|
|
1653
|
-
}
|
|
1654
|
-
this.clearSpill(`${colName}${rowNum}`);
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
try {
|
|
1658
|
-
this.evaluatingRowIndex = rowNum - 1;
|
|
1659
|
-
this.evaluatingCell = { col: colName, row: rowNum };
|
|
1660
|
-
const result = this.evaluator.evaluateWithDiagnostics(cell.raw);
|
|
1661
|
-
if (!this.document.cells[row.id]) {
|
|
1662
|
-
this.document.cells[row.id] = {};
|
|
1663
|
-
}
|
|
1664
|
-
let { value, error, message } = result;
|
|
1665
|
-
if (result.array) {
|
|
1666
|
-
const blocked = this.placeSpill(`${colName}${rowNum}`, rowNum - 1, this.document.columns.indexOf(col), result.array);
|
|
1667
|
-
if (blocked) {
|
|
1668
|
-
value = "#SPILL!";
|
|
1669
|
-
error = "#SPILL!";
|
|
1670
|
-
message = blocked;
|
|
1671
|
-
}
|
|
1672
|
-
} else {
|
|
1673
|
-
this.clearSpill(`${colName}${rowNum}`);
|
|
1674
|
-
}
|
|
1675
|
-
this.document.cells[row.id][col.id] = {
|
|
1676
|
-
...cell,
|
|
1677
|
-
value,
|
|
1678
|
-
// `#ERROR!` (malformed formula, wrong argument count) keeps its
|
|
1679
|
-
// message in `error`, as before; other errors store their code.
|
|
1680
|
-
error: error === "#ERROR!" ? message : error,
|
|
1681
|
-
errorMessage: message
|
|
1682
|
-
};
|
|
1683
|
-
} finally {
|
|
1684
|
-
this.evaluatingRowIndex = null;
|
|
1685
|
-
this.evaluatingCell = null;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
// --- Dynamic arrays (spilling) ---
|
|
1689
|
-
/**
|
|
1690
|
-
* Spills `array` from the anchor at (row, col) into the cells below and to
|
|
1691
|
-
* the right. Returns why it can't (the anchor then shows `#SPILL!`), or
|
|
1692
|
-
* null once placed. The grid never grows to fit an array, as in Excel.
|
|
1693
|
-
*/
|
|
1694
|
-
placeSpill(anchor, row, col, array) {
|
|
1695
|
-
const { rows, cols } = array;
|
|
1696
|
-
const area = { startRow: row, startCol: col, endRow: row + rows - 1, endCol: col + cols - 1 };
|
|
1697
|
-
const label = `${indexToColName(area.startCol)}${area.startRow + 1}:${indexToColName(area.endCol)}${area.endRow + 1}`;
|
|
1698
|
-
let reason = null;
|
|
1699
|
-
const blockers = [];
|
|
1700
|
-
if (area.endRow >= this.document.rows.length || area.endCol >= this.document.columns.length) {
|
|
1701
|
-
reason = `The ${rows}\xD7${cols} result needs ${label}, which extends past the edge of the sheet.`;
|
|
1702
|
-
} else if (this.getMerges().some((m) => rangesIntersect(m, area))) {
|
|
1703
|
-
reason = `The result can't spill into merged cells (it needs ${label}).`;
|
|
1704
|
-
} else {
|
|
1705
|
-
for (let c = area.startCol; c <= area.endCol; c++) {
|
|
1706
|
-
const column = this.document.columns[c];
|
|
1707
|
-
for (let r = area.startRow; r <= area.endRow; r++) {
|
|
1708
|
-
if (r === row && c === col) continue;
|
|
1709
|
-
const rowObj = this.document.rows[r];
|
|
1710
|
-
const key = `${column.key}${r + 1}`.toUpperCase();
|
|
1711
|
-
const stored = this.document.cells[rowObj.id]?.[column.id];
|
|
1712
|
-
const raw = stored ? stored.raw : this.getDerivedCell(rowObj, column).raw;
|
|
1713
|
-
const owner = this.spilledBy.get(key);
|
|
1714
|
-
if (raw !== "") {
|
|
1715
|
-
blockers.push(key);
|
|
1716
|
-
reason ??= `The result needs ${label}, but ${key} isn't empty.`;
|
|
1717
|
-
} else if (owner && owner !== anchor) {
|
|
1718
|
-
blockers.push(key);
|
|
1719
|
-
reason ??= `The result needs ${label}, but ${key} already holds values spilled from ${owner}.`;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
if (reason) {
|
|
1725
|
-
this.clearSpill(anchor);
|
|
1726
|
-
this.depGraph.setSpillEdges(anchor, [], blockers);
|
|
1727
|
-
return reason;
|
|
1728
|
-
}
|
|
1729
|
-
const previous = this.spills.get(anchor);
|
|
1730
|
-
const cells = [];
|
|
1731
|
-
for (let c = 0; c < cols; c++) {
|
|
1732
|
-
const column = this.document.columns[col + c];
|
|
1733
|
-
for (let r = 0; r < rows; r++) {
|
|
1734
|
-
if (r === 0 && c === 0) continue;
|
|
1735
|
-
const rowObj = this.document.rows[row + r];
|
|
1736
|
-
const key = `${column.key}${row + r + 1}`.toUpperCase();
|
|
1737
|
-
const value = array.values[c * rows + r] ?? "";
|
|
1738
|
-
const display = {
|
|
1739
|
-
raw: "",
|
|
1740
|
-
value,
|
|
1741
|
-
format: column.format,
|
|
1742
|
-
spillAnchor: anchor,
|
|
1743
|
-
...this.fixedNumberFormatFields(column, this.document.cells[rowObj.id]?.[column.id])
|
|
1744
|
-
};
|
|
1745
|
-
if (isFormulaError(value)) {
|
|
1746
|
-
display.error = value;
|
|
1747
|
-
display.errorMessage = describeFormulaError(value);
|
|
1748
|
-
}
|
|
1749
|
-
this.spillCells.set(spillCellId(rowObj.id, column.id), display);
|
|
1750
|
-
if (this.spilledBy.get(key) !== anchor) {
|
|
1751
|
-
this.spilledBy.set(key, anchor);
|
|
1752
|
-
this.spillChanges?.add(key);
|
|
1753
|
-
}
|
|
1754
|
-
cells.push({ key, rowId: rowObj.id, colId: column.id });
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
if (previous) {
|
|
1758
|
-
const kept = new Set(cells.map((cell) => cell.key));
|
|
1759
|
-
for (const old of previous.cells) {
|
|
1760
|
-
if (kept.has(old.key)) continue;
|
|
1761
|
-
this.forgetSpilledCell(anchor, old);
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
this.spills.set(anchor, { anchor, row, col, rows, cols, values: array.values, cells });
|
|
1765
|
-
this.depGraph.setSpillEdges(anchor, cells.map((cell) => cell.key));
|
|
1766
|
-
return null;
|
|
1767
|
-
}
|
|
1768
|
-
/** Removes the array spilled from `anchor`. With `keepEdges`, its dependency edges stay. */
|
|
1769
|
-
clearSpill(anchor, keepEdges = false) {
|
|
1770
|
-
const record = this.spills.get(anchor);
|
|
1771
|
-
if (record) {
|
|
1772
|
-
for (const cell of record.cells) this.forgetSpilledCell(anchor, cell);
|
|
1773
|
-
this.spills.delete(anchor);
|
|
1774
|
-
}
|
|
1775
|
-
if (!keepEdges) this.depGraph.setSpillEdges(anchor, []);
|
|
1776
|
-
}
|
|
1777
|
-
forgetSpilledCell(anchor, cell) {
|
|
1778
|
-
if (this.spilledBy.get(cell.key) !== anchor) return;
|
|
1779
|
-
this.spilledBy.delete(cell.key);
|
|
1780
|
-
this.spillCells.delete(spillCellId(cell.rowId, cell.colId));
|
|
1781
|
-
this.spillChanges?.add(cell.key);
|
|
1782
|
-
}
|
|
1783
|
-
resolveSpillByCoord(colStr, rowNum) {
|
|
1784
|
-
const record = this.spills.get(`${colStr}${rowNum}`.toUpperCase());
|
|
1785
|
-
if (record) return { rows: record.rows, cols: record.cols, values: record.values };
|
|
1786
|
-
const value = this.resolveCellByCoord(colStr, rowNum);
|
|
1787
|
-
return isFormulaError(value) ? value : "#REF!";
|
|
1788
|
-
}
|
|
1789
|
-
toSpill(record) {
|
|
1790
|
-
const row = this.document.rows[record.row];
|
|
1791
|
-
const col = this.document.columns[record.col];
|
|
1792
|
-
return {
|
|
1793
|
-
key: record.anchor,
|
|
1794
|
-
anchor: { row: record.row, col: record.col },
|
|
1795
|
-
range: {
|
|
1796
|
-
startRow: record.row,
|
|
1797
|
-
startCol: record.col,
|
|
1798
|
-
endRow: record.row + record.rows - 1,
|
|
1799
|
-
endCol: record.col + record.cols - 1
|
|
1800
|
-
},
|
|
1801
|
-
formula: row && col ? this.getCell(row.id, col.id).raw : ""
|
|
1802
|
-
};
|
|
1803
|
-
}
|
|
1804
|
-
/**
|
|
1805
|
-
* The dynamic array covering (row, col), whether it is the anchor (the cell
|
|
1806
|
-
* holding the formula) or a spilled cell; null when there is none.
|
|
1807
|
-
*/
|
|
1808
|
-
getSpillAt(row, col) {
|
|
1809
|
-
if (this.spills.size === 0) return null;
|
|
1810
|
-
const key = `${indexToColName(col)}${row + 1}`;
|
|
1811
|
-
const record = this.spills.get(key) ?? this.spills.get(this.spilledBy.get(key) ?? "");
|
|
1812
|
-
return record ? this.toSpill(record) : null;
|
|
1813
|
-
}
|
|
1814
|
-
/** Every dynamic array currently spilled. */
|
|
1815
|
-
getSpills() {
|
|
1816
|
-
return Array.from(this.spills.values(), (record) => this.toSpill(record));
|
|
1817
|
-
}
|
|
1818
|
-
/** Whether `range` overlaps a spilled array (its anchor or spilled cells). */
|
|
1819
|
-
intersectsSpill(range) {
|
|
1820
|
-
for (const record of this.spills.values()) {
|
|
1821
|
-
const area = {
|
|
1822
|
-
startRow: record.row,
|
|
1823
|
-
startCol: record.col,
|
|
1824
|
-
endRow: record.row + record.rows - 1,
|
|
1825
|
-
endCol: record.col + record.cols - 1
|
|
1826
|
-
};
|
|
1827
|
-
if (rangesIntersect(area, range)) return true;
|
|
1828
|
-
}
|
|
1829
|
-
return false;
|
|
1830
|
-
}
|
|
1831
|
-
resolveCellByCoord(colStr, rowNum) {
|
|
1832
|
-
const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
|
|
1833
|
-
const row = this.document.rows[rowNum - 1];
|
|
1834
|
-
if (!col || !row) return 0;
|
|
1835
|
-
return this.getCell(row.id, col.id).value ?? "";
|
|
1836
|
-
}
|
|
1837
|
-
resolveRangeByCoords(sCol, sRow, eCol, eRow) {
|
|
1838
|
-
const startC = colNameToIndex(sCol);
|
|
1839
|
-
const endC = colNameToIndex(eCol);
|
|
1840
|
-
const minC = Math.min(startC, endC);
|
|
1841
|
-
const maxC = Math.max(startC, endC);
|
|
1842
|
-
const minR = Math.min(sRow, eRow);
|
|
1843
|
-
const maxR = Math.min(Math.max(sRow, eRow), WHOLE_COLUMN_END_ROW, this.document.rows.length);
|
|
1844
|
-
const results = [];
|
|
1845
|
-
for (let c = minC; c <= maxC; c++) {
|
|
1846
|
-
const colName = indexToColName(c);
|
|
1847
|
-
const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);
|
|
1848
|
-
if (!col) continue;
|
|
1849
|
-
for (let r = minR; r <= maxR; r++) {
|
|
1850
|
-
const row = this.document.rows[r - 1];
|
|
1851
|
-
if (!row) continue;
|
|
1852
|
-
results.push(this.getCell(row.id, col.id).value ?? "");
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
return results;
|
|
1856
|
-
}
|
|
1857
|
-
resolveCurrentRowProperty(prop) {
|
|
1858
|
-
const rowIndex = this.evaluatingRowIndex ?? this.activeCell?.row;
|
|
1859
|
-
if (rowIndex === void 0 || rowIndex === null) return 0;
|
|
1860
|
-
const row = this.document.rows[rowIndex];
|
|
1861
|
-
if (!row || !row.recordId || !this.primaryDataSourceId) return 0;
|
|
1862
|
-
return this.binder.resolveBoundValue(this.primaryDataSourceId, row.recordId, prop) ?? 0;
|
|
1863
|
-
}
|
|
1864
|
-
// --- Selection and Navigation ---
|
|
1865
|
-
selectCell(row, col, extendSelection = false) {
|
|
1866
|
-
const clampedRow = Math.max(0, Math.min(row, this.document.rows.length - 1));
|
|
1867
|
-
const clampedCol = Math.max(0, Math.min(col, this.document.columns.length - 1));
|
|
1868
|
-
const merge = extendSelection ? null : this.getMergeAt(clampedRow, clampedCol);
|
|
1869
|
-
const activeRow = merge ? merge.startRow : clampedRow;
|
|
1870
|
-
const activeCol = merge ? merge.startCol : clampedCol;
|
|
1871
|
-
const rowObj = this.document.rows[activeRow];
|
|
1872
|
-
const colObj = this.document.columns[activeCol];
|
|
1873
|
-
this.activeCell = {
|
|
1874
|
-
row: activeRow,
|
|
1875
|
-
col: activeCol,
|
|
1876
|
-
rowId: rowObj?.id,
|
|
1877
|
-
colId: colObj?.id
|
|
1878
|
-
};
|
|
1879
|
-
if (extendSelection && this.selectedRange) {
|
|
1880
|
-
this.selectedRange = this.expandRangeToMerges({
|
|
1881
|
-
startRow: this.selectedRange.startRow,
|
|
1882
|
-
startCol: this.selectedRange.startCol,
|
|
1883
|
-
endRow: clampedRow,
|
|
1884
|
-
endCol: clampedCol
|
|
1885
|
-
});
|
|
1886
|
-
} else if (merge) {
|
|
1887
|
-
this.selectedRange = { ...merge };
|
|
1888
|
-
} else {
|
|
1889
|
-
this.selectedRange = {
|
|
1890
|
-
startRow: clampedRow,
|
|
1891
|
-
startCol: clampedCol,
|
|
1892
|
-
endRow: clampedRow,
|
|
1893
|
-
endCol: clampedCol
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
this.onSelectionChange?.(this.activeCell, this.selectedRange);
|
|
1897
|
-
}
|
|
1898
|
-
selectRange(range) {
|
|
1899
|
-
this.selectedRange = range;
|
|
1900
|
-
this.onSelectionChange?.(this.activeCell, this.selectedRange);
|
|
1901
|
-
}
|
|
1902
|
-
startEditing(row, col, initialValue) {
|
|
1903
|
-
this.selectCell(row ?? this.activeCell?.row ?? 0, col ?? this.activeCell?.col ?? 0);
|
|
1904
|
-
const targetRow = this.activeCell?.row ?? 0;
|
|
1905
|
-
const targetCol = this.activeCell?.col ?? 0;
|
|
1906
|
-
const rowObj = this.document.rows[targetRow];
|
|
1907
|
-
const colObj = this.document.columns[targetCol];
|
|
1908
|
-
if (!rowObj || !colObj) return;
|
|
1909
|
-
this.editingCell = {
|
|
1910
|
-
row: targetRow,
|
|
1911
|
-
col: targetCol,
|
|
1912
|
-
rowId: rowObj.id,
|
|
1913
|
-
colId: colObj.id
|
|
1914
|
-
};
|
|
1915
|
-
const cell = this.getCell(rowObj.id, colObj.id);
|
|
1916
|
-
this.draftValue = initialValue !== void 0 ? initialValue : cell.raw;
|
|
1917
|
-
}
|
|
1918
|
-
commitEditing(valueToCommit) {
|
|
1919
|
-
if (!this.editingCell) return;
|
|
1920
|
-
const val = valueToCommit !== void 0 ? valueToCommit : this.draftValue;
|
|
1921
|
-
if (this.editingCell.rowId && this.editingCell.colId) {
|
|
1922
|
-
this.setCellValue(this.editingCell.rowId, this.editingCell.colId, val);
|
|
1923
|
-
}
|
|
1924
|
-
this.editingCell = null;
|
|
1925
|
-
this.draftValue = "";
|
|
1926
|
-
}
|
|
1927
|
-
cancelEditing() {
|
|
1928
|
-
this.editingCell = null;
|
|
1929
|
-
this.draftValue = "";
|
|
1930
|
-
}
|
|
1931
|
-
moveCursor(direction, extendSelection = false) {
|
|
1932
|
-
if (!this.activeCell) {
|
|
1933
|
-
this.selectCell(0, 0);
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
const { rowOrder, columnOrder } = this.getViewModel();
|
|
1937
|
-
let nextRow = this.activeCell.row;
|
|
1938
|
-
let nextCol = this.activeCell.col;
|
|
1939
|
-
const merge = extendSelection ? null : this.getMergeAt(nextRow, nextCol);
|
|
1940
|
-
switch (direction) {
|
|
1941
|
-
case "up":
|
|
1942
|
-
nextRow = stepInOrder(rowOrder, merge ? merge.startRow : nextRow, -1);
|
|
1943
|
-
break;
|
|
1944
|
-
case "down":
|
|
1945
|
-
nextRow = stepInOrder(rowOrder, merge ? merge.endRow : nextRow, 1);
|
|
1946
|
-
break;
|
|
1947
|
-
case "left":
|
|
1948
|
-
nextCol = stepInOrder(columnOrder, merge ? merge.startCol : nextCol, -1);
|
|
1949
|
-
break;
|
|
1950
|
-
case "right":
|
|
1951
|
-
nextCol = stepInOrder(columnOrder, merge ? merge.endCol : nextCol, 1);
|
|
1952
|
-
break;
|
|
1953
|
-
}
|
|
1954
|
-
this.selectCell(nextRow, nextCol, extendSelection);
|
|
1955
|
-
}
|
|
1956
|
-
// --- Grid Structure Mutations ---
|
|
1957
|
-
insertColumn(index, config) {
|
|
1958
|
-
this.recordSnapshot();
|
|
1959
|
-
const newId = config?.id || this.generateId("col");
|
|
1960
|
-
const newCol = {
|
|
1961
|
-
id: newId,
|
|
1962
|
-
key: indexToColName(index),
|
|
1963
|
-
title: config?.title || indexToColName(index),
|
|
1964
|
-
width: config?.width || 130,
|
|
1965
|
-
type: config?.type || "freeform",
|
|
1966
|
-
binding: config?.binding,
|
|
1967
|
-
formula: config?.formula,
|
|
1968
|
-
format: config?.format || "text",
|
|
1969
|
-
...config?.numberFormat ? { numberFormat: config.numberFormat } : {},
|
|
1970
|
-
readOnly: config?.readOnly
|
|
1971
|
-
};
|
|
1972
|
-
this.shiftReferences({ axis: "col", index, type: "insert" });
|
|
1973
|
-
this.adjustTableForStructuralChange({ axis: "col", index, type: "insert" });
|
|
1974
|
-
this.document.columns.splice(index, 0, newCol);
|
|
1975
|
-
this.recomputeRowIndices();
|
|
1976
|
-
this.recalculateAll();
|
|
1977
|
-
this.notifyChange();
|
|
1978
|
-
}
|
|
1979
|
-
deleteColumn(colId) {
|
|
1980
|
-
this.recordSnapshot();
|
|
1981
|
-
const colIndex = this.document.columns.findIndex((c) => c.id === colId);
|
|
1982
|
-
if (colIndex === -1) return;
|
|
1983
|
-
this.shiftReferences({ axis: "col", index: colIndex, type: "delete" });
|
|
1984
|
-
this.adjustTableForStructuralChange({ axis: "col", index: colIndex, type: "delete" });
|
|
1985
|
-
this.document.columns = this.document.columns.filter((c) => c.id !== colId);
|
|
1986
|
-
for (const rowId of Object.keys(this.document.cells)) {
|
|
1987
|
-
delete this.document.cells[rowId][colId];
|
|
1988
|
-
}
|
|
1989
|
-
this.recomputeRowIndices();
|
|
1990
|
-
this.recalculateAll();
|
|
1991
|
-
this.notifyChange();
|
|
1992
|
-
}
|
|
1993
|
-
insertRow(index, config) {
|
|
1994
|
-
this.recordSnapshot();
|
|
1995
|
-
const newId = config?.id || this.generateId("row");
|
|
1996
|
-
const newRow = {
|
|
1997
|
-
id: newId,
|
|
1998
|
-
index,
|
|
1999
|
-
height: config?.height || 32,
|
|
2000
|
-
recordId: config?.recordId,
|
|
2001
|
-
type: config?.type || "freeform"
|
|
2002
|
-
};
|
|
2003
|
-
this.shiftReferences({ axis: "row", index, type: "insert" });
|
|
2004
|
-
this.adjustTableForStructuralChange({ axis: "row", index, type: "insert" });
|
|
2005
|
-
this.document.rows.splice(index, 0, newRow);
|
|
2006
|
-
this.recomputeRowIndices();
|
|
2007
|
-
this.recalculateAll();
|
|
2008
|
-
this.notifyChange();
|
|
2009
|
-
}
|
|
2010
|
-
deleteRow(rowId) {
|
|
2011
|
-
this.recordSnapshot();
|
|
2012
|
-
const rowIndex = this.document.rows.findIndex((r) => r.id === rowId);
|
|
2013
|
-
if (rowIndex === -1) return;
|
|
2014
|
-
this.shiftReferences({ axis: "row", index: rowIndex, type: "delete" });
|
|
2015
|
-
this.adjustTableForStructuralChange({ axis: "row", index: rowIndex, type: "delete" });
|
|
2016
|
-
this.document.rows = this.document.rows.filter((r) => r.id !== rowId);
|
|
2017
|
-
delete this.document.cells[rowId];
|
|
2018
|
-
this.recomputeRowIndices();
|
|
2019
|
-
this.recalculateAll();
|
|
2020
|
-
this.notifyChange();
|
|
2021
|
-
}
|
|
2022
|
-
generateId(prefix) {
|
|
2023
|
-
return `${prefix}_${Date.now()}_${(nextIdSequence++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
2024
|
-
}
|
|
2025
|
-
/**
|
|
2026
|
-
* Rewrites references in every cell formula (and, for column changes, every
|
|
2027
|
-
* column formula) so they keep pointing at the same cells.
|
|
2028
|
-
*/
|
|
2029
|
-
shiftReferences(change) {
|
|
2030
|
-
for (const rowCells of Object.values(this.document.cells)) {
|
|
2031
|
-
for (const cell of Object.values(rowCells)) {
|
|
2032
|
-
if (cell.raw?.startsWith("=")) {
|
|
2033
|
-
cell.raw = shiftFormulaReferences(cell.raw, change);
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
if (change.axis === "col") {
|
|
2038
|
-
for (const col of this.document.columns) {
|
|
2039
|
-
if (col.formula?.startsWith("=")) {
|
|
2040
|
-
col.formula = shiftFormulaReferences(col.formula, change);
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
setColumnWidth(colId, width) {
|
|
2046
|
-
const col = this.document.columns.find((c) => c.id === colId);
|
|
2047
|
-
if (col) {
|
|
2048
|
-
col.width = Math.max(30, width);
|
|
2049
|
-
this.notifyChange();
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
autoFitColumnWidth(colId, customWidth) {
|
|
2053
|
-
if (customWidth !== void 0) {
|
|
2054
|
-
this.setColumnWidth(colId, customWidth);
|
|
2055
|
-
return;
|
|
2056
|
-
}
|
|
2057
|
-
const col = this.document.columns.find((c) => c.id === colId);
|
|
2058
|
-
if (!col) return;
|
|
2059
|
-
let maxLen = (col.title || "").length + (col.key ? col.key.length + 3 : 0);
|
|
2060
|
-
for (const row of this.document.rows) {
|
|
2061
|
-
const cell = this.getCell(row.id, col.id);
|
|
2062
|
-
const valStr = formatCellDisplay(cell);
|
|
2063
|
-
if (valStr.length > maxLen) {
|
|
2064
|
-
maxLen = valStr.length;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
const estimatedWidth = Math.min(600, Math.max(50, Math.ceil(maxLen * 8.5 + 32)));
|
|
2068
|
-
this.setColumnWidth(colId, estimatedWidth);
|
|
2069
|
-
}
|
|
2070
|
-
// --- Clipboard TSV/CSV ---
|
|
2071
|
-
/**
|
|
2072
|
-
* Exports cell values as TSV: the whole sheet, or only `range` (e.g. the
|
|
2073
|
-
* current selection) when given.
|
|
2074
|
-
*/
|
|
2075
|
-
exportToTsv(range) {
|
|
2076
|
-
const rows = range ? this.document.rows.slice(
|
|
2077
|
-
Math.min(range.startRow, range.endRow),
|
|
2078
|
-
Math.max(range.startRow, range.endRow) + 1
|
|
2079
|
-
) : this.document.rows;
|
|
2080
|
-
const columns = range ? this.document.columns.slice(
|
|
2081
|
-
Math.min(range.startCol, range.endCol),
|
|
2082
|
-
Math.max(range.startCol, range.endCol) + 1
|
|
2083
|
-
) : this.document.columns;
|
|
2084
|
-
return rows.map(
|
|
2085
|
-
(row) => columns.map((col) => {
|
|
2086
|
-
return formatCellDisplay(this.getCell(row.id, col.id)).replace(/\t/g, " ");
|
|
2087
|
-
}).join(" ")
|
|
2088
|
-
).join("\n");
|
|
2089
|
-
}
|
|
2090
|
-
/**
|
|
2091
|
-
* Pastes tab-separated values starting at (startRow, startCol), adding rows
|
|
2092
|
-
* and columns as needed. The whole paste is a single undo step.
|
|
2093
|
-
*/
|
|
2094
|
-
importFromTsv(tsvData, startRow = 0, startCol = 0) {
|
|
2095
|
-
const normalized = tsvData.replace(/\r\n?/g, "\n").replace(/\n$/, "");
|
|
2096
|
-
if (normalized === "") return;
|
|
2097
|
-
this.writeMatrix(normalized.split("\n").map((line) => line.split(" ")), startRow, startCol);
|
|
2098
|
-
}
|
|
2099
|
-
/** Adds rows/columns at the end until (rowIndex, colIndex) exists. */
|
|
2100
|
-
growGrid(rowIndex, colIndex) {
|
|
2101
|
-
while (rowIndex >= this.document.rows.length) {
|
|
2102
|
-
const index = this.document.rows.length;
|
|
2103
|
-
this.document.rows.push({ id: this.generateId("row"), index, height: 32, type: "freeform" });
|
|
2104
|
-
}
|
|
2105
|
-
while (colIndex >= this.document.columns.length) {
|
|
2106
|
-
const key = indexToColName(this.document.columns.length);
|
|
2107
|
-
this.document.columns.push({
|
|
2108
|
-
id: this.generateId("col"),
|
|
2109
|
-
key,
|
|
2110
|
-
title: key,
|
|
2111
|
-
width: 130,
|
|
2112
|
-
type: "freeform",
|
|
2113
|
-
format: "text"
|
|
2114
|
-
});
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
/**
|
|
2118
|
-
* Writes raw inputs starting at (startRow, startCol), growing the grid as
|
|
2119
|
-
* needed, as a single undo step with one recalculation.
|
|
2120
|
-
*/
|
|
2121
|
-
writeMatrix(matrix, startRow, startCol) {
|
|
2122
|
-
if (matrix.length === 0) return;
|
|
2123
|
-
this.runBatch(() => {
|
|
2124
|
-
matrix.forEach((line, rIdx) => {
|
|
2125
|
-
line.forEach((val, cIdx) => {
|
|
2126
|
-
this.growGrid(startRow + rIdx, startCol + cIdx);
|
|
2127
|
-
const row = this.document.rows[startRow + rIdx];
|
|
2128
|
-
this.setCellValue(row.id, this.document.columns[startCol + cIdx].id, val);
|
|
2129
|
-
});
|
|
2130
|
-
});
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
/**
|
|
2134
|
-
* Runs `fn` as one undo step: cell writes inside it are batched and
|
|
2135
|
-
* recalculated once, then listeners are notified.
|
|
2136
|
-
*/
|
|
2137
|
-
runBatch(fn) {
|
|
2138
|
-
this.recordSnapshot();
|
|
2139
|
-
this.batching = true;
|
|
2140
|
-
this.pendingCommits = [];
|
|
2141
|
-
try {
|
|
2142
|
-
fn();
|
|
2143
|
-
} finally {
|
|
2144
|
-
this.batching = false;
|
|
2145
|
-
}
|
|
2146
|
-
this.recomputeRowIndices();
|
|
2147
|
-
this.recalculateAll();
|
|
2148
|
-
this.document.updatedAt = Date.now();
|
|
2149
|
-
this.notifyChange();
|
|
2150
|
-
const commits = this.pendingCommits;
|
|
2151
|
-
this.pendingCommits = [];
|
|
2152
|
-
for (const [rowId, colId, raw] of commits) {
|
|
2153
|
-
this.onCellCommit?.(rowId, colId, raw, this.document.cells[rowId]?.[colId]?.value);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
// --- CSV ---
|
|
2157
|
-
/**
|
|
2158
|
-
* Exports the sheet (or `range`) as RFC 4180 CSV. Values are exported as
|
|
2159
|
-
* displayed unless `formulas` is set.
|
|
2160
|
-
*/
|
|
2161
|
-
exportToCsv(options = {}) {
|
|
2162
|
-
const matrix = this.readMatrix(options.range ?? null).map(
|
|
2163
|
-
(row) => row.map((cell) => options.formulas ? cell.raw : formatCellDisplay(cell))
|
|
2164
|
-
);
|
|
2165
|
-
return serializeCsv(matrix, {
|
|
2166
|
-
delimiter: options.delimiter,
|
|
2167
|
-
lineEnding: options.lineEnding,
|
|
2168
|
-
bom: options.bom
|
|
2169
|
-
});
|
|
2170
|
-
}
|
|
2171
|
-
/**
|
|
2172
|
-
* Imports CSV text at (startRow, startCol) (default A1), growing the grid
|
|
2173
|
-
* as needed. The whole import is a single undo step.
|
|
2174
|
-
*/
|
|
2175
|
-
importFromCsv(csv, options = {}) {
|
|
2176
|
-
const matrix = parseCsv(csv, { delimiter: options.delimiter });
|
|
2177
|
-
this.writeMatrix(matrix, options.startRow ?? 0, options.startCol ?? 0);
|
|
2178
|
-
}
|
|
2179
|
-
/**
|
|
2180
|
-
* Writes already-parsed rows of raw inputs (e.g. from an import adapter)
|
|
2181
|
-
* starting at (startRow, startCol), as a single undo step.
|
|
2182
|
-
*/
|
|
2183
|
-
importRows(rows, options = {}) {
|
|
2184
|
-
this.writeMatrix(rows, options.startRow ?? 0, options.startCol ?? 0);
|
|
2185
|
-
}
|
|
2186
|
-
/** The cells of `range` (or the whole sheet), row by row. */
|
|
2187
|
-
readMatrix(range) {
|
|
2188
|
-
const r = range ? normalizeRange(range) : null;
|
|
2189
|
-
const rows = r ? this.document.rows.slice(r.startRow, r.endRow + 1) : this.document.rows;
|
|
2190
|
-
const columns = r ? this.document.columns.slice(r.startCol, r.endCol + 1) : this.document.columns;
|
|
2191
|
-
return rows.map((row) => columns.map((col) => this.getCell(row.id, col.id)));
|
|
2192
|
-
}
|
|
2193
|
-
// --- Spreadsheet clipboard ---
|
|
2194
|
-
/**
|
|
2195
|
-
* Builds clipboard data for `range` (default: the selection) as TSV,
|
|
2196
|
-
* an HTML table and the lossless internal JSON format. Write every entry
|
|
2197
|
-
* with `clipboardData.setData(type, data)` in a `copy` event.
|
|
2198
|
-
*/
|
|
2199
|
-
copyToClipboardData(range = this.selectedRange) {
|
|
2200
|
-
const r = range ? normalizeRange(range) : { startRow: 0, startCol: 0, endRow: this.document.rows.length - 1, endCol: this.document.columns.length - 1 };
|
|
2201
|
-
const cells = this.readMatrix(r).map(
|
|
2202
|
-
(row) => row.map((cell) => {
|
|
2203
|
-
const value = cell.error || cell.value;
|
|
2204
|
-
const text = formatCellDisplay(cell);
|
|
2205
|
-
if (cell.spillAnchor && !this.spillAnchorInRange(cell.spillAnchor, r)) {
|
|
2206
|
-
return { raw: text, value, text };
|
|
2207
|
-
}
|
|
2208
|
-
return { raw: cell.raw, value, text };
|
|
2209
|
-
})
|
|
2210
|
-
);
|
|
2211
|
-
return createSpreadsheetClipboardData(cells, { row: r.startRow, col: r.startCol });
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Pastes clipboard data at `target` (default: the selection's top-left).
|
|
2215
|
-
* Prefers the internal format (formulas re-based to the target), then an
|
|
2216
|
-
* HTML table, then TSV. Returns false when there was nothing to paste.
|
|
2217
|
-
*/
|
|
2218
|
-
pasteClipboardData(source, target) {
|
|
2219
|
-
const parsed = readSpreadsheetClipboard(source);
|
|
2220
|
-
if (!parsed) return false;
|
|
2221
|
-
const at = target ?? this.getSelectionTopLeft();
|
|
2222
|
-
const rows = parsed.origin ? rebaseClipboardRows(parsed.rows, parsed.origin, at) : parsed.rows;
|
|
2223
|
-
this.writeMatrix(rows, at.row, at.col);
|
|
2224
|
-
const r = normalizeRange({
|
|
2225
|
-
startRow: at.row,
|
|
2226
|
-
startCol: at.col,
|
|
2227
|
-
endRow: at.row + rows.length - 1,
|
|
2228
|
-
endCol: at.col + rows.reduce((max, row) => Math.max(max, row.length), 1) - 1
|
|
2229
|
-
});
|
|
2230
|
-
this.selectRange(r);
|
|
2231
|
-
return true;
|
|
2232
|
-
}
|
|
2233
|
-
spillAnchorInRange(anchor, range) {
|
|
2234
|
-
const record = this.spills.get(anchor);
|
|
2235
|
-
return !!record && rangeContains(range, record.row, record.col);
|
|
2236
|
-
}
|
|
2237
|
-
getSelectionTopLeft() {
|
|
2238
|
-
if (this.selectedRange) {
|
|
2239
|
-
const r = normalizeRange(this.selectedRange);
|
|
2240
|
-
return { row: r.startRow, col: r.startCol };
|
|
2241
|
-
}
|
|
2242
|
-
return { row: this.activeCell?.row ?? 0, col: this.activeCell?.col ?? 0 };
|
|
2243
|
-
}
|
|
2244
|
-
// --- Table operations: shared helpers ---
|
|
2245
|
-
/** The table settings object, created on first use. */
|
|
2246
|
-
get table() {
|
|
2247
|
-
if (!this.document.table) this.document.table = {};
|
|
2248
|
-
return this.document.table;
|
|
2249
|
-
}
|
|
2250
|
-
/** Records an undo step, applies `fn`, then notifies listeners. */
|
|
2251
|
-
applyTableChange(fn, options = {}) {
|
|
2252
|
-
this.recordSnapshot();
|
|
2253
|
-
fn();
|
|
2254
|
-
if (options.recalculate) {
|
|
2255
|
-
this.recomputeRowIndices();
|
|
2256
|
-
this.recalculateAll();
|
|
2257
|
-
}
|
|
2258
|
-
this.document.updatedAt = Date.now();
|
|
2259
|
-
this.notifyChange();
|
|
2260
|
-
}
|
|
2261
|
-
/** First and last (inclusive) body row: rows between header and footer rows. */
|
|
2262
|
-
getBodyRowBounds() {
|
|
2263
|
-
const { headerRows, footerRows } = getTableSections(this.document);
|
|
2264
|
-
return { start: headerRows, end: this.document.rows.length - footerRows - 1 };
|
|
2265
|
-
}
|
|
2266
|
-
getCellValueAt = (rowIndex, colIndex) => {
|
|
2267
|
-
const row = this.document.rows[rowIndex];
|
|
2268
|
-
const col = this.document.columns[colIndex];
|
|
2269
|
-
if (!row || !col) return "";
|
|
2270
|
-
return this.getCell(row.id, col.id).value;
|
|
2271
|
-
};
|
|
2272
|
-
/** Rewrites every formula so references follow cells that moved. */
|
|
2273
|
-
remapAllFormulas(moveCell) {
|
|
2274
|
-
for (const rowCells of Object.values(this.document.cells)) {
|
|
2275
|
-
for (const cell of Object.values(rowCells)) {
|
|
2276
|
-
if (cell.raw?.startsWith("=")) {
|
|
2277
|
-
cell.raw = remapFormulaReferences(cell.raw, moveCell);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
/**
|
|
2283
|
-
* Keeps merges in place when rows/columns are inserted or deleted:
|
|
2284
|
-
* merges after the change shift, merges across it grow or shrink, and
|
|
2285
|
-
* merges reduced to a single cell are dropped.
|
|
2286
|
-
*/
|
|
2287
|
-
adjustTableForStructuralChange(change) {
|
|
2288
|
-
const merges = this.document.table?.merges;
|
|
2289
|
-
if (!merges?.length) return;
|
|
2290
|
-
const startKey = change.axis === "row" ? "startRow" : "startCol";
|
|
2291
|
-
const endKey = change.axis === "row" ? "endRow" : "endCol";
|
|
2292
|
-
const next = [];
|
|
2293
|
-
for (const merge of merges) {
|
|
2294
|
-
const m = { ...merge };
|
|
2295
|
-
if (change.type === "insert") {
|
|
2296
|
-
if (m[startKey] >= change.index) m[startKey]++;
|
|
2297
|
-
if (m[endKey] >= change.index) m[endKey]++;
|
|
2298
|
-
} else {
|
|
2299
|
-
if (m[startKey] === change.index && m[endKey] === change.index) continue;
|
|
2300
|
-
if (m[startKey] > change.index) m[startKey]--;
|
|
2301
|
-
if (m[endKey] >= change.index) m[endKey]--;
|
|
2302
|
-
}
|
|
2303
|
-
if (!isSingleCellRange(m)) next.push(m);
|
|
2304
|
-
}
|
|
2305
|
-
this.document.table.merges = next;
|
|
2306
|
-
}
|
|
2307
|
-
// --- Sort ---
|
|
2308
|
-
/**
|
|
2309
|
-
* Sorts rows by one or more columns (stable; numbers before text, blanks
|
|
2310
|
-
* last). Without a range (or with a range spanning every column) whole rows
|
|
2311
|
-
* move; with a range only the cells inside it move. Header and footer rows
|
|
2312
|
-
* never move. References follow the moved cells. Returns false when the
|
|
2313
|
-
* sort is rejected (no valid keys, a key outside the range, or merged
|
|
2314
|
-
* cells in the sorted area).
|
|
2315
|
-
*/
|
|
2316
|
-
sortRows(keys, options = {}) {
|
|
2317
|
-
const colCount = this.document.columns.length;
|
|
2318
|
-
const body = this.getBodyRowBounds();
|
|
2319
|
-
const range = options.range ? normalizeRange(options.range) : null;
|
|
2320
|
-
const startRow = Math.max(range?.startRow ?? body.start, body.start);
|
|
2321
|
-
const endRow = Math.min(range?.endRow ?? body.end, body.end);
|
|
2322
|
-
const startCol = range ? Math.max(0, range.startCol) : 0;
|
|
2323
|
-
const endCol = range ? Math.min(colCount - 1, range.endCol) : colCount - 1;
|
|
2324
|
-
if (endRow - startRow < 1 || startCol > endCol) return false;
|
|
2325
|
-
const keyCols = keys.map((key) => ({
|
|
2326
|
-
index: this.document.columns.findIndex((c) => c.id === key.colId),
|
|
2327
|
-
direction: key.direction ?? "asc"
|
|
2328
|
-
}));
|
|
2329
|
-
if (keyCols.length === 0 || keyCols.some((k) => k.index < startCol || k.index > endCol)) return false;
|
|
2330
|
-
const area = { startRow, startCol, endRow, endCol };
|
|
2331
|
-
if ((this.document.table?.merges ?? []).some((m) => rangesIntersect(m, area))) return false;
|
|
2332
|
-
if (this.intersectsSpill(area)) return false;
|
|
2333
|
-
const sourceRows = [];
|
|
2334
|
-
for (let r = startRow; r <= endRow; r++) sourceRows.push(r);
|
|
2335
|
-
const sorted = sourceRows.map((r) => ({ r, values: keyCols.map((k) => this.getCellValueAt(r, k.index)) })).sort((a, b) => {
|
|
2336
|
-
for (let i = 0; i < keyCols.length; i++) {
|
|
2337
|
-
const result = compareCellValues(a.values[i], b.values[i], keyCols[i].direction);
|
|
2338
|
-
if (result !== 0) return result;
|
|
2339
|
-
}
|
|
2340
|
-
return a.r - b.r;
|
|
2341
|
-
}).map((entry) => entry.r);
|
|
2342
|
-
if (sorted.every((r, i) => r === sourceRows[i])) return true;
|
|
2343
|
-
const newIndexOf = /* @__PURE__ */ new Map();
|
|
2344
|
-
sorted.forEach((oldRow, i) => newIndexOf.set(oldRow, startRow + i));
|
|
2345
|
-
const wholeRows = startCol === 0 && endCol === colCount - 1;
|
|
2346
|
-
this.applyTableChange(() => {
|
|
2347
|
-
this.remapAllFormulas(({ row, col }) => {
|
|
2348
|
-
const target = newIndexOf.get(row);
|
|
2349
|
-
if (target === void 0 || col < startCol || col > endCol) return null;
|
|
2350
|
-
return { row: target, col };
|
|
2351
|
-
});
|
|
2352
|
-
if (wholeRows) {
|
|
2353
|
-
const moved = sorted.map((r) => this.document.rows[r]);
|
|
2354
|
-
this.document.rows.splice(startRow, moved.length, ...moved);
|
|
2355
|
-
} else {
|
|
2356
|
-
const colIds = this.document.columns.slice(startCol, endCol + 1).map((c) => c.id);
|
|
2357
|
-
const snapshot = sorted.map((oldRow) => {
|
|
2358
|
-
const rowId = this.document.rows[oldRow].id;
|
|
2359
|
-
return colIds.map((colId) => this.document.cells[rowId]?.[colId]);
|
|
2360
|
-
});
|
|
2361
|
-
snapshot.forEach((cells, i) => {
|
|
2362
|
-
const rowId = this.document.rows[startRow + i].id;
|
|
2363
|
-
if (!this.document.cells[rowId]) this.document.cells[rowId] = {};
|
|
2364
|
-
colIds.forEach((colId, c) => {
|
|
2365
|
-
if (cells[c]) this.document.cells[rowId][colId] = cells[c];
|
|
2366
|
-
else delete this.document.cells[rowId][colId];
|
|
2367
|
-
});
|
|
2368
|
-
});
|
|
2369
|
-
}
|
|
2370
|
-
}, { recalculate: true });
|
|
2371
|
-
return true;
|
|
2372
|
-
}
|
|
2373
|
-
// --- Filters ---
|
|
2374
|
-
getFilterState() {
|
|
2375
|
-
return this.document.table?.filter ?? { enabled: false, rules: [] };
|
|
2376
|
-
}
|
|
2377
|
-
/** Replaces all filter rules (and turns filters on when there are any). */
|
|
2378
|
-
setFilterRules(rules) {
|
|
2379
|
-
this.applyTableChange(() => {
|
|
2380
|
-
this.table.filter = {
|
|
2381
|
-
enabled: rules.length > 0,
|
|
2382
|
-
rules: rules.map((rule) => ({ ...rule, id: rule.id ?? this.generateFilterId() }))
|
|
2383
|
-
};
|
|
2384
|
-
});
|
|
2385
|
-
}
|
|
2386
|
-
/** Adds a filter rule (AND-combined with the others) and turns filters on. Returns its id. */
|
|
2387
|
-
addFilterRule(rule) {
|
|
2388
|
-
const id = rule.id ?? this.generateFilterId();
|
|
2389
|
-
this.applyTableChange(() => {
|
|
2390
|
-
const current = this.getFilterState();
|
|
2391
|
-
this.table.filter = { enabled: true, rules: [...current.rules, { ...rule, id }] };
|
|
2392
|
-
});
|
|
2393
|
-
return id;
|
|
2394
|
-
}
|
|
2395
|
-
updateFilterRule(ruleId, changes) {
|
|
2396
|
-
const current = this.getFilterState();
|
|
2397
|
-
if (!current.rules.some((r) => r.id === ruleId)) return;
|
|
2398
|
-
this.applyTableChange(() => {
|
|
2399
|
-
this.table.filter = {
|
|
2400
|
-
...current,
|
|
2401
|
-
rules: current.rules.map((r) => r.id === ruleId ? { ...r, ...changes } : r)
|
|
2402
|
-
};
|
|
2403
|
-
});
|
|
2404
|
-
}
|
|
2405
|
-
removeFilterRule(ruleId) {
|
|
2406
|
-
const current = this.getFilterState();
|
|
2407
|
-
if (!current.rules.some((r) => r.id === ruleId)) return;
|
|
2408
|
-
this.applyTableChange(() => {
|
|
2409
|
-
this.table.filter = { ...current, rules: current.rules.filter((r) => r.id !== ruleId) };
|
|
2410
|
-
});
|
|
2411
|
-
}
|
|
2412
|
-
clearFilters() {
|
|
2413
|
-
if (this.getFilterState().rules.length === 0) return;
|
|
2414
|
-
this.applyTableChange(() => {
|
|
2415
|
-
this.table.filter = { enabled: false, rules: [] };
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
/** Turns filtering on or off, keeping the rules. */
|
|
2419
|
-
setFiltersEnabled(enabled) {
|
|
2420
|
-
const current = this.getFilterState();
|
|
2421
|
-
if (current.enabled === enabled) return;
|
|
2422
|
-
this.applyTableChange(() => {
|
|
2423
|
-
this.table.filter = { ...current, enabled };
|
|
2424
|
-
});
|
|
2425
|
-
}
|
|
2426
|
-
toggleFilters() {
|
|
2427
|
-
this.setFiltersEnabled(!this.getFilterState().enabled);
|
|
2428
|
-
}
|
|
2429
|
-
generateFilterId() {
|
|
2430
|
-
return `filter_${Date.now()}_${(nextIdSequence++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
2431
|
-
}
|
|
2432
|
-
// --- Header, footer and frozen rows/columns ---
|
|
2433
|
-
setHeaderRowCount(count) {
|
|
2434
|
-
this.setTableCount("headerRowCount", count, this.document.rows.length);
|
|
2435
|
-
}
|
|
2436
|
-
setFooterRowCount(count) {
|
|
2437
|
-
this.setTableCount("footerRowCount", count, this.document.rows.length);
|
|
2438
|
-
}
|
|
2439
|
-
setHeaderColumnCount(count) {
|
|
2440
|
-
this.setTableCount("headerColumnCount", count, this.document.columns.length);
|
|
2441
|
-
}
|
|
2442
|
-
/** Keeps the first `count` rows visible while scrolling. 0 unfreezes. */
|
|
2443
|
-
setFrozenRowCount(count) {
|
|
2444
|
-
this.setTableCount("frozenRowCount", count, this.document.rows.length);
|
|
2445
|
-
}
|
|
2446
|
-
/** Keeps the first `count` columns visible while scrolling. 0 unfreezes. */
|
|
2447
|
-
setFrozenColumnCount(count) {
|
|
2448
|
-
this.setTableCount("frozenColumnCount", count, this.document.columns.length);
|
|
2449
|
-
}
|
|
2450
|
-
setTableCount(key, count, max) {
|
|
2451
|
-
const value = Math.max(0, Math.min(Math.floor(count), max));
|
|
2452
|
-
if ((this.document.table?.[key] ?? 0) === value) return;
|
|
2453
|
-
this.applyTableChange(() => {
|
|
2454
|
-
this.table[key] = value;
|
|
2455
|
-
});
|
|
2456
|
-
}
|
|
2457
|
-
// --- Number formats ---
|
|
2458
|
-
//
|
|
2459
|
-
// Cells are replaced, never mutated in place: framework bindings compare
|
|
2460
|
-
// cell objects by identity to decide what to re-render.
|
|
2461
|
-
/**
|
|
2462
|
-
* Sets an explicit number format code (e.g. `0.00`, `"$"#,##0.00`,
|
|
2463
|
-
* `m/d/yyyy`, `[Red]0;[Blue]-0`) on every cell in `range`, including blank
|
|
2464
|
-
* cells, so values typed later keep it. `null` removes explicit formats: the
|
|
2465
|
-
* cells go back to their column's format or the one chosen for typed input.
|
|
2466
|
-
* Undoable as one step. Returns false when there is no range.
|
|
2467
|
-
*/
|
|
2468
|
-
setCellNumberFormat(range, code) {
|
|
2469
|
-
if (!range) return false;
|
|
2470
|
-
const r = normalizeRange(range);
|
|
2471
|
-
this.recordSnapshot();
|
|
2472
|
-
const kind = code === null ? void 0 : cellFormatForNumberFormat(code);
|
|
2473
|
-
for (let ri = r.startRow; ri <= r.endRow; ri++) {
|
|
2474
|
-
const row = this.document.rows[ri];
|
|
2475
|
-
if (!row) continue;
|
|
2476
|
-
for (let ci = r.startCol; ci <= r.endCol; ci++) {
|
|
2477
|
-
const col = this.document.columns[ci];
|
|
2478
|
-
if (!col) continue;
|
|
2479
|
-
const cells = this.document.cells[row.id] ??= {};
|
|
2480
|
-
const existing = cells[col.id];
|
|
2481
|
-
if (code === null) {
|
|
2482
|
-
if (!existing?.explicitNumberFormat) continue;
|
|
2483
|
-
if (existing.raw === "" && existing.value === "" && !existing.error) {
|
|
2484
|
-
delete cells[col.id];
|
|
2485
|
-
} else {
|
|
2486
|
-
const { explicitNumberFormat: _explicit, ...rest } = existing;
|
|
2487
|
-
cells[col.id] = this.withNumberFormat(rest, this.implicitNumberFormat(rest, col, row));
|
|
2488
|
-
}
|
|
2489
|
-
} else {
|
|
2490
|
-
cells[col.id] = {
|
|
2491
|
-
...existing ?? { raw: "", value: "" },
|
|
2492
|
-
format: kind ?? existing?.format ?? col.format,
|
|
2493
|
-
numberFormat: code,
|
|
2494
|
-
explicitNumberFormat: true
|
|
2495
|
-
};
|
|
2496
|
-
}
|
|
2497
|
-
this.refreshSpilledNumberFormat(row, col);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
this.document.updatedAt = Date.now();
|
|
2501
|
-
this.notifyChange();
|
|
2502
|
-
return true;
|
|
2503
|
-
}
|
|
2504
|
-
/**
|
|
2505
|
-
* Sets the number format code of a column (`null` or `General` clears it).
|
|
2506
|
-
* Its cells without an explicit format of their own use it, including
|
|
2507
|
-
* values typed later; when cleared they go back to the format chosen for
|
|
2508
|
-
* typed input. Undoable.
|
|
2509
|
-
*/
|
|
2510
|
-
setColumnNumberFormat(colId, code) {
|
|
2511
|
-
const col = this.getColumn(colId);
|
|
2512
|
-
if (!col) return;
|
|
2513
|
-
this.recordSnapshot();
|
|
2514
|
-
if (code === null || isGeneralNumberFormat(code)) delete col.numberFormat;
|
|
2515
|
-
else col.numberFormat = code;
|
|
2516
|
-
for (const row of this.document.rows) {
|
|
2517
|
-
const cells = this.document.cells[row.id];
|
|
2518
|
-
const cell = cells?.[col.id];
|
|
2519
|
-
if (cells && cell && !cell.explicitNumberFormat) {
|
|
2520
|
-
cells[col.id] = this.withNumberFormat(cell, this.implicitNumberFormat(cell, col, row));
|
|
2521
|
-
}
|
|
2522
|
-
this.refreshSpilledNumberFormat(row, col);
|
|
2523
|
-
}
|
|
2524
|
-
this.document.updatedAt = Date.now();
|
|
2525
|
-
this.notifyChange();
|
|
2526
|
-
}
|
|
2527
|
-
/** The column's format, or the one typed input chose (typed cells only). */
|
|
2528
|
-
implicitNumberFormat(cell, col, row) {
|
|
2529
|
-
const typed = cell.raw !== "" && !cell.raw.startsWith("=") && !(col.type === "bound" && col.binding && row.recordId);
|
|
2530
|
-
return this.fixedNumberFormat(col, void 0) ?? (typed ? parseCellInput(cell.raw).numberFormat : void 0);
|
|
2531
|
-
}
|
|
2532
|
-
/** A copy of `cell` with `numberFormat` set, or removed when undefined. */
|
|
2533
|
-
withNumberFormat(cell, numberFormat) {
|
|
2534
|
-
const { numberFormat: _old, ...rest } = cell;
|
|
2535
|
-
return numberFormat === void 0 ? rest : { ...rest, numberFormat };
|
|
2536
|
-
}
|
|
2537
|
-
/** Re-applies the stored cell's (or the column's) format to a spilled value shown there. */
|
|
2538
|
-
refreshSpilledNumberFormat(row, col) {
|
|
2539
|
-
const id = spillCellId(row.id, col.id);
|
|
2540
|
-
const spilled = this.spillCells.get(id);
|
|
2541
|
-
if (!spilled) return;
|
|
2542
|
-
const { numberFormat: _old, explicitNumberFormat: _explicit, ...rest } = spilled;
|
|
2543
|
-
this.spillCells.set(id, { ...rest, ...this.fixedNumberFormatFields(col, this.document.cells[row.id]?.[col.id]) });
|
|
2544
|
-
}
|
|
2545
|
-
// --- Hide / unhide ---
|
|
2546
|
-
hideRows(indices) {
|
|
2547
|
-
this.setHidden("row", indices, true);
|
|
2548
|
-
}
|
|
2549
|
-
unhideRows(indices) {
|
|
2550
|
-
this.setHidden("row", indices, false);
|
|
2551
|
-
}
|
|
2552
|
-
hideColumns(indices) {
|
|
2553
|
-
this.setHidden("col", indices, true);
|
|
2554
|
-
}
|
|
2555
|
-
unhideColumns(indices) {
|
|
2556
|
-
this.setHidden("col", indices, false);
|
|
2557
|
-
}
|
|
2558
|
-
/** Unhides every hidden row and column (filters are unaffected). */
|
|
2559
|
-
unhideAll() {
|
|
2560
|
-
const rows = this.document.rows.flatMap((r, i) => r.hidden ? [i] : []);
|
|
2561
|
-
const cols = this.document.columns.flatMap((c, i) => c.hidden ? [i] : []);
|
|
2562
|
-
if (rows.length === 0 && cols.length === 0) return;
|
|
2563
|
-
this.applyTableChange(() => {
|
|
2564
|
-
rows.forEach((i) => delete this.document.rows[i].hidden);
|
|
2565
|
-
cols.forEach((i) => delete this.document.columns[i].hidden);
|
|
2566
|
-
});
|
|
2567
|
-
}
|
|
2568
|
-
setHidden(axis, indices, hidden) {
|
|
2569
|
-
const items = axis === "row" ? this.document.rows : this.document.columns;
|
|
2570
|
-
const targets = indices.filter((i) => items[i] && !!items[i].hidden !== hidden);
|
|
2571
|
-
if (targets.length === 0) return;
|
|
2572
|
-
this.applyTableChange(() => {
|
|
2573
|
-
for (const i of targets) {
|
|
2574
|
-
if (hidden) items[i].hidden = true;
|
|
2575
|
-
else delete items[i].hidden;
|
|
2576
|
-
}
|
|
2577
|
-
});
|
|
2578
|
-
}
|
|
2579
|
-
// --- Merged cells ---
|
|
2580
|
-
getMerges() {
|
|
2581
|
-
return this.document.table?.merges ?? [];
|
|
2582
|
-
}
|
|
2583
|
-
/** The merge containing (row, col), or null. */
|
|
2584
|
-
getMergeAt(row, col) {
|
|
2585
|
-
return this.getMerges().find((m) => rangeContains(m, row, col)) ?? null;
|
|
2586
|
-
}
|
|
2587
|
-
/** Grows `range` until no merge crosses its edge. Keeps the drag direction. */
|
|
2588
|
-
expandRangeToMerges(range) {
|
|
2589
|
-
let r = normalizeRange(range);
|
|
2590
|
-
let changed = true;
|
|
2591
|
-
while (changed) {
|
|
2592
|
-
changed = false;
|
|
2593
|
-
for (const m of this.getMerges()) {
|
|
2594
|
-
if (rangesIntersect(m, r) && !rangeContainsRange(r, m)) {
|
|
2595
|
-
r = {
|
|
2596
|
-
startRow: Math.min(r.startRow, m.startRow),
|
|
2597
|
-
startCol: Math.min(r.startCol, m.startCol),
|
|
2598
|
-
endRow: Math.max(r.endRow, m.endRow),
|
|
2599
|
-
endCol: Math.max(r.endCol, m.endCol)
|
|
2600
|
-
};
|
|
2601
|
-
changed = true;
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
const rowsReversed = range.startRow > range.endRow;
|
|
2606
|
-
const colsReversed = range.startCol > range.endCol;
|
|
2607
|
-
return {
|
|
2608
|
-
startRow: rowsReversed ? r.endRow : r.startRow,
|
|
2609
|
-
endRow: rowsReversed ? r.startRow : r.endRow,
|
|
2610
|
-
startCol: colsReversed ? r.endCol : r.startCol,
|
|
2611
|
-
endCol: colsReversed ? r.startCol : r.endCol
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
/**
|
|
2615
|
-
* Merges `range` (default: the selection) into one cell anchored on its
|
|
2616
|
-
* top-left cell; the other cells are cleared. Merges fully inside the range
|
|
2617
|
-
* are absorbed. Returns false for single cells and for ranges that
|
|
2618
|
-
* partially overlap an existing merge.
|
|
2619
|
-
*/
|
|
2620
|
-
mergeCells(range = this.selectedRange) {
|
|
2621
|
-
if (!range) return false;
|
|
2622
|
-
const r = normalizeRange(range);
|
|
2623
|
-
if (isSingleCellRange(r) || r.startRow < 0 || r.startCol < 0 || r.endRow >= this.document.rows.length || r.endCol >= this.document.columns.length) {
|
|
2624
|
-
return false;
|
|
2625
|
-
}
|
|
2626
|
-
const merges = this.getMerges();
|
|
2627
|
-
if (merges.some((m) => rangesIntersect(m, r) && !rangeContainsRange(r, m))) return false;
|
|
2628
|
-
if (this.intersectsSpill(r)) return false;
|
|
2629
|
-
this.applyTableChange(() => {
|
|
2630
|
-
for (let row = r.startRow; row <= r.endRow; row++) {
|
|
2631
|
-
const rowId = this.document.rows[row].id;
|
|
2632
|
-
for (let col = r.startCol; col <= r.endCol; col++) {
|
|
2633
|
-
if (row === r.startRow && col === r.startCol) continue;
|
|
2634
|
-
delete this.document.cells[rowId]?.[this.document.columns[col].id];
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
this.table.merges = [...merges.filter((m) => !rangeContainsRange(r, m)), r];
|
|
2638
|
-
}, { recalculate: true });
|
|
2639
|
-
this.selectCell(r.startRow, r.startCol);
|
|
2640
|
-
return true;
|
|
2641
|
-
}
|
|
2642
|
-
/** Removes every merge intersecting `range` (default: the selection). */
|
|
2643
|
-
unmergeCells(range = this.selectedRange) {
|
|
2644
|
-
if (!range) return false;
|
|
2645
|
-
const merges = this.getMerges();
|
|
2646
|
-
const remaining = merges.filter((m) => !rangesIntersect(m, range));
|
|
2647
|
-
if (remaining.length === merges.length) return false;
|
|
2648
|
-
this.applyTableChange(() => {
|
|
2649
|
-
this.table.merges = remaining;
|
|
2650
|
-
}, { recalculate: true });
|
|
2651
|
-
return true;
|
|
2652
|
-
}
|
|
2653
|
-
// --- Autofill ---
|
|
2654
|
-
/**
|
|
2655
|
-
* Fills `target` from `source` like dragging the fill handle. `target` is
|
|
2656
|
-
* the extended range (source included) and must extend `source` down, up,
|
|
2657
|
-
* right or left. Numbers and dates continue their series, text ending in a
|
|
2658
|
-
* number counts up, other text repeats, and formulas are copied with
|
|
2659
|
-
* their relative references shifted (`$` parts stay). Returns false when
|
|
2660
|
-
* the ranges don't line up.
|
|
2661
|
-
*/
|
|
2662
|
-
autofill(source, target) {
|
|
2663
|
-
const s = normalizeRange(source);
|
|
2664
|
-
const t = normalizeRange(target);
|
|
2665
|
-
const direction = getFillDirection(s, t);
|
|
2666
|
-
if (!direction || t.startRow < 0 || t.startCol < 0) return false;
|
|
2667
|
-
if (s.endRow >= this.document.rows.length || s.endCol >= this.document.columns.length) return false;
|
|
2668
|
-
const vertical = direction === "down" || direction === "up";
|
|
2669
|
-
const forward = direction === "down" || direction === "right";
|
|
2670
|
-
const sourceSteps = [];
|
|
2671
|
-
const [sLo, sHi] = vertical ? [s.startRow, s.endRow] : [s.startCol, s.endCol];
|
|
2672
|
-
for (let i = sLo; i <= sHi; i++) sourceSteps.push(i);
|
|
2673
|
-
if (!forward) sourceSteps.reverse();
|
|
2674
|
-
const fillSteps = [];
|
|
2675
|
-
if (forward) for (let i = sHi + 1; i <= (vertical ? t.endRow : t.endCol); i++) fillSteps.push(i);
|
|
2676
|
-
else for (let i = sLo - 1; i >= (vertical ? t.startRow : t.startCol); i--) fillSteps.push(i);
|
|
2677
|
-
const [lLo, lHi] = vertical ? [s.startCol, s.endCol] : [s.startRow, s.endRow];
|
|
2678
|
-
this.runBatch(() => {
|
|
2679
|
-
this.growGrid(vertical ? Math.max(t.endRow, 0) : s.endRow, vertical ? s.endCol : Math.max(t.endCol, 0));
|
|
2680
|
-
for (let line = lLo; line <= lHi; line++) {
|
|
2681
|
-
const at = (step) => vertical ? { row: step, col: line } : { row: line, col: step };
|
|
2682
|
-
const raws = sourceSteps.map((step) => {
|
|
2683
|
-
const p = at(step);
|
|
2684
|
-
return this.getCell(this.document.rows[p.row].id, this.document.columns[p.col].id).raw;
|
|
2685
|
-
});
|
|
2686
|
-
const hasFormula = raws.some((raw) => raw.startsWith("="));
|
|
2687
|
-
const series = hasFormula ? null : fillSeries(raws, fillSteps.length);
|
|
2688
|
-
fillSteps.forEach((step, i) => {
|
|
2689
|
-
const p = at(step);
|
|
2690
|
-
let raw;
|
|
2691
|
-
if (series) {
|
|
2692
|
-
raw = series[i];
|
|
2693
|
-
} else {
|
|
2694
|
-
const from = sourceSteps[i % sourceSteps.length];
|
|
2695
|
-
raw = raws[i % raws.length];
|
|
2696
|
-
if (raw.startsWith("=")) {
|
|
2697
|
-
const delta = step - from;
|
|
2698
|
-
raw = vertical ? offsetFormulaReferences(raw, delta, 0) : offsetFormulaReferences(raw, 0, delta);
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
this.setCellValue(this.document.rows[p.row].id, this.document.columns[p.col].id, raw);
|
|
2702
|
-
});
|
|
2703
|
-
}
|
|
2704
|
-
});
|
|
2705
|
-
this.selectRange(t);
|
|
2706
|
-
return true;
|
|
2707
|
-
}
|
|
2708
|
-
// --- Categories (row groups) ---
|
|
2709
|
-
/**
|
|
2710
|
-
* Groups body rows by the value in `colId`, with optional per-group
|
|
2711
|
-
* summaries (e.g. `[{ colId, aggregate: 'sum' }]`).
|
|
2712
|
-
*/
|
|
2713
|
-
groupRowsBy(colId, summaries = []) {
|
|
2714
|
-
if (!this.getColumn(colId)) return;
|
|
2715
|
-
this.applyTableChange(() => {
|
|
2716
|
-
this.table.grouping = { colId, summaries, collapsed: [] };
|
|
2717
|
-
});
|
|
2718
|
-
}
|
|
2719
|
-
ungroupRows() {
|
|
2720
|
-
if (!this.document.table?.grouping) return;
|
|
2721
|
-
this.applyTableChange(() => {
|
|
2722
|
-
delete this.table.grouping;
|
|
2723
|
-
});
|
|
2724
|
-
}
|
|
2725
|
-
/** Sets (or with `null`, removes) the group summary for a column. */
|
|
2726
|
-
setGroupSummary(colId, aggregate) {
|
|
2727
|
-
const grouping = this.document.table?.grouping;
|
|
2728
|
-
if (!grouping) return;
|
|
2729
|
-
this.applyTableChange(() => {
|
|
2730
|
-
const others = grouping.summaries.filter((s) => s.colId !== colId);
|
|
2731
|
-
this.table.grouping = {
|
|
2732
|
-
...grouping,
|
|
2733
|
-
summaries: aggregate ? [...others, { colId, aggregate }] : others
|
|
2734
|
-
};
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2737
|
-
setGroupCollapsed(key, collapsed) {
|
|
2738
|
-
const grouping = this.document.table?.grouping;
|
|
2739
|
-
if (!grouping || grouping.collapsed.includes(key) === collapsed) return;
|
|
2740
|
-
this.applyTableChange(() => {
|
|
2741
|
-
this.table.grouping = {
|
|
2742
|
-
...grouping,
|
|
2743
|
-
collapsed: collapsed ? [...grouping.collapsed, key] : grouping.collapsed.filter((k) => k !== key)
|
|
2744
|
-
};
|
|
2745
|
-
});
|
|
2746
|
-
}
|
|
2747
|
-
toggleGroupCollapsed(key) {
|
|
2748
|
-
const grouping = this.document.table?.grouping;
|
|
2749
|
-
if (!grouping) return;
|
|
2750
|
-
this.setGroupCollapsed(key, !grouping.collapsed.includes(key));
|
|
2751
|
-
}
|
|
2752
|
-
/** The current row groups with their summaries (empty when not grouped). */
|
|
2753
|
-
getGroups() {
|
|
2754
|
-
return computeRowGroups(this.document, this.getCellValueAt);
|
|
2755
|
-
}
|
|
2756
|
-
// --- Transpose ---
|
|
2757
|
-
/**
|
|
2758
|
-
* Transposes `range` (default: the selection) in place around its top-left
|
|
2759
|
-
* cell: rows become columns. Cells left uncovered are cleared, the grid
|
|
2760
|
-
* grows if needed, and references follow the moved cells. Returns false
|
|
2761
|
-
* for single cells or when merged cells are involved.
|
|
2762
|
-
*/
|
|
2763
|
-
transposeRange(range = this.selectedRange) {
|
|
2764
|
-
if (!range) return false;
|
|
2765
|
-
const s = normalizeRange(range);
|
|
2766
|
-
if (isSingleCellRange(s)) return false;
|
|
2767
|
-
const height = s.endRow - s.startRow + 1;
|
|
2768
|
-
const width = s.endCol - s.startCol + 1;
|
|
2769
|
-
const t = { startRow: s.startRow, startCol: s.startCol, endRow: s.startRow + width - 1, endCol: s.startCol + height - 1 };
|
|
2770
|
-
if (this.getMerges().some((m) => rangesIntersect(m, s) || rangesIntersect(m, t))) return false;
|
|
2771
|
-
if (this.intersectsSpill(s) || this.intersectsSpill(t)) return false;
|
|
2772
|
-
this.applyTableChange(() => {
|
|
2773
|
-
this.growGrid(t.endRow, t.endCol);
|
|
2774
|
-
this.remapAllFormulas(({ row, col }) => {
|
|
2775
|
-
if (!rangeContains(s, row, col)) return null;
|
|
2776
|
-
return { row: s.startRow + (col - s.startCol), col: s.startCol + (row - s.startRow) };
|
|
2777
|
-
});
|
|
2778
|
-
const rowId = (r) => this.document.rows[r].id;
|
|
2779
|
-
const colId = (c) => this.document.columns[c].id;
|
|
2780
|
-
const moved = [];
|
|
2781
|
-
for (let r = s.startRow; r <= s.endRow; r++) {
|
|
2782
|
-
for (let c = s.startCol; c <= s.endCol; c++) {
|
|
2783
|
-
moved.push({
|
|
2784
|
-
row: s.startRow + (c - s.startCol),
|
|
2785
|
-
col: s.startCol + (r - s.startRow),
|
|
2786
|
-
cell: this.document.cells[rowId(r)]?.[colId(c)]
|
|
2787
|
-
});
|
|
2788
|
-
delete this.document.cells[rowId(r)]?.[colId(c)];
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
for (const { row, col, cell } of moved) {
|
|
2792
|
-
if (!cell) continue;
|
|
2793
|
-
if (!this.document.cells[rowId(row)]) this.document.cells[rowId(row)] = {};
|
|
2794
|
-
this.document.cells[rowId(row)][colId(col)] = cell;
|
|
2795
|
-
}
|
|
2796
|
-
}, { recalculate: true });
|
|
2797
|
-
this.selectRange(t);
|
|
2798
|
-
return true;
|
|
2799
|
-
}
|
|
2800
|
-
// --- View model ---
|
|
2801
|
-
/**
|
|
2802
|
-
* What to render: visible rows (with group headers) and columns in display
|
|
2803
|
-
* order, frozen offsets and merged-cell spans. Filters, hidden rows/columns
|
|
2804
|
-
* and collapsed groups only affect this view, never the document.
|
|
2805
|
-
*/
|
|
2806
|
-
getViewModel() {
|
|
2807
|
-
return buildSpreadsheetView(this.document, this.getCellValueAt);
|
|
2808
|
-
}
|
|
2809
|
-
// --- Undo/Redo Snapshots ---
|
|
2810
|
-
recordSnapshot() {
|
|
2811
|
-
if (this.isApplyingHistory || this.batching) return;
|
|
2812
|
-
this.undoStack.push(JSON.stringify(this.document));
|
|
2813
|
-
if (this.undoStack.length > 50) this.undoStack.shift();
|
|
2814
|
-
this.redoStack = [];
|
|
2815
|
-
this.trimHistory();
|
|
2816
|
-
}
|
|
2817
|
-
/** Drops the oldest snapshots beyond `maxHistoryBytes`, always keeping the newest. */
|
|
2818
|
-
trimHistory() {
|
|
2819
|
-
if (this.maxHistoryBytes <= 0 || this.undoStack.length <= 1) return;
|
|
2820
|
-
let total = 0;
|
|
2821
|
-
for (const s of this.undoStack) total += s.length * 2;
|
|
2822
|
-
for (const s of this.redoStack) total += s.length * 2;
|
|
2823
|
-
while (total > this.maxHistoryBytes && this.undoStack.length > 1) {
|
|
2824
|
-
total -= this.undoStack.shift().length * 2;
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
/** Approximate bytes held by the undo/redo snapshots (UTF-16 size). */
|
|
2828
|
-
getHistoryByteSize() {
|
|
2829
|
-
let total = 0;
|
|
2830
|
-
for (const s of this.undoStack) total += s.length * 2;
|
|
2831
|
-
for (const s of this.redoStack) total += s.length * 2;
|
|
2832
|
-
return total;
|
|
2833
|
-
}
|
|
2834
|
-
/** Number of undo / redo snapshots currently held. */
|
|
2835
|
-
getHistoryDepth() {
|
|
2836
|
-
return { undo: this.undoStack.length, redo: this.redoStack.length };
|
|
2837
|
-
}
|
|
2838
|
-
undo() {
|
|
2839
|
-
if (this.undoStack.length === 0) return;
|
|
2840
|
-
this.isApplyingHistory = true;
|
|
2841
|
-
try {
|
|
2842
|
-
this.redoStack.push(JSON.stringify(this.document));
|
|
2843
|
-
this.document = JSON.parse(this.undoStack.pop());
|
|
2844
|
-
this.recalculateAll();
|
|
2845
|
-
this.notifyChange();
|
|
2846
|
-
} finally {
|
|
2847
|
-
this.isApplyingHistory = false;
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
redo() {
|
|
2851
|
-
if (this.redoStack.length === 0) return;
|
|
2852
|
-
this.isApplyingHistory = true;
|
|
2853
|
-
try {
|
|
2854
|
-
this.undoStack.push(JSON.stringify(this.document));
|
|
2855
|
-
this.document = JSON.parse(this.redoStack.pop());
|
|
2856
|
-
this.recalculateAll();
|
|
2857
|
-
this.notifyChange();
|
|
2858
|
-
} finally {
|
|
2859
|
-
this.isApplyingHistory = false;
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
notifyChange() {
|
|
2863
|
-
this.onDocumentChange?.(this.document);
|
|
2864
|
-
}
|
|
2865
|
-
};
|
|
2866
|
-
function stepInOrder(order, from, delta) {
|
|
2867
|
-
if (order.length === 0) return from;
|
|
2868
|
-
const pos = order.indexOf(from);
|
|
2869
|
-
if (pos === -1) {
|
|
2870
|
-
const candidates = order.filter((i) => delta > 0 ? i > from : i < from);
|
|
2871
|
-
if (candidates.length === 0) return from;
|
|
2872
|
-
return candidates.reduce((best, i) => delta > 0 ? Math.min(best, i) : Math.max(best, i));
|
|
2873
|
-
}
|
|
2874
|
-
return order[Math.max(0, Math.min(order.length - 1, pos + delta))];
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
|
-
// src/spreadsheet/menu.ts
|
|
2878
|
-
function runSpreadsheetMenuItem(item, ask) {
|
|
2879
|
-
if (!item.input) {
|
|
2880
|
-
item.run();
|
|
2881
|
-
return true;
|
|
2882
|
-
}
|
|
2883
|
-
const prompt = ask ?? ((label, value) => typeof window !== "undefined" && window.prompt ? window.prompt(label, value) : null);
|
|
2884
|
-
const text = prompt(item.input.label, item.input.defaultValue ?? "");
|
|
2885
|
-
if (text === null || text === void 0) return false;
|
|
2886
|
-
item.run(text);
|
|
2887
|
-
return true;
|
|
2888
|
-
}
|
|
2889
|
-
function clampMenuPosition(point, size, viewport, margin = 4) {
|
|
2890
|
-
const place = (start, length, limit) => {
|
|
2891
|
-
if (start + length <= limit - margin) return Math.max(margin, start);
|
|
2892
|
-
const flipped = start - length;
|
|
2893
|
-
return Math.max(margin, flipped >= margin ? flipped : limit - margin - length);
|
|
2894
|
-
};
|
|
2895
|
-
return {
|
|
2896
|
-
x: place(point.x, size.width, viewport.width),
|
|
2897
|
-
y: place(point.y, size.height, viewport.height)
|
|
2898
|
-
};
|
|
2899
|
-
}
|
|
2900
|
-
var quote = (value) => {
|
|
2901
|
-
const text = String(value ?? "");
|
|
2902
|
-
return text.length > 24 ? `"${text.slice(0, 23)}\u2026"` : `"${text}"`;
|
|
2903
|
-
};
|
|
2904
|
-
function getSpreadsheetMenuItems(controller, cell) {
|
|
2905
|
-
const doc = controller.document;
|
|
2906
|
-
const column = doc.columns[cell.col];
|
|
2907
|
-
const row = doc.rows[cell.row];
|
|
2908
|
-
if (!column || !row) return [];
|
|
2909
|
-
const selection = controller.selectedRange && rangeContains(controller.selectedRange, cell.row, cell.col) ? normalizeRange(controller.selectedRange) : { startRow: cell.row, startCol: cell.col, endRow: cell.row, endCol: cell.col };
|
|
2910
|
-
const colName = indexToColName(cell.col);
|
|
2911
|
-
const cellData = controller.getCell(row.id, column.id);
|
|
2912
|
-
const value = cellData.value;
|
|
2913
|
-
const table = doc.table ?? {};
|
|
2914
|
-
const filter = controller.getFilterState();
|
|
2915
|
-
const grouping = table.grouping;
|
|
2916
|
-
const rows = Array.from({ length: selection.endRow - selection.startRow + 1 }, (_, i) => selection.startRow + i);
|
|
2917
|
-
const cols = Array.from({ length: selection.endCol - selection.startCol + 1 }, (_, i) => selection.startCol + i);
|
|
2918
|
-
const anyHidden = doc.rows.some((r) => r.hidden) || doc.columns.some((c) => c.hidden);
|
|
2919
|
-
const hasMerge = controller.getMerges().some((m) => rangesIntersect(m, selection));
|
|
2920
|
-
const sortRange = isSingleCellRange(selection) ? null : selection;
|
|
2921
|
-
const items = [
|
|
2922
|
-
{
|
|
2923
|
-
id: "sort-asc",
|
|
2924
|
-
label: `Sort ascending by column ${colName}`,
|
|
2925
|
-
run: () => controller.sortRows([{ colId: column.id, direction: "asc" }], { range: sortRange })
|
|
2926
|
-
},
|
|
2927
|
-
{
|
|
2928
|
-
id: "sort-desc",
|
|
2929
|
-
label: `Sort descending by column ${colName}`,
|
|
2930
|
-
run: () => controller.sortRows([{ colId: column.id, direction: "desc" }], { range: sortRange })
|
|
2931
|
-
},
|
|
2932
|
-
{
|
|
2933
|
-
id: "filter-equals",
|
|
2934
|
-
label: `Show only rows where ${colName} = ${quote(formatCellDisplay(cellData))}`,
|
|
2935
|
-
separatorBefore: true,
|
|
2936
|
-
run: () => controller.addFilterRule({ colId: column.id, operator: "equals", value: String(value ?? "") })
|
|
2937
|
-
},
|
|
2938
|
-
{
|
|
2939
|
-
id: "filter-toggle",
|
|
2940
|
-
label: filter.enabled ? "Turn filters off" : "Turn filters on",
|
|
2941
|
-
disabled: filter.rules.length === 0,
|
|
2942
|
-
run: () => controller.toggleFilters()
|
|
2943
|
-
},
|
|
2944
|
-
{
|
|
2945
|
-
id: "filter-clear",
|
|
2946
|
-
label: "Clear filters",
|
|
2947
|
-
disabled: filter.rules.length === 0,
|
|
2948
|
-
run: () => controller.clearFilters()
|
|
2949
|
-
},
|
|
2950
|
-
{
|
|
2951
|
-
id: "freeze-rows",
|
|
2952
|
-
label: `Freeze rows 1\u2013${cell.row + 1}`,
|
|
2953
|
-
separatorBefore: true,
|
|
2954
|
-
run: () => controller.setFrozenRowCount(cell.row + 1)
|
|
2955
|
-
},
|
|
2956
|
-
{
|
|
2957
|
-
id: "freeze-columns",
|
|
2958
|
-
label: `Freeze columns A\u2013${colName}`,
|
|
2959
|
-
run: () => controller.setFrozenColumnCount(cell.col + 1)
|
|
2960
|
-
},
|
|
2961
|
-
{
|
|
2962
|
-
id: "unfreeze",
|
|
2963
|
-
label: "Unfreeze rows and columns",
|
|
2964
|
-
disabled: !table.frozenRowCount && !table.frozenColumnCount,
|
|
2965
|
-
run: () => {
|
|
2966
|
-
controller.setFrozenRowCount(0);
|
|
2967
|
-
controller.setFrozenColumnCount(0);
|
|
2968
|
-
}
|
|
2969
|
-
},
|
|
2970
|
-
{
|
|
2971
|
-
id: "header-rows",
|
|
2972
|
-
label: `Use rows 1\u2013${cell.row + 1} as header rows`,
|
|
2973
|
-
run: () => controller.setHeaderRowCount(cell.row + 1)
|
|
2974
|
-
},
|
|
2975
|
-
{
|
|
2976
|
-
id: "footer-rows",
|
|
2977
|
-
label: `Use rows ${cell.row + 1}\u2013${doc.rows.length} as footer rows`,
|
|
2978
|
-
run: () => controller.setFooterRowCount(doc.rows.length - cell.row)
|
|
2979
|
-
},
|
|
2980
|
-
{
|
|
2981
|
-
id: "hide-rows",
|
|
2982
|
-
label: rows.length > 1 ? `Hide ${rows.length} rows` : `Hide row ${cell.row + 1}`,
|
|
2983
|
-
separatorBefore: true,
|
|
2984
|
-
run: () => controller.hideRows(rows)
|
|
2985
|
-
},
|
|
2986
|
-
{
|
|
2987
|
-
id: "hide-columns",
|
|
2988
|
-
label: cols.length > 1 ? `Hide ${cols.length} columns` : `Hide column ${colName}`,
|
|
2989
|
-
run: () => controller.hideColumns(cols)
|
|
2990
|
-
},
|
|
2991
|
-
{
|
|
2992
|
-
id: "unhide-all",
|
|
2993
|
-
label: "Unhide all rows and columns",
|
|
2994
|
-
disabled: !anyHidden,
|
|
2995
|
-
run: () => controller.unhideAll()
|
|
2996
|
-
},
|
|
2997
|
-
{
|
|
2998
|
-
id: "merge",
|
|
2999
|
-
label: "Merge cells",
|
|
3000
|
-
separatorBefore: true,
|
|
3001
|
-
disabled: isSingleCellRange(selection),
|
|
3002
|
-
run: () => {
|
|
3003
|
-
controller.mergeCells(selection);
|
|
3004
|
-
}
|
|
3005
|
-
},
|
|
3006
|
-
{
|
|
3007
|
-
id: "unmerge",
|
|
3008
|
-
label: "Unmerge cells",
|
|
3009
|
-
disabled: !hasMerge,
|
|
3010
|
-
run: () => {
|
|
3011
|
-
controller.unmergeCells(selection);
|
|
3012
|
-
}
|
|
3013
|
-
},
|
|
3014
|
-
{
|
|
3015
|
-
id: "transpose",
|
|
3016
|
-
label: "Transpose",
|
|
3017
|
-
disabled: isSingleCellRange(selection),
|
|
3018
|
-
run: () => {
|
|
3019
|
-
controller.transposeRange(selection);
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
];
|
|
3023
|
-
const currentFormat = cellData.numberFormat;
|
|
3024
|
-
const presetMatch = NUMBER_FORMAT_PRESETS.find(
|
|
3025
|
-
(p) => p.id === "general" ? isGeneralNumberFormat(currentFormat) : p.code === currentFormat
|
|
3026
|
-
);
|
|
3027
|
-
NUMBER_FORMAT_PRESETS.forEach((preset, i) => {
|
|
3028
|
-
items.push({
|
|
3029
|
-
id: `number-format-${preset.id}`,
|
|
3030
|
-
label: `Format cells: ${preset.label}`,
|
|
3031
|
-
separatorBefore: i === 0,
|
|
3032
|
-
checked: presetMatch === preset,
|
|
3033
|
-
run: () => {
|
|
3034
|
-
controller.setCellNumberFormat(selection, preset.code);
|
|
3035
|
-
}
|
|
3036
|
-
});
|
|
3037
|
-
});
|
|
3038
|
-
items.push({
|
|
3039
|
-
id: "number-format-custom",
|
|
3040
|
-
label: "Format cells: Custom\u2026",
|
|
3041
|
-
checked: !presetMatch,
|
|
3042
|
-
input: { label: "Number format code, e.g. #,##0.00;[Red](#,##0.00)", defaultValue: currentFormat ?? "General" },
|
|
3043
|
-
run: (code) => {
|
|
3044
|
-
if (code === void 0) return;
|
|
3045
|
-
controller.setCellNumberFormat(selection, code.trim() === "" ? null : code);
|
|
3046
|
-
}
|
|
3047
|
-
});
|
|
3048
|
-
if (grouping) {
|
|
3049
|
-
items.push(
|
|
3050
|
-
{
|
|
3051
|
-
id: "ungroup",
|
|
3052
|
-
label: "Remove categories",
|
|
3053
|
-
separatorBefore: true,
|
|
3054
|
-
run: () => controller.ungroupRows()
|
|
3055
|
-
},
|
|
3056
|
-
...["sum", "count", "average"].map((aggregate) => ({
|
|
3057
|
-
id: `summary-${aggregate}`,
|
|
3058
|
-
label: `Summarize column ${colName}: ${aggregate.toUpperCase()}`,
|
|
3059
|
-
run: () => controller.setGroupSummary(column.id, aggregate)
|
|
3060
|
-
}))
|
|
3061
|
-
);
|
|
3062
|
-
} else {
|
|
3063
|
-
items.push({
|
|
3064
|
-
id: "group-by",
|
|
3065
|
-
label: `Categorize by column ${colName}`,
|
|
3066
|
-
separatorBefore: true,
|
|
3067
|
-
run: () => controller.groupRowsBy(column.id)
|
|
3068
|
-
});
|
|
3069
|
-
}
|
|
3070
|
-
return items;
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
// src/spreadsheet/actions.ts
|
|
3074
|
-
var SPREADSHEET_TABLE_ACTIONS = [
|
|
3075
|
-
"sortRows",
|
|
3076
|
-
"getFilterState",
|
|
3077
|
-
"setFilterRules",
|
|
3078
|
-
"addFilterRule",
|
|
3079
|
-
"updateFilterRule",
|
|
3080
|
-
"removeFilterRule",
|
|
3081
|
-
"clearFilters",
|
|
3082
|
-
"setFiltersEnabled",
|
|
3083
|
-
"toggleFilters",
|
|
3084
|
-
"setHeaderRowCount",
|
|
3085
|
-
"setFooterRowCount",
|
|
3086
|
-
"setHeaderColumnCount",
|
|
3087
|
-
"setFrozenRowCount",
|
|
3088
|
-
"setFrozenColumnCount",
|
|
3089
|
-
"hideRows",
|
|
3090
|
-
"unhideRows",
|
|
3091
|
-
"hideColumns",
|
|
3092
|
-
"unhideColumns",
|
|
3093
|
-
"unhideAll",
|
|
3094
|
-
"getMerges",
|
|
3095
|
-
"getMergeAt",
|
|
3096
|
-
"mergeCells",
|
|
3097
|
-
"unmergeCells",
|
|
3098
|
-
"autofill",
|
|
3099
|
-
"groupRowsBy",
|
|
3100
|
-
"ungroupRows",
|
|
3101
|
-
"setGroupSummary",
|
|
3102
|
-
"setGroupCollapsed",
|
|
3103
|
-
"toggleGroupCollapsed",
|
|
3104
|
-
"getGroups",
|
|
3105
|
-
"transposeRange",
|
|
3106
|
-
"setCellNumberFormat",
|
|
3107
|
-
"setColumnNumberFormat",
|
|
3108
|
-
"exportToCsv",
|
|
3109
|
-
"importFromCsv",
|
|
3110
|
-
"copyToClipboardData",
|
|
3111
|
-
"pasteClipboardData"
|
|
3112
|
-
];
|
|
3113
|
-
function bindSpreadsheetTableActions(controller) {
|
|
3114
|
-
const actions = {};
|
|
3115
|
-
for (const name of SPREADSHEET_TABLE_ACTIONS) {
|
|
3116
|
-
actions[name] = controller[name].bind(controller);
|
|
3117
|
-
}
|
|
3118
|
-
return actions;
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
|
-
// src/spreadsheet/import.ts
|
|
3122
|
-
function delimitedAdapter(id, label, delimiter, mimeTypes, extensions) {
|
|
3123
|
-
return {
|
|
3124
|
-
id,
|
|
3125
|
-
label,
|
|
3126
|
-
mimeTypes,
|
|
3127
|
-
extensions,
|
|
3128
|
-
target: "spreadsheet",
|
|
3129
|
-
async import(_input, context) {
|
|
3130
|
-
return { rows: parseCsv(await context.readText(), { delimiter }), delimiter };
|
|
3131
|
-
}
|
|
3132
|
-
};
|
|
3133
|
-
}
|
|
3134
|
-
var csvImportAdapter = delimitedAdapter(
|
|
3135
|
-
"csv",
|
|
3136
|
-
"CSV",
|
|
3137
|
-
",",
|
|
3138
|
-
["text/csv", "application/csv", "text/comma-separated-values"],
|
|
3139
|
-
[".csv"]
|
|
3140
|
-
);
|
|
3141
|
-
var tsvImportAdapter = delimitedAdapter(
|
|
3142
|
-
"tsv",
|
|
3143
|
-
"Tab-separated values",
|
|
3144
|
-
" ",
|
|
3145
|
-
["text/tab-separated-values"],
|
|
3146
|
-
[".tsv", ".tab"]
|
|
3147
|
-
);
|
|
3148
|
-
var SPREADSHEET_IMPORT_ADAPTERS = [
|
|
3149
|
-
csvImportAdapter,
|
|
3150
|
-
tsvImportAdapter
|
|
3151
|
-
];
|
|
3152
|
-
function registerSpreadsheetImportAdapters(registry) {
|
|
3153
|
-
const unregisters = SPREADSHEET_IMPORT_ADAPTERS.map(
|
|
3154
|
-
(adapter) => registry.register(adapter, { replace: true })
|
|
3155
|
-
);
|
|
3156
|
-
return () => unregisters.forEach((unregister) => unregister());
|
|
3157
|
-
}
|
|
3158
|
-
async function importFileIntoSpreadsheet(controller, input, options) {
|
|
3159
|
-
const { registry, startRow, startCol, ...rest } = options;
|
|
3160
|
-
const outcome = await registry.importFile(input, { ...rest, target: "spreadsheet" });
|
|
3161
|
-
controller.importRows(outcome.result.rows, { startRow, startCol });
|
|
3162
|
-
return outcome;
|
|
3163
|
-
}
|
|
3164
|
-
|
|
3165
|
-
export {
|
|
3166
|
-
columnDependencyKey,
|
|
3167
|
-
extractCellDependencies,
|
|
3168
|
-
DependencyGraph,
|
|
3169
|
-
shiftFormulaReferences,
|
|
3170
|
-
mapFormulaReferences,
|
|
3171
|
-
offsetFormulaReferences,
|
|
3172
|
-
remapFormulaReferences,
|
|
3173
|
-
getFormulaCaretContext,
|
|
3174
|
-
getFormulaHints,
|
|
3175
|
-
applyFormulaSuggestion,
|
|
3176
|
-
NUMBER_FORMAT_PRESETS,
|
|
3177
|
-
numberFormatForCellFormat,
|
|
3178
|
-
cellFormatForNumberFormat,
|
|
3179
|
-
isGeneralNumberFormat,
|
|
3180
|
-
formatCellForDisplay,
|
|
3181
|
-
formatCellDisplay,
|
|
3182
|
-
formatValueWithCode,
|
|
3183
|
-
DataSourceBinder,
|
|
3184
|
-
normalizeRange,
|
|
3185
|
-
rangeContains,
|
|
3186
|
-
rangesIntersect,
|
|
3187
|
-
rangeContainsRange,
|
|
3188
|
-
isSingleCellRange,
|
|
3189
|
-
isBlankValue,
|
|
3190
|
-
toNumber,
|
|
3191
|
-
compareCellValues,
|
|
3192
|
-
matchesFilterRule,
|
|
3193
|
-
aggregateValues,
|
|
3194
|
-
roundFloat,
|
|
3195
|
-
fillSeries,
|
|
3196
|
-
computeFillTarget,
|
|
3197
|
-
getFillDirection,
|
|
3198
|
-
SPREADSHEET_CLIPBOARD_MIME,
|
|
3199
|
-
serializeHtmlTable,
|
|
3200
|
-
parseHtmlTable,
|
|
3201
|
-
createSpreadsheetClipboardData,
|
|
3202
|
-
readSpreadsheetClipboard,
|
|
3203
|
-
rebaseClipboardRows,
|
|
3204
|
-
DEFAULT_ROW_HEIGHT,
|
|
3205
|
-
DEFAULT_COLUMN_WIDTH,
|
|
3206
|
-
getTableSections,
|
|
3207
|
-
getRowSection,
|
|
3208
|
-
rowPassesFilters,
|
|
3209
|
-
computeRowGroups,
|
|
3210
|
-
buildSpreadsheetView,
|
|
3211
|
-
SpreadsheetController,
|
|
3212
|
-
runSpreadsheetMenuItem,
|
|
3213
|
-
clampMenuPosition,
|
|
3214
|
-
getSpreadsheetMenuItems,
|
|
3215
|
-
SPREADSHEET_TABLE_ACTIONS,
|
|
3216
|
-
bindSpreadsheetTableActions,
|
|
3217
|
-
csvImportAdapter,
|
|
3218
|
-
tsvImportAdapter,
|
|
3219
|
-
SPREADSHEET_IMPORT_ADAPTERS,
|
|
3220
|
-
registerSpreadsheetImportAdapters,
|
|
3221
|
-
importFileIntoSpreadsheet
|
|
3222
|
-
};
|
|
3223
|
-
//# sourceMappingURL=chunk-U5KWZU4X.js.map
|