@pixerate/editor 0.24.0 → 0.25.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 +71 -1
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/agent/index.cjs +10 -9
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +4 -3
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +3173 -340
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +617 -4
- package/dist/canvas/index.d.ts +617 -4
- package/dist/canvas/index.js +3105 -272
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-4BVP64JJ.cjs +1406 -0
- package/dist/chunk-4BVP64JJ.cjs.map +1 -0
- package/dist/{chunk-XNORY7M6.cjs → chunk-7WISYZFO.cjs} +105 -50
- package/dist/chunk-7WISYZFO.cjs.map +1 -0
- package/dist/{chunk-OLZJXL4R.cjs → chunk-AL6JMGWW.cjs} +59 -25
- package/dist/chunk-AL6JMGWW.cjs.map +1 -0
- package/dist/chunk-BVP3TSLH.cjs +69 -0
- package/dist/chunk-BVP3TSLH.cjs.map +1 -0
- package/dist/{chunk-MCSJFWAO.cjs → chunk-C7P22VIP.cjs} +670 -96
- package/dist/chunk-C7P22VIP.cjs.map +1 -0
- package/dist/{chunk-PGNZKFQS.js → chunk-ECCCYKGL.js} +76 -21
- package/dist/chunk-ECCCYKGL.js.map +1 -0
- package/dist/{chunk-3CN2WD3X.js → chunk-FH6F67DN.js} +646 -72
- package/dist/chunk-FH6F67DN.js.map +1 -0
- package/dist/chunk-HRMEDT2L.js +1406 -0
- package/dist/chunk-HRMEDT2L.js.map +1 -0
- package/dist/{chunk-ITBSKI26.js → chunk-IXHSYOVY.js} +941 -9
- package/dist/chunk-IXHSYOVY.js.map +1 -0
- package/dist/{chunk-FODNLX5N.js → chunk-NAXVVS5I.js} +38 -4
- package/dist/chunk-NAXVVS5I.js.map +1 -0
- package/dist/chunk-O5RYH2PG.js +69 -0
- package/dist/chunk-O5RYH2PG.js.map +1 -0
- package/dist/{chunk-SBCZ6RN5.cjs → chunk-PO6A6QQH.cjs} +1039 -107
- package/dist/chunk-PO6A6QQH.cjs.map +1 -0
- package/dist/{chunk-3LV6RRPH.js → chunk-QGYDWPRM.js} +2 -2
- package/dist/{chunk-U5KWZU4X.js → chunk-RR7F5GUM.js} +2632 -129
- package/dist/chunk-RR7F5GUM.js.map +1 -0
- package/dist/{chunk-5S3Z4PPF.js → chunk-UAKDG62Z.js} +232 -25
- package/dist/chunk-UAKDG62Z.js.map +1 -0
- package/dist/{chunk-SML64VJG.cjs → chunk-V3CHHWOE.cjs} +4 -4
- package/dist/{chunk-SML64VJG.cjs.map → chunk-V3CHHWOE.cjs.map} +1 -1
- package/dist/chunk-WSQKHXW7.cjs +5726 -0
- package/dist/chunk-WSQKHXW7.cjs.map +1 -0
- package/dist/{chunk-D27YFC46.cjs → chunk-X4N4WUKB.cjs} +267 -60
- package/dist/chunk-X4N4WUKB.cjs.map +1 -0
- package/dist/commands/index.cjs +74 -1
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +34 -1
- package/dist/commands/index.d.ts +34 -1
- package/dist/commands/index.js +73 -0
- 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-BIJiNoVs.d.cts +1842 -0
- package/dist/controller-BIJiNoVs.d.ts +1842 -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/extensions/index.cjs +53 -6
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +12 -4
- package/dist/extensions/index.d.ts +12 -4
- package/dist/extensions/index.js +53 -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-7a6bHTLy.d.ts} +494 -5
- package/dist/{index-BhJBSm2d.d.cts → index-Csd5qI8u.d.cts} +494 -5
- package/dist/index.cjs +1491 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -19
- package/dist/index.d.ts +110 -19
- package/dist/index.js +1464 -18
- 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-GJI4gToU.d.cts → registry-CEgwXED1.d.cts} +12 -1
- package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.ts} +12 -1
- package/dist/serializers/index.cjs +12 -4
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +39 -12
- package/dist/serializers/index.d.ts +39 -12
- package/dist/serializers/index.js +12 -4
- package/dist/spreadsheet/index.cjs +127 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +431 -11
- package/dist/spreadsheet/index.d.ts +431 -11
- package/dist/spreadsheet/index.js +127 -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 +6 -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.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/{chunk-3LV6RRPH.js.map → chunk-QGYDWPRM.js.map} +0 -0
|
@@ -19,30 +19,64 @@ import {
|
|
|
19
19
|
indexToColName,
|
|
20
20
|
isBlank,
|
|
21
21
|
isFormulaError,
|
|
22
|
+
numberToText,
|
|
22
23
|
numericValue,
|
|
23
24
|
parseCellInput,
|
|
24
25
|
parseFormula,
|
|
25
26
|
parseIsoDate,
|
|
26
27
|
searchFormulaFunctions,
|
|
28
|
+
serialFromYmd,
|
|
29
|
+
serialToDate,
|
|
27
30
|
sortCompareValues,
|
|
31
|
+
toBoolean,
|
|
28
32
|
toText,
|
|
29
33
|
tokenizeFormula
|
|
30
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-FH6F67DN.js";
|
|
31
35
|
|
|
32
36
|
// src/spreadsheet/formula/dependency-graph.ts
|
|
37
|
+
function extractNameDependencies(node) {
|
|
38
|
+
const names = [];
|
|
39
|
+
const stack = [node];
|
|
40
|
+
while (stack.length > 0) {
|
|
41
|
+
const curr = stack.pop();
|
|
42
|
+
switch (curr.type) {
|
|
43
|
+
case "Name":
|
|
44
|
+
names.push(curr.sheet === void 0 ? { name: curr.name, call: false } : { name: curr.name, sheet: curr.sheet, call: false });
|
|
45
|
+
break;
|
|
46
|
+
case "FunctionCall":
|
|
47
|
+
names.push({ name: curr.name, call: true });
|
|
48
|
+
for (const arg of curr.args) stack.push(arg);
|
|
49
|
+
break;
|
|
50
|
+
case "Invoke":
|
|
51
|
+
stack.push(curr.callee);
|
|
52
|
+
for (const arg of curr.args) stack.push(arg);
|
|
53
|
+
break;
|
|
54
|
+
case "BinaryOp":
|
|
55
|
+
stack.push(curr.left, curr.right);
|
|
56
|
+
break;
|
|
57
|
+
case "UnaryOp":
|
|
58
|
+
stack.push(curr.argument);
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
33
66
|
var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
|
|
34
67
|
function extractCellDependencies(node) {
|
|
35
68
|
const deps = [];
|
|
36
69
|
const visit = (curr) => {
|
|
70
|
+
const sheet = "sheet" in curr && curr.sheet !== void 0 ? "sheetEnd" in curr && curr.sheetEnd !== void 0 ? { sheet: curr.sheet, sheetEnd: curr.sheetEnd } : { sheet: curr.sheet } : {};
|
|
37
71
|
if (curr.type === "CellRef") {
|
|
38
|
-
deps.push({ col: curr.col, row: curr.row });
|
|
72
|
+
deps.push({ col: curr.col, row: curr.row, ...sheet });
|
|
39
73
|
} else if (curr.type === "SpillRef") {
|
|
40
|
-
deps.push({ col: curr.col, row: curr.row });
|
|
74
|
+
deps.push({ col: curr.col, row: curr.row, ...sheet });
|
|
41
75
|
} else if (curr.type === "Range" && curr.endRow >= WHOLE_COLUMN_END_ROW) {
|
|
42
76
|
const startC = colNameToIndex(curr.startCol);
|
|
43
77
|
const endC = colNameToIndex(curr.endCol);
|
|
44
78
|
for (let c = Math.min(startC, endC); c <= Math.max(startC, endC); c++) {
|
|
45
|
-
deps.push({ col: indexToColName(c), row: 0 });
|
|
79
|
+
deps.push({ col: indexToColName(c), row: 0, ...sheet });
|
|
46
80
|
}
|
|
47
81
|
} else if (curr.type === "Range") {
|
|
48
82
|
const startC = colNameToIndex(curr.startCol);
|
|
@@ -53,7 +87,7 @@ function extractCellDependencies(node) {
|
|
|
53
87
|
const maxR = Math.max(curr.startRow, curr.endRow);
|
|
54
88
|
for (let c = minC; c <= maxC; c++) {
|
|
55
89
|
for (let r = minR; r <= maxR; r++) {
|
|
56
|
-
deps.push({ col: indexToColName(c), row: r });
|
|
90
|
+
deps.push({ col: indexToColName(c), row: r, ...sheet });
|
|
57
91
|
}
|
|
58
92
|
}
|
|
59
93
|
} else if (curr.type === "BinaryOp") {
|
|
@@ -63,6 +97,9 @@ function extractCellDependencies(node) {
|
|
|
63
97
|
visit(curr.argument);
|
|
64
98
|
} else if (curr.type === "FunctionCall") {
|
|
65
99
|
curr.args.forEach(visit);
|
|
100
|
+
} else if (curr.type === "Invoke") {
|
|
101
|
+
visit(curr.callee);
|
|
102
|
+
curr.args.forEach(visit);
|
|
66
103
|
}
|
|
67
104
|
};
|
|
68
105
|
visit(node);
|
|
@@ -81,8 +118,29 @@ var DependencyGraph = class {
|
|
|
81
118
|
spillDependents = /* @__PURE__ */ new Map();
|
|
82
119
|
// anchor -> the edges it registered (so they can be replaced).
|
|
83
120
|
spillEdges = /* @__PURE__ */ new Map();
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Sets the cells `cellKey` depends on directly (keys such as `B2`, or a
|
|
123
|
+
* column key from `columnDependencyKey`), replacing its previous
|
|
124
|
+
* dependencies. Used by spreadsheets whose keys carry more than A1 text
|
|
125
|
+
* (e.g. sheet-qualified keys); `setCellFormula` derives them from a formula.
|
|
126
|
+
*/
|
|
127
|
+
setCellDependencies(cellKey2, dependencyKeys) {
|
|
128
|
+
const key = cellKey2.toUpperCase();
|
|
129
|
+
for (const dep of this.dependencies.get(key) ?? []) {
|
|
130
|
+
this.dependents.get(dep)?.delete(key);
|
|
131
|
+
}
|
|
132
|
+
this.dependencies.delete(key);
|
|
133
|
+
const deps = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const raw of dependencyKeys) {
|
|
135
|
+
const dep = raw.toUpperCase();
|
|
136
|
+
deps.add(dep);
|
|
137
|
+
if (!this.dependents.has(dep)) this.dependents.set(dep, /* @__PURE__ */ new Set());
|
|
138
|
+
this.dependents.get(dep).add(key);
|
|
139
|
+
}
|
|
140
|
+
if (deps.size > 0) this.dependencies.set(key, deps);
|
|
141
|
+
}
|
|
142
|
+
setCellFormula(cellKey2, formulaStr) {
|
|
143
|
+
const key = cellKey2.toUpperCase();
|
|
86
144
|
const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
|
|
87
145
|
for (const dep of oldDeps) {
|
|
88
146
|
this.dependents.get(dep)?.delete(key);
|
|
@@ -146,20 +204,20 @@ var DependencyGraph = class {
|
|
|
146
204
|
return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);
|
|
147
205
|
}
|
|
148
206
|
/** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */
|
|
149
|
-
getDependencies(
|
|
150
|
-
const key =
|
|
207
|
+
getDependencies(cellKey2) {
|
|
208
|
+
const key = cellKey2.toUpperCase();
|
|
151
209
|
return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));
|
|
152
210
|
}
|
|
153
211
|
/** Cells depending on `cellKey`, through formulas or spills. */
|
|
154
|
-
getDependents(
|
|
155
|
-
const key =
|
|
212
|
+
getDependents(cellKey2) {
|
|
213
|
+
const key = cellKey2.toUpperCase();
|
|
156
214
|
return unionOf(this.dependents.get(key), this.spillDependents.get(key));
|
|
157
215
|
}
|
|
158
216
|
/**
|
|
159
217
|
* Detects if there is a cycle containing cellKey.
|
|
160
218
|
*/
|
|
161
|
-
hasCycle(
|
|
162
|
-
const key =
|
|
219
|
+
hasCycle(cellKey2) {
|
|
220
|
+
const key = cellKey2.toUpperCase();
|
|
163
221
|
const visited = /* @__PURE__ */ new Set();
|
|
164
222
|
const recStack = /* @__PURE__ */ new Set();
|
|
165
223
|
const dfs = (node) => {
|
|
@@ -390,8 +448,11 @@ function rewriteReference(text, change) {
|
|
|
390
448
|
const newEnd = { ...end, [key]: startValue <= endValue ? hi : lo };
|
|
391
449
|
return `${formatPart(newStart)}:${formatPart(newEnd)}`;
|
|
392
450
|
}
|
|
393
|
-
function shiftFormulaReferences(formula, change) {
|
|
394
|
-
return mapFormulaReferences(
|
|
451
|
+
function shiftFormulaReferences(formula, change, options = {}) {
|
|
452
|
+
return mapFormulaReferences(
|
|
453
|
+
formula,
|
|
454
|
+
(text, info) => !options.appliesTo || options.appliesTo(info.sheet, info.sheetEnd) ? rewriteReference(text, change) : text
|
|
455
|
+
);
|
|
395
456
|
}
|
|
396
457
|
function mapFormulaReferences(formula, rewrite) {
|
|
397
458
|
if (!formula.startsWith("=")) return formula;
|
|
@@ -407,16 +468,20 @@ function mapFormulaReferences(formula, rewrite) {
|
|
|
407
468
|
const token = tokens[i];
|
|
408
469
|
if (token.type !== "CELL_REF" && token.type !== "RANGE" && token.type !== "SPILL_REF") continue;
|
|
409
470
|
if (tokens[i + 1]?.type === "LPAREN") continue;
|
|
410
|
-
|
|
471
|
+
const refStart = token.refStart ?? token.position;
|
|
472
|
+
let end = refStart;
|
|
411
473
|
while (end < formula.length && REF_CHAR.test(formula[end])) end++;
|
|
412
|
-
const original = formula.slice(
|
|
474
|
+
const original = formula.slice(refStart, end);
|
|
475
|
+
const info = token.sheet === void 0 ? {} : token.sheetEnd === void 0 ? { sheet: token.sheet } : { sheet: token.sheet, sheetEnd: token.sheetEnd };
|
|
476
|
+
const prefix = formula.slice(token.position, refStart);
|
|
413
477
|
if (token.type === "SPILL_REF") {
|
|
414
|
-
const
|
|
415
|
-
result += formula.slice(cursor, token.position) + (
|
|
478
|
+
const rewritten2 = rewrite(original, info);
|
|
479
|
+
result += formula.slice(cursor, token.position) + (rewritten2 === "#REF!" ? rewritten2 : `${prefix}${rewritten2}#`);
|
|
416
480
|
cursor = end + 1;
|
|
417
481
|
continue;
|
|
418
482
|
}
|
|
419
|
-
|
|
483
|
+
const rewritten = rewrite(original, info);
|
|
484
|
+
result += formula.slice(cursor, token.position) + (rewritten === "#REF!" ? rewritten : prefix + rewritten);
|
|
420
485
|
cursor = end;
|
|
421
486
|
}
|
|
422
487
|
return result + formula.slice(cursor);
|
|
@@ -437,8 +502,9 @@ function offsetFormulaReferences(formula, rowOffset, colOffset) {
|
|
|
437
502
|
});
|
|
438
503
|
}
|
|
439
504
|
var MAX_REMAP_RANGE_CELLS = 1e5;
|
|
440
|
-
function remapFormulaReferences(formula, moveCell) {
|
|
441
|
-
return mapFormulaReferences(formula, (text) => {
|
|
505
|
+
function remapFormulaReferences(formula, moveCell, options = {}) {
|
|
506
|
+
return mapFormulaReferences(formula, (text, info) => {
|
|
507
|
+
if (options.appliesTo && !options.appliesTo(info.sheet, info.sheetEnd)) return text;
|
|
442
508
|
const parts = text.split(":").map(parsePart);
|
|
443
509
|
if (parts.some((p) => p === null)) return text;
|
|
444
510
|
const [start, end] = parts;
|
|
@@ -475,6 +541,101 @@ function remapFormulaReferences(formula, moveCell) {
|
|
|
475
541
|
return `${formatPart({ ...start, row: minR, col: minC })}:${formatPart({ ...end, row: maxR, col: maxC })}`;
|
|
476
542
|
});
|
|
477
543
|
}
|
|
544
|
+
var CELL_LIKE = /^[A-Za-z]{1,3}\d{1,7}$/;
|
|
545
|
+
var isCellLike = (name) => {
|
|
546
|
+
if (!CELL_LIKE.test(name)) return false;
|
|
547
|
+
const letters = name.match(/^[A-Za-z]+/)[0].toUpperCase();
|
|
548
|
+
const col = Array.from(letters).reduce((n, ch) => n * 26 + ch.charCodeAt(0) - 64, 0);
|
|
549
|
+
const row = Number(name.slice(letters.length));
|
|
550
|
+
return col <= 16384 && row >= 1 && row <= 1048576;
|
|
551
|
+
};
|
|
552
|
+
var R1C1_LIKE = /^[Rr]\d*(?:[Cc]\d*)?$|^[Cc]\d*$/;
|
|
553
|
+
function sheetNameNeedsQuotes(name) {
|
|
554
|
+
if (!/^[A-Za-z_\p{L}][\p{L}\p{N}_.]*$/u.test(name)) return true;
|
|
555
|
+
const upper = name.toUpperCase();
|
|
556
|
+
return isCellLike(name) || R1C1_LIKE.test(name) || upper === "TRUE" || upper === "FALSE";
|
|
557
|
+
}
|
|
558
|
+
function quoteSheetName(name) {
|
|
559
|
+
return sheetNameNeedsQuotes(name) ? `'${name.replace(/'/g, "''")}'` : name;
|
|
560
|
+
}
|
|
561
|
+
function formatSheetPrefix(sheet, sheetEnd) {
|
|
562
|
+
if (sheetEnd === void 0) return `${quoteSheetName(sheet)}!`;
|
|
563
|
+
const quoted = sheetNameNeedsQuotes(sheet) || sheetNameNeedsQuotes(sheetEnd);
|
|
564
|
+
return quoted ? `'${`${sheet}:${sheetEnd}`.replace(/'/g, "''")}'!` : `${sheet}:${sheetEnd}!`;
|
|
565
|
+
}
|
|
566
|
+
function tokensOf(formula) {
|
|
567
|
+
if (!formula.startsWith("=")) return null;
|
|
568
|
+
try {
|
|
569
|
+
return tokenizeFormula(formula);
|
|
570
|
+
} catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function rewriteTokens(formula, edit) {
|
|
575
|
+
const tokens = tokensOf(formula);
|
|
576
|
+
if (!tokens) return formula;
|
|
577
|
+
let result = "";
|
|
578
|
+
let cursor = 0;
|
|
579
|
+
for (const token of tokens) {
|
|
580
|
+
const change = edit(token);
|
|
581
|
+
if (!change) continue;
|
|
582
|
+
result += formula.slice(cursor, change.from) + change.text;
|
|
583
|
+
cursor = change.to;
|
|
584
|
+
}
|
|
585
|
+
return cursor === 0 ? formula : result + formula.slice(cursor);
|
|
586
|
+
}
|
|
587
|
+
var sameName = (a, b) => a.toUpperCase() === b.toUpperCase();
|
|
588
|
+
function renameSheetInFormula(formula, oldName, newName) {
|
|
589
|
+
return rewriteTokens(formula, (token) => {
|
|
590
|
+
if (token.sheet === void 0 || token.refStart === void 0) return null;
|
|
591
|
+
const first2 = sameName(token.sheet, oldName);
|
|
592
|
+
const last = token.sheetEnd !== void 0 && sameName(token.sheetEnd, oldName);
|
|
593
|
+
if (!first2 && !last) return null;
|
|
594
|
+
const sheet = first2 ? newName : token.sheet;
|
|
595
|
+
const sheetEnd = token.sheetEnd === void 0 ? void 0 : last ? newName : token.sheetEnd;
|
|
596
|
+
return { from: token.position, to: token.refStart, text: formatSheetPrefix(sheet, sheetEnd) };
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function deleteSheetInFormula(formula, name, order = []) {
|
|
600
|
+
const position = (sheet) => order.findIndex((s) => sameName(s, sheet));
|
|
601
|
+
return rewriteTokens(formula, (token) => {
|
|
602
|
+
if (token.sheet === void 0 || token.refStart === void 0) return null;
|
|
603
|
+
const end = token.end ?? token.refStart;
|
|
604
|
+
if (token.sheetEnd === void 0) {
|
|
605
|
+
return sameName(token.sheet, name) ? { from: token.position, to: end, text: "#REF!" } : null;
|
|
606
|
+
}
|
|
607
|
+
const first2 = sameName(token.sheet, name);
|
|
608
|
+
const last = sameName(token.sheetEnd, name);
|
|
609
|
+
if (!first2 && !last) return null;
|
|
610
|
+
const a = position(token.sheet);
|
|
611
|
+
const b = position(token.sheetEnd);
|
|
612
|
+
if (a < 0 || b < 0 || a === b) return { from: token.position, to: end, text: "#REF!" };
|
|
613
|
+
const inward = (from, to) => order[from < to ? from + 1 : from - 1];
|
|
614
|
+
const newFirst = first2 ? inward(a, b) : token.sheet;
|
|
615
|
+
const newLast = last ? inward(b, a) : token.sheetEnd;
|
|
616
|
+
const prefix = sameName(newFirst, newLast) ? formatSheetPrefix(newFirst) : formatSheetPrefix(newFirst, newLast);
|
|
617
|
+
return { from: token.position, to: token.refStart, text: prefix };
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function renameNameInFormula(formula, oldName, newName, options = {}) {
|
|
621
|
+
return rewriteTokens(formula, (token) => {
|
|
622
|
+
if (token.type !== "IDENTIFIER" || !sameName(token.value, oldName)) return null;
|
|
623
|
+
if (token.sheet !== void 0 ? options.appliesTo && !options.appliesTo(token.sheet) : options.qualifiedOnly) return null;
|
|
624
|
+
const from = token.refStart ?? token.position;
|
|
625
|
+
return { from, to: token.end ?? from + oldName.length, text: newName };
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function formulaIdentifiers(formula) {
|
|
629
|
+
const tokens = tokensOf(formula);
|
|
630
|
+
if (!tokens) return [];
|
|
631
|
+
const out = [];
|
|
632
|
+
tokens.forEach((token, i) => {
|
|
633
|
+
if (token.type !== "IDENTIFIER") return;
|
|
634
|
+
const call = tokens[i + 1]?.type === "LPAREN";
|
|
635
|
+
out.push(token.sheet === void 0 ? { name: token.value, call } : { name: token.value, sheet: token.sheet, call });
|
|
636
|
+
});
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
478
639
|
|
|
479
640
|
// src/spreadsheet/formula/caret.ts
|
|
480
641
|
var EMPTY = { prefix: null, call: null };
|
|
@@ -544,9 +705,20 @@ function getFormulaHints(input, caret = input.length, options = {}) {
|
|
|
544
705
|
let suggestions = [];
|
|
545
706
|
if (context.prefix) {
|
|
546
707
|
const query = context.prefix.text.toUpperCase();
|
|
547
|
-
|
|
708
|
+
const names = (options.names ?? []).filter((n) => n.name.toUpperCase().startsWith(query)).map((n) => ({
|
|
709
|
+
name: n.name,
|
|
710
|
+
signature: n.isLambda ? `${n.name}(\u2026)` : n.name,
|
|
711
|
+
description: n.description ?? "",
|
|
712
|
+
category: "lookup",
|
|
713
|
+
kind: "name",
|
|
714
|
+
...n.isLambda ? { isLambda: true } : {}
|
|
715
|
+
}));
|
|
716
|
+
suggestions = [
|
|
717
|
+
...names,
|
|
718
|
+
...searchFormulaFunctions(query, { limit: void 0 }).filter((def) => def.name.startsWith(query) && !names.some((n) => n.name.toUpperCase() === def.name)).map(toSuggestion)
|
|
719
|
+
].slice(0, options.limit ?? 8);
|
|
548
720
|
const followedByParen = input.slice(context.prefix.end).trimStart().startsWith("(");
|
|
549
|
-
if (followedByParen || suggestions.length === 1 && suggestions[0].name === query) suggestions = [];
|
|
721
|
+
if (followedByParen || suggestions.length === 1 && suggestions[0].name.toUpperCase() === query) suggestions = [];
|
|
550
722
|
}
|
|
551
723
|
let activeCall = null;
|
|
552
724
|
if (context.call) {
|
|
@@ -565,12 +737,20 @@ function getFormulaHints(input, caret = input.length, options = {}) {
|
|
|
565
737
|
}
|
|
566
738
|
return { context, suggestions, activeCall };
|
|
567
739
|
}
|
|
568
|
-
function applyFormulaSuggestion(input, caret,
|
|
740
|
+
function applyFormulaSuggestion(input, caret, suggestion, options = {}) {
|
|
569
741
|
const context = getFormulaCaretContext(input, caret);
|
|
570
742
|
const start = context.prefix?.start ?? caret;
|
|
571
743
|
const end = context.prefix?.end ?? caret;
|
|
744
|
+
const definedName = typeof suggestion !== "string" && suggestion.kind === "name";
|
|
745
|
+
const name = typeof suggestion === "string" ? suggestion : suggestion.name;
|
|
746
|
+
const call = options.call ?? (definedName ? !!suggestion.isLambda : true);
|
|
747
|
+
if (!call) {
|
|
748
|
+
const value2 = input.slice(0, start) + name + input.slice(end);
|
|
749
|
+
return { value: value2, caret: start + name.length };
|
|
750
|
+
}
|
|
572
751
|
const hasParen = input[end] === "(";
|
|
573
|
-
const
|
|
752
|
+
const spelled = definedName ? name : name.toUpperCase();
|
|
753
|
+
const insert = hasParen ? spelled : `${spelled}(`;
|
|
574
754
|
const value = input.slice(0, start) + insert + input.slice(end);
|
|
575
755
|
return { value, caret: start + insert.length + (hasParen ? 1 : 0) };
|
|
576
756
|
}
|
|
@@ -707,6 +887,61 @@ var DataSourceBinder = class {
|
|
|
707
887
|
}
|
|
708
888
|
};
|
|
709
889
|
|
|
890
|
+
// src/spreadsheet/workbook.ts
|
|
891
|
+
var MAX_SHEET_NAME_LENGTH = 31;
|
|
892
|
+
var MAX_DEFINED_NAME_LENGTH = 255;
|
|
893
|
+
var sameName2 = (a, b) => a.toUpperCase() === b.toUpperCase();
|
|
894
|
+
function validateSheetName(name, existingNames = []) {
|
|
895
|
+
const length = Array.from(name).length;
|
|
896
|
+
if (length === 0 || name.trim() === "") return "A sheet name can't be blank.";
|
|
897
|
+
if (length > MAX_SHEET_NAME_LENGTH) return `A sheet name can't be longer than ${MAX_SHEET_NAME_LENGTH} characters.`;
|
|
898
|
+
if (/[:\\/?*[\]]/.test(name)) return "A sheet name can't contain any of these characters: : \\ / ? * [ ]";
|
|
899
|
+
if (name.startsWith("'") || name.endsWith("'")) return "A sheet name can't begin or end with an apostrophe.";
|
|
900
|
+
if (sameName2(name, "History")) return '"History" is a reserved name.';
|
|
901
|
+
if (existingNames.some((existing) => sameName2(existing, name))) return "That name is already taken. Try a different one.";
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
function nextSheetName(existingNames, base = "Sheet") {
|
|
905
|
+
for (let n = 1; ; n++) {
|
|
906
|
+
const candidate = `${base} ${n}`;
|
|
907
|
+
if (!existingNames.some((existing) => sameName2(existing, candidate))) return candidate;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function copySheetName(name, existingNames) {
|
|
911
|
+
for (let k = 2; ; k++) {
|
|
912
|
+
const suffix = ` (${k})`;
|
|
913
|
+
const keep = Math.max(0, MAX_SHEET_NAME_LENGTH - suffix.length);
|
|
914
|
+
const candidate = `${Array.from(name).slice(0, keep).join("").trimEnd()}${suffix}`;
|
|
915
|
+
if (!existingNames.some((existing) => sameName2(existing, candidate))) return candidate;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function isR1C1Like(name) {
|
|
919
|
+
let i = 0;
|
|
920
|
+
if (/[Rr]/.test(name[i] ?? "")) {
|
|
921
|
+
i++;
|
|
922
|
+
while (i < name.length && /\d/.test(name[i])) i++;
|
|
923
|
+
}
|
|
924
|
+
if (/[Cc]/.test(name[i] ?? "")) {
|
|
925
|
+
i++;
|
|
926
|
+
while (i < name.length && /\d/.test(name[i])) i++;
|
|
927
|
+
}
|
|
928
|
+
return name.length > 0 && i === name.length;
|
|
929
|
+
}
|
|
930
|
+
function validateDefinedName(name) {
|
|
931
|
+
if (name === "") return "A name can't be blank.";
|
|
932
|
+
if (Array.from(name).length > MAX_DEFINED_NAME_LENGTH) return `A name can't be longer than ${MAX_DEFINED_NAME_LENGTH} characters.`;
|
|
933
|
+
if (!/^[\p{L}_\\][\p{L}\p{N}_.\\?]*$/u.test(name)) {
|
|
934
|
+
return /^[\p{L}_\\]/u.test(name) ? "A name can only contain letters, numbers, periods, underscores, backslashes and question marks." : "A name must start with a letter, an underscore or a backslash.";
|
|
935
|
+
}
|
|
936
|
+
if (/^\$?[A-Za-z]+\$?\d+$/.test(name)) return "A name can't look like a cell reference (such as A1).";
|
|
937
|
+
if (isR1C1Like(name)) return "A name can't look like an R1C1 reference (such as R1C1, R or C).";
|
|
938
|
+
if (sameName2(name, "TRUE") || sameName2(name, "FALSE")) return "A name can't be TRUE or FALSE.";
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
function isLambdaDefinition(definition) {
|
|
942
|
+
return /^=?\s*LAMBDA\s*\(/i.test(definition.refersTo);
|
|
943
|
+
}
|
|
944
|
+
|
|
710
945
|
// src/spreadsheet/table.ts
|
|
711
946
|
function normalizeRange(range) {
|
|
712
947
|
return {
|
|
@@ -1118,6 +1353,988 @@ function rebaseClipboardRows(rows, origin, target) {
|
|
|
1118
1353
|
);
|
|
1119
1354
|
}
|
|
1120
1355
|
|
|
1356
|
+
// src/spreadsheet/rule-ranges.ts
|
|
1357
|
+
function adjustRangeForStructuralChange(range, change) {
|
|
1358
|
+
const r = normalizeRange(range);
|
|
1359
|
+
const startKey = change.axis === "row" ? "startRow" : "startCol";
|
|
1360
|
+
const endKey = change.axis === "row" ? "endRow" : "endCol";
|
|
1361
|
+
if (change.type === "insert") {
|
|
1362
|
+
if (r[startKey] >= change.index) r[startKey]++;
|
|
1363
|
+
if (r[endKey] >= change.index) r[endKey]++;
|
|
1364
|
+
return r;
|
|
1365
|
+
}
|
|
1366
|
+
if (r[startKey] === change.index && r[endKey] === change.index) return null;
|
|
1367
|
+
if (r[startKey] > change.index) r[startKey]--;
|
|
1368
|
+
if (r[endKey] >= change.index) r[endKey]--;
|
|
1369
|
+
return r;
|
|
1370
|
+
}
|
|
1371
|
+
function adjustRangesForStructuralChange(ranges, change) {
|
|
1372
|
+
const out = [];
|
|
1373
|
+
for (const range of ranges) {
|
|
1374
|
+
const next = adjustRangeForStructuralChange(range, change);
|
|
1375
|
+
if (next) out.push(next);
|
|
1376
|
+
}
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
function subtractRange(range, cut) {
|
|
1380
|
+
const r = normalizeRange(range);
|
|
1381
|
+
const c = normalizeRange(cut);
|
|
1382
|
+
if (!rangesIntersect(r, c)) return [r];
|
|
1383
|
+
const out = [];
|
|
1384
|
+
const top = Math.max(r.startRow, c.startRow);
|
|
1385
|
+
const bottom = Math.min(r.endRow, c.endRow);
|
|
1386
|
+
if (r.startRow < c.startRow) out.push({ startRow: r.startRow, startCol: r.startCol, endRow: c.startRow - 1, endCol: r.endCol });
|
|
1387
|
+
if (r.endRow > c.endRow) out.push({ startRow: c.endRow + 1, startCol: r.startCol, endRow: r.endRow, endCol: r.endCol });
|
|
1388
|
+
if (r.startCol < c.startCol) out.push({ startRow: top, startCol: r.startCol, endRow: bottom, endCol: c.startCol - 1 });
|
|
1389
|
+
if (r.endCol > c.endCol) out.push({ startRow: top, startCol: c.endCol + 1, endRow: bottom, endCol: r.endCol });
|
|
1390
|
+
return out;
|
|
1391
|
+
}
|
|
1392
|
+
function subtractRangeFromRanges(ranges, cut) {
|
|
1393
|
+
return ranges.flatMap((range) => subtractRange(range, cut));
|
|
1394
|
+
}
|
|
1395
|
+
function rangesContain(ranges, row, col) {
|
|
1396
|
+
for (const r of ranges) {
|
|
1397
|
+
const minR = Math.min(r.startRow, r.endRow);
|
|
1398
|
+
const maxR = Math.max(r.startRow, r.endRow);
|
|
1399
|
+
const minC = Math.min(r.startCol, r.endCol);
|
|
1400
|
+
const maxC = Math.max(r.startCol, r.endCol);
|
|
1401
|
+
if (row >= minR && row <= maxR && col >= minC && col <= maxC) return true;
|
|
1402
|
+
}
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
function ruleAnchor(ranges) {
|
|
1406
|
+
const first2 = ranges[0];
|
|
1407
|
+
if (!first2) return { row: 0, col: 0 };
|
|
1408
|
+
return { row: Math.min(first2.startRow, first2.endRow), col: Math.min(first2.startCol, first2.endCol) };
|
|
1409
|
+
}
|
|
1410
|
+
var withEquals = (formula) => formula.trimStart().startsWith("=") ? formula.trimStart() : `=${formula}`;
|
|
1411
|
+
function ruleFormulaAt(formula, anchor, row, col) {
|
|
1412
|
+
return offsetFormulaReferences(withEquals(formula), row - anchor.row, col - anchor.col);
|
|
1413
|
+
}
|
|
1414
|
+
function shiftRuleFormula(formula, change) {
|
|
1415
|
+
const hadEquals = formula.trimStart().startsWith("=");
|
|
1416
|
+
const shifted = shiftFormulaReferences(withEquals(formula), change);
|
|
1417
|
+
return hadEquals ? shifted : shifted.slice(1);
|
|
1418
|
+
}
|
|
1419
|
+
function forEachRangeCell(ranges, rowCount, colCount, fn) {
|
|
1420
|
+
const seen = ranges.length > 1 ? /* @__PURE__ */ new Set() : null;
|
|
1421
|
+
for (const range of ranges) {
|
|
1422
|
+
const r = normalizeRange(range);
|
|
1423
|
+
const endRow = Math.min(r.endRow, rowCount - 1);
|
|
1424
|
+
const endCol = Math.min(r.endCol, colCount - 1);
|
|
1425
|
+
for (let row = Math.max(0, r.startRow); row <= endRow; row++) {
|
|
1426
|
+
for (let col = Math.max(0, r.startCol); col <= endCol; col++) {
|
|
1427
|
+
if (seen) {
|
|
1428
|
+
const key = row * (colCount + 1) + col;
|
|
1429
|
+
if (seen.has(key)) continue;
|
|
1430
|
+
seen.add(key);
|
|
1431
|
+
}
|
|
1432
|
+
fn(row, col);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function formatRuleRanges(ranges) {
|
|
1438
|
+
return ranges.map((range) => {
|
|
1439
|
+
const r = normalizeRange(range);
|
|
1440
|
+
const start = `${indexToColName(r.startCol)}${r.startRow + 1}`;
|
|
1441
|
+
const end = `${indexToColName(r.endCol)}${r.endRow + 1}`;
|
|
1442
|
+
return start === end ? start : `${start}:${end}`;
|
|
1443
|
+
}).join(", ");
|
|
1444
|
+
}
|
|
1445
|
+
var A1_RANGE = /^\$?([A-Za-z]{1,3})\$?(\d{1,7})(?::\$?([A-Za-z]{1,3})\$?(\d{1,7}))?$/;
|
|
1446
|
+
function parseRuleRanges(text) {
|
|
1447
|
+
const parts = text.split(/[,;\s]+/).filter((p) => p !== "");
|
|
1448
|
+
if (parts.length === 0) return null;
|
|
1449
|
+
const out = [];
|
|
1450
|
+
for (const part of parts) {
|
|
1451
|
+
const m = A1_RANGE.exec(part);
|
|
1452
|
+
if (!m) return null;
|
|
1453
|
+
const startRow = Number(m[2]) - 1;
|
|
1454
|
+
const startCol = colNameToIndex(m[1]);
|
|
1455
|
+
const endRow = m[4] ? Number(m[4]) - 1 : startRow;
|
|
1456
|
+
const endCol = m[3] ? colNameToIndex(m[3]) : startCol;
|
|
1457
|
+
if (startRow < 0 || endRow < 0 || startCol < 0 || endCol < 0) return null;
|
|
1458
|
+
out.push(normalizeRange({ startRow, startCol, endRow, endCol }));
|
|
1459
|
+
}
|
|
1460
|
+
return out;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/spreadsheet/conditional-format/icons.ts
|
|
1464
|
+
var RED = "#d9534f";
|
|
1465
|
+
var YELLOW = "#e0a800";
|
|
1466
|
+
var GREEN = "#2e9d4f";
|
|
1467
|
+
var GRAY = "#7a7f87";
|
|
1468
|
+
var BLACK = "#3b3f45";
|
|
1469
|
+
var BLUE = "#3b73b9";
|
|
1470
|
+
var g = (glyph, color, label) => ({ glyph, color, label });
|
|
1471
|
+
var ICON_SETS = {
|
|
1472
|
+
"3Arrows": [g("\u2193", RED, "Down arrow"), g("\u2192", YELLOW, "Side arrow"), g("\u2191", GREEN, "Up arrow")],
|
|
1473
|
+
"3ArrowsGray": [g("\u2193", GRAY, "Down arrow"), g("\u2192", GRAY, "Side arrow"), g("\u2191", GRAY, "Up arrow")],
|
|
1474
|
+
"3TrafficLights1": [g("\u25CF", RED, "Red light"), g("\u25CF", YELLOW, "Yellow light"), g("\u25CF", GREEN, "Green light")],
|
|
1475
|
+
"3Flags": [g("\u2691", RED, "Red flag"), g("\u2691", YELLOW, "Yellow flag"), g("\u2691", GREEN, "Green flag")],
|
|
1476
|
+
"3Symbols": [g("\u2716", RED, "Cross"), g("!", YELLOW, "Exclamation"), g("\u2714", GREEN, "Check")],
|
|
1477
|
+
"3Stars": [g("\u2606", YELLOW, "Empty star"), g("\u272C", YELLOW, "Half star"), g("\u2605", YELLOW, "Full star")],
|
|
1478
|
+
"4Arrows": [g("\u2193", RED, "Down arrow"), g("\u2198", YELLOW, "Down-right arrow"), g("\u2197", YELLOW, "Up-right arrow"), g("\u2191", GREEN, "Up arrow")],
|
|
1479
|
+
"4Rating": [g("\u2582", BLUE, "Rating 1 of 4"), g("\u2584", BLUE, "Rating 2 of 4"), g("\u2586", BLUE, "Rating 3 of 4"), g("\u2588", BLUE, "Rating 4 of 4")],
|
|
1480
|
+
"4TrafficLights": [g("\u25CF", BLACK, "Black light"), g("\u25CF", RED, "Red light"), g("\u25CF", YELLOW, "Yellow light"), g("\u25CF", GREEN, "Green light")],
|
|
1481
|
+
"5Arrows": [
|
|
1482
|
+
g("\u2193", RED, "Down arrow"),
|
|
1483
|
+
g("\u2198", YELLOW, "Down-right arrow"),
|
|
1484
|
+
g("\u2192", YELLOW, "Side arrow"),
|
|
1485
|
+
g("\u2197", GREEN, "Up-right arrow"),
|
|
1486
|
+
g("\u2191", GREEN, "Up arrow")
|
|
1487
|
+
],
|
|
1488
|
+
"5Rating": [
|
|
1489
|
+
g("\u2581", BLUE, "Rating 1 of 5"),
|
|
1490
|
+
g("\u2582", BLUE, "Rating 2 of 5"),
|
|
1491
|
+
g("\u2584", BLUE, "Rating 3 of 5"),
|
|
1492
|
+
g("\u2586", BLUE, "Rating 4 of 5"),
|
|
1493
|
+
g("\u2588", BLUE, "Rating 5 of 5")
|
|
1494
|
+
],
|
|
1495
|
+
"5Quarters": [g("\u25CB", BLACK, "Empty"), g("\u25D4", BLACK, "Quarter"), g("\u25D1", BLACK, "Half"), g("\u25D5", BLACK, "Three quarters"), g("\u25CF", BLACK, "Full")]
|
|
1496
|
+
};
|
|
1497
|
+
var CONDITIONAL_FORMAT_ICON_SETS = Object.keys(ICON_SETS);
|
|
1498
|
+
function conditionalFormatIconCount(set) {
|
|
1499
|
+
const n = Number(set.charAt(0));
|
|
1500
|
+
return Number.isFinite(n) ? Math.min(5, Math.max(3, n)) : 3;
|
|
1501
|
+
}
|
|
1502
|
+
function getConditionalFormatIcon(set, index) {
|
|
1503
|
+
const icons = ICON_SETS[set] ?? ICON_SETS["3TrafficLights1"];
|
|
1504
|
+
return icons[Math.max(0, Math.min(icons.length - 1, index))];
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/spreadsheet/conditional-format/evaluate.ts
|
|
1508
|
+
var DEFAULT_BAR_COLOR = "#638ec6";
|
|
1509
|
+
var isBlank2 = (v) => v === "" || v === null || v === void 0;
|
|
1510
|
+
var ISO_DATE = /^(\d{4})-(\d{1,2})-(\d{1,2})(?: (\d{1,2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
1511
|
+
function conditionalFormatNumber(value) {
|
|
1512
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
1513
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 19) return null;
|
|
1514
|
+
const m = ISO_DATE.exec(value);
|
|
1515
|
+
if (!m) return null;
|
|
1516
|
+
const serial = serialFromYmd(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
1517
|
+
if (serial === null) return null;
|
|
1518
|
+
const time = m[4] ? (Number(m[4]) * 3600 + Number(m[5]) * 60 + Number(m[6] ?? 0)) / 86400 : 0;
|
|
1519
|
+
return serial + time;
|
|
1520
|
+
}
|
|
1521
|
+
function valueText(value) {
|
|
1522
|
+
if (isBlank2(value)) return "";
|
|
1523
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
1524
|
+
if (typeof value === "number") return numberToText(value);
|
|
1525
|
+
return String(value);
|
|
1526
|
+
}
|
|
1527
|
+
function firstValue(value) {
|
|
1528
|
+
return Array.isArray(value) ? value.length ? value[0] : "" : value;
|
|
1529
|
+
}
|
|
1530
|
+
function percentile(sorted, p) {
|
|
1531
|
+
if (sorted.length === 0) return 0;
|
|
1532
|
+
const rank = Math.min(Math.max(p, 0), 100) / 100 * (sorted.length - 1);
|
|
1533
|
+
const lo = Math.floor(rank);
|
|
1534
|
+
const hi = Math.ceil(rank);
|
|
1535
|
+
const a = sorted[lo] ?? 0;
|
|
1536
|
+
const b = sorted[hi] ?? a;
|
|
1537
|
+
return a + (b - a) * (rank - lo);
|
|
1538
|
+
}
|
|
1539
|
+
function parseHexColor(color) {
|
|
1540
|
+
if (!color) return null;
|
|
1541
|
+
let hex = color.trim().replace(/^#/, "");
|
|
1542
|
+
if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
|
|
1543
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
1544
|
+
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
1545
|
+
}
|
|
1546
|
+
var toHex = (rgb) => `#${rgb.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
1547
|
+
function blendConditionalFormatColors(a, b, t) {
|
|
1548
|
+
const ca = parseHexColor(a) ?? [255, 255, 255];
|
|
1549
|
+
const cb = parseHexColor(b) ?? [255, 255, 255];
|
|
1550
|
+
const k = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
|
|
1551
|
+
return toHex([0, 1, 2].map((i) => Math.round(ca[i] + (cb[i] - ca[i]) * k)));
|
|
1552
|
+
}
|
|
1553
|
+
function defaultIconThresholds(set) {
|
|
1554
|
+
const k = conditionalFormatIconCount(set);
|
|
1555
|
+
return Array.from({ length: k }, (_, i) => ({ type: "percent", value: Math.round(i * 100 / k) }));
|
|
1556
|
+
}
|
|
1557
|
+
var usesStats = (rule) => rule.type === "colorScale" || rule.type === "dataBar" || rule.type === "iconSet" || rule.type === "top10" || rule.type === "aboveAverage" || rule.type === "duplicate" || rule.type === "unique";
|
|
1558
|
+
var isFormulaText = (text) => !!text && text.trimStart().startsWith("=");
|
|
1559
|
+
function usesFormulas(rule) {
|
|
1560
|
+
switch (rule.type) {
|
|
1561
|
+
case "expression":
|
|
1562
|
+
return true;
|
|
1563
|
+
case "cellIs":
|
|
1564
|
+
return isFormulaText(rule.value) || isFormulaText(rule.value2);
|
|
1565
|
+
case "colorScale":
|
|
1566
|
+
return rule.stops.some((s) => s.value.type === "formula");
|
|
1567
|
+
case "dataBar":
|
|
1568
|
+
return rule.min?.type === "formula" || rule.max?.type === "formula";
|
|
1569
|
+
case "iconSet":
|
|
1570
|
+
return !!rule.thresholds?.some((t) => t.type === "formula");
|
|
1571
|
+
default:
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function layerStyle(out, style) {
|
|
1576
|
+
if (out.fill === void 0 && style.fill) out.fill = style.fill;
|
|
1577
|
+
if (out.color === void 0 && style.color) out.color = style.color;
|
|
1578
|
+
if (style.bold) out.bold = true;
|
|
1579
|
+
if (style.italic) out.italic = true;
|
|
1580
|
+
if (style.underline) out.underline = true;
|
|
1581
|
+
if (style.strikethrough) out.strikethrough = true;
|
|
1582
|
+
}
|
|
1583
|
+
var COL_SPAN = 1048576;
|
|
1584
|
+
var cellKey = (row, col) => row * COL_SPAN + col;
|
|
1585
|
+
var Overlay = class {
|
|
1586
|
+
constructor(cells) {
|
|
1587
|
+
this.cells = cells;
|
|
1588
|
+
}
|
|
1589
|
+
cells;
|
|
1590
|
+
get(row, col) {
|
|
1591
|
+
return this.cells.size === 0 ? void 0 : this.cells.get(cellKey(row, col));
|
|
1592
|
+
}
|
|
1593
|
+
get size() {
|
|
1594
|
+
return this.cells.size;
|
|
1595
|
+
}
|
|
1596
|
+
*entries() {
|
|
1597
|
+
for (const [key, format] of this.cells) {
|
|
1598
|
+
const row = Math.floor(key / COL_SPAN);
|
|
1599
|
+
const col = key - row * COL_SPAN;
|
|
1600
|
+
yield [{ row, col }, format];
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
var EMPTY_CONDITIONAL_FORMAT_OVERLAY = new Overlay(/* @__PURE__ */ new Map());
|
|
1605
|
+
var ConditionalFormatEngine = class {
|
|
1606
|
+
constructor(sheet) {
|
|
1607
|
+
this.sheet = sheet;
|
|
1608
|
+
}
|
|
1609
|
+
sheet;
|
|
1610
|
+
cells = /* @__PURE__ */ new Map();
|
|
1611
|
+
stats = /* @__PURE__ */ new Map();
|
|
1612
|
+
rulesKey = null;
|
|
1613
|
+
current = EMPTY_CONDITIONAL_FORMAT_OVERLAY;
|
|
1614
|
+
/** Replaces the sheet accessor (e.g. after rows or columns were added). */
|
|
1615
|
+
setSheet(sheet) {
|
|
1616
|
+
this.sheet = sheet;
|
|
1617
|
+
}
|
|
1618
|
+
/** The last evaluated overlay. */
|
|
1619
|
+
get overlay() {
|
|
1620
|
+
return this.current;
|
|
1621
|
+
}
|
|
1622
|
+
/** Evaluates every rule over every cell it applies to. */
|
|
1623
|
+
evaluate(rules) {
|
|
1624
|
+
this.rulesKey = JSON.stringify(rules);
|
|
1625
|
+
this.stats.clear();
|
|
1626
|
+
this.cells = /* @__PURE__ */ new Map();
|
|
1627
|
+
const { rowCount, colCount } = this.sheet;
|
|
1628
|
+
const done = /* @__PURE__ */ new Set();
|
|
1629
|
+
for (const rule of rules) {
|
|
1630
|
+
forEachRangeCell(rule.ranges, rowCount, colCount, (row, col) => {
|
|
1631
|
+
const key = cellKey(row, col);
|
|
1632
|
+
if (done.has(key)) return;
|
|
1633
|
+
done.add(key);
|
|
1634
|
+
const look = this.computeLook(rules, row, col);
|
|
1635
|
+
if (look) this.cells.set(key, look);
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
this.current = new Overlay(this.cells);
|
|
1639
|
+
return this.current;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Re-evaluates after the values of `changed` cells changed (the edited
|
|
1643
|
+
* cells and the formulas that depend on them). Rules that look at the whole
|
|
1644
|
+
* range (scales, bars, icons, top/bottom, average, duplicates) are
|
|
1645
|
+
* re-evaluated over their range when a changed cell is inside it; formula
|
|
1646
|
+
* rules always are, since their formulas may read any cell; other rules
|
|
1647
|
+
* only at the changed cells. Falls back to `evaluate` when the rules
|
|
1648
|
+
* changed.
|
|
1649
|
+
*/
|
|
1650
|
+
update(rules, changed) {
|
|
1651
|
+
const key = JSON.stringify(rules);
|
|
1652
|
+
if (key !== this.rulesKey) return this.evaluate(rules);
|
|
1653
|
+
if (rules.length === 0) return this.current;
|
|
1654
|
+
const changedCells = Array.from(changed);
|
|
1655
|
+
const { rowCount, colCount } = this.sheet;
|
|
1656
|
+
const affected = /* @__PURE__ */ new Map();
|
|
1657
|
+
const addCell = (row, col) => affected.set(cellKey(row, col), { row, col });
|
|
1658
|
+
for (const rule of rules) {
|
|
1659
|
+
const wholeRange = usesFormulas(rule);
|
|
1660
|
+
const touched = changedCells.some((c) => rangesContain(rule.ranges, c.row, c.col));
|
|
1661
|
+
if (usesStats(rule) && touched) this.stats.delete(rule.id);
|
|
1662
|
+
if (wholeRange || usesStats(rule) && touched) {
|
|
1663
|
+
forEachRangeCell(rule.ranges, rowCount, colCount, addCell);
|
|
1664
|
+
} else if (touched) {
|
|
1665
|
+
for (const c of changedCells) {
|
|
1666
|
+
if (c.row < rowCount && c.col < colCount && rangesContain(rule.ranges, c.row, c.col)) addCell(c.row, c.col);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (affected.size === 0) return this.current;
|
|
1671
|
+
const next = new Map(this.cells);
|
|
1672
|
+
for (const [k, { row, col }] of affected) {
|
|
1673
|
+
const look = this.computeLook(rules, row, col);
|
|
1674
|
+
if (look) next.set(k, look);
|
|
1675
|
+
else next.delete(k);
|
|
1676
|
+
}
|
|
1677
|
+
this.cells = next;
|
|
1678
|
+
this.current = new Overlay(next);
|
|
1679
|
+
return this.current;
|
|
1680
|
+
}
|
|
1681
|
+
statsFor(rule) {
|
|
1682
|
+
let st = this.stats.get(rule.id);
|
|
1683
|
+
if (st) return st;
|
|
1684
|
+
const sorted = [];
|
|
1685
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1686
|
+
forEachRangeCell(rule.ranges, this.sheet.rowCount, this.sheet.colCount, (row, col) => {
|
|
1687
|
+
const v = this.sheet.getValue(row, col);
|
|
1688
|
+
const n2 = conditionalFormatNumber(v);
|
|
1689
|
+
if (n2 !== null) sorted.push(n2);
|
|
1690
|
+
if (!isBlank2(v)) {
|
|
1691
|
+
const text = valueText(v).toLowerCase();
|
|
1692
|
+
counts.set(text, (counts.get(text) ?? 0) + 1);
|
|
1693
|
+
}
|
|
1694
|
+
});
|
|
1695
|
+
sorted.sort((a, b) => a - b);
|
|
1696
|
+
const n = Math.max(sorted.length, 1);
|
|
1697
|
+
let sum = 0;
|
|
1698
|
+
for (const x of sorted) sum += x;
|
|
1699
|
+
const avg = sum / n;
|
|
1700
|
+
let sq = 0;
|
|
1701
|
+
for (const x of sorted) sq += (x - avg) * (x - avg);
|
|
1702
|
+
st = { sorted, avg, std: Math.sqrt(sq / n), counts };
|
|
1703
|
+
this.stats.set(rule.id, st);
|
|
1704
|
+
return st;
|
|
1705
|
+
}
|
|
1706
|
+
evalFormula(formula, rule, row, col) {
|
|
1707
|
+
if (!this.sheet.evaluateFormula) return "#NAME?";
|
|
1708
|
+
try {
|
|
1709
|
+
return firstValue(this.sheet.evaluateFormula(ruleFormulaAt(formula, ruleAnchor(rule.ranges), row, col), row, col));
|
|
1710
|
+
} catch {
|
|
1711
|
+
return "#ERROR!";
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
threshold(rule, st, k, row, col) {
|
|
1715
|
+
const min = st.sorted[0] ?? 0;
|
|
1716
|
+
const max = st.sorted[st.sorted.length - 1] ?? 0;
|
|
1717
|
+
switch (k.type) {
|
|
1718
|
+
case "min":
|
|
1719
|
+
return min;
|
|
1720
|
+
case "max":
|
|
1721
|
+
return max;
|
|
1722
|
+
case "number":
|
|
1723
|
+
return k.value;
|
|
1724
|
+
case "percent":
|
|
1725
|
+
return min + (max - min) * k.value / 100;
|
|
1726
|
+
case "percentile":
|
|
1727
|
+
return percentile(st.sorted, k.value);
|
|
1728
|
+
case "formula": {
|
|
1729
|
+
const n = conditionalFormatNumber(this.evalFormula(k.formula, rule, row, col));
|
|
1730
|
+
return n ?? 0;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
/** The operand of a `cellIs` rule: a formula result, or typed input. */
|
|
1735
|
+
operand(text, rule, row, col) {
|
|
1736
|
+
if (text === void 0) return "";
|
|
1737
|
+
if (isFormulaText(text)) return this.evalFormula(text, rule, row, col);
|
|
1738
|
+
return parseCellInput(text).value;
|
|
1739
|
+
}
|
|
1740
|
+
/** Combines every rule that applies to (row, col). */
|
|
1741
|
+
computeLook(rules, row, col) {
|
|
1742
|
+
const out = {};
|
|
1743
|
+
let any = false;
|
|
1744
|
+
let v;
|
|
1745
|
+
let loaded = false;
|
|
1746
|
+
const value = () => {
|
|
1747
|
+
if (!loaded) {
|
|
1748
|
+
v = this.sheet.getValue(row, col);
|
|
1749
|
+
loaded = true;
|
|
1750
|
+
}
|
|
1751
|
+
return v;
|
|
1752
|
+
};
|
|
1753
|
+
for (const rule of rules) {
|
|
1754
|
+
if (!rangesContain(rule.ranges, row, col)) continue;
|
|
1755
|
+
const matched = this.matchRule(rule, value(), row, col, out);
|
|
1756
|
+
if (matched === "look") any = true;
|
|
1757
|
+
if (matched && matched !== "look") {
|
|
1758
|
+
any = true;
|
|
1759
|
+
layerStyle(out, matched);
|
|
1760
|
+
if (rule.stopIfTrue) break;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return any ? out : void 0;
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Checks one rule: returns its style when it matches, 'look' when it set a
|
|
1767
|
+
* scale fill, bar or icon on `out`, or null.
|
|
1768
|
+
*/
|
|
1769
|
+
matchRule(rule, v, row, col, out) {
|
|
1770
|
+
const error = isFormulaError(v);
|
|
1771
|
+
switch (rule.type) {
|
|
1772
|
+
case "cellIs": {
|
|
1773
|
+
if (error) return null;
|
|
1774
|
+
const a = this.operand(rule.value, rule, row, col);
|
|
1775
|
+
let ok;
|
|
1776
|
+
if (rule.operator === "between" || rule.operator === "notBetween") {
|
|
1777
|
+
const b = this.operand(rule.value2, rule, row, col);
|
|
1778
|
+
const [lo, hi] = compareValues(a, b) > 0 ? [b, a] : [a, b];
|
|
1779
|
+
const inside = compareValues(v, lo) >= 0 && compareValues(v, hi) <= 0;
|
|
1780
|
+
ok = rule.operator === "between" ? inside : !inside;
|
|
1781
|
+
} else {
|
|
1782
|
+
if (isFormulaError(a)) return null;
|
|
1783
|
+
const cmp = compareValues(v, a);
|
|
1784
|
+
ok = rule.operator === "equal" ? cmp === 0 : rule.operator === "notEqual" ? cmp !== 0 : rule.operator === "greaterThan" ? cmp > 0 : rule.operator === "lessThan" ? cmp < 0 : rule.operator === "greaterThanOrEqual" ? cmp >= 0 : cmp <= 0;
|
|
1785
|
+
}
|
|
1786
|
+
return ok ? rule.style : null;
|
|
1787
|
+
}
|
|
1788
|
+
case "expression": {
|
|
1789
|
+
const result = toBoolean(this.evalFormula(rule.formula, rule, row, col));
|
|
1790
|
+
return result === true ? rule.style : null;
|
|
1791
|
+
}
|
|
1792
|
+
case "containsText":
|
|
1793
|
+
case "notContainsText":
|
|
1794
|
+
case "beginsWith":
|
|
1795
|
+
case "endsWith": {
|
|
1796
|
+
const text = valueText(v).toLowerCase();
|
|
1797
|
+
const t = rule.text.toLowerCase();
|
|
1798
|
+
const ok = rule.type === "containsText" ? text.includes(t) : rule.type === "notContainsText" ? !text.includes(t) : rule.type === "beginsWith" ? text.startsWith(t) : text.endsWith(t);
|
|
1799
|
+
return ok ? rule.style : null;
|
|
1800
|
+
}
|
|
1801
|
+
case "blanks":
|
|
1802
|
+
return isBlank2(v) || valueText(v).trim() === "" ? rule.style : null;
|
|
1803
|
+
case "noBlanks":
|
|
1804
|
+
return !isBlank2(v) && valueText(v).trim() !== "" ? rule.style : null;
|
|
1805
|
+
case "errors":
|
|
1806
|
+
return error ? rule.style : null;
|
|
1807
|
+
case "noErrors":
|
|
1808
|
+
return !error ? rule.style : null;
|
|
1809
|
+
case "duplicate":
|
|
1810
|
+
case "unique": {
|
|
1811
|
+
if (isBlank2(v)) return null;
|
|
1812
|
+
const count = this.statsFor(rule).counts.get(valueText(v).toLowerCase()) ?? 0;
|
|
1813
|
+
return (rule.type === "duplicate" ? count > 1 : count === 1) ? rule.style : null;
|
|
1814
|
+
}
|
|
1815
|
+
case "top10": {
|
|
1816
|
+
const n = conditionalFormatNumber(v);
|
|
1817
|
+
const st = this.statsFor(rule);
|
|
1818
|
+
if (n === null || st.sorted.length === 0) return null;
|
|
1819
|
+
const rank = Math.max(1, Math.floor(rule.rank || 1));
|
|
1820
|
+
let k = rule.percent ? Math.max(1, Math.floor(st.sorted.length * rank / 100)) : rank;
|
|
1821
|
+
k = Math.min(Math.max(k, 1), st.sorted.length);
|
|
1822
|
+
const ok = rule.bottom ? n <= st.sorted[k - 1] : n >= st.sorted[st.sorted.length - k];
|
|
1823
|
+
return ok ? rule.style : null;
|
|
1824
|
+
}
|
|
1825
|
+
case "aboveAverage": {
|
|
1826
|
+
const n = conditionalFormatNumber(v);
|
|
1827
|
+
if (n === null) return null;
|
|
1828
|
+
const st = this.statsFor(rule);
|
|
1829
|
+
const thr = st.avg + (rule.below ? -1 : 1) * st.std * (rule.stdDev ?? 0);
|
|
1830
|
+
const ok = rule.below ? n < thr || !!rule.equal && n === thr : n > thr || !!rule.equal && n === thr;
|
|
1831
|
+
return ok ? rule.style : null;
|
|
1832
|
+
}
|
|
1833
|
+
case "timePeriod": {
|
|
1834
|
+
const n = conditionalFormatNumber(v);
|
|
1835
|
+
if (n === null) return null;
|
|
1836
|
+
return matchesTimePeriod(rule.period, Math.floor(n), this.sheet.today?.() ?? /* @__PURE__ */ new Date()) ? rule.style : null;
|
|
1837
|
+
}
|
|
1838
|
+
case "colorScale": {
|
|
1839
|
+
const n = conditionalFormatNumber(v);
|
|
1840
|
+
if (n === null || out.fill !== void 0) return null;
|
|
1841
|
+
const st = this.statsFor(rule);
|
|
1842
|
+
const pts = rule.stops.map((s) => ({ at: this.threshold(rule, st, s.value, row, col), color: s.color }));
|
|
1843
|
+
let fill;
|
|
1844
|
+
if (pts.length === 2) {
|
|
1845
|
+
const [a, b] = pts;
|
|
1846
|
+
fill = blendConditionalFormatColors(a.color, b.color, b.at > a.at ? (n - a.at) / (b.at - a.at) : 0);
|
|
1847
|
+
} else if (pts.length >= 3) {
|
|
1848
|
+
const [a, m, b] = pts;
|
|
1849
|
+
fill = n <= m.at ? blendConditionalFormatColors(a.color, m.color, m.at > a.at ? (n - a.at) / (m.at - a.at) : 0) : blendConditionalFormatColors(m.color, b.color, b.at > m.at ? (n - m.at) / (b.at - m.at) : 1);
|
|
1850
|
+
}
|
|
1851
|
+
if (!fill) return null;
|
|
1852
|
+
out.fill = fill;
|
|
1853
|
+
return "look";
|
|
1854
|
+
}
|
|
1855
|
+
case "dataBar": {
|
|
1856
|
+
const n = conditionalFormatNumber(v);
|
|
1857
|
+
if (n === null || out.dataBar) return null;
|
|
1858
|
+
const st = this.statsFor(rule);
|
|
1859
|
+
let lo = this.threshold(rule, st, rule.min ?? { type: "min" }, row, col);
|
|
1860
|
+
const hi = this.threshold(rule, st, rule.max ?? { type: "max" }, row, col);
|
|
1861
|
+
if (lo > 0) lo = 0;
|
|
1862
|
+
const frac = hi > lo ? Math.min(Math.max((n - lo) / (hi - lo), 0), 1) : 1;
|
|
1863
|
+
out.dataBar = {
|
|
1864
|
+
// A minimum length keeps small positive values visible.
|
|
1865
|
+
fraction: frac * 0.9 + (frac > 0 ? 0.1 : 0),
|
|
1866
|
+
color: rule.color ?? DEFAULT_BAR_COLOR,
|
|
1867
|
+
gradient: rule.gradient ?? true
|
|
1868
|
+
};
|
|
1869
|
+
if (rule.showValue === false) out.hideValue = true;
|
|
1870
|
+
return "look";
|
|
1871
|
+
}
|
|
1872
|
+
case "iconSet": {
|
|
1873
|
+
const n = conditionalFormatNumber(v);
|
|
1874
|
+
if (n === null || out.icon) return null;
|
|
1875
|
+
const set = rule.set ?? "3TrafficLights1";
|
|
1876
|
+
const k = conditionalFormatIconCount(set);
|
|
1877
|
+
const thresholds = rule.thresholds ?? [];
|
|
1878
|
+
const lower = thresholds.length === k ? thresholds.slice(1) : thresholds.length + 1 === k ? thresholds : defaultIconThresholds(set).slice(1);
|
|
1879
|
+
const st = this.statsFor(rule);
|
|
1880
|
+
let idx = 0;
|
|
1881
|
+
for (const t of lower) if (n >= this.threshold(rule, st, t, row, col)) idx++;
|
|
1882
|
+
if (rule.reverse) idx = k - 1 - Math.min(idx, k - 1);
|
|
1883
|
+
idx = Math.min(idx, k - 1);
|
|
1884
|
+
out.icon = { set, index: idx, id: `${set}:${idx}` };
|
|
1885
|
+
if (rule.showValue === false) out.hideValue = true;
|
|
1886
|
+
return "look";
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
function matchesTimePeriod(period, d, today) {
|
|
1893
|
+
const t = serialFromYmd(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
|
1894
|
+
if (t === null) return false;
|
|
1895
|
+
const weekStart = t - today.getDay();
|
|
1896
|
+
const date = serialToDate(d);
|
|
1897
|
+
const monthOffset = (k) => {
|
|
1898
|
+
if (!date) return false;
|
|
1899
|
+
const m = today.getMonth() + k;
|
|
1900
|
+
const y = today.getFullYear() + Math.floor(m / 12);
|
|
1901
|
+
const mm = (m % 12 + 12) % 12;
|
|
1902
|
+
return date.getFullYear() === y && date.getMonth() === mm;
|
|
1903
|
+
};
|
|
1904
|
+
switch (period) {
|
|
1905
|
+
case "yesterday":
|
|
1906
|
+
return d === t - 1;
|
|
1907
|
+
case "today":
|
|
1908
|
+
return d === t;
|
|
1909
|
+
case "tomorrow":
|
|
1910
|
+
return d === t + 1;
|
|
1911
|
+
case "last7Days":
|
|
1912
|
+
return d > t - 7 && d <= t;
|
|
1913
|
+
case "lastWeek":
|
|
1914
|
+
return d >= weekStart - 7 && d < weekStart;
|
|
1915
|
+
case "thisWeek":
|
|
1916
|
+
return d >= weekStart && d < weekStart + 7;
|
|
1917
|
+
case "nextWeek":
|
|
1918
|
+
return d >= weekStart + 7 && d < weekStart + 14;
|
|
1919
|
+
case "lastMonth":
|
|
1920
|
+
return monthOffset(-1);
|
|
1921
|
+
case "thisMonth":
|
|
1922
|
+
return monthOffset(0);
|
|
1923
|
+
case "nextMonth":
|
|
1924
|
+
return monthOffset(1);
|
|
1925
|
+
default:
|
|
1926
|
+
return false;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
function evaluateConditionalFormats(sheet, rules) {
|
|
1930
|
+
return new ConditionalFormatEngine(sheet).evaluate(rules);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/spreadsheet/conditional-format/describe.ts
|
|
1934
|
+
var OPERATOR_TEXT = {
|
|
1935
|
+
between: "between",
|
|
1936
|
+
notBetween: "not between",
|
|
1937
|
+
equal: "=",
|
|
1938
|
+
notEqual: "\u2260",
|
|
1939
|
+
greaterThan: ">",
|
|
1940
|
+
lessThan: "<",
|
|
1941
|
+
greaterThanOrEqual: "\u2265",
|
|
1942
|
+
lessThanOrEqual: "\u2264"
|
|
1943
|
+
};
|
|
1944
|
+
var anchorText = (v, fallback) => {
|
|
1945
|
+
if (!v) return fallback;
|
|
1946
|
+
switch (v.type) {
|
|
1947
|
+
case "min":
|
|
1948
|
+
return "min";
|
|
1949
|
+
case "max":
|
|
1950
|
+
return "max";
|
|
1951
|
+
case "number":
|
|
1952
|
+
return String(v.value);
|
|
1953
|
+
case "percent":
|
|
1954
|
+
return `${v.value}%`;
|
|
1955
|
+
case "percentile":
|
|
1956
|
+
return `P${v.value}`;
|
|
1957
|
+
case "formula":
|
|
1958
|
+
return v.formula;
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
function describeConditionalFormat(rule) {
|
|
1962
|
+
switch (rule.type) {
|
|
1963
|
+
case "cellIs":
|
|
1964
|
+
return rule.operator === "between" || rule.operator === "notBetween" ? `Cell value ${OPERATOR_TEXT[rule.operator]} ${rule.value} and ${rule.value2 ?? ""}` : `Cell value ${OPERATOR_TEXT[rule.operator]} ${rule.value}`;
|
|
1965
|
+
case "expression":
|
|
1966
|
+
return `Formula: ${rule.formula.startsWith("=") ? rule.formula : `=${rule.formula}`}`;
|
|
1967
|
+
case "containsText":
|
|
1968
|
+
return `Text contains "${rule.text}"`;
|
|
1969
|
+
case "notContainsText":
|
|
1970
|
+
return `Text does not contain "${rule.text}"`;
|
|
1971
|
+
case "beginsWith":
|
|
1972
|
+
return `Text begins with "${rule.text}"`;
|
|
1973
|
+
case "endsWith":
|
|
1974
|
+
return `Text ends with "${rule.text}"`;
|
|
1975
|
+
case "blanks":
|
|
1976
|
+
return "Blank cells";
|
|
1977
|
+
case "noBlanks":
|
|
1978
|
+
return "Non-blank cells";
|
|
1979
|
+
case "errors":
|
|
1980
|
+
return "Errors";
|
|
1981
|
+
case "noErrors":
|
|
1982
|
+
return "No errors";
|
|
1983
|
+
case "duplicate":
|
|
1984
|
+
return "Duplicate values";
|
|
1985
|
+
case "unique":
|
|
1986
|
+
return "Unique values";
|
|
1987
|
+
case "top10":
|
|
1988
|
+
return `${rule.bottom ? "Bottom" : "Top"} ${rule.rank}${rule.percent ? "%" : ""}`;
|
|
1989
|
+
case "aboveAverage":
|
|
1990
|
+
return `${rule.below ? "Below" : "Above"} average${rule.stdDev ? ` by ${rule.stdDev} std dev` : ""}`;
|
|
1991
|
+
case "timePeriod":
|
|
1992
|
+
return `Date occurring: ${rule.period}`;
|
|
1993
|
+
case "colorScale":
|
|
1994
|
+
return `Color scale (${rule.stops.map((s) => anchorText(s.value, "")).join(" \u2192 ")})`;
|
|
1995
|
+
case "dataBar":
|
|
1996
|
+
return `Data bar (${anchorText(rule.min, "min")} \u2192 ${anchorText(rule.max, "max")})`;
|
|
1997
|
+
case "iconSet":
|
|
1998
|
+
return `Icon set ${rule.set ?? "3TrafficLights1"}${rule.reverse ? " (reversed)" : ""}`;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
var VALIDATION_OPERATOR_TEXT = OPERATOR_TEXT;
|
|
2002
|
+
function describeDataValidation(rule) {
|
|
2003
|
+
const op = rule.operator ?? "between";
|
|
2004
|
+
const bounds = op === "between" || op === "notBetween" ? `${VALIDATION_OPERATOR_TEXT[op]} ${rule.formula1 ?? ""} and ${rule.formula2 ?? ""}` : `${VALIDATION_OPERATOR_TEXT[op]} ${rule.formula1 ?? ""}`;
|
|
2005
|
+
switch (rule.type) {
|
|
2006
|
+
case "any":
|
|
2007
|
+
return "Any value";
|
|
2008
|
+
case "whole":
|
|
2009
|
+
return `Whole number ${bounds}`;
|
|
2010
|
+
case "decimal":
|
|
2011
|
+
return `Decimal ${bounds}`;
|
|
2012
|
+
case "date":
|
|
2013
|
+
return `Date ${bounds}`;
|
|
2014
|
+
case "time":
|
|
2015
|
+
return `Time ${bounds}`;
|
|
2016
|
+
case "textLength":
|
|
2017
|
+
return `Text length ${bounds}`;
|
|
2018
|
+
case "list":
|
|
2019
|
+
return `List: ${rule.formula1 ?? ""}`;
|
|
2020
|
+
case "custom":
|
|
2021
|
+
return `Custom: ${rule.formula1 ?? ""}`;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/spreadsheet/conditional-format/presets.ts
|
|
2026
|
+
var CONDITIONAL_FORMAT_STYLE_PRESETS = [
|
|
2027
|
+
{ id: "lightRed", label: "Light red fill, dark red text", style: { fill: "#ffc7ce", color: "#9c0006" } },
|
|
2028
|
+
{ id: "yellow", label: "Yellow fill, dark yellow text", style: { fill: "#ffeb9c", color: "#9c5700" } },
|
|
2029
|
+
{ id: "green", label: "Green fill, dark green text", style: { fill: "#c6efce", color: "#006100" } },
|
|
2030
|
+
{ id: "redText", label: "Red text", style: { color: "#9c0006" } },
|
|
2031
|
+
{ id: "bold", label: "Bold", style: { bold: true } }
|
|
2032
|
+
];
|
|
2033
|
+
var DEFAULT_CONDITIONAL_FORMAT_STYLE = CONDITIONAL_FORMAT_STYLE_PRESETS[0].style;
|
|
2034
|
+
var CONDITIONAL_FORMAT_PRESETS = [
|
|
2035
|
+
{ kind: "greaterThan", label: "Cell value greater than", fields: ["value"], usesStyle: true },
|
|
2036
|
+
{ kind: "lessThan", label: "Cell value less than", fields: ["value"], usesStyle: true },
|
|
2037
|
+
{ kind: "between", label: "Cell value between", fields: ["value", "value2"], usesStyle: true },
|
|
2038
|
+
{ kind: "equal", label: "Cell value equal to", fields: ["value"], usesStyle: true },
|
|
2039
|
+
{ kind: "textContains", label: "Text that contains", fields: ["text"], usesStyle: true },
|
|
2040
|
+
{ kind: "dateOccurring", label: "A date occurring", fields: ["period"], usesStyle: true },
|
|
2041
|
+
{ kind: "duplicate", label: "Duplicate values", fields: [], usesStyle: true },
|
|
2042
|
+
{ kind: "unique", label: "Unique values", fields: [], usesStyle: true },
|
|
2043
|
+
{ kind: "top10", label: "Top N items", fields: ["rank"], usesStyle: true },
|
|
2044
|
+
{ kind: "bottom10", label: "Bottom N items", fields: ["rank"], usesStyle: true },
|
|
2045
|
+
{ kind: "aboveAverage", label: "Above average", fields: [], usesStyle: true },
|
|
2046
|
+
{ kind: "belowAverage", label: "Below average", fields: [], usesStyle: true },
|
|
2047
|
+
{ kind: "blanks", label: "Blank cells", fields: [], usesStyle: true },
|
|
2048
|
+
{ kind: "errors", label: "Errors", fields: [], usesStyle: true },
|
|
2049
|
+
{ kind: "formula", label: "Use a formula", fields: ["formula"], usesStyle: true },
|
|
2050
|
+
{ kind: "colorScale2", label: "2-color scale", fields: [], usesStyle: false },
|
|
2051
|
+
{ kind: "colorScale3", label: "3-color scale", fields: [], usesStyle: false },
|
|
2052
|
+
{ kind: "dataBar", label: "Data bars", fields: ["color"], usesStyle: false },
|
|
2053
|
+
{ kind: "iconSet", label: "Icon set", fields: ["iconSet"], usesStyle: false }
|
|
2054
|
+
];
|
|
2055
|
+
var CONDITIONAL_FORMAT_TIME_PERIODS = [
|
|
2056
|
+
{ period: "yesterday", label: "Yesterday" },
|
|
2057
|
+
{ period: "today", label: "Today" },
|
|
2058
|
+
{ period: "tomorrow", label: "Tomorrow" },
|
|
2059
|
+
{ period: "last7Days", label: "In the last 7 days" },
|
|
2060
|
+
{ period: "lastWeek", label: "Last week" },
|
|
2061
|
+
{ period: "thisWeek", label: "This week" },
|
|
2062
|
+
{ period: "nextWeek", label: "Next week" },
|
|
2063
|
+
{ period: "lastMonth", label: "Last month" },
|
|
2064
|
+
{ period: "thisMonth", label: "This month" },
|
|
2065
|
+
{ period: "nextMonth", label: "Next month" }
|
|
2066
|
+
];
|
|
2067
|
+
function createConditionalFormatRule(input) {
|
|
2068
|
+
const style = input.style ?? DEFAULT_CONDITIONAL_FORMAT_STYLE;
|
|
2069
|
+
const base = { ranges: input.ranges, ...input.stopIfTrue ? { stopIfTrue: true } : {} };
|
|
2070
|
+
switch (input.kind) {
|
|
2071
|
+
case "greaterThan":
|
|
2072
|
+
case "lessThan":
|
|
2073
|
+
case "equal":
|
|
2074
|
+
return { ...base, type: "cellIs", operator: input.kind, value: input.value ?? "0", style };
|
|
2075
|
+
case "between":
|
|
2076
|
+
return { ...base, type: "cellIs", operator: "between", value: input.value ?? "0", value2: input.value2 ?? "0", style };
|
|
2077
|
+
case "textContains":
|
|
2078
|
+
return { ...base, type: "containsText", text: input.text ?? "", style };
|
|
2079
|
+
case "dateOccurring":
|
|
2080
|
+
return { ...base, type: "timePeriod", period: input.period ?? "today", style };
|
|
2081
|
+
case "duplicate":
|
|
2082
|
+
case "unique":
|
|
2083
|
+
case "blanks":
|
|
2084
|
+
case "errors":
|
|
2085
|
+
return { ...base, type: input.kind, style };
|
|
2086
|
+
case "top10":
|
|
2087
|
+
case "bottom10":
|
|
2088
|
+
return { ...base, type: "top10", bottom: input.kind === "bottom10", rank: input.rank ?? 10, style };
|
|
2089
|
+
case "aboveAverage":
|
|
2090
|
+
case "belowAverage":
|
|
2091
|
+
return { ...base, type: "aboveAverage", below: input.kind === "belowAverage", style };
|
|
2092
|
+
case "formula":
|
|
2093
|
+
return { ...base, type: "expression", formula: input.formula ?? "=TRUE", style };
|
|
2094
|
+
case "colorScale2":
|
|
2095
|
+
return {
|
|
2096
|
+
...base,
|
|
2097
|
+
type: "colorScale",
|
|
2098
|
+
stops: [
|
|
2099
|
+
{ value: { type: "min" }, color: "#ffffff" },
|
|
2100
|
+
{ value: { type: "max" }, color: "#63be7b" }
|
|
2101
|
+
]
|
|
2102
|
+
};
|
|
2103
|
+
case "colorScale3":
|
|
2104
|
+
return {
|
|
2105
|
+
...base,
|
|
2106
|
+
type: "colorScale",
|
|
2107
|
+
stops: [
|
|
2108
|
+
{ value: { type: "min" }, color: "#f8696b" },
|
|
2109
|
+
{ value: { type: "percentile", value: 50 }, color: "#ffeb84" },
|
|
2110
|
+
{ value: { type: "max" }, color: "#63be7b" }
|
|
2111
|
+
]
|
|
2112
|
+
};
|
|
2113
|
+
case "dataBar":
|
|
2114
|
+
return { ...base, type: "dataBar", color: input.color ?? "#638ec6" };
|
|
2115
|
+
case "iconSet":
|
|
2116
|
+
return { ...base, type: "iconSet", set: input.iconSet ?? "3TrafficLights1" };
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
var DATA_VALIDATION_TYPE_OPTIONS = [
|
|
2120
|
+
{ type: "any", label: "Any value" },
|
|
2121
|
+
{ type: "whole", label: "Whole number" },
|
|
2122
|
+
{ type: "decimal", label: "Decimal" },
|
|
2123
|
+
{ type: "list", label: "List" },
|
|
2124
|
+
{ type: "date", label: "Date" },
|
|
2125
|
+
{ type: "time", label: "Time" },
|
|
2126
|
+
{ type: "textLength", label: "Text length" },
|
|
2127
|
+
{ type: "custom", label: "Custom formula" }
|
|
2128
|
+
];
|
|
2129
|
+
var DATA_VALIDATION_OPERATOR_OPTIONS = [
|
|
2130
|
+
{ operator: "between", label: "between" },
|
|
2131
|
+
{ operator: "notBetween", label: "not between" },
|
|
2132
|
+
{ operator: "equal", label: "equal to" },
|
|
2133
|
+
{ operator: "notEqual", label: "not equal to" },
|
|
2134
|
+
{ operator: "greaterThan", label: "greater than" },
|
|
2135
|
+
{ operator: "lessThan", label: "less than" },
|
|
2136
|
+
{ operator: "greaterThanOrEqual", label: "greater than or equal to" },
|
|
2137
|
+
{ operator: "lessThanOrEqual", label: "less than or equal to" }
|
|
2138
|
+
];
|
|
2139
|
+
var DATA_VALIDATION_ERROR_STYLE_OPTIONS = [
|
|
2140
|
+
{ style: "stop", label: "Stop" },
|
|
2141
|
+
{ style: "warning", label: "Warning" },
|
|
2142
|
+
{ style: "information", label: "Information" }
|
|
2143
|
+
];
|
|
2144
|
+
|
|
2145
|
+
// src/spreadsheet/conditional-format/render.ts
|
|
2146
|
+
function conditionalFormatContrastColor(fill) {
|
|
2147
|
+
let hex = fill.trim().replace(/^#/, "");
|
|
2148
|
+
if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
|
|
2149
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return void 0;
|
|
2150
|
+
const [r, g2, b] = [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
2151
|
+
return 0.299 * r + 0.587 * g2 + 0.114 * b > 0.55 ? "#1f2328" : "#ffffff";
|
|
2152
|
+
}
|
|
2153
|
+
function getConditionalFormatCss(format) {
|
|
2154
|
+
if (!format) return {};
|
|
2155
|
+
const css = {};
|
|
2156
|
+
if (format.fill) css.background = format.fill;
|
|
2157
|
+
if (format.color) css.color = format.color;
|
|
2158
|
+
else if (format.fill) {
|
|
2159
|
+
const contrast = conditionalFormatContrastColor(format.fill);
|
|
2160
|
+
if (contrast) css.color = contrast;
|
|
2161
|
+
}
|
|
2162
|
+
if (format.bold) css.fontWeight = "700";
|
|
2163
|
+
if (format.italic) css.fontStyle = "italic";
|
|
2164
|
+
const decorations = [format.underline ? "underline" : "", format.strikethrough ? "line-through" : ""].filter(Boolean);
|
|
2165
|
+
if (decorations.length) css.textDecoration = decorations.join(" ");
|
|
2166
|
+
return css;
|
|
2167
|
+
}
|
|
2168
|
+
function getDataBarCss(bar) {
|
|
2169
|
+
const width = `${Math.round(Math.min(Math.max(bar.fraction, 0), 1) * 1e3) / 10}%`;
|
|
2170
|
+
return {
|
|
2171
|
+
position: "absolute",
|
|
2172
|
+
left: "0",
|
|
2173
|
+
top: "2px",
|
|
2174
|
+
bottom: "2px",
|
|
2175
|
+
width,
|
|
2176
|
+
pointerEvents: "none",
|
|
2177
|
+
borderRadius: "1px",
|
|
2178
|
+
background: bar.gradient ? `linear-gradient(90deg, ${bar.color}, color-mix(in srgb, ${bar.color} 20%, transparent))` : bar.color,
|
|
2179
|
+
opacity: bar.gradient ? "0.9" : "0.75"
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
var kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
2183
|
+
function conditionalFormatStyleString(css) {
|
|
2184
|
+
let out = "";
|
|
2185
|
+
for (const [key, value] of Object.entries(css)) {
|
|
2186
|
+
if (value !== void 0 && value !== "") out += `${kebab(key)}: ${value}; `;
|
|
2187
|
+
}
|
|
2188
|
+
return out.trim();
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/spreadsheet/validation/validate.ts
|
|
2192
|
+
var DEFAULT_DATA_VALIDATION_MESSAGE = "This value doesn't match the data validation restrictions defined for this cell.";
|
|
2193
|
+
var DEFAULT_DATA_VALIDATION_TITLE = "Invalid value";
|
|
2194
|
+
var BARE_REFERENCE = /^\$?[A-Za-z]{1,3}\$?\d{1,7}(?::\$?[A-Za-z]{1,3}\$?\d{1,7})?$/;
|
|
2195
|
+
var isBlank3 = (v) => v === "" || v === null || v === void 0;
|
|
2196
|
+
function valueText2(value) {
|
|
2197
|
+
if (isBlank3(value)) return "";
|
|
2198
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
2199
|
+
if (typeof value === "number") return numberToText(value);
|
|
2200
|
+
return String(value);
|
|
2201
|
+
}
|
|
2202
|
+
function findDataValidation(rules, row, col) {
|
|
2203
|
+
if (!rules) return void 0;
|
|
2204
|
+
for (const rule of rules) if (rangesContain(rule.ranges, row, col)) return rule;
|
|
2205
|
+
return void 0;
|
|
2206
|
+
}
|
|
2207
|
+
function evaluate(sheet, rule, formula, row, col, entered) {
|
|
2208
|
+
if (!sheet.evaluateFormula) return "#NAME?";
|
|
2209
|
+
try {
|
|
2210
|
+
return sheet.evaluateFormula(ruleFormulaAt(formula, ruleAnchor(rule.ranges), row, col), row, col, entered);
|
|
2211
|
+
} catch {
|
|
2212
|
+
return "#ERROR!";
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
var first = (v) => Array.isArray(v) ? v.length ? v[0] : "" : v;
|
|
2216
|
+
function getValidationListValues(sheet, rule) {
|
|
2217
|
+
const f = (rule.formula1 ?? "").trim();
|
|
2218
|
+
if (f === "") return [];
|
|
2219
|
+
if (f.startsWith("=") || BARE_REFERENCE.test(f)) {
|
|
2220
|
+
const anchor = ruleAnchor(rule.ranges);
|
|
2221
|
+
const result = evaluate(sheet, rule, f, anchor.row, anchor.col);
|
|
2222
|
+
const values = Array.isArray(result) ? result : [result];
|
|
2223
|
+
const out = [];
|
|
2224
|
+
for (const v of values) if (!isBlank3(v) && !isFormulaError(v)) out.push(valueText2(v));
|
|
2225
|
+
return out;
|
|
2226
|
+
}
|
|
2227
|
+
const unquoted = f.length >= 2 && f.startsWith('"') && f.endsWith('"') ? f.slice(1, -1) : f;
|
|
2228
|
+
return unquoted.split(",").map((x) => x.trim()).filter((x) => x !== "");
|
|
2229
|
+
}
|
|
2230
|
+
function compareOp(op, n, a, b) {
|
|
2231
|
+
if (a === null) return false;
|
|
2232
|
+
switch (op) {
|
|
2233
|
+
case "between":
|
|
2234
|
+
return b !== null && n >= Math.min(a, b) && n <= Math.max(a, b);
|
|
2235
|
+
case "notBetween":
|
|
2236
|
+
return b !== null && (n < Math.min(a, b) || n > Math.max(a, b));
|
|
2237
|
+
case "equal":
|
|
2238
|
+
return n === a;
|
|
2239
|
+
case "notEqual":
|
|
2240
|
+
return n !== a;
|
|
2241
|
+
case "greaterThan":
|
|
2242
|
+
return n > a;
|
|
2243
|
+
case "lessThan":
|
|
2244
|
+
return n < a;
|
|
2245
|
+
case "greaterThanOrEqual":
|
|
2246
|
+
return n >= a;
|
|
2247
|
+
case "lessThanOrEqual":
|
|
2248
|
+
return n <= a;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
function bound(sheet, rule, text, row, col, entered) {
|
|
2252
|
+
if (text === void 0 || text.trim() === "") return null;
|
|
2253
|
+
const value = text.trim().startsWith("=") ? first(evaluate(sheet, rule, text, row, col, entered)) : parseCellInput(text.trim()).value;
|
|
2254
|
+
return conditionalFormatNumber(value);
|
|
2255
|
+
}
|
|
2256
|
+
function satisfies(sheet, rule, row, col, value, text, entered) {
|
|
2257
|
+
const op = rule.operator ?? "between";
|
|
2258
|
+
switch (rule.type) {
|
|
2259
|
+
case "any":
|
|
2260
|
+
return true;
|
|
2261
|
+
case "list": {
|
|
2262
|
+
const typed = text.trim().toLowerCase();
|
|
2263
|
+
return getValidationListValues(sheet, rule).some(
|
|
2264
|
+
(item) => item.toLowerCase() === typed || compareValues(parseCellInput(item).value, value) === 0
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
case "custom":
|
|
2268
|
+
return toBoolean(first(evaluate(sheet, rule, rule.formula1 ?? "", row, col, entered))) === true;
|
|
2269
|
+
case "textLength":
|
|
2270
|
+
return compareOp(op, Array.from(text).length, bound(sheet, rule, rule.formula1, row, col, entered), bound(sheet, rule, rule.formula2, row, col, entered));
|
|
2271
|
+
case "whole":
|
|
2272
|
+
case "decimal":
|
|
2273
|
+
case "date":
|
|
2274
|
+
case "time": {
|
|
2275
|
+
if (isFormulaError(value)) return false;
|
|
2276
|
+
let n = conditionalFormatNumber(value);
|
|
2277
|
+
if (n === null) return false;
|
|
2278
|
+
if (rule.type === "whole" && !Number.isInteger(n)) return false;
|
|
2279
|
+
if (rule.type === "time" && typeof value === "string") n -= Math.floor(n);
|
|
2280
|
+
return compareOp(op, n, bound(sheet, rule, rule.formula1, row, col, entered), bound(sheet, rule, rule.formula2, row, col, entered));
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
function invalid(rule) {
|
|
2285
|
+
return {
|
|
2286
|
+
valid: false,
|
|
2287
|
+
rule,
|
|
2288
|
+
errorStyle: rule.errorStyle ?? "stop",
|
|
2289
|
+
title: rule.errorTitle || DEFAULT_DATA_VALIDATION_TITLE,
|
|
2290
|
+
message: rule.errorMessage || DEFAULT_DATA_VALIDATION_MESSAGE
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
function validateCellInput(sheet, rules, row, col, input) {
|
|
2294
|
+
const rule = findDataValidation(rules, row, col);
|
|
2295
|
+
if (!rule) return { valid: true };
|
|
2296
|
+
if (rule.type === "any" || rule.showErrorAlert === false) return { valid: true, rule };
|
|
2297
|
+
if (input.trim() === "" && (rule.ignoreBlank ?? true)) return { valid: true, rule };
|
|
2298
|
+
let value;
|
|
2299
|
+
let text = input;
|
|
2300
|
+
if (input.startsWith("=")) {
|
|
2301
|
+
value = first(sheet.evaluateFormula ? safeEval(sheet, input, row, col) : "#NAME?");
|
|
2302
|
+
text = valueText2(value);
|
|
2303
|
+
} else {
|
|
2304
|
+
value = parseCellInput(input).value;
|
|
2305
|
+
if (input.startsWith("'")) text = input.slice(1);
|
|
2306
|
+
}
|
|
2307
|
+
const entered = { row, col, value };
|
|
2308
|
+
return satisfies(sheet, rule, row, col, value, text, entered) ? { valid: true, rule } : invalid(rule);
|
|
2309
|
+
}
|
|
2310
|
+
function safeEval(sheet, formula, row, col) {
|
|
2311
|
+
try {
|
|
2312
|
+
return sheet.evaluateFormula(formula, row, col);
|
|
2313
|
+
} catch {
|
|
2314
|
+
return "#ERROR!";
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
function findInvalidCells(sheet, rules) {
|
|
2318
|
+
const out = [];
|
|
2319
|
+
if (!rules?.length) return out;
|
|
2320
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2321
|
+
for (const rule of rules) {
|
|
2322
|
+
if (rule.type === "any") continue;
|
|
2323
|
+
forEachRangeCell(rule.ranges, sheet.rowCount, sheet.colCount, (row, col) => {
|
|
2324
|
+
const key = `${row}:${col}`;
|
|
2325
|
+
if (seen.has(key)) return;
|
|
2326
|
+
seen.add(key);
|
|
2327
|
+
if (findDataValidation(rules, row, col) !== rule) return;
|
|
2328
|
+
const value = sheet.getValue(row, col);
|
|
2329
|
+
if (isBlank3(value)) return;
|
|
2330
|
+
const raw = sheet.getInput?.(row, col);
|
|
2331
|
+
const text = raw !== void 0 && raw !== "" && !raw.startsWith("=") ? raw.startsWith("'") ? raw.slice(1) : raw : valueText2(value);
|
|
2332
|
+
if (!satisfies(sheet, rule, row, col, value, text)) out.push({ row, col });
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
return out;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
1121
2338
|
// src/spreadsheet/view.ts
|
|
1122
2339
|
var DEFAULT_ROW_HEIGHT = 32;
|
|
1123
2340
|
var DEFAULT_COLUMN_WIDTH = 130;
|
|
@@ -1278,11 +2495,19 @@ function buildSpreadsheetView(doc, getValue) {
|
|
|
1278
2495
|
|
|
1279
2496
|
// src/spreadsheet/controller.ts
|
|
1280
2497
|
var nextIdSequence = 0;
|
|
2498
|
+
var createRuntime = (doc) => ({
|
|
2499
|
+
doc,
|
|
2500
|
+
spills: /* @__PURE__ */ new Map(),
|
|
2501
|
+
spilledBy: /* @__PURE__ */ new Map(),
|
|
2502
|
+
spillCells: /* @__PURE__ */ new Map(),
|
|
2503
|
+
activeCell: null,
|
|
2504
|
+
selectedRange: null
|
|
2505
|
+
});
|
|
2506
|
+
var sameName3 = (a, b) => a.toUpperCase() === b.toUpperCase();
|
|
1281
2507
|
var DEFAULT_MAX_HISTORY_BYTES = 64 * 1024 * 1024;
|
|
1282
2508
|
var MAX_SPILL_PASSES = 25;
|
|
1283
2509
|
var spillCellId = (rowId, colId) => `${rowId}\0${colId}`;
|
|
1284
2510
|
var SpreadsheetController = class {
|
|
1285
|
-
document;
|
|
1286
2511
|
activeCell = null;
|
|
1287
2512
|
selectedRange = null;
|
|
1288
2513
|
editingCell = null;
|
|
@@ -1292,6 +2517,17 @@ var SpreadsheetController = class {
|
|
|
1292
2517
|
evaluator;
|
|
1293
2518
|
primaryDataSourceId;
|
|
1294
2519
|
onDocumentChange;
|
|
2520
|
+
onWorkbookChange;
|
|
2521
|
+
onActiveSheetChange;
|
|
2522
|
+
// Workbook: sheets in tab order, the active one, and the one being worked
|
|
2523
|
+
// on (the active sheet, or another sheet while one of its cells is read or
|
|
2524
|
+
// evaluated). Everything that uses `this.document` works on `ctx`.
|
|
2525
|
+
runtimes = [];
|
|
2526
|
+
active;
|
|
2527
|
+
ctx;
|
|
2528
|
+
// The sheet bound to the primary data source (the first sheet).
|
|
2529
|
+
boundSheetId = null;
|
|
2530
|
+
names = [];
|
|
1295
2531
|
onSelectionChange;
|
|
1296
2532
|
onCellCommit;
|
|
1297
2533
|
// History stack for undo/redo
|
|
@@ -1306,16 +2542,39 @@ var SpreadsheetController = class {
|
|
|
1306
2542
|
evaluatingRowIndex = null;
|
|
1307
2543
|
// Cell being evaluated, for implicit intersection (`=@A:A`).
|
|
1308
2544
|
evaluatingCell = null;
|
|
1309
|
-
// Dynamic arrays. Spilled values are derived state: they live
|
|
1310
|
-
// the
|
|
1311
|
-
|
|
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.
|
|
2545
|
+
// Dynamic arrays. Spilled values are derived state: they live in the sheet
|
|
2546
|
+
// runtimes, not in the documents, and every recalculation rebuilds them.
|
|
2547
|
+
// Cells (sheet-qualified keys) whose spill ownership changed during the current pass.
|
|
1318
2548
|
spillChanges = null;
|
|
2549
|
+
/** The active sheet's document (the sheet the editor shows). */
|
|
2550
|
+
get document() {
|
|
2551
|
+
return this.ctx.doc;
|
|
2552
|
+
}
|
|
2553
|
+
set document(doc) {
|
|
2554
|
+
this.ctx.doc = doc;
|
|
2555
|
+
}
|
|
2556
|
+
get spills() {
|
|
2557
|
+
return this.ctx.spills;
|
|
2558
|
+
}
|
|
2559
|
+
get spilledBy() {
|
|
2560
|
+
return this.ctx.spilledBy;
|
|
2561
|
+
}
|
|
2562
|
+
get spillCells() {
|
|
2563
|
+
return this.ctx.spillCells;
|
|
2564
|
+
}
|
|
2565
|
+
// Conditional formatting is evaluated lazily (getConditionalFormatOverlay).
|
|
2566
|
+
// Cell keys whose values changed since, or 'all' to evaluate from scratch.
|
|
2567
|
+
cfEngine = null;
|
|
2568
|
+
cfDirty = "all";
|
|
2569
|
+
// Set while a single cell edit notifies, so the change stays incremental.
|
|
2570
|
+
cfIncremental = false;
|
|
2571
|
+
// Value being entered, seen by validation formulas instead of the cell's value.
|
|
2572
|
+
valueOverride = null;
|
|
2573
|
+
// "Circle invalid data": on/off and the cached cells.
|
|
2574
|
+
circlesShown = false;
|
|
2575
|
+
invalidCircles = null;
|
|
2576
|
+
/** Invalid input waiting for the user's answer (see `resolveValidationPrompt`). */
|
|
2577
|
+
validationPrompt = null;
|
|
1319
2578
|
constructor(options = {}) {
|
|
1320
2579
|
const defaultCols = [
|
|
1321
2580
|
{ id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
|
|
@@ -1329,7 +2588,7 @@ var SpreadsheetController = class {
|
|
|
1329
2588
|
height: 32,
|
|
1330
2589
|
type: "freeform"
|
|
1331
2590
|
}));
|
|
1332
|
-
|
|
2591
|
+
const first2 = {
|
|
1333
2592
|
id: options.document?.id || `sheet_${Date.now()}`,
|
|
1334
2593
|
name: options.document?.name || "Sheet 1",
|
|
1335
2594
|
columns: options.document?.columns || defaultCols,
|
|
@@ -1339,27 +2598,123 @@ var SpreadsheetController = class {
|
|
|
1339
2598
|
updatedAt: options.document?.updatedAt || Date.now(),
|
|
1340
2599
|
...options.document?.table ? { table: options.document.table } : {}
|
|
1341
2600
|
};
|
|
2601
|
+
if (options.workbook && options.workbook.sheets.length > 0) {
|
|
2602
|
+
this.setWorkbookState(options.workbook, false);
|
|
2603
|
+
} else {
|
|
2604
|
+
this.runtimes = [createRuntime(first2)];
|
|
2605
|
+
this.active = this.ctx = this.runtimes[0];
|
|
2606
|
+
}
|
|
2607
|
+
this.boundSheetId = this.runtimes[0].doc.id;
|
|
1342
2608
|
this.primaryDataSourceId = options.primaryDataSourceId;
|
|
1343
2609
|
this.maxHistoryBytes = Math.max(0, options.maxHistoryBytes ?? DEFAULT_MAX_HISTORY_BYTES);
|
|
1344
2610
|
this.onDocumentChange = options.onDocumentChange;
|
|
1345
2611
|
this.onSelectionChange = options.onSelectionChange;
|
|
1346
2612
|
this.onCellCommit = options.onCellCommit;
|
|
2613
|
+
this.onWorkbookChange = options.onWorkbookChange;
|
|
2614
|
+
this.onActiveSheetChange = options.onActiveSheetChange;
|
|
1347
2615
|
if (options.dataSources) {
|
|
1348
2616
|
options.dataSources.forEach((ds) => this.binder.register(ds));
|
|
1349
2617
|
}
|
|
1350
2618
|
this.evaluator = new FormulaEvaluator({
|
|
1351
|
-
resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
|
|
1352
|
-
resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
|
|
2619
|
+
resolveCell: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveCellByCoord(colStr, rowNum)),
|
|
2620
|
+
resolveRange: (sCol, sRow, eCol, eRow, sheet) => this.onSheet(sheet, () => this.resolveRangeByCoords(sCol, sRow, eCol, eRow)),
|
|
1353
2621
|
resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),
|
|
1354
|
-
resolveSpill: (colStr, rowNum) => this.resolveSpillByCoord(colStr, rowNum),
|
|
2622
|
+
resolveSpill: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveSpillByCoord(colStr, rowNum)),
|
|
2623
|
+
resolveSheetSpan: (first3, last) => this.resolveSheetSpan(first3, last),
|
|
2624
|
+
resolveName: (name, sheet) => this.resolveDefinedName(name, sheet),
|
|
1355
2625
|
getFormulaCell: () => this.evaluatingCell
|
|
1356
2626
|
});
|
|
1357
2627
|
this.syncWithDataSource();
|
|
1358
|
-
this.rederiveTypedValues();
|
|
2628
|
+
for (const rt of this.runtimes) this.withSheet(rt, () => this.rederiveTypedValues());
|
|
1359
2629
|
this.recalculateAll();
|
|
1360
2630
|
}
|
|
2631
|
+
// --- Workbook: sheet runtimes ---
|
|
2632
|
+
get boundRuntime() {
|
|
2633
|
+
return this.boundSheetId === null ? null : this.findRuntimeById(this.boundSheetId) ?? null;
|
|
2634
|
+
}
|
|
2635
|
+
/** Runs `fn` with `rt` as the sheet `this.document` (and spills) refer to. */
|
|
2636
|
+
withSheet(rt, fn) {
|
|
2637
|
+
if (rt === this.ctx) return fn();
|
|
2638
|
+
const saved = this.ctx;
|
|
2639
|
+
this.ctx = rt;
|
|
2640
|
+
try {
|
|
2641
|
+
return fn();
|
|
2642
|
+
} finally {
|
|
2643
|
+
this.ctx = saved;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Runs `fn` on the sheet named `sheet` (a sheet-qualified reference), or
|
|
2648
|
+
* on the current sheet when undefined. `#REF!` when no sheet has that name.
|
|
2649
|
+
*/
|
|
2650
|
+
onSheet(sheet, fn) {
|
|
2651
|
+
if (sheet === void 0) return fn();
|
|
2652
|
+
const rt = this.findRuntimeByName(sheet);
|
|
2653
|
+
return rt ? this.withSheet(rt, fn) : "#REF!";
|
|
2654
|
+
}
|
|
2655
|
+
findRuntimeByName(name) {
|
|
2656
|
+
return this.runtimes.find((rt) => sameName3(rt.doc.name, name));
|
|
2657
|
+
}
|
|
2658
|
+
findRuntimeById(id) {
|
|
2659
|
+
return this.runtimes.find((rt) => rt.doc.id === id) ?? this.runtimes.find((rt) => sameName3(rt.doc.id, id));
|
|
2660
|
+
}
|
|
2661
|
+
/** A sheet by id or name. */
|
|
2662
|
+
findRuntime(idOrName) {
|
|
2663
|
+
return this.runtimes.find((rt) => rt.doc.id === idOrName) ?? this.findRuntimeByName(idOrName);
|
|
2664
|
+
}
|
|
2665
|
+
/** Dependency-graph key of a cell (`A1`) on a sheet: sheet-qualified, so keys are unique in the workbook. */
|
|
2666
|
+
qualify(key, rt = this.ctx) {
|
|
2667
|
+
return `${rt.doc.id}!${key}`.toUpperCase();
|
|
2668
|
+
}
|
|
2669
|
+
/** The sheet and A1 key of a qualified key. */
|
|
2670
|
+
unqualify(qualified) {
|
|
2671
|
+
const bang = qualified.lastIndexOf("!");
|
|
2672
|
+
if (bang < 0) return null;
|
|
2673
|
+
const id = qualified.slice(0, bang);
|
|
2674
|
+
const rt = this.runtimes.find((r) => r.doc.id.toUpperCase() === id);
|
|
2675
|
+
return rt ? { rt, key: qualified.slice(bang + 1) } : null;
|
|
2676
|
+
}
|
|
2677
|
+
resolveSheetSpan(first2, last) {
|
|
2678
|
+
const a = this.runtimes.findIndex((rt) => sameName3(rt.doc.name, first2));
|
|
2679
|
+
const b = this.runtimes.findIndex((rt) => sameName3(rt.doc.name, last));
|
|
2680
|
+
if (a < 0 || b < 0) return "#REF!";
|
|
2681
|
+
return this.runtimes.slice(Math.min(a, b), Math.max(a, b) + 1).map((rt) => rt.doc.name);
|
|
2682
|
+
}
|
|
2683
|
+
/** The defined name `name` visible from `rt`: its sheet-scoped name first, then the workbook one. */
|
|
2684
|
+
findName(name, rt) {
|
|
2685
|
+
return (rt ? this.names.find((n) => n.sheetId === rt.doc.id && sameName3(n.name, name)) : void 0) ?? this.names.find((n) => n.sheetId === void 0 && sameName3(n.name, name));
|
|
2686
|
+
}
|
|
2687
|
+
resolveDefinedName(name, sheet) {
|
|
2688
|
+
const rt = sheet === void 0 ? this.ctx : this.findRuntimeByName(sheet);
|
|
2689
|
+
if (!rt) return null;
|
|
2690
|
+
const def = this.findName(name, rt);
|
|
2691
|
+
if (!def) return null;
|
|
2692
|
+
const scope = def.sheetId !== void 0 ? this.findRuntimeById(def.sheetId) : void 0;
|
|
2693
|
+
return {
|
|
2694
|
+
formula: def.refersTo,
|
|
2695
|
+
...scope ? { sheet: scope.doc.name } : {},
|
|
2696
|
+
key: `${def.sheetId ?? ""}!${def.name}`
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Replaces the active sheet with `document` (its name becomes the sheet's
|
|
2701
|
+
* name, made unique). Clears undo history.
|
|
2702
|
+
*/
|
|
1361
2703
|
loadDocument(document) {
|
|
1362
|
-
|
|
2704
|
+
const loaded = JSON.parse(JSON.stringify(document));
|
|
2705
|
+
const others = this.runtimes.filter((rt) => rt !== this.active);
|
|
2706
|
+
if (others.some((rt) => rt.doc.id === loaded.id)) loaded.id = this.generateSheetId();
|
|
2707
|
+
const otherNames = others.map((rt) => rt.doc.name);
|
|
2708
|
+
if (!loaded.name || otherNames.some((n) => sameName3(n, loaded.name))) {
|
|
2709
|
+
loaded.name = loaded.name ? copySheetName(loaded.name, otherNames) : nextSheetName(otherNames);
|
|
2710
|
+
}
|
|
2711
|
+
const previousId = this.active.doc.id;
|
|
2712
|
+
this.ctx = this.active;
|
|
2713
|
+
this.document = loaded;
|
|
2714
|
+
if (previousId !== loaded.id) {
|
|
2715
|
+
for (const n of this.names) if (n.sheetId === previousId) n.sheetId = loaded.id;
|
|
2716
|
+
if (this.boundSheetId === previousId) this.boundSheetId = loaded.id;
|
|
2717
|
+
}
|
|
1363
2718
|
this.activeCell = null;
|
|
1364
2719
|
this.selectedRange = null;
|
|
1365
2720
|
this.editingCell = null;
|
|
@@ -1371,6 +2726,60 @@ var SpreadsheetController = class {
|
|
|
1371
2726
|
this.recalculateAll();
|
|
1372
2727
|
this.notifyChange();
|
|
1373
2728
|
}
|
|
2729
|
+
// --- Workbook: loading and saving ---
|
|
2730
|
+
/** A deep copy of the workbook: every sheet, the active sheet and the defined names. */
|
|
2731
|
+
getWorkbook() {
|
|
2732
|
+
return JSON.parse(JSON.stringify(this.workbookSnapshot()));
|
|
2733
|
+
}
|
|
2734
|
+
/** Replaces the whole workbook (sheets, names, active sheet). Clears undo history. */
|
|
2735
|
+
loadWorkbook(workbook) {
|
|
2736
|
+
if (!workbook.sheets.length) return;
|
|
2737
|
+
this.setWorkbookState(JSON.parse(JSON.stringify(workbook)), true);
|
|
2738
|
+
this.boundSheetId = this.runtimes[0].doc.id;
|
|
2739
|
+
this.editingCell = null;
|
|
2740
|
+
this.draftValue = "";
|
|
2741
|
+
this.undoStack = [];
|
|
2742
|
+
this.redoStack = [];
|
|
2743
|
+
this.syncWithDataSource();
|
|
2744
|
+
for (const rt of this.runtimes) this.withSheet(rt, () => this.rederiveTypedValues());
|
|
2745
|
+
this.recalculateAll();
|
|
2746
|
+
this.notifyChange();
|
|
2747
|
+
this.onSelectionChange?.(this.activeCell, this.selectedRange);
|
|
2748
|
+
}
|
|
2749
|
+
workbookSnapshot() {
|
|
2750
|
+
return {
|
|
2751
|
+
sheets: this.runtimes.map((rt) => rt.doc),
|
|
2752
|
+
activeSheetId: this.active.doc.id,
|
|
2753
|
+
names: this.names
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Installs a workbook's sheets and names (taking ownership of the objects).
|
|
2758
|
+
* Duplicate sheet ids and names are made unique.
|
|
2759
|
+
*/
|
|
2760
|
+
setWorkbookState(workbook, resetSelection) {
|
|
2761
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2762
|
+
const names = [];
|
|
2763
|
+
const runtimes = workbook.sheets.map((doc) => {
|
|
2764
|
+
if (!doc.id || ids.has(doc.id)) doc.id = this.generateSheetId();
|
|
2765
|
+
ids.add(doc.id);
|
|
2766
|
+
if (!doc.name || names.some((n) => sameName3(n, doc.name))) {
|
|
2767
|
+
doc.name = doc.name ? copySheetName(doc.name, names) : nextSheetName(names);
|
|
2768
|
+
}
|
|
2769
|
+
names.push(doc.name);
|
|
2770
|
+
doc.columns ??= [];
|
|
2771
|
+
doc.rows ??= [];
|
|
2772
|
+
doc.cells ??= {};
|
|
2773
|
+
return createRuntime(doc);
|
|
2774
|
+
});
|
|
2775
|
+
this.runtimes = runtimes;
|
|
2776
|
+
this.active = this.ctx = runtimes.find((rt) => rt.doc.id === workbook.activeSheetId) ?? runtimes[0];
|
|
2777
|
+
this.names = (workbook.names ?? []).filter((n) => n.sheetId === void 0 || ids.has(n.sheetId)).map((n) => ({ ...n, refersTo: n.refersTo.startsWith("=") ? n.refersTo : `=${n.refersTo}` }));
|
|
2778
|
+
if (resetSelection) {
|
|
2779
|
+
this.activeCell = null;
|
|
2780
|
+
this.selectedRange = null;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
1374
2783
|
/**
|
|
1375
2784
|
* Re-parses the value of every stored typed (non-formula) cell from its
|
|
1376
2785
|
* `raw` input, so documents saved before typed input was parsed (whose
|
|
@@ -1452,9 +2861,12 @@ var SpreadsheetController = class {
|
|
|
1452
2861
|
this.recalculateAll();
|
|
1453
2862
|
}
|
|
1454
2863
|
syncWithDataSource() {
|
|
1455
|
-
if (!this.primaryDataSourceId) return;
|
|
1456
|
-
|
|
1457
|
-
this.
|
|
2864
|
+
if (!this.primaryDataSourceId || !this.boundRuntime) return;
|
|
2865
|
+
const id = this.primaryDataSourceId;
|
|
2866
|
+
this.withSheet(this.boundRuntime, () => {
|
|
2867
|
+
this.document.rows = this.binder.syncRows(this.document.rows, id);
|
|
2868
|
+
this.recomputeRowIndices();
|
|
2869
|
+
});
|
|
1458
2870
|
}
|
|
1459
2871
|
recomputeRowIndices() {
|
|
1460
2872
|
this.document.rows.forEach((r, idx) => {
|
|
@@ -1534,18 +2946,24 @@ var SpreadsheetController = class {
|
|
|
1534
2946
|
} else {
|
|
1535
2947
|
cell = { raw, ...this.typedCell(raw, col?.format, col, existing) };
|
|
1536
2948
|
}
|
|
1537
|
-
const
|
|
1538
|
-
this.
|
|
2949
|
+
const cellKey2 = this.getCellKey(rowId, colId);
|
|
2950
|
+
this.registerFormula(cellKey2, isFormula ? raw : null);
|
|
1539
2951
|
this.document.cells[rowId][colId] = cell;
|
|
1540
2952
|
this.document.updatedAt = Date.now();
|
|
1541
2953
|
if (this.batching) {
|
|
1542
2954
|
this.pendingCommits.push([rowId, colId, raw]);
|
|
1543
2955
|
return;
|
|
1544
2956
|
}
|
|
2957
|
+
if (this.cfDirty !== "all") this.cfDirty.add(cellKey2);
|
|
1545
2958
|
this.recalculateCell(rowId, colId);
|
|
1546
|
-
this.
|
|
1547
|
-
|
|
1548
|
-
|
|
2959
|
+
this.cfIncremental = true;
|
|
2960
|
+
try {
|
|
2961
|
+
this.notifyChange();
|
|
2962
|
+
} finally {
|
|
2963
|
+
this.cfIncremental = false;
|
|
2964
|
+
}
|
|
2965
|
+
const evaluatedVal = this.document.cells[rowId][colId]?.value;
|
|
2966
|
+
this.onCellCommit?.(rowId, colId, raw, evaluatedVal);
|
|
1549
2967
|
}
|
|
1550
2968
|
getCellKey(rowId, colId) {
|
|
1551
2969
|
const row = this.document.rows.find((r) => r.id === rowId);
|
|
@@ -1558,9 +2976,66 @@ var SpreadsheetController = class {
|
|
|
1558
2976
|
* order. Every cell in a reference cycle is marked `#CYCLE!`.
|
|
1559
2977
|
*/
|
|
1560
2978
|
recalculateCell(rowId, colId) {
|
|
1561
|
-
const
|
|
1562
|
-
const anchor = this.spilledBy.get(
|
|
1563
|
-
this.recalculateKeys(anchor ? [
|
|
2979
|
+
const cellKey2 = this.getCellKey(rowId, colId);
|
|
2980
|
+
const anchor = this.spilledBy.get(cellKey2);
|
|
2981
|
+
this.recalculateKeys(anchor ? [this.qualify(cellKey2), this.qualify(anchor)] : [this.qualify(cellKey2)], true);
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* Registers the dependencies of the formula in `cellKey` (A1 key on the
|
|
2985
|
+
* current sheet), or removes them when `formula` is null: its references
|
|
2986
|
+
* (sheet-qualified ones on their sheet, 3-D ones on every sheet of the
|
|
2987
|
+
* span) and, through defined names, the cells those names refer to.
|
|
2988
|
+
*/
|
|
2989
|
+
registerFormula(cellKey2, formula) {
|
|
2990
|
+
const key = this.qualify(cellKey2);
|
|
2991
|
+
if (!formula || !formula.startsWith("=")) {
|
|
2992
|
+
this.depGraph.setCellDependencies(key, []);
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
let ast;
|
|
2996
|
+
try {
|
|
2997
|
+
ast = parseFormula(formula);
|
|
2998
|
+
} catch {
|
|
2999
|
+
this.depGraph.setCellDependencies(key, []);
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
const deps = /* @__PURE__ */ new Set();
|
|
3003
|
+
this.collectDependencies(ast, this.ctx, deps, /* @__PURE__ */ new Set());
|
|
3004
|
+
this.depGraph.setCellDependencies(key, deps);
|
|
3005
|
+
}
|
|
3006
|
+
/** Adds the qualified keys `ast` depends on, with unqualified references on `home`. */
|
|
3007
|
+
collectDependencies(ast, home, out, seenNames) {
|
|
3008
|
+
for (const dep of extractCellDependencies(ast)) {
|
|
3009
|
+
let sheets;
|
|
3010
|
+
if (dep.sheet === void 0) {
|
|
3011
|
+
sheets = [home];
|
|
3012
|
+
} else if (dep.sheetEnd === void 0) {
|
|
3013
|
+
const rt = this.findRuntimeByName(dep.sheet);
|
|
3014
|
+
sheets = rt ? [rt] : [];
|
|
3015
|
+
} else {
|
|
3016
|
+
const span = this.resolveSheetSpan(dep.sheet, dep.sheetEnd);
|
|
3017
|
+
sheets = Array.isArray(span) ? span.map((name) => this.findRuntimeByName(name)) : [];
|
|
3018
|
+
}
|
|
3019
|
+
for (const rt of sheets) {
|
|
3020
|
+
out.add(dep.row === 0 ? columnDependencyKey(`${rt.doc.id}!${dep.col}`) : this.qualify(`${dep.col}${dep.row}`, rt));
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
for (const used of extractNameDependencies(ast)) {
|
|
3024
|
+
if (used.call && getFormulaFunction(used.name)) continue;
|
|
3025
|
+
const lookupSheet = used.sheet === void 0 ? home : this.findRuntimeByName(used.sheet);
|
|
3026
|
+
if (!lookupSheet) continue;
|
|
3027
|
+
const def = this.findName(used.name, lookupSheet);
|
|
3028
|
+
if (!def) continue;
|
|
3029
|
+
const scope = def.sheetId !== void 0 ? this.findRuntimeById(def.sheetId) : void 0;
|
|
3030
|
+
const nameHome = scope ?? home;
|
|
3031
|
+
const seenKey = `${def.sheetId ?? ""}!${def.name.toUpperCase()}@${nameHome.doc.id}`;
|
|
3032
|
+
if (seenNames.has(seenKey)) continue;
|
|
3033
|
+
seenNames.add(seenKey);
|
|
3034
|
+
try {
|
|
3035
|
+
this.collectDependencies(parseFormula(def.refersTo), nameHome, out, seenNames);
|
|
3036
|
+
} catch {
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
1564
3039
|
}
|
|
1565
3040
|
/**
|
|
1566
3041
|
* Recalculates `keys` (and, with `includeDependents`, their dependents) in
|
|
@@ -1589,56 +3064,69 @@ var SpreadsheetController = class {
|
|
|
1589
3064
|
* in dependency order.
|
|
1590
3065
|
*/
|
|
1591
3066
|
recalculateAll() {
|
|
3067
|
+
this.cfDirty = "all";
|
|
1592
3068
|
this.depGraph.clear();
|
|
1593
|
-
this.spills.clear();
|
|
1594
|
-
this.spilledBy.clear();
|
|
1595
|
-
this.spillCells.clear();
|
|
1596
3069
|
const formulaKeys = [];
|
|
1597
|
-
for (const
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
3070
|
+
for (const rt of this.runtimes) {
|
|
3071
|
+
this.withSheet(rt, () => {
|
|
3072
|
+
this.spills.clear();
|
|
3073
|
+
this.spilledBy.clear();
|
|
3074
|
+
this.spillCells.clear();
|
|
3075
|
+
for (const row of this.document.rows) {
|
|
3076
|
+
for (const col of this.document.columns) {
|
|
3077
|
+
const cell = this.document.cells[row.id]?.[col.id] ?? this.getDerivedCell(row, col);
|
|
3078
|
+
if (cell.raw.startsWith("=")) {
|
|
3079
|
+
const cellKey2 = `${col.key}${row.index + 1}`.toUpperCase();
|
|
3080
|
+
this.registerFormula(cellKey2, cell.raw);
|
|
3081
|
+
formulaKeys.push(this.qualify(cellKey2));
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
1604
3084
|
}
|
|
1605
|
-
}
|
|
3085
|
+
});
|
|
1606
3086
|
}
|
|
1607
3087
|
this.recalculateKeys(formulaKeys, false);
|
|
1608
3088
|
}
|
|
1609
3089
|
runEvaluationPlan(plan) {
|
|
1610
|
-
for (const
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
3090
|
+
for (const qualified of plan.order) {
|
|
3091
|
+
const located = this.unqualify(qualified);
|
|
3092
|
+
if (!located) continue;
|
|
3093
|
+
const onActive = located.rt === this.active;
|
|
3094
|
+
this.withSheet(located.rt, () => {
|
|
3095
|
+
if (onActive) this.markValueChanged(located.key);
|
|
3096
|
+
if (plan.cyclic.has(qualified)) {
|
|
3097
|
+
this.markCycle(located.key);
|
|
3098
|
+
} else {
|
|
3099
|
+
this.evalSingleCellByKey(located.key);
|
|
3100
|
+
}
|
|
3101
|
+
if (onActive) this.markValueChanged(located.key);
|
|
3102
|
+
});
|
|
1616
3103
|
}
|
|
1617
3104
|
}
|
|
1618
|
-
locateCellKey(
|
|
1619
|
-
const match =
|
|
3105
|
+
locateCellKey(cellKey2) {
|
|
3106
|
+
const match = cellKey2.match(/^([A-Za-z]+)(\d+)$/);
|
|
1620
3107
|
if (!match) return null;
|
|
1621
3108
|
const col = this.document.columns.find((c) => c.key.toUpperCase() === match[1].toUpperCase());
|
|
1622
3109
|
const row = this.document.rows[parseInt(match[2], 10) - 1];
|
|
1623
3110
|
return col && row ? { row, col } : null;
|
|
1624
3111
|
}
|
|
1625
|
-
markCycle(
|
|
1626
|
-
const located = this.locateCellKey(
|
|
3112
|
+
markCycle(cellKey2) {
|
|
3113
|
+
const located = this.locateCellKey(cellKey2);
|
|
1627
3114
|
if (!located) return;
|
|
1628
3115
|
const { row, col } = located;
|
|
1629
3116
|
const cell = this.getCell(row.id, col.id);
|
|
1630
3117
|
if (!cell.raw.startsWith("=")) return;
|
|
1631
3118
|
if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
|
|
1632
|
-
const
|
|
1633
|
-
const
|
|
1634
|
-
const
|
|
3119
|
+
const qualified = this.qualify(cellKey2);
|
|
3120
|
+
const deps = this.depGraph.getDependencies(qualified);
|
|
3121
|
+
const selfReference = deps.includes(qualified) || deps.includes(columnDependencyKey(`${this.document.id}!${col.key}`));
|
|
3122
|
+
const spilledKeys = this.depGraph.getSpilledKeys(qualified);
|
|
1635
3123
|
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(
|
|
3124
|
+
const errorMessage = selfReference ? "This formula can't reference its own cell." : ownSpill ? `This formula refers to ${ownSpill.slice(ownSpill.lastIndexOf("!") + 1)}, 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.";
|
|
3125
|
+
this.clearSpill(cellKey2.toUpperCase(), true);
|
|
1638
3126
|
this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!", errorMessage };
|
|
1639
3127
|
}
|
|
1640
|
-
evalSingleCellByKey(
|
|
1641
|
-
const match =
|
|
3128
|
+
evalSingleCellByKey(cellKey2) {
|
|
3129
|
+
const match = cellKey2.match(/^([A-Za-z]+)(\d+)$/);
|
|
1642
3130
|
if (!match) return;
|
|
1643
3131
|
const colName = match[1].toUpperCase();
|
|
1644
3132
|
const rowNum = parseInt(match[2], 10);
|
|
@@ -1723,7 +3211,7 @@ var SpreadsheetController = class {
|
|
|
1723
3211
|
}
|
|
1724
3212
|
if (reason) {
|
|
1725
3213
|
this.clearSpill(anchor);
|
|
1726
|
-
this.depGraph.setSpillEdges(anchor, [], blockers);
|
|
3214
|
+
this.depGraph.setSpillEdges(this.qualify(anchor), [], blockers.map((key) => this.qualify(key)));
|
|
1727
3215
|
return reason;
|
|
1728
3216
|
}
|
|
1729
3217
|
const previous = this.spills.get(anchor);
|
|
@@ -1749,7 +3237,7 @@ var SpreadsheetController = class {
|
|
|
1749
3237
|
this.spillCells.set(spillCellId(rowObj.id, column.id), display);
|
|
1750
3238
|
if (this.spilledBy.get(key) !== anchor) {
|
|
1751
3239
|
this.spilledBy.set(key, anchor);
|
|
1752
|
-
this.spillChanges?.add(key);
|
|
3240
|
+
this.spillChanges?.add(this.qualify(key));
|
|
1753
3241
|
}
|
|
1754
3242
|
cells.push({ key, rowId: rowObj.id, colId: column.id });
|
|
1755
3243
|
}
|
|
@@ -1762,7 +3250,7 @@ var SpreadsheetController = class {
|
|
|
1762
3250
|
}
|
|
1763
3251
|
}
|
|
1764
3252
|
this.spills.set(anchor, { anchor, row, col, rows, cols, values: array.values, cells });
|
|
1765
|
-
this.depGraph.setSpillEdges(anchor, cells.map((cell) => cell.key));
|
|
3253
|
+
this.depGraph.setSpillEdges(this.qualify(anchor), cells.map((cell) => this.qualify(cell.key)));
|
|
1766
3254
|
return null;
|
|
1767
3255
|
}
|
|
1768
3256
|
/** Removes the array spilled from `anchor`. With `keepEdges`, its dependency edges stay. */
|
|
@@ -1772,13 +3260,13 @@ var SpreadsheetController = class {
|
|
|
1772
3260
|
for (const cell of record.cells) this.forgetSpilledCell(anchor, cell);
|
|
1773
3261
|
this.spills.delete(anchor);
|
|
1774
3262
|
}
|
|
1775
|
-
if (!keepEdges) this.depGraph.setSpillEdges(anchor, []);
|
|
3263
|
+
if (!keepEdges) this.depGraph.setSpillEdges(this.qualify(anchor), []);
|
|
1776
3264
|
}
|
|
1777
3265
|
forgetSpilledCell(anchor, cell) {
|
|
1778
3266
|
if (this.spilledBy.get(cell.key) !== anchor) return;
|
|
1779
3267
|
this.spilledBy.delete(cell.key);
|
|
1780
3268
|
this.spillCells.delete(spillCellId(cell.rowId, cell.colId));
|
|
1781
|
-
this.spillChanges?.add(cell.key);
|
|
3269
|
+
this.spillChanges?.add(this.qualify(cell.key));
|
|
1782
3270
|
}
|
|
1783
3271
|
resolveSpillByCoord(colStr, rowNum) {
|
|
1784
3272
|
const record = this.spills.get(`${colStr}${rowNum}`.toUpperCase());
|
|
@@ -1832,6 +3320,8 @@ var SpreadsheetController = class {
|
|
|
1832
3320
|
const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
|
|
1833
3321
|
const row = this.document.rows[rowNum - 1];
|
|
1834
3322
|
if (!col || !row) return 0;
|
|
3323
|
+
const override = this.valueOverride;
|
|
3324
|
+
if (override && override.row === rowNum - 1 && this.document.columns[override.col] === col) return override.value;
|
|
1835
3325
|
return this.getCell(row.id, col.id).value ?? "";
|
|
1836
3326
|
}
|
|
1837
3327
|
resolveRangeByCoords(sCol, sRow, eCol, eRow) {
|
|
@@ -1846,9 +3336,14 @@ var SpreadsheetController = class {
|
|
|
1846
3336
|
const colName = indexToColName(c);
|
|
1847
3337
|
const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);
|
|
1848
3338
|
if (!col) continue;
|
|
3339
|
+
const override = this.valueOverride && this.document.columns[this.valueOverride.col] === col ? this.valueOverride : null;
|
|
1849
3340
|
for (let r = minR; r <= maxR; r++) {
|
|
1850
3341
|
const row = this.document.rows[r - 1];
|
|
1851
3342
|
if (!row) continue;
|
|
3343
|
+
if (override && override.row === r - 1) {
|
|
3344
|
+
results.push(override.value);
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
1852
3347
|
results.push(this.getCell(row.id, col.id).value ?? "");
|
|
1853
3348
|
}
|
|
1854
3349
|
}
|
|
@@ -1915,14 +3410,75 @@ var SpreadsheetController = class {
|
|
|
1915
3410
|
const cell = this.getCell(rowObj.id, colObj.id);
|
|
1916
3411
|
this.draftValue = initialValue !== void 0 ? initialValue : cell.raw;
|
|
1917
3412
|
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Commits the cell editor's draft (or `valueToCommit`). Input that breaks
|
|
3415
|
+
* the cell's data validation is not committed: the editor stays open and
|
|
3416
|
+
* `validationPrompt` holds the alert until `resolveValidationPrompt`.
|
|
3417
|
+
* Unchanged input is not validated.
|
|
3418
|
+
*/
|
|
1918
3419
|
commitEditing(valueToCommit) {
|
|
1919
|
-
if (!this.editingCell) return;
|
|
3420
|
+
if (!this.editingCell) return { status: "none" };
|
|
3421
|
+
if (this.validationPrompt) return { status: "pending", prompt: this.validationPrompt };
|
|
1920
3422
|
const val = valueToCommit !== void 0 ? valueToCommit : this.draftValue;
|
|
1921
|
-
|
|
1922
|
-
|
|
3423
|
+
const { rowId, colId, row, col } = this.editingCell;
|
|
3424
|
+
if (rowId && colId) {
|
|
3425
|
+
const failed = this.checkInput(rowId, colId, row, col, val, true);
|
|
3426
|
+
if (failed) return failed;
|
|
3427
|
+
this.setCellValue(rowId, colId, val);
|
|
1923
3428
|
}
|
|
1924
3429
|
this.editingCell = null;
|
|
1925
3430
|
this.draftValue = "";
|
|
3431
|
+
return { status: "committed" };
|
|
3432
|
+
}
|
|
3433
|
+
/**
|
|
3434
|
+
* Commits typed input to a cell (e.g. from the formula bar or a list
|
|
3435
|
+
* dropdown) with data validation, like `commitEditing` without an open
|
|
3436
|
+
* editor. `setCellValue` never validates (paste, import and code bypass
|
|
3437
|
+
* validation, as in Excel).
|
|
3438
|
+
*/
|
|
3439
|
+
commitCellInput(rowId, colId, raw) {
|
|
3440
|
+
if (this.validationPrompt) return { status: "pending", prompt: this.validationPrompt };
|
|
3441
|
+
const row = this.document.rows.findIndex((r) => r.id === rowId);
|
|
3442
|
+
const col = this.document.columns.findIndex((c) => c.id === colId);
|
|
3443
|
+
if (row === -1 || col === -1) return { status: "none" };
|
|
3444
|
+
const failed = this.checkInput(rowId, colId, row, col, raw, false);
|
|
3445
|
+
if (failed) return failed;
|
|
3446
|
+
this.setCellValue(rowId, colId, raw);
|
|
3447
|
+
return { status: "committed" };
|
|
3448
|
+
}
|
|
3449
|
+
/** Validates input; when it fails, records and returns the prompt. */
|
|
3450
|
+
checkInput(rowId, colId, row, col, raw, editing) {
|
|
3451
|
+
if (!this.document.table?.dataValidations?.length) return null;
|
|
3452
|
+
if (this.getCell(rowId, colId).raw === raw) return null;
|
|
3453
|
+
const result = this.validateCellInput(row, col, raw);
|
|
3454
|
+
if (result.valid) return null;
|
|
3455
|
+
const prompt = { rowId, colId, row, col, raw, result, editing };
|
|
3456
|
+
this.validationPrompt = prompt;
|
|
3457
|
+
return { status: result.errorStyle === "stop" ? "rejected" : "confirm", prompt };
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Answers `validationPrompt`: `accept` keeps the input (warning and
|
|
3461
|
+
* information alerts only; for a stop alert it acts as `retry`), `retry`
|
|
3462
|
+
* returns to editing it, `cancel` discards it.
|
|
3463
|
+
*/
|
|
3464
|
+
resolveValidationPrompt(action) {
|
|
3465
|
+
const prompt = this.validationPrompt;
|
|
3466
|
+
if (!prompt) return;
|
|
3467
|
+
this.validationPrompt = null;
|
|
3468
|
+
const editingSameCell = prompt.editing && this.editingCell?.rowId === prompt.rowId && this.editingCell?.colId === prompt.colId;
|
|
3469
|
+
if (action === "accept" && prompt.result.errorStyle !== "stop") {
|
|
3470
|
+
this.setCellValue(prompt.rowId, prompt.colId, prompt.raw);
|
|
3471
|
+
if (editingSameCell) {
|
|
3472
|
+
this.editingCell = null;
|
|
3473
|
+
this.draftValue = "";
|
|
3474
|
+
}
|
|
3475
|
+
} else if (action === "cancel") {
|
|
3476
|
+
if (editingSameCell) this.cancelEditing();
|
|
3477
|
+
} else if (editingSameCell) {
|
|
3478
|
+
this.draftValue = prompt.raw;
|
|
3479
|
+
} else {
|
|
3480
|
+
this.startEditing(prompt.row, prompt.col, prompt.raw);
|
|
3481
|
+
}
|
|
1926
3482
|
}
|
|
1927
3483
|
cancelEditing() {
|
|
1928
3484
|
this.editingCell = null;
|
|
@@ -2027,20 +3583,36 @@ var SpreadsheetController = class {
|
|
|
2027
3583
|
* column formula) so they keep pointing at the same cells.
|
|
2028
3584
|
*/
|
|
2029
3585
|
shiftReferences(change) {
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
3586
|
+
const changed = this.ctx;
|
|
3587
|
+
for (const rt of this.runtimes) {
|
|
3588
|
+
const filter = this.referencesTo(changed, rt);
|
|
3589
|
+
for (const rowCells of Object.values(rt.doc.cells)) {
|
|
3590
|
+
for (const cell of Object.values(rowCells)) {
|
|
3591
|
+
if (cell.raw?.startsWith("=")) {
|
|
3592
|
+
cell.raw = shiftFormulaReferences(cell.raw, change, filter);
|
|
3593
|
+
}
|
|
2034
3594
|
}
|
|
2035
3595
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
3596
|
+
if (change.axis === "col") {
|
|
3597
|
+
for (const col of rt.doc.columns) {
|
|
3598
|
+
if (col.formula?.startsWith("=")) {
|
|
3599
|
+
col.formula = shiftFormulaReferences(col.formula, change, filter);
|
|
3600
|
+
}
|
|
2041
3601
|
}
|
|
2042
3602
|
}
|
|
2043
3603
|
}
|
|
3604
|
+
for (const n of this.names) {
|
|
3605
|
+
const scope = n.sheetId !== void 0 ? this.findRuntimeById(n.sheetId) : void 0;
|
|
3606
|
+
n.refersTo = shiftFormulaReferences(n.refersTo, change, {
|
|
3607
|
+
appliesTo: (sheet, sheetEnd) => sheetEnd === void 0 && (sheet === void 0 ? scope === changed : sameName3(sheet, changed.doc.name))
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
/** Filter for references, in formulas on sheet `on`, that point at sheet `target`. */
|
|
3612
|
+
referencesTo(target, on) {
|
|
3613
|
+
return {
|
|
3614
|
+
appliesTo: (sheet, sheetEnd) => sheetEnd === void 0 && (sheet === void 0 ? on === target : sameName3(sheet, target.doc.name))
|
|
3615
|
+
};
|
|
2044
3616
|
}
|
|
2045
3617
|
setColumnWidth(colId, width) {
|
|
2046
3618
|
const col = this.document.columns.find((c) => c.id === colId);
|
|
@@ -2174,14 +3746,24 @@ var SpreadsheetController = class {
|
|
|
2174
3746
|
*/
|
|
2175
3747
|
importFromCsv(csv, options = {}) {
|
|
2176
3748
|
const matrix = parseCsv(csv, { delimiter: options.delimiter });
|
|
2177
|
-
this.writeMatrix(matrix, options.startRow ?? 0, options.startCol ?? 0);
|
|
3749
|
+
this.onTargetSheet(options.sheet, () => this.writeMatrix(matrix, options.startRow ?? 0, options.startCol ?? 0));
|
|
3750
|
+
}
|
|
3751
|
+
/** Runs `fn` on the sheet `sheet` (id or name), default the active one. Throws for an unknown sheet. */
|
|
3752
|
+
onTargetSheet(sheet, fn) {
|
|
3753
|
+
if (sheet === void 0) {
|
|
3754
|
+
fn();
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
const rt = this.findRuntime(sheet);
|
|
3758
|
+
if (!rt) throw new Error(`There is no sheet "${sheet}".`);
|
|
3759
|
+
this.withSheet(rt, fn);
|
|
2178
3760
|
}
|
|
2179
3761
|
/**
|
|
2180
3762
|
* Writes already-parsed rows of raw inputs (e.g. from an import adapter)
|
|
2181
3763
|
* starting at (startRow, startCol), as a single undo step.
|
|
2182
3764
|
*/
|
|
2183
3765
|
importRows(rows, options = {}) {
|
|
2184
|
-
this.writeMatrix(rows, options.startRow ?? 0, options.startCol ?? 0);
|
|
3766
|
+
this.onTargetSheet(options.sheet, () => this.writeMatrix(rows, options.startRow ?? 0, options.startCol ?? 0));
|
|
2185
3767
|
}
|
|
2186
3768
|
/** The cells of `range` (or the whole sheet), row by row. */
|
|
2187
3769
|
readMatrix(range) {
|
|
@@ -2271,10 +3853,14 @@ var SpreadsheetController = class {
|
|
|
2271
3853
|
};
|
|
2272
3854
|
/** Rewrites every formula so references follow cells that moved. */
|
|
2273
3855
|
remapAllFormulas(moveCell) {
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
3856
|
+
const moved = this.ctx;
|
|
3857
|
+
for (const rt of this.runtimes) {
|
|
3858
|
+
const filter = this.referencesTo(moved, rt);
|
|
3859
|
+
for (const rowCells of Object.values(rt.doc.cells)) {
|
|
3860
|
+
for (const cell of Object.values(rowCells)) {
|
|
3861
|
+
if (cell.raw?.startsWith("=")) {
|
|
3862
|
+
cell.raw = remapFormulaReferences(cell.raw, moveCell, filter);
|
|
3863
|
+
}
|
|
2278
3864
|
}
|
|
2279
3865
|
}
|
|
2280
3866
|
}
|
|
@@ -2285,6 +3871,7 @@ var SpreadsheetController = class {
|
|
|
2285
3871
|
* merges reduced to a single cell are dropped.
|
|
2286
3872
|
*/
|
|
2287
3873
|
adjustTableForStructuralChange(change) {
|
|
3874
|
+
this.adjustRulesForStructuralChange(change);
|
|
2288
3875
|
const merges = this.document.table?.merges;
|
|
2289
3876
|
if (!merges?.length) return;
|
|
2290
3877
|
const startKey = change.axis === "row" ? "startRow" : "startCol";
|
|
@@ -2806,10 +4393,361 @@ var SpreadsheetController = class {
|
|
|
2806
4393
|
getViewModel() {
|
|
2807
4394
|
return buildSpreadsheetView(this.document, this.getCellValueAt);
|
|
2808
4395
|
}
|
|
4396
|
+
// --- Sheets ---
|
|
4397
|
+
/** The sheets in tab order. */
|
|
4398
|
+
getSheets() {
|
|
4399
|
+
return this.runtimes.map((rt, index) => ({ id: rt.doc.id, name: rt.doc.name, index, active: rt === this.active }));
|
|
4400
|
+
}
|
|
4401
|
+
/** Id of the active sheet (the one `document` is). */
|
|
4402
|
+
get activeSheetId() {
|
|
4403
|
+
return this.active.doc.id;
|
|
4404
|
+
}
|
|
4405
|
+
/** A sheet's document by id or name (live: don't mutate it directly). */
|
|
4406
|
+
getSheetDocument(idOrName) {
|
|
4407
|
+
return this.findRuntime(idOrName)?.doc;
|
|
4408
|
+
}
|
|
4409
|
+
/** Why `name` can't name a sheet (or null): see `validateSheetName`. `exceptSheet` is the sheet being renamed. */
|
|
4410
|
+
validateSheetName(name, exceptSheet) {
|
|
4411
|
+
const except = exceptSheet === void 0 ? void 0 : this.findRuntime(exceptSheet);
|
|
4412
|
+
return validateSheetName(
|
|
4413
|
+
name,
|
|
4414
|
+
this.runtimes.filter((rt) => rt !== except).map((rt) => rt.doc.name)
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* Shows another sheet (by id or name). The current edit is committed and
|
|
4419
|
+
* each sheet keeps its own selection. Returns false for an unknown sheet.
|
|
4420
|
+
*/
|
|
4421
|
+
setActiveSheet(idOrName) {
|
|
4422
|
+
const rt = this.findRuntime(idOrName);
|
|
4423
|
+
if (!rt) return false;
|
|
4424
|
+
if (rt === this.active) return true;
|
|
4425
|
+
if (this.editingCell) this.commitEditing();
|
|
4426
|
+
this.activateRuntime(rt);
|
|
4427
|
+
this.notifyChange();
|
|
4428
|
+
return true;
|
|
4429
|
+
}
|
|
4430
|
+
activateRuntime(rt) {
|
|
4431
|
+
if (rt === this.active) return;
|
|
4432
|
+
if (this.runtimes.includes(this.active)) {
|
|
4433
|
+
this.active.activeCell = this.activeCell;
|
|
4434
|
+
this.active.selectedRange = this.selectedRange;
|
|
4435
|
+
}
|
|
4436
|
+
this.active = this.ctx = rt;
|
|
4437
|
+
this.activeCell = rt.activeCell;
|
|
4438
|
+
this.selectedRange = rt.selectedRange;
|
|
4439
|
+
this.editingCell = null;
|
|
4440
|
+
this.draftValue = "";
|
|
4441
|
+
this.onSelectionChange?.(this.activeCell, this.selectedRange);
|
|
4442
|
+
this.onActiveSheetChange?.(rt.doc.id);
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* Adds a sheet (default: `Sheet N`, empty, after the last sheet) and
|
|
4446
|
+
* activates it unless `activate` is false. Returns its id. Throws when
|
|
4447
|
+
* `name` is invalid or taken. Undoable.
|
|
4448
|
+
*/
|
|
4449
|
+
addSheet(options = {}) {
|
|
4450
|
+
const existing = this.runtimes.map((rt2) => rt2.doc.name);
|
|
4451
|
+
const name = options.name ?? options.document?.name ?? nextSheetName(existing);
|
|
4452
|
+
const invalid2 = validateSheetName(name, existing);
|
|
4453
|
+
if (invalid2) throw new Error(invalid2);
|
|
4454
|
+
this.recordWorkbookSnapshot();
|
|
4455
|
+
const id = options.document?.id && !this.findRuntimeById(options.document.id) ? options.document.id : this.generateSheetId();
|
|
4456
|
+
const doc = {
|
|
4457
|
+
columns: Array.from({ length: 4 }, (_, i) => ({
|
|
4458
|
+
id: `col_${String.fromCharCode(97 + i)}`,
|
|
4459
|
+
key: indexToColName(i),
|
|
4460
|
+
title: indexToColName(i),
|
|
4461
|
+
width: 120,
|
|
4462
|
+
type: "freeform"
|
|
4463
|
+
})),
|
|
4464
|
+
rows: Array.from({ length: 10 }, (_, i) => ({ id: `row_${i + 1}`, index: i, height: 32, type: "freeform" })),
|
|
4465
|
+
cells: {},
|
|
4466
|
+
createdAt: Date.now(),
|
|
4467
|
+
updatedAt: Date.now(),
|
|
4468
|
+
...JSON.parse(JSON.stringify(options.document ?? {})),
|
|
4469
|
+
id,
|
|
4470
|
+
name
|
|
4471
|
+
};
|
|
4472
|
+
const rt = createRuntime(doc);
|
|
4473
|
+
const index = Math.max(0, Math.min(options.index ?? this.runtimes.length, this.runtimes.length));
|
|
4474
|
+
this.runtimes.splice(index, 0, rt);
|
|
4475
|
+
this.withSheet(rt, () => {
|
|
4476
|
+
this.recomputeRowIndices();
|
|
4477
|
+
this.rederiveTypedValues();
|
|
4478
|
+
});
|
|
4479
|
+
if (options.activate !== false) this.activateRuntime(rt);
|
|
4480
|
+
this.recalculateAll();
|
|
4481
|
+
this.notifyChange();
|
|
4482
|
+
return id;
|
|
4483
|
+
}
|
|
4484
|
+
/**
|
|
4485
|
+
* Renames a sheet. Formulas on every sheet and defined names that refer to
|
|
4486
|
+
* it follow (`=Old!A1` becomes `='New Name'!A1`). Throws when the name is
|
|
4487
|
+
* invalid or taken. Undoable.
|
|
4488
|
+
*/
|
|
4489
|
+
renameSheet(idOrName, newName) {
|
|
4490
|
+
const rt = this.findRuntime(idOrName);
|
|
4491
|
+
if (!rt) throw new Error(`There is no sheet "${idOrName}".`);
|
|
4492
|
+
if (rt.doc.name === newName) return;
|
|
4493
|
+
const invalid2 = this.validateSheetName(newName, rt.doc.id);
|
|
4494
|
+
if (invalid2) throw new Error(invalid2);
|
|
4495
|
+
this.recordWorkbookSnapshot();
|
|
4496
|
+
const oldName = rt.doc.name;
|
|
4497
|
+
this.rewriteAllFormulas((formula) => renameSheetInFormula(formula, oldName, newName));
|
|
4498
|
+
rt.doc.name = newName;
|
|
4499
|
+
rt.doc.updatedAt = Date.now();
|
|
4500
|
+
this.recalculateAll();
|
|
4501
|
+
this.notifyChange();
|
|
4502
|
+
}
|
|
4503
|
+
/** Moves a sheet to position `toIndex` in tab order. Returns false for an unknown sheet. Undoable. */
|
|
4504
|
+
moveSheet(idOrName, toIndex) {
|
|
4505
|
+
const rt = this.findRuntime(idOrName);
|
|
4506
|
+
if (!rt) return false;
|
|
4507
|
+
const from = this.runtimes.indexOf(rt);
|
|
4508
|
+
const to = Math.max(0, Math.min(Math.floor(toIndex), this.runtimes.length - 1));
|
|
4509
|
+
if (from === to) return true;
|
|
4510
|
+
this.recordWorkbookSnapshot();
|
|
4511
|
+
this.runtimes.splice(from, 1);
|
|
4512
|
+
this.runtimes.splice(to, 0, rt);
|
|
4513
|
+
this.recalculateAll();
|
|
4514
|
+
this.notifyChange();
|
|
4515
|
+
return true;
|
|
4516
|
+
}
|
|
4517
|
+
/**
|
|
4518
|
+
* Deletes a sheet and its sheet-scoped names. References to it become
|
|
4519
|
+
* `#REF!` (a 3-D reference losing an end sheet shrinks). The last sheet
|
|
4520
|
+
* can't be deleted (returns false). Undoable.
|
|
4521
|
+
*/
|
|
4522
|
+
deleteSheet(idOrName) {
|
|
4523
|
+
const rt = this.findRuntime(idOrName);
|
|
4524
|
+
if (!rt || this.runtimes.length <= 1) return false;
|
|
4525
|
+
this.recordWorkbookSnapshot();
|
|
4526
|
+
const order = this.runtimes.map((r) => r.doc.name);
|
|
4527
|
+
const index = this.runtimes.indexOf(rt);
|
|
4528
|
+
this.runtimes.splice(index, 1);
|
|
4529
|
+
this.names = this.names.filter((n) => n.sheetId !== rt.doc.id);
|
|
4530
|
+
this.rewriteAllFormulas((formula) => deleteSheetInFormula(formula, rt.doc.name, order));
|
|
4531
|
+
if (this.active === rt) {
|
|
4532
|
+
this.ctx = this.active;
|
|
4533
|
+
this.activateRuntime(this.runtimes[Math.min(index, this.runtimes.length - 1)]);
|
|
4534
|
+
} else if (this.ctx === rt) {
|
|
4535
|
+
this.ctx = this.active;
|
|
4536
|
+
}
|
|
4537
|
+
this.recalculateAll();
|
|
4538
|
+
this.notifyChange();
|
|
4539
|
+
return true;
|
|
4540
|
+
}
|
|
4541
|
+
/**
|
|
4542
|
+
* Copies a sheet (default name `Name (2)`, placed after it) with its
|
|
4543
|
+
* sheet-scoped names, which refer to the copy. Activates the copy unless
|
|
4544
|
+
* `activate` is false. Returns the new sheet's id, or null for an unknown
|
|
4545
|
+
* sheet. Undoable.
|
|
4546
|
+
*/
|
|
4547
|
+
duplicateSheet(idOrName, options = {}) {
|
|
4548
|
+
const source = this.findRuntime(idOrName);
|
|
4549
|
+
if (!source) return null;
|
|
4550
|
+
const existing = this.runtimes.map((rt2) => rt2.doc.name);
|
|
4551
|
+
const name = options.name ?? copySheetName(source.doc.name, existing);
|
|
4552
|
+
const invalid2 = validateSheetName(name, existing);
|
|
4553
|
+
if (invalid2) throw new Error(invalid2);
|
|
4554
|
+
this.recordWorkbookSnapshot();
|
|
4555
|
+
const doc = JSON.parse(JSON.stringify(source.doc));
|
|
4556
|
+
doc.id = this.generateSheetId();
|
|
4557
|
+
doc.name = name;
|
|
4558
|
+
doc.createdAt = doc.updatedAt = Date.now();
|
|
4559
|
+
const copies = this.names.filter((n) => n.sheetId === source.doc.id).map((n) => ({ ...n, sheetId: doc.id, refersTo: renameSheetInFormula(n.refersTo, source.doc.name, name) }));
|
|
4560
|
+
this.names.push(...copies);
|
|
4561
|
+
const rt = createRuntime(doc);
|
|
4562
|
+
const index = Math.max(0, Math.min(options.index ?? this.runtimes.indexOf(source) + 1, this.runtimes.length));
|
|
4563
|
+
this.runtimes.splice(index, 0, rt);
|
|
4564
|
+
if (options.activate !== false) this.activateRuntime(rt);
|
|
4565
|
+
this.recalculateAll();
|
|
4566
|
+
this.notifyChange();
|
|
4567
|
+
return doc.id;
|
|
4568
|
+
}
|
|
4569
|
+
/** Applies `rewrite` to every formula: cells and column formulas on every sheet, and defined names. */
|
|
4570
|
+
rewriteAllFormulas(rewrite) {
|
|
4571
|
+
for (const rt of this.runtimes) {
|
|
4572
|
+
for (const [rowId, rowCells] of Object.entries(rt.doc.cells)) {
|
|
4573
|
+
for (const [colId, cell] of Object.entries(rowCells)) {
|
|
4574
|
+
if (!cell.raw?.startsWith("=")) continue;
|
|
4575
|
+
const raw = rewrite(cell.raw);
|
|
4576
|
+
if (raw !== cell.raw) rt.doc.cells[rowId][colId] = { ...cell, raw };
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
for (const col of rt.doc.columns) {
|
|
4580
|
+
if (col.formula?.startsWith("=")) col.formula = rewrite(col.formula);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
for (const n of this.names) n.refersTo = rewrite(n.refersTo);
|
|
4584
|
+
}
|
|
4585
|
+
generateSheetId() {
|
|
4586
|
+
return `sheet_${Date.now()}_${(nextIdSequence++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4587
|
+
}
|
|
4588
|
+
// --- Defined names (Name Manager) ---
|
|
4589
|
+
/** Every defined name, workbook-scoped and sheet-scoped. */
|
|
4590
|
+
getNames() {
|
|
4591
|
+
return this.names.map((n) => {
|
|
4592
|
+
const scope = n.sheetId !== void 0 ? this.findRuntimeById(n.sheetId) : void 0;
|
|
4593
|
+
return {
|
|
4594
|
+
...n,
|
|
4595
|
+
...scope ? { sheetName: scope.doc.name } : {},
|
|
4596
|
+
isLambda: isLambdaDefinition(n)
|
|
4597
|
+
};
|
|
4598
|
+
});
|
|
4599
|
+
}
|
|
4600
|
+
/** The defined name `name` visible from the active sheet (its own sheet-scoped name first), or undefined. */
|
|
4601
|
+
getName(name, scope) {
|
|
4602
|
+
const rt = scope?.sheet !== void 0 ? this.findRuntime(scope.sheet) : void 0;
|
|
4603
|
+
const def = scope?.sheet !== void 0 ? rt ? this.names.find((n) => n.sheetId === rt.doc.id && sameName3(n.name, name)) : void 0 : this.findName(name, this.active);
|
|
4604
|
+
return def ? this.getNames().find((n) => n.name === def.name && n.sheetId === def.sheetId) : void 0;
|
|
4605
|
+
}
|
|
4606
|
+
/** Why `name` can't be a defined name (or null): see `validateDefinedName`. */
|
|
4607
|
+
validateDefinedName(name) {
|
|
4608
|
+
return validateDefinedName(name);
|
|
4609
|
+
}
|
|
4610
|
+
scopeRuntime(scope) {
|
|
4611
|
+
if (scope?.sheet === void 0) return void 0;
|
|
4612
|
+
const rt = this.findRuntime(scope.sheet);
|
|
4613
|
+
if (!rt) throw new Error(`There is no sheet "${scope.sheet}".`);
|
|
4614
|
+
return rt;
|
|
4615
|
+
}
|
|
4616
|
+
/**
|
|
4617
|
+
* Defines (or redefines) a name in the workbook, or on one sheet with
|
|
4618
|
+
* `sheet`. `refersTo` is a formula: a range (`=Data!$A$2:$A$20`), a
|
|
4619
|
+
* constant (`=0.07`), a formula or a LAMBDA (`=LAMBDA(x, x*2)`, called as
|
|
4620
|
+
* `=Double(4)`). Formulas using the name recalculate. Throws when the name
|
|
4621
|
+
* is invalid or the formula can't be read. Undoable.
|
|
4622
|
+
*/
|
|
4623
|
+
defineName(name, refersTo, options = {}) {
|
|
4624
|
+
const invalid2 = validateDefinedName(name);
|
|
4625
|
+
if (invalid2) throw new Error(invalid2);
|
|
4626
|
+
const formula = refersTo.trim().startsWith("=") ? refersTo.trim() : `=${refersTo.trim()}`;
|
|
4627
|
+
try {
|
|
4628
|
+
parseFormula(formula);
|
|
4629
|
+
} catch (err) {
|
|
4630
|
+
throw new Error(`There's a problem with this formula: ${err?.message ?? formula}`);
|
|
4631
|
+
}
|
|
4632
|
+
const rt = this.scopeRuntime(options);
|
|
4633
|
+
this.recordWorkbookSnapshot();
|
|
4634
|
+
const sheetId = rt?.doc.id;
|
|
4635
|
+
const index = this.names.findIndex((n) => n.sheetId === sheetId && sameName3(n.name, name));
|
|
4636
|
+
const def = {
|
|
4637
|
+
name,
|
|
4638
|
+
refersTo: formula,
|
|
4639
|
+
...sheetId !== void 0 ? { sheetId } : {},
|
|
4640
|
+
...options.comment ? { comment: options.comment } : {}
|
|
4641
|
+
};
|
|
4642
|
+
if (index >= 0) this.names[index] = def;
|
|
4643
|
+
else this.names.push(def);
|
|
4644
|
+
this.recalculateAll();
|
|
4645
|
+
this.notifyChange();
|
|
4646
|
+
}
|
|
4647
|
+
/**
|
|
4648
|
+
* Renames a defined name. Formulas that use it (cells, column formulas and
|
|
4649
|
+
* other names) follow, on the sheets where the old name meant this one.
|
|
4650
|
+
* Throws when the name doesn't exist or the new name is invalid or taken.
|
|
4651
|
+
* Undoable.
|
|
4652
|
+
*/
|
|
4653
|
+
renameName(oldName, newName, options = {}) {
|
|
4654
|
+
const rt = this.scopeRuntime(options);
|
|
4655
|
+
const sheetId = rt?.doc.id;
|
|
4656
|
+
const def = this.names.find((n) => n.sheetId === sheetId && sameName3(n.name, oldName));
|
|
4657
|
+
if (!def) throw new Error(`There is no name "${oldName}"${rt ? ` on sheet ${rt.doc.name}` : ""}.`);
|
|
4658
|
+
if (def.name === newName) return;
|
|
4659
|
+
const invalid2 = validateDefinedName(newName);
|
|
4660
|
+
if (invalid2) throw new Error(invalid2);
|
|
4661
|
+
if (!sameName3(oldName, newName) && this.names.some((n) => n.sheetId === sheetId && sameName3(n.name, newName))) {
|
|
4662
|
+
throw new Error("The name entered already exists. Enter a unique name.");
|
|
4663
|
+
}
|
|
4664
|
+
this.recordWorkbookSnapshot();
|
|
4665
|
+
const shadowedOn = new Set(this.names.filter((n) => n.sheetId !== void 0 && sameName3(n.name, oldName)).map((n) => n.sheetId));
|
|
4666
|
+
const appliesOn = (sheet) => sheetId !== void 0 ? sheet?.doc.id === sheetId : !sheet || !shadowedOn.has(sheet.doc.id);
|
|
4667
|
+
const appliesToSheetName = (sheetName) => appliesOn(this.findRuntimeByName(sheetName));
|
|
4668
|
+
for (const sheet of this.runtimes) {
|
|
4669
|
+
const options2 = { appliesTo: appliesToSheetName, qualifiedOnly: !appliesOn(sheet) };
|
|
4670
|
+
for (const [rowId, rowCells] of Object.entries(sheet.doc.cells)) {
|
|
4671
|
+
for (const [colId, cell] of Object.entries(rowCells)) {
|
|
4672
|
+
if (!cell.raw?.startsWith("=")) continue;
|
|
4673
|
+
const raw = renameNameInFormula(cell.raw, oldName, newName, options2);
|
|
4674
|
+
if (raw !== cell.raw) sheet.doc.cells[rowId][colId] = { ...cell, raw };
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
for (const col of sheet.doc.columns) {
|
|
4678
|
+
if (col.formula?.startsWith("=")) col.formula = renameNameInFormula(col.formula, oldName, newName, options2);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
for (const n of this.names) {
|
|
4682
|
+
const scope = n.sheetId !== void 0 ? this.findRuntimeById(n.sheetId) : void 0;
|
|
4683
|
+
n.refersTo = renameNameInFormula(n.refersTo, oldName, newName, {
|
|
4684
|
+
appliesTo: appliesToSheetName,
|
|
4685
|
+
qualifiedOnly: scope ? !appliesOn(scope) : sheetId !== void 0
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
def.name = newName;
|
|
4689
|
+
this.recalculateAll();
|
|
4690
|
+
this.notifyChange();
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
4693
|
+
* Deletes a defined name. Formulas still using it show `#NAME?`. Returns
|
|
4694
|
+
* false when there is no such name. Undoable.
|
|
4695
|
+
*/
|
|
4696
|
+
deleteName(name, options = {}) {
|
|
4697
|
+
const rt = this.scopeRuntime(options);
|
|
4698
|
+
const sheetId = rt?.doc.id;
|
|
4699
|
+
const index = this.names.findIndex((n) => n.sheetId === sheetId && sameName3(n.name, name));
|
|
4700
|
+
if (index < 0) return false;
|
|
4701
|
+
this.recordWorkbookSnapshot();
|
|
4702
|
+
this.names.splice(index, 1);
|
|
4703
|
+
this.recalculateAll();
|
|
4704
|
+
this.notifyChange();
|
|
4705
|
+
return true;
|
|
4706
|
+
}
|
|
4707
|
+
/**
|
|
4708
|
+
* Names formula autocomplete can offer on the active sheet: its
|
|
4709
|
+
* sheet-scoped names and the workbook names they don't hide.
|
|
4710
|
+
*/
|
|
4711
|
+
getNameSuggestions() {
|
|
4712
|
+
const visible = /* @__PURE__ */ new Map();
|
|
4713
|
+
for (const n of this.names) {
|
|
4714
|
+
if (n.sheetId === this.active.doc.id) visible.set(n.name.toUpperCase(), n);
|
|
4715
|
+
}
|
|
4716
|
+
for (const n of this.names) {
|
|
4717
|
+
if (n.sheetId === void 0 && !visible.has(n.name.toUpperCase())) visible.set(n.name.toUpperCase(), n);
|
|
4718
|
+
}
|
|
4719
|
+
return Array.from(visible.values(), (n) => ({
|
|
4720
|
+
name: n.name,
|
|
4721
|
+
description: n.comment ? `${n.comment} (${n.refersTo})` : n.refersTo,
|
|
4722
|
+
isLambda: isLambdaDefinition(n)
|
|
4723
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4724
|
+
}
|
|
4725
|
+
/**
|
|
4726
|
+
* Evaluates a formula (e.g. `=Rate*2`, `=SUM(Sales)`) as if it were in a
|
|
4727
|
+
* cell of `sheet` (id or name; default the active sheet), without storing
|
|
4728
|
+
* it. Used by the Name Manager to show a name's current value.
|
|
4729
|
+
*/
|
|
4730
|
+
evaluateFormula(formula, options = {}) {
|
|
4731
|
+
const rt = options.sheet === void 0 ? this.active : this.findRuntime(options.sheet);
|
|
4732
|
+
if (!rt) return { value: "#REF!", error: "#REF!", message: `There is no sheet "${options.sheet}".` };
|
|
4733
|
+
const text = formula.startsWith("=") ? formula : `=${formula}`;
|
|
4734
|
+
return this.withSheet(rt, () => this.evaluator.evaluateWithDiagnostics(text));
|
|
4735
|
+
}
|
|
2809
4736
|
// --- Undo/Redo Snapshots ---
|
|
4737
|
+
//
|
|
4738
|
+
// Most steps snapshot one sheet (the one changed); sheet and name
|
|
4739
|
+
// operations snapshot the whole workbook. Undoing a sheet's step shows
|
|
4740
|
+
// that sheet.
|
|
2810
4741
|
recordSnapshot() {
|
|
2811
4742
|
if (this.isApplyingHistory || this.batching) return;
|
|
2812
|
-
this.
|
|
4743
|
+
this.pushUndo({ kind: "sheet", sheetId: this.ctx.doc.id, data: JSON.stringify(this.ctx.doc) });
|
|
4744
|
+
}
|
|
4745
|
+
recordWorkbookSnapshot() {
|
|
4746
|
+
if (this.isApplyingHistory || this.batching) return;
|
|
4747
|
+
this.pushUndo({ kind: "workbook", data: JSON.stringify(this.workbookSnapshot()) });
|
|
4748
|
+
}
|
|
4749
|
+
pushUndo(entry) {
|
|
4750
|
+
this.undoStack.push(entry);
|
|
2813
4751
|
if (this.undoStack.length > 50) this.undoStack.shift();
|
|
2814
4752
|
this.redoStack = [];
|
|
2815
4753
|
this.trimHistory();
|
|
@@ -2817,18 +4755,16 @@ var SpreadsheetController = class {
|
|
|
2817
4755
|
/** Drops the oldest snapshots beyond `maxHistoryBytes`, always keeping the newest. */
|
|
2818
4756
|
trimHistory() {
|
|
2819
4757
|
if (this.maxHistoryBytes <= 0 || this.undoStack.length <= 1) return;
|
|
2820
|
-
let total =
|
|
2821
|
-
for (const s of this.undoStack) total += s.length * 2;
|
|
2822
|
-
for (const s of this.redoStack) total += s.length * 2;
|
|
4758
|
+
let total = this.getHistoryByteSize();
|
|
2823
4759
|
while (total > this.maxHistoryBytes && this.undoStack.length > 1) {
|
|
2824
|
-
total -= this.undoStack.shift().length * 2;
|
|
4760
|
+
total -= this.undoStack.shift().data.length * 2;
|
|
2825
4761
|
}
|
|
2826
4762
|
}
|
|
2827
4763
|
/** Approximate bytes held by the undo/redo snapshots (UTF-16 size). */
|
|
2828
4764
|
getHistoryByteSize() {
|
|
2829
4765
|
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;
|
|
4766
|
+
for (const s of this.undoStack) total += s.data.length * 2;
|
|
4767
|
+
for (const s of this.redoStack) total += s.data.length * 2;
|
|
2832
4768
|
return total;
|
|
2833
4769
|
}
|
|
2834
4770
|
/** Number of undo / redo snapshots currently held. */
|
|
@@ -2836,32 +4772,340 @@ var SpreadsheetController = class {
|
|
|
2836
4772
|
return { undo: this.undoStack.length, redo: this.redoStack.length };
|
|
2837
4773
|
}
|
|
2838
4774
|
undo() {
|
|
2839
|
-
|
|
4775
|
+
this.applyHistory(this.undoStack, this.redoStack);
|
|
4776
|
+
}
|
|
4777
|
+
redo() {
|
|
4778
|
+
this.applyHistory(this.redoStack, this.undoStack);
|
|
4779
|
+
}
|
|
4780
|
+
/** Restores the newest entry of `from`, saving the current state of the same scope on `to`. */
|
|
4781
|
+
applyHistory(from, to) {
|
|
4782
|
+
const entry = from.pop();
|
|
4783
|
+
if (!entry) return;
|
|
2840
4784
|
this.isApplyingHistory = true;
|
|
2841
4785
|
try {
|
|
2842
|
-
|
|
2843
|
-
|
|
4786
|
+
if (entry.kind === "sheet") {
|
|
4787
|
+
const rt = this.findRuntimeById(entry.sheetId);
|
|
4788
|
+
if (!rt) return;
|
|
4789
|
+
to.push({ kind: "sheet", sheetId: rt.doc.id, data: JSON.stringify(rt.doc) });
|
|
4790
|
+
rt.doc = JSON.parse(entry.data);
|
|
4791
|
+
if (rt !== this.active) {
|
|
4792
|
+
this.ctx = this.active;
|
|
4793
|
+
this.activateRuntime(rt);
|
|
4794
|
+
}
|
|
4795
|
+
} else {
|
|
4796
|
+
to.push({ kind: "workbook", data: JSON.stringify(this.workbookSnapshot()) });
|
|
4797
|
+
const previousActive = this.active.doc.id;
|
|
4798
|
+
const selection = { activeCell: this.activeCell, selectedRange: this.selectedRange };
|
|
4799
|
+
const saved = new Map(this.runtimes.map((rt) => [rt.doc.id, rt]));
|
|
4800
|
+
this.setWorkbookState(JSON.parse(entry.data), false);
|
|
4801
|
+
for (const rt of this.runtimes) {
|
|
4802
|
+
const old = saved.get(rt.doc.id);
|
|
4803
|
+
if (old) {
|
|
4804
|
+
rt.activeCell = old.activeCell;
|
|
4805
|
+
rt.selectedRange = old.selectedRange;
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
if (this.active.doc.id === previousActive) {
|
|
4809
|
+
this.activeCell = selection.activeCell;
|
|
4810
|
+
this.selectedRange = selection.selectedRange;
|
|
4811
|
+
} else {
|
|
4812
|
+
this.activeCell = this.active.activeCell;
|
|
4813
|
+
this.selectedRange = this.active.selectedRange;
|
|
4814
|
+
this.onSelectionChange?.(this.activeCell, this.selectedRange);
|
|
4815
|
+
this.onActiveSheetChange?.(this.active.doc.id);
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
2844
4818
|
this.recalculateAll();
|
|
2845
4819
|
this.notifyChange();
|
|
2846
4820
|
} finally {
|
|
2847
4821
|
this.isApplyingHistory = false;
|
|
2848
4822
|
}
|
|
2849
4823
|
}
|
|
2850
|
-
|
|
2851
|
-
if (this.
|
|
2852
|
-
this.
|
|
4824
|
+
notifyChange() {
|
|
4825
|
+
if (!this.cfIncremental) this.cfDirty = "all";
|
|
4826
|
+
this.invalidCircles = null;
|
|
4827
|
+
this.onDocumentChange?.(this.active.doc);
|
|
4828
|
+
if (this.onWorkbookChange) {
|
|
4829
|
+
const snapshot = this.workbookSnapshot();
|
|
4830
|
+
this.onWorkbookChange({ sheets: snapshot.sheets, activeSheetId: snapshot.activeSheetId, names: snapshot.names });
|
|
4831
|
+
}
|
|
4832
|
+
}
|
|
4833
|
+
// --- Conditional formatting and data validation ---
|
|
4834
|
+
/** The sheet as rule evaluation sees it. */
|
|
4835
|
+
ruleSheet() {
|
|
4836
|
+
return {
|
|
4837
|
+
rowCount: this.document.rows.length,
|
|
4838
|
+
colCount: this.document.columns.length,
|
|
4839
|
+
getValue: (row, col) => this.getCellValueAt(row, col) ?? "",
|
|
4840
|
+
getInput: (row, col) => {
|
|
4841
|
+
const r = this.document.rows[row];
|
|
4842
|
+
const c = this.document.columns[col];
|
|
4843
|
+
return r && c ? this.getCell(r.id, c.id).raw : "";
|
|
4844
|
+
},
|
|
4845
|
+
evaluateFormula: (formula, row, col, entered) => this.evaluateFormulaAt(formula, row, col, entered)
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
4848
|
+
/**
|
|
4849
|
+
* Evaluates `formula` as if it were in (row, col), without storing it:
|
|
4850
|
+
* relative references and `[property]` resolve from that cell. With
|
|
4851
|
+
* `entered`, that cell reads as the given value (input being validated).
|
|
4852
|
+
* Returns the value, or an array's values (column-major) for a range.
|
|
4853
|
+
*/
|
|
4854
|
+
evaluateFormulaAt(formula, row, col, entered) {
|
|
4855
|
+
const saved = { rowIndex: this.evaluatingRowIndex, cell: this.evaluatingCell, override: this.valueOverride };
|
|
4856
|
+
this.evaluatingRowIndex = row;
|
|
4857
|
+
this.evaluatingCell = { col: this.document.columns[col]?.key ?? indexToColName(col), row: row + 1 };
|
|
4858
|
+
this.valueOverride = entered ?? null;
|
|
2853
4859
|
try {
|
|
2854
|
-
this.
|
|
2855
|
-
|
|
2856
|
-
this.recalculateAll();
|
|
2857
|
-
this.notifyChange();
|
|
4860
|
+
const result = this.evaluator.evaluateWithDiagnostics(formula.startsWith("=") ? formula : `=${formula}`);
|
|
4861
|
+
return result.array ? result.array.values : result.value;
|
|
2858
4862
|
} finally {
|
|
2859
|
-
this.
|
|
4863
|
+
this.evaluatingRowIndex = saved.rowIndex;
|
|
4864
|
+
this.evaluatingCell = saved.cell;
|
|
4865
|
+
this.valueOverride = saved.override;
|
|
2860
4866
|
}
|
|
2861
4867
|
}
|
|
2862
|
-
|
|
4868
|
+
/** Records that the value of `cellKey` (and of the array it spills) may have changed. */
|
|
4869
|
+
markValueChanged(cellKey2) {
|
|
4870
|
+
if (this.cfDirty === "all") return;
|
|
4871
|
+
this.cfDirty.add(cellKey2);
|
|
4872
|
+
const spill = this.spills.get(cellKey2);
|
|
4873
|
+
if (spill) for (const cell of spill.cells) this.cfDirty.add(cell.key);
|
|
4874
|
+
}
|
|
4875
|
+
generateRuleId(prefix) {
|
|
4876
|
+
return `${prefix}_${Date.now()}_${(nextIdSequence++).toString(36)}`;
|
|
4877
|
+
}
|
|
4878
|
+
/** Moves rule ranges and rule formulas with inserted / deleted rows and columns. */
|
|
4879
|
+
adjustRulesForStructuralChange(change) {
|
|
4880
|
+
const table = this.document.table;
|
|
4881
|
+
if (!table) return;
|
|
4882
|
+
const shiftIfFormula = (text) => text !== void 0 && text.trimStart().startsWith("=") ? shiftRuleFormula(text, change) : text;
|
|
4883
|
+
const shiftAnchor = (value) => value.type === "formula" ? { ...value, formula: shiftRuleFormula(value.formula, change) } : value;
|
|
4884
|
+
if (table.conditionalFormats?.length) {
|
|
4885
|
+
table.conditionalFormats = table.conditionalFormats.map((rule) => {
|
|
4886
|
+
const next = { ...rule, ranges: adjustRangesForStructuralChange(rule.ranges, change) };
|
|
4887
|
+
if (next.type === "expression") next.formula = shiftRuleFormula(next.formula, change);
|
|
4888
|
+
if (next.type === "cellIs") {
|
|
4889
|
+
next.value = shiftIfFormula(next.value) ?? "";
|
|
4890
|
+
if (next.value2 !== void 0) next.value2 = shiftIfFormula(next.value2);
|
|
4891
|
+
}
|
|
4892
|
+
if (next.type === "colorScale") next.stops = next.stops.map((s) => ({ ...s, value: shiftAnchor(s.value) }));
|
|
4893
|
+
if (next.type === "dataBar") {
|
|
4894
|
+
if (next.min) next.min = shiftAnchor(next.min);
|
|
4895
|
+
if (next.max) next.max = shiftAnchor(next.max);
|
|
4896
|
+
}
|
|
4897
|
+
if (next.type === "iconSet" && next.thresholds) next.thresholds = next.thresholds.map(shiftAnchor);
|
|
4898
|
+
return next;
|
|
4899
|
+
}).filter((rule) => rule.ranges.length > 0);
|
|
4900
|
+
}
|
|
4901
|
+
if (table.dataValidations?.length) {
|
|
4902
|
+
table.dataValidations = table.dataValidations.map((rule) => {
|
|
4903
|
+
const next = { ...rule, ranges: adjustRangesForStructuralChange(rule.ranges, change) };
|
|
4904
|
+
if (rule.formula1 !== void 0) next.formula1 = shiftIfFormula(rule.formula1);
|
|
4905
|
+
if (rule.formula2 !== void 0) next.formula2 = shiftIfFormula(rule.formula2);
|
|
4906
|
+
return next;
|
|
4907
|
+
}).filter((rule) => rule.ranges.length > 0);
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
/** Conditional formats of the sheet, highest priority first. */
|
|
4911
|
+
getConditionalFormats() {
|
|
4912
|
+
return this.document.table?.conditionalFormats ?? [];
|
|
4913
|
+
}
|
|
4914
|
+
/**
|
|
4915
|
+
* Adds a conditional format (one undo step). New rules get the highest
|
|
4916
|
+
* priority, as in Excel; pass `priority: 'last'` to add it below the
|
|
4917
|
+
* others. Returns the rule id.
|
|
4918
|
+
*/
|
|
4919
|
+
addConditionalFormat(rule, options = {}) {
|
|
4920
|
+
const id = rule.id || this.generateRuleId("cf");
|
|
4921
|
+
const stored = { ...rule, id, ranges: rule.ranges.map((r) => normalizeRange(r)) };
|
|
4922
|
+
this.applyTableChange(() => {
|
|
4923
|
+
const rules = [...this.table.conditionalFormats ?? []];
|
|
4924
|
+
if (options.priority === "last") rules.push(stored);
|
|
4925
|
+
else rules.unshift(stored);
|
|
4926
|
+
this.table.conditionalFormats = rules;
|
|
4927
|
+
});
|
|
4928
|
+
return id;
|
|
4929
|
+
}
|
|
4930
|
+
/** Changes a conditional format (its condition, ranges or stop-if-true). Returns false when not found. */
|
|
4931
|
+
updateConditionalFormat(id, changes) {
|
|
4932
|
+
const rules = this.document.table?.conditionalFormats ?? [];
|
|
4933
|
+
const index = rules.findIndex((r) => r.id === id);
|
|
4934
|
+
if (index === -1) return false;
|
|
4935
|
+
this.applyTableChange(() => {
|
|
4936
|
+
const next = [...rules];
|
|
4937
|
+
const updated = { ...rules[index], ...changes, id };
|
|
4938
|
+
if (changes.ranges) updated.ranges = changes.ranges.map((r) => normalizeRange(r));
|
|
4939
|
+
next[index] = updated;
|
|
4940
|
+
this.table.conditionalFormats = next;
|
|
4941
|
+
});
|
|
4942
|
+
return true;
|
|
4943
|
+
}
|
|
4944
|
+
/** Removes a conditional format. Returns false when not found. */
|
|
4945
|
+
deleteConditionalFormat(id) {
|
|
4946
|
+
const rules = this.document.table?.conditionalFormats ?? [];
|
|
4947
|
+
if (!rules.some((r) => r.id === id)) return false;
|
|
4948
|
+
this.applyTableChange(() => {
|
|
4949
|
+
this.table.conditionalFormats = rules.filter((r) => r.id !== id);
|
|
4950
|
+
});
|
|
4951
|
+
return true;
|
|
4952
|
+
}
|
|
4953
|
+
/**
|
|
4954
|
+
* Moves a conditional format to priority `to` (0 = highest), or by
|
|
4955
|
+
* `{ by: -1 }` (up) / `{ by: 1 }` (down). Returns false when nothing moved.
|
|
4956
|
+
*/
|
|
4957
|
+
moveConditionalFormat(id, to) {
|
|
4958
|
+
const rules = this.document.table?.conditionalFormats ?? [];
|
|
4959
|
+
const from = rules.findIndex((r) => r.id === id);
|
|
4960
|
+
if (from === -1) return false;
|
|
4961
|
+
const target = Math.max(0, Math.min(rules.length - 1, typeof to === "number" ? to : from + to.by));
|
|
4962
|
+
if (target === from) return false;
|
|
4963
|
+
this.applyTableChange(() => {
|
|
4964
|
+
const next = [...rules];
|
|
4965
|
+
const [moved] = next.splice(from, 1);
|
|
4966
|
+
next.splice(target, 0, moved);
|
|
4967
|
+
this.table.conditionalFormats = next;
|
|
4968
|
+
});
|
|
4969
|
+
return true;
|
|
4970
|
+
}
|
|
4971
|
+
/**
|
|
4972
|
+
* Removes conditional formatting from the cells of `range` (rules keep
|
|
4973
|
+
* their other cells; rules left without cells are deleted), or every rule
|
|
4974
|
+
* when `range` is null.
|
|
4975
|
+
*/
|
|
4976
|
+
clearConditionalFormats(range = null) {
|
|
4977
|
+
const rules = this.document.table?.conditionalFormats ?? [];
|
|
4978
|
+
if (rules.length === 0) return;
|
|
4979
|
+
this.applyTableChange(() => {
|
|
4980
|
+
this.table.conditionalFormats = range ? rules.map((rule) => ({ ...rule, ranges: subtractRangeFromRanges(rule.ranges, range) })).filter((rule) => rule.ranges.length > 0) : [];
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
/**
|
|
4984
|
+
* The conditional look of every formatted cell. Evaluated on demand and
|
|
4985
|
+
* cached: after a cell edit only the affected cells are re-evaluated.
|
|
4986
|
+
*/
|
|
4987
|
+
getConditionalFormatOverlay() {
|
|
4988
|
+
const rules = this.document.table?.conditionalFormats ?? [];
|
|
4989
|
+
if (rules.length === 0) {
|
|
4990
|
+
this.cfEngine = null;
|
|
4991
|
+
this.cfDirty = "all";
|
|
4992
|
+
return EMPTY_CONDITIONAL_FORMAT_OVERLAY;
|
|
4993
|
+
}
|
|
4994
|
+
const sheet = this.ruleSheet();
|
|
4995
|
+
if (!this.cfEngine || this.cfDirty === "all") {
|
|
4996
|
+
this.cfEngine = new ConditionalFormatEngine(sheet);
|
|
4997
|
+
this.cfEngine.evaluate(rules);
|
|
4998
|
+
} else if (this.cfDirty.size > 0) {
|
|
4999
|
+
const changed = [];
|
|
5000
|
+
for (const key of this.cfDirty) {
|
|
5001
|
+
const located = this.locateCellKey(key);
|
|
5002
|
+
if (located) changed.push({ row: located.row.index, col: this.document.columns.indexOf(located.col) });
|
|
5003
|
+
}
|
|
5004
|
+
this.cfEngine.setSheet(sheet);
|
|
5005
|
+
this.cfEngine.update(rules, changed);
|
|
5006
|
+
}
|
|
5007
|
+
this.cfDirty = /* @__PURE__ */ new Set();
|
|
5008
|
+
return this.cfEngine.overlay;
|
|
5009
|
+
}
|
|
5010
|
+
/** The conditional look of (row, col), if any. */
|
|
5011
|
+
getConditionalFormat(row, col) {
|
|
5012
|
+
return this.getConditionalFormatOverlay().get(row, col);
|
|
5013
|
+
}
|
|
5014
|
+
/** Data validation rules of the sheet. */
|
|
5015
|
+
getDataValidations() {
|
|
5016
|
+
return this.document.table?.dataValidations ?? [];
|
|
5017
|
+
}
|
|
5018
|
+
/** The rule that applies to (row, col), if any. */
|
|
5019
|
+
getDataValidationAt(row, col) {
|
|
5020
|
+
return findDataValidation(this.document.table?.dataValidations, row, col);
|
|
5021
|
+
}
|
|
5022
|
+
/**
|
|
5023
|
+
* Adds a data validation rule (one undo step). Its cells are first
|
|
5024
|
+
* removed from other rules, so each cell has at most one. Returns the id.
|
|
5025
|
+
*/
|
|
5026
|
+
addDataValidation(rule) {
|
|
5027
|
+
const id = rule.id || this.generateRuleId("dv");
|
|
5028
|
+
const ranges = rule.ranges.map((r) => normalizeRange(r));
|
|
5029
|
+
this.applyTableChange(() => {
|
|
5030
|
+
let rules = this.table.dataValidations ?? [];
|
|
5031
|
+
for (const cut of ranges) {
|
|
5032
|
+
rules = rules.map((r) => ({ ...r, ranges: subtractRangeFromRanges(r.ranges, cut) })).filter((r) => r.ranges.length > 0);
|
|
5033
|
+
}
|
|
5034
|
+
this.table.dataValidations = [...rules, { ...rule, id, ranges }];
|
|
5035
|
+
});
|
|
5036
|
+
return id;
|
|
5037
|
+
}
|
|
5038
|
+
/** Changes a data validation rule. Returns false when not found. */
|
|
5039
|
+
updateDataValidation(id, changes) {
|
|
5040
|
+
const rules = this.document.table?.dataValidations ?? [];
|
|
5041
|
+
const index = rules.findIndex((r) => r.id === id);
|
|
5042
|
+
if (index === -1) return false;
|
|
5043
|
+
this.applyTableChange(() => {
|
|
5044
|
+
const next = [...rules];
|
|
5045
|
+
const updated = { ...rules[index], ...changes, id };
|
|
5046
|
+
if (changes.ranges) updated.ranges = changes.ranges.map((r) => normalizeRange(r));
|
|
5047
|
+
next[index] = updated;
|
|
5048
|
+
this.table.dataValidations = next;
|
|
5049
|
+
});
|
|
5050
|
+
return true;
|
|
5051
|
+
}
|
|
5052
|
+
/** Removes a data validation rule. Returns false when not found. */
|
|
5053
|
+
deleteDataValidation(id) {
|
|
5054
|
+
const rules = this.document.table?.dataValidations ?? [];
|
|
5055
|
+
if (!rules.some((r) => r.id === id)) return false;
|
|
5056
|
+
this.applyTableChange(() => {
|
|
5057
|
+
this.table.dataValidations = rules.filter((r) => r.id !== id);
|
|
5058
|
+
});
|
|
5059
|
+
return true;
|
|
5060
|
+
}
|
|
5061
|
+
/** Removes validation from the cells of `range`, or every rule when `range` is null. */
|
|
5062
|
+
clearDataValidation(range = null) {
|
|
5063
|
+
const rules = this.document.table?.dataValidations ?? [];
|
|
5064
|
+
if (rules.length === 0) return;
|
|
5065
|
+
this.applyTableChange(() => {
|
|
5066
|
+
this.table.dataValidations = range ? rules.map((r) => ({ ...r, ranges: subtractRangeFromRanges(r.ranges, range) })).filter((r) => r.ranges.length > 0) : [];
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
/** Checks input for (row, col) against its data validation, without committing it. */
|
|
5070
|
+
validateCellInput(row, col, raw) {
|
|
5071
|
+
return validateCellInput(this.ruleSheet(), this.document.table?.dataValidations, row, col, raw);
|
|
5072
|
+
}
|
|
5073
|
+
/**
|
|
5074
|
+
* Dropdown items of a list-validated cell, or null when the cell has no
|
|
5075
|
+
* list rule (or its in-cell dropdown is off).
|
|
5076
|
+
*/
|
|
5077
|
+
getValidationListValues(row, col) {
|
|
5078
|
+
const rule = this.getDataValidationAt(row, col);
|
|
5079
|
+
if (!rule || rule.type !== "list" || rule.inCellDropdown === false) return null;
|
|
5080
|
+
return getValidationListValues(this.ruleSheet(), rule);
|
|
5081
|
+
}
|
|
5082
|
+
/** Cells whose current value breaks their validation rule. */
|
|
5083
|
+
findInvalidCells() {
|
|
5084
|
+
return findInvalidCells(this.ruleSheet(), this.document.table?.dataValidations);
|
|
5085
|
+
}
|
|
5086
|
+
/**
|
|
5087
|
+
* Turns "circle invalid data" on or off. View state, not part of the
|
|
5088
|
+
* document (not undoable, not saved); listeners are notified so the grid
|
|
5089
|
+
* redraws.
|
|
5090
|
+
*/
|
|
5091
|
+
setInvalidDataCircles(show) {
|
|
5092
|
+
if (this.circlesShown === show) return;
|
|
5093
|
+
this.circlesShown = show;
|
|
5094
|
+
this.invalidCircles = null;
|
|
2863
5095
|
this.onDocumentChange?.(this.document);
|
|
2864
5096
|
}
|
|
5097
|
+
/** Whether invalid cells are circled. */
|
|
5098
|
+
getInvalidDataCircles() {
|
|
5099
|
+
return this.circlesShown;
|
|
5100
|
+
}
|
|
5101
|
+
/** Whether (row, col) is circled as invalid (always false while circles are off). */
|
|
5102
|
+
isInvalidDataCircled(row, col) {
|
|
5103
|
+
if (!this.circlesShown) return false;
|
|
5104
|
+
if (!this.invalidCircles) {
|
|
5105
|
+
this.invalidCircles = new Set(this.findInvalidCells().map((c) => `${c.row}:${c.col}`));
|
|
5106
|
+
}
|
|
5107
|
+
return this.invalidCircles.has(`${row}:${col}`);
|
|
5108
|
+
}
|
|
2865
5109
|
};
|
|
2866
5110
|
function stepInOrder(order, from, delta) {
|
|
2867
5111
|
if (order.length === 0) return from;
|
|
@@ -2875,7 +5119,11 @@ function stepInOrder(order, from, delta) {
|
|
|
2875
5119
|
}
|
|
2876
5120
|
|
|
2877
5121
|
// src/spreadsheet/menu.ts
|
|
2878
|
-
function runSpreadsheetMenuItem(item, ask) {
|
|
5122
|
+
function runSpreadsheetMenuItem(item, ask, confirm) {
|
|
5123
|
+
if (item.confirm) {
|
|
5124
|
+
const confirmed = confirm ? confirm(item.confirm) : typeof window !== "undefined" && typeof window.confirm === "function" ? window.confirm(item.confirm) : true;
|
|
5125
|
+
if (!confirmed) return false;
|
|
5126
|
+
}
|
|
2879
5127
|
if (!item.input) {
|
|
2880
5128
|
item.run();
|
|
2881
5129
|
return true;
|
|
@@ -3067,8 +5315,91 @@ function getSpreadsheetMenuItems(controller, cell) {
|
|
|
3067
5315
|
run: () => controller.groupRowsBy(column.id)
|
|
3068
5316
|
});
|
|
3069
5317
|
}
|
|
5318
|
+
items.push(...getRuleMenuItems(controller, selection));
|
|
3070
5319
|
return items;
|
|
3071
5320
|
}
|
|
5321
|
+
function getRuleMenuItems(controller, selection) {
|
|
5322
|
+
const ranges = [selection];
|
|
5323
|
+
const hasFormats = controller.getConditionalFormats().some((rule) => rule.ranges.some((r) => rangesIntersect(r, selection)));
|
|
5324
|
+
const hasValidation = controller.getDataValidations().some((rule) => rule.ranges.some((r) => rangesIntersect(r, selection)));
|
|
5325
|
+
const add = (input) => {
|
|
5326
|
+
controller.addConditionalFormat(createConditionalFormatRule({ ...input, ranges }));
|
|
5327
|
+
};
|
|
5328
|
+
const circles = controller.getInvalidDataCircles();
|
|
5329
|
+
return [
|
|
5330
|
+
{
|
|
5331
|
+
id: "conditional-format-manage",
|
|
5332
|
+
label: "Conditional formatting\u2026",
|
|
5333
|
+
separatorBefore: true,
|
|
5334
|
+
panel: "conditionalFormatting",
|
|
5335
|
+
run: () => {
|
|
5336
|
+
}
|
|
5337
|
+
},
|
|
5338
|
+
{
|
|
5339
|
+
id: "conditional-format-greater-than",
|
|
5340
|
+
label: "Conditional formatting: Highlight cells greater than\u2026",
|
|
5341
|
+
input: { label: "Highlight cells greater than", defaultValue: "0" },
|
|
5342
|
+
run: (value) => {
|
|
5343
|
+
if (value !== void 0 && value.trim() !== "") add({ kind: "greaterThan", value: value.trim() });
|
|
5344
|
+
}
|
|
5345
|
+
},
|
|
5346
|
+
{
|
|
5347
|
+
id: "conditional-format-duplicates",
|
|
5348
|
+
label: "Conditional formatting: Highlight duplicate values",
|
|
5349
|
+
run: () => add({ kind: "duplicate" })
|
|
5350
|
+
},
|
|
5351
|
+
{
|
|
5352
|
+
id: "conditional-format-color-scale",
|
|
5353
|
+
label: "Conditional formatting: Color scale",
|
|
5354
|
+
run: () => add({ kind: "colorScale3" })
|
|
5355
|
+
},
|
|
5356
|
+
{
|
|
5357
|
+
id: "conditional-format-data-bars",
|
|
5358
|
+
label: "Conditional formatting: Data bars",
|
|
5359
|
+
run: () => add({ kind: "dataBar" })
|
|
5360
|
+
},
|
|
5361
|
+
{
|
|
5362
|
+
id: "conditional-format-icon-set",
|
|
5363
|
+
label: "Conditional formatting: Icon set",
|
|
5364
|
+
run: () => add({ kind: "iconSet", iconSet: "3Arrows" })
|
|
5365
|
+
},
|
|
5366
|
+
{
|
|
5367
|
+
id: "conditional-format-clear",
|
|
5368
|
+
label: "Clear conditional formatting from selection",
|
|
5369
|
+
disabled: !hasFormats,
|
|
5370
|
+
run: () => controller.clearConditionalFormats(selection)
|
|
5371
|
+
},
|
|
5372
|
+
{
|
|
5373
|
+
id: "data-validation-manage",
|
|
5374
|
+
label: "Data validation\u2026",
|
|
5375
|
+
separatorBefore: true,
|
|
5376
|
+
panel: "dataValidation",
|
|
5377
|
+
run: () => {
|
|
5378
|
+
}
|
|
5379
|
+
},
|
|
5380
|
+
{
|
|
5381
|
+
id: "data-validation-list",
|
|
5382
|
+
label: "Data validation: List of values\u2026",
|
|
5383
|
+
input: { label: "Allowed values, separated by commas (or a range such as =$H$1:$H$5)", defaultValue: "" },
|
|
5384
|
+
run: (value) => {
|
|
5385
|
+
if (value === void 0 || value.trim() === "") return;
|
|
5386
|
+
controller.addDataValidation({ ranges, type: "list", formula1: value.trim() });
|
|
5387
|
+
}
|
|
5388
|
+
},
|
|
5389
|
+
{
|
|
5390
|
+
id: "data-validation-clear",
|
|
5391
|
+
label: "Clear data validation from selection",
|
|
5392
|
+
disabled: !hasValidation,
|
|
5393
|
+
run: () => controller.clearDataValidation(selection)
|
|
5394
|
+
},
|
|
5395
|
+
{
|
|
5396
|
+
id: "data-validation-circles",
|
|
5397
|
+
label: circles ? "Clear validation circles" : "Circle invalid data",
|
|
5398
|
+
disabled: !circles && controller.getDataValidations().length === 0,
|
|
5399
|
+
run: () => controller.setInvalidDataCircles(!circles)
|
|
5400
|
+
}
|
|
5401
|
+
];
|
|
5402
|
+
}
|
|
3072
5403
|
|
|
3073
5404
|
// src/spreadsheet/actions.ts
|
|
3074
5405
|
var SPREADSHEET_TABLE_ACTIONS = [
|
|
@@ -3108,7 +5439,28 @@ var SPREADSHEET_TABLE_ACTIONS = [
|
|
|
3108
5439
|
"exportToCsv",
|
|
3109
5440
|
"importFromCsv",
|
|
3110
5441
|
"copyToClipboardData",
|
|
3111
|
-
"pasteClipboardData"
|
|
5442
|
+
"pasteClipboardData",
|
|
5443
|
+
// Conditional formatting and data validation
|
|
5444
|
+
"getConditionalFormats",
|
|
5445
|
+
"addConditionalFormat",
|
|
5446
|
+
"updateConditionalFormat",
|
|
5447
|
+
"deleteConditionalFormat",
|
|
5448
|
+
"moveConditionalFormat",
|
|
5449
|
+
"clearConditionalFormats",
|
|
5450
|
+
"getConditionalFormatOverlay",
|
|
5451
|
+
"getConditionalFormat",
|
|
5452
|
+
"getDataValidations",
|
|
5453
|
+
"getDataValidationAt",
|
|
5454
|
+
"addDataValidation",
|
|
5455
|
+
"updateDataValidation",
|
|
5456
|
+
"deleteDataValidation",
|
|
5457
|
+
"clearDataValidation",
|
|
5458
|
+
"validateCellInput",
|
|
5459
|
+
"getValidationListValues",
|
|
5460
|
+
"findInvalidCells",
|
|
5461
|
+
"setInvalidDataCircles",
|
|
5462
|
+
"getInvalidDataCircles",
|
|
5463
|
+
"isInvalidDataCircled"
|
|
3112
5464
|
];
|
|
3113
5465
|
function bindSpreadsheetTableActions(controller) {
|
|
3114
5466
|
const actions = {};
|
|
@@ -3156,13 +5508,106 @@ function registerSpreadsheetImportAdapters(registry) {
|
|
|
3156
5508
|
return () => unregisters.forEach((unregister) => unregister());
|
|
3157
5509
|
}
|
|
3158
5510
|
async function importFileIntoSpreadsheet(controller, input, options) {
|
|
3159
|
-
const { registry, startRow, startCol, ...rest } = options;
|
|
5511
|
+
const { registry, startRow, startCol, sheet, ...rest } = options;
|
|
3160
5512
|
const outcome = await registry.importFile(input, { ...rest, target: "spreadsheet" });
|
|
3161
|
-
controller.importRows(outcome.result.rows, { startRow, startCol });
|
|
5513
|
+
controller.importRows(outcome.result.rows, { startRow, startCol, sheet });
|
|
3162
5514
|
return outcome;
|
|
3163
5515
|
}
|
|
3164
5516
|
|
|
5517
|
+
// src/spreadsheet/sheet-tabs.ts
|
|
5518
|
+
function getSheetTabMenuItems(controller, sheetId, options = {}) {
|
|
5519
|
+
const sheets = controller.getSheets();
|
|
5520
|
+
const sheet = sheets.find((s) => s.id === sheetId);
|
|
5521
|
+
if (!sheet) return [];
|
|
5522
|
+
const guard = (fn) => () => {
|
|
5523
|
+
try {
|
|
5524
|
+
fn();
|
|
5525
|
+
} catch (err) {
|
|
5526
|
+
if (!options.onError) throw err;
|
|
5527
|
+
options.onError(err?.message ?? String(err));
|
|
5528
|
+
}
|
|
5529
|
+
};
|
|
5530
|
+
return [
|
|
5531
|
+
{
|
|
5532
|
+
id: "sheet-insert",
|
|
5533
|
+
label: "Insert sheet",
|
|
5534
|
+
run: guard(() => {
|
|
5535
|
+
controller.addSheet({ index: sheet.index + 1 });
|
|
5536
|
+
})
|
|
5537
|
+
},
|
|
5538
|
+
{
|
|
5539
|
+
id: "sheet-rename",
|
|
5540
|
+
label: "Rename\u2026",
|
|
5541
|
+
input: { label: "Sheet name", defaultValue: sheet.name },
|
|
5542
|
+
run: (name) => guard(() => {
|
|
5543
|
+
const next = (name ?? "").trim();
|
|
5544
|
+
if (next && next !== sheet.name) controller.renameSheet(sheetId, next);
|
|
5545
|
+
})()
|
|
5546
|
+
},
|
|
5547
|
+
{
|
|
5548
|
+
id: "sheet-duplicate",
|
|
5549
|
+
label: "Duplicate",
|
|
5550
|
+
run: guard(() => {
|
|
5551
|
+
controller.duplicateSheet(sheetId);
|
|
5552
|
+
})
|
|
5553
|
+
},
|
|
5554
|
+
{
|
|
5555
|
+
id: "sheet-move-left",
|
|
5556
|
+
label: "Move left",
|
|
5557
|
+
disabled: sheet.index === 0,
|
|
5558
|
+
separatorBefore: true,
|
|
5559
|
+
run: () => {
|
|
5560
|
+
controller.moveSheet(sheetId, sheet.index - 1);
|
|
5561
|
+
}
|
|
5562
|
+
},
|
|
5563
|
+
{
|
|
5564
|
+
id: "sheet-move-right",
|
|
5565
|
+
label: "Move right",
|
|
5566
|
+
disabled: sheet.index === sheets.length - 1,
|
|
5567
|
+
run: () => {
|
|
5568
|
+
controller.moveSheet(sheetId, sheet.index + 1);
|
|
5569
|
+
}
|
|
5570
|
+
},
|
|
5571
|
+
{
|
|
5572
|
+
id: "sheet-delete",
|
|
5573
|
+
label: "Delete",
|
|
5574
|
+
disabled: sheets.length <= 1,
|
|
5575
|
+
separatorBefore: true,
|
|
5576
|
+
confirm: `Delete the sheet "${sheet.name}"? Formulas that refer to it will show #REF!.`,
|
|
5577
|
+
run: () => {
|
|
5578
|
+
controller.deleteSheet(sheetId);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
];
|
|
5582
|
+
}
|
|
5583
|
+
function sheetDropIndex(fromIndex, targetIndex, dropAfter) {
|
|
5584
|
+
let to = dropAfter ? targetIndex + 1 : targetIndex;
|
|
5585
|
+
if (fromIndex < to) to--;
|
|
5586
|
+
return to;
|
|
5587
|
+
}
|
|
5588
|
+
function getNameManagerRows(controller) {
|
|
5589
|
+
return controller.getNames().map((n) => {
|
|
5590
|
+
let value;
|
|
5591
|
+
if (n.isLambda) {
|
|
5592
|
+
value = "[LAMBDA]";
|
|
5593
|
+
} else {
|
|
5594
|
+
const result = controller.evaluateFormula(`=${n.name}`, n.sheetId ? { sheet: n.sheetId } : {});
|
|
5595
|
+
value = result.array ? `{${result.array.rows}\xD7${result.array.cols} array}` : isFormulaError(result.value) ? String(result.value) : formatCellDisplay({ value: result.value });
|
|
5596
|
+
}
|
|
5597
|
+
return {
|
|
5598
|
+
name: n.name,
|
|
5599
|
+
refersTo: n.refersTo,
|
|
5600
|
+
scopeLabel: n.sheetName ?? "Workbook",
|
|
5601
|
+
...n.sheetId ? { sheetId: n.sheetId } : {},
|
|
5602
|
+
value,
|
|
5603
|
+
...n.comment ? { comment: n.comment } : {},
|
|
5604
|
+
isLambda: n.isLambda
|
|
5605
|
+
};
|
|
5606
|
+
}).sort((a, b) => a.name.localeCompare(b.name) || a.scopeLabel.localeCompare(b.scopeLabel));
|
|
5607
|
+
}
|
|
5608
|
+
|
|
3165
5609
|
export {
|
|
5610
|
+
extractNameDependencies,
|
|
3166
5611
|
columnDependencyKey,
|
|
3167
5612
|
extractCellDependencies,
|
|
3168
5613
|
DependencyGraph,
|
|
@@ -3170,6 +5615,13 @@ export {
|
|
|
3170
5615
|
mapFormulaReferences,
|
|
3171
5616
|
offsetFormulaReferences,
|
|
3172
5617
|
remapFormulaReferences,
|
|
5618
|
+
sheetNameNeedsQuotes,
|
|
5619
|
+
quoteSheetName,
|
|
5620
|
+
formatSheetPrefix,
|
|
5621
|
+
renameSheetInFormula,
|
|
5622
|
+
deleteSheetInFormula,
|
|
5623
|
+
renameNameInFormula,
|
|
5624
|
+
formulaIdentifiers,
|
|
3173
5625
|
getFormulaCaretContext,
|
|
3174
5626
|
getFormulaHints,
|
|
3175
5627
|
applyFormulaSuggestion,
|
|
@@ -3181,6 +5633,13 @@ export {
|
|
|
3181
5633
|
formatCellDisplay,
|
|
3182
5634
|
formatValueWithCode,
|
|
3183
5635
|
DataSourceBinder,
|
|
5636
|
+
MAX_SHEET_NAME_LENGTH,
|
|
5637
|
+
MAX_DEFINED_NAME_LENGTH,
|
|
5638
|
+
validateSheetName,
|
|
5639
|
+
nextSheetName,
|
|
5640
|
+
copySheetName,
|
|
5641
|
+
validateDefinedName,
|
|
5642
|
+
isLambdaDefinition,
|
|
3184
5643
|
normalizeRange,
|
|
3185
5644
|
rangeContains,
|
|
3186
5645
|
rangesIntersect,
|
|
@@ -3201,6 +5660,47 @@ export {
|
|
|
3201
5660
|
createSpreadsheetClipboardData,
|
|
3202
5661
|
readSpreadsheetClipboard,
|
|
3203
5662
|
rebaseClipboardRows,
|
|
5663
|
+
adjustRangeForStructuralChange,
|
|
5664
|
+
adjustRangesForStructuralChange,
|
|
5665
|
+
subtractRange,
|
|
5666
|
+
subtractRangeFromRanges,
|
|
5667
|
+
rangesContain,
|
|
5668
|
+
ruleAnchor,
|
|
5669
|
+
ruleFormulaAt,
|
|
5670
|
+
shiftRuleFormula,
|
|
5671
|
+
forEachRangeCell,
|
|
5672
|
+
formatRuleRanges,
|
|
5673
|
+
parseRuleRanges,
|
|
5674
|
+
CONDITIONAL_FORMAT_ICON_SETS,
|
|
5675
|
+
conditionalFormatIconCount,
|
|
5676
|
+
getConditionalFormatIcon,
|
|
5677
|
+
conditionalFormatNumber,
|
|
5678
|
+
blendConditionalFormatColors,
|
|
5679
|
+
defaultIconThresholds,
|
|
5680
|
+
EMPTY_CONDITIONAL_FORMAT_OVERLAY,
|
|
5681
|
+
ConditionalFormatEngine,
|
|
5682
|
+
matchesTimePeriod,
|
|
5683
|
+
evaluateConditionalFormats,
|
|
5684
|
+
describeConditionalFormat,
|
|
5685
|
+
describeDataValidation,
|
|
5686
|
+
CONDITIONAL_FORMAT_STYLE_PRESETS,
|
|
5687
|
+
DEFAULT_CONDITIONAL_FORMAT_STYLE,
|
|
5688
|
+
CONDITIONAL_FORMAT_PRESETS,
|
|
5689
|
+
CONDITIONAL_FORMAT_TIME_PERIODS,
|
|
5690
|
+
createConditionalFormatRule,
|
|
5691
|
+
DATA_VALIDATION_TYPE_OPTIONS,
|
|
5692
|
+
DATA_VALIDATION_OPERATOR_OPTIONS,
|
|
5693
|
+
DATA_VALIDATION_ERROR_STYLE_OPTIONS,
|
|
5694
|
+
conditionalFormatContrastColor,
|
|
5695
|
+
getConditionalFormatCss,
|
|
5696
|
+
getDataBarCss,
|
|
5697
|
+
conditionalFormatStyleString,
|
|
5698
|
+
DEFAULT_DATA_VALIDATION_MESSAGE,
|
|
5699
|
+
DEFAULT_DATA_VALIDATION_TITLE,
|
|
5700
|
+
findDataValidation,
|
|
5701
|
+
getValidationListValues,
|
|
5702
|
+
validateCellInput,
|
|
5703
|
+
findInvalidCells,
|
|
3204
5704
|
DEFAULT_ROW_HEIGHT,
|
|
3205
5705
|
DEFAULT_COLUMN_WIDTH,
|
|
3206
5706
|
getTableSections,
|
|
@@ -3218,6 +5718,9 @@ export {
|
|
|
3218
5718
|
tsvImportAdapter,
|
|
3219
5719
|
SPREADSHEET_IMPORT_ADAPTERS,
|
|
3220
5720
|
registerSpreadsheetImportAdapters,
|
|
3221
|
-
importFileIntoSpreadsheet
|
|
5721
|
+
importFileIntoSpreadsheet,
|
|
5722
|
+
getSheetTabMenuItems,
|
|
5723
|
+
sheetDropIndex,
|
|
5724
|
+
getNameManagerRows
|
|
3222
5725
|
};
|
|
3223
|
-
//# sourceMappingURL=chunk-
|
|
5726
|
+
//# sourceMappingURL=chunk-RR7F5GUM.js.map
|