@pixerate/editor 0.21.0 → 0.23.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 +100 -13
- package/THIRD_PARTY_NOTICES.md +88 -0
- package/dist/agent/index.cjs +86 -111
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +69 -211
- package/dist/agent/index.d.ts +69 -211
- package/dist/agent/index.js +77 -102
- package/dist/agent/index.js.map +1 -1
- package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
- package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
- package/dist/canvas/index.cjs +2112 -60
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +712 -181
- package/dist/canvas/index.d.ts +712 -181
- package/dist/canvas/index.js +2105 -53
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-26BVFKHU.cjs +453 -0
- package/dist/chunk-26BVFKHU.cjs.map +1 -0
- package/dist/chunk-2H7ZHPEJ.js +762 -0
- package/dist/chunk-2H7ZHPEJ.js.map +1 -0
- package/dist/chunk-7DWMJCAT.js +453 -0
- package/dist/chunk-7DWMJCAT.js.map +1 -0
- package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
- package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
- package/dist/chunk-ARRTP33I.js +101 -0
- package/dist/chunk-ARRTP33I.js.map +1 -0
- package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
- package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
- package/dist/chunk-CPOKFNBG.cjs +101 -0
- package/dist/chunk-CPOKFNBG.cjs.map +1 -0
- package/dist/chunk-G2I4QJOT.cjs +762 -0
- package/dist/chunk-G2I4QJOT.cjs.map +1 -0
- package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
- package/dist/chunk-GVSMRPHB.cjs.map +1 -0
- package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
- package/dist/{chunk-GVW4P7BG.js → chunk-K3R5NG2D.js} +448 -54
- package/dist/chunk-K3R5NG2D.js.map +1 -0
- package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
- package/dist/chunk-MJZBNWG7.js.map +1 -0
- package/dist/{chunk-A7DKIA72.js → chunk-QBJ22YZR.js} +1059 -93
- package/dist/chunk-QBJ22YZR.js.map +1 -0
- package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
- package/dist/chunk-QGHGJ56O.js.map +1 -0
- package/dist/{chunk-JYRIOFEN.cjs → chunk-RIKBJLS2.cjs} +1064 -98
- package/dist/chunk-RIKBJLS2.cjs.map +1 -0
- package/dist/{chunk-U6NDQFOK.cjs → chunk-XDMXXC4Z.cjs} +569 -175
- package/dist/chunk-XDMXXC4Z.cjs.map +1 -0
- package/dist/commands/index.cjs +860 -0
- package/dist/commands/index.cjs.map +1 -0
- package/dist/commands/index.d.cts +592 -0
- package/dist/commands/index.d.ts +592 -0
- package/dist/commands/index.js +860 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/{controller-OUdxPY-X.d.ts → controller-pePCJLT1.d.cts} +216 -11
- package/dist/{controller-OUdxPY-X.d.cts → controller-pePCJLT1.d.ts} +216 -11
- package/dist/extensions/index.cjs +14 -4
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +3 -3
- package/dist/extensions/index.d.ts +3 -3
- package/dist/extensions/index.js +13 -3
- package/dist/history/index.cjs +12 -2
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +211 -7
- package/dist/history/index.d.ts +211 -7
- package/dist/history/index.js +13 -3
- package/dist/image-editor/index.cjs +418 -13
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +154 -3
- package/dist/image-editor/index.d.ts +154 -3
- package/dist/image-editor/index.js +417 -12
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
- package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
- package/dist/index.cjs +275 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +296 -47
- package/dist/index.d.ts +296 -47
- package/dist/index.js +266 -74
- package/dist/index.js.map +1 -1
- package/dist/registry-D5-R90p0.d.cts +209 -0
- package/dist/registry-D5-R90p0.d.ts +209 -0
- package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
- package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
- package/dist/serializers/index.cjs +3 -3
- package/dist/serializers/index.d.cts +1 -1
- package/dist/serializers/index.d.ts +1 -1
- package/dist/serializers/index.js +2 -2
- package/dist/spreadsheet/index.cjs +23 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +97 -10
- package/dist/spreadsheet/index.d.ts +97 -10
- package/dist/spreadsheet/index.js +22 -2
- package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
- package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
- package/dist/{types-DMe0vFB1.d.cts → types-DX4Mh4SL.d.cts} +1 -1
- package/dist/{types-DMe0vFB1.d.ts → types-DX4Mh4SL.d.ts} +1 -1
- package/dist/types-GT5a5YAX.d.cts +86 -0
- package/dist/types-GT5a5YAX.d.ts +86 -0
- package/package.json +8 -2
- package/dist/chunk-2TWGLC5K.cjs +0 -262
- package/dist/chunk-2TWGLC5K.cjs.map +0 -1
- package/dist/chunk-3TYMNULT.js.map +0 -1
- package/dist/chunk-4QPLEUL2.cjs.map +0 -1
- package/dist/chunk-A7DKIA72.js.map +0 -1
- package/dist/chunk-CYFJCKC4.js.map +0 -1
- package/dist/chunk-GVW4P7BG.js.map +0 -1
- package/dist/chunk-JYRIOFEN.cjs.map +0 -1
- package/dist/chunk-KAADNAPF.js +0 -262
- package/dist/chunk-KAADNAPF.js.map +0 -1
- package/dist/chunk-TVRC4KVT.cjs.map +0 -1
- package/dist/chunk-U6NDQFOK.cjs.map +0 -1
- package/dist/types-BZgcDsC5.d.cts +0 -42
- package/dist/types-BZgcDsC5.d.ts +0 -42
- /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
|
@@ -2,15 +2,23 @@ import {
|
|
|
2
2
|
FormulaEvaluator,
|
|
3
3
|
WHOLE_COLUMN_END_ROW,
|
|
4
4
|
colNameToIndex,
|
|
5
|
+
compareValues,
|
|
6
|
+
describeFormulaError,
|
|
7
|
+
formatCellDisplay,
|
|
5
8
|
getFormulaArgumentSpecIndex,
|
|
6
9
|
getFormulaFunction,
|
|
7
10
|
getFormulaSignature,
|
|
8
11
|
getFormulaSignatureParts,
|
|
9
12
|
indexToColName,
|
|
13
|
+
isFormulaError,
|
|
14
|
+
numericValue,
|
|
15
|
+
parseCellInput,
|
|
10
16
|
parseFormula,
|
|
17
|
+
parseIsoDate,
|
|
11
18
|
searchFormulaFunctions,
|
|
19
|
+
sortCompareValues,
|
|
12
20
|
tokenizeFormula
|
|
13
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-QBJ22YZR.js";
|
|
14
22
|
|
|
15
23
|
// src/spreadsheet/formula/dependency-graph.ts
|
|
16
24
|
var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
|
|
@@ -19,6 +27,8 @@ function extractCellDependencies(node) {
|
|
|
19
27
|
const visit = (curr) => {
|
|
20
28
|
if (curr.type === "CellRef") {
|
|
21
29
|
deps.push({ col: curr.col, row: curr.row });
|
|
30
|
+
} else if (curr.type === "SpillRef") {
|
|
31
|
+
deps.push({ col: curr.col, row: curr.row });
|
|
22
32
|
} else if (curr.type === "Range" && curr.endRow >= WHOLE_COLUMN_END_ROW) {
|
|
23
33
|
const startC = colNameToIndex(curr.startCol);
|
|
24
34
|
const endC = colNameToIndex(curr.endCol);
|
|
@@ -54,6 +64,14 @@ var DependencyGraph = class {
|
|
|
54
64
|
dependencies = /* @__PURE__ */ new Map();
|
|
55
65
|
// Map of cellKey (e.g. "A1") -> Set of cellKeys that depend ON this cell (dependents)
|
|
56
66
|
dependents = /* @__PURE__ */ new Map();
|
|
67
|
+
// Dynamic-array edges, set by the controller after evaluating an anchor:
|
|
68
|
+
// spilled cells depend on their anchor, and a blocked (#SPILL!) anchor
|
|
69
|
+
// depends on the cells blocking it. Kept apart from formula dependencies,
|
|
70
|
+
// so re-entering a formula doesn't drop them.
|
|
71
|
+
spillDependencies = /* @__PURE__ */ new Map();
|
|
72
|
+
spillDependents = /* @__PURE__ */ new Map();
|
|
73
|
+
// anchor -> the edges it registered (so they can be replaced).
|
|
74
|
+
spillEdges = /* @__PURE__ */ new Map();
|
|
57
75
|
setCellFormula(cellKey, formulaStr) {
|
|
58
76
|
const key = cellKey.toUpperCase();
|
|
59
77
|
const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
|
|
@@ -84,12 +102,49 @@ var DependencyGraph = class {
|
|
|
84
102
|
clear() {
|
|
85
103
|
this.dependencies.clear();
|
|
86
104
|
this.dependents.clear();
|
|
105
|
+
this.spillDependencies.clear();
|
|
106
|
+
this.spillDependents.clear();
|
|
107
|
+
this.spillEdges.clear();
|
|
87
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Records the dynamic-array edges of the anchor `anchorKey`, replacing its
|
|
111
|
+
* previous ones: every key in `spilledKeys` (cells holding its spilled
|
|
112
|
+
* values) depends on the anchor, and the anchor depends on every key in
|
|
113
|
+
* `blockingKeys` (cells that stop it spilling). Pass empty lists to remove
|
|
114
|
+
* the anchor's edges.
|
|
115
|
+
*/
|
|
116
|
+
setSpillEdges(anchorKey, spilledKeys, blockingKeys = []) {
|
|
117
|
+
const anchor = anchorKey.toUpperCase();
|
|
118
|
+
for (const [from, to] of this.spillEdges.get(anchor) ?? []) {
|
|
119
|
+
this.spillDependencies.get(from)?.delete(to);
|
|
120
|
+
this.spillDependents.get(to)?.delete(from);
|
|
121
|
+
}
|
|
122
|
+
const edges = [];
|
|
123
|
+
for (const key of spilledKeys) edges.push([key.toUpperCase(), anchor]);
|
|
124
|
+
for (const key of blockingKeys) edges.push([anchor, key.toUpperCase()]);
|
|
125
|
+
for (const [from, to] of edges) {
|
|
126
|
+
if (!this.spillDependencies.has(from)) this.spillDependencies.set(from, /* @__PURE__ */ new Set());
|
|
127
|
+
this.spillDependencies.get(from).add(to);
|
|
128
|
+
if (!this.spillDependents.has(to)) this.spillDependents.set(to, /* @__PURE__ */ new Set());
|
|
129
|
+
this.spillDependents.get(to).add(from);
|
|
130
|
+
}
|
|
131
|
+
if (edges.length > 0) this.spillEdges.set(anchor, edges);
|
|
132
|
+
else this.spillEdges.delete(anchor);
|
|
133
|
+
}
|
|
134
|
+
/** The spilled cells registered for `anchorKey` by `setSpillEdges`. */
|
|
135
|
+
getSpilledKeys(anchorKey) {
|
|
136
|
+
const anchor = anchorKey.toUpperCase();
|
|
137
|
+
return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);
|
|
138
|
+
}
|
|
139
|
+
/** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */
|
|
88
140
|
getDependencies(cellKey) {
|
|
89
|
-
|
|
141
|
+
const key = cellKey.toUpperCase();
|
|
142
|
+
return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));
|
|
90
143
|
}
|
|
144
|
+
/** Cells depending on `cellKey`, through formulas or spills. */
|
|
91
145
|
getDependents(cellKey) {
|
|
92
|
-
|
|
146
|
+
const key = cellKey.toUpperCase();
|
|
147
|
+
return unionOf(this.dependents.get(key), this.spillDependents.get(key));
|
|
93
148
|
}
|
|
94
149
|
/**
|
|
95
150
|
* Detects if there is a cycle containing cellKey.
|
|
@@ -101,7 +156,7 @@ var DependencyGraph = class {
|
|
|
101
156
|
const dfs = (node) => {
|
|
102
157
|
visited.add(node);
|
|
103
158
|
recStack.add(node);
|
|
104
|
-
const deps = this.
|
|
159
|
+
const deps = this.getDependencies(node);
|
|
105
160
|
for (const neighbor of deps) {
|
|
106
161
|
if (!visited.has(neighbor)) {
|
|
107
162
|
if (dfs(neighbor)) return true;
|
|
@@ -180,6 +235,7 @@ var DependencyGraph = class {
|
|
|
180
235
|
const node = queue.pop();
|
|
181
236
|
const direct = [
|
|
182
237
|
...this.dependents.get(node) ?? [],
|
|
238
|
+
...this.spillDependents.get(node) ?? [],
|
|
183
239
|
...this.dependents.get(columnDependencyKey(node.replace(/\d+$/, ""))) ?? []
|
|
184
240
|
];
|
|
185
241
|
for (const dep of direct) {
|
|
@@ -192,7 +248,7 @@ var DependencyGraph = class {
|
|
|
192
248
|
}
|
|
193
249
|
const neighbors = (node) => {
|
|
194
250
|
const result = [];
|
|
195
|
-
for (const dep of this.
|
|
251
|
+
for (const dep of this.getDependencies(node)) {
|
|
196
252
|
if (dep.endsWith(":")) {
|
|
197
253
|
const col = dep.slice(0, -1);
|
|
198
254
|
for (const candidate of nodes) {
|
|
@@ -258,6 +314,11 @@ var DependencyGraph = class {
|
|
|
258
314
|
return { order, cyclic };
|
|
259
315
|
}
|
|
260
316
|
};
|
|
317
|
+
function unionOf(a, b) {
|
|
318
|
+
if (!b || b.size === 0) return Array.from(a ?? []);
|
|
319
|
+
if (!a || a.size === 0) return Array.from(b);
|
|
320
|
+
return Array.from(/* @__PURE__ */ new Set([...a, ...b]));
|
|
321
|
+
}
|
|
261
322
|
|
|
262
323
|
// src/spreadsheet/formula/references.ts
|
|
263
324
|
var CELL_PART = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
|
|
@@ -335,11 +396,17 @@ function mapFormulaReferences(formula, rewrite) {
|
|
|
335
396
|
let cursor = 0;
|
|
336
397
|
for (let i = 0; i < tokens.length; i++) {
|
|
337
398
|
const token = tokens[i];
|
|
338
|
-
if (token.type !== "CELL_REF" && token.type !== "RANGE") continue;
|
|
399
|
+
if (token.type !== "CELL_REF" && token.type !== "RANGE" && token.type !== "SPILL_REF") continue;
|
|
339
400
|
if (tokens[i + 1]?.type === "LPAREN") continue;
|
|
340
401
|
let end = token.position;
|
|
341
402
|
while (end < formula.length && REF_CHAR.test(formula[end])) end++;
|
|
342
403
|
const original = formula.slice(token.position, end);
|
|
404
|
+
if (token.type === "SPILL_REF") {
|
|
405
|
+
const rewritten = rewrite(original);
|
|
406
|
+
result += formula.slice(cursor, token.position) + (rewritten === "#REF!" ? rewritten : `${rewritten}#`);
|
|
407
|
+
cursor = end + 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
343
410
|
result += formula.slice(cursor, token.position) + rewrite(original);
|
|
344
411
|
cursor = end;
|
|
345
412
|
}
|
|
@@ -617,28 +684,41 @@ function compareCellValues(a, b, direction = "asc") {
|
|
|
617
684
|
const aBlank = isBlankValue(a);
|
|
618
685
|
const bBlank = isBlankValue(b);
|
|
619
686
|
if (aBlank || bBlank) return aBlank === bBlank ? 0 : aBlank ? 1 : -1;
|
|
620
|
-
const aNum = toNumber(a);
|
|
621
|
-
const bNum = toNumber(b);
|
|
622
687
|
let result;
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
else
|
|
626
|
-
|
|
688
|
+
if (isPlainText(a) && isPlainText(b) && !(parseIsoDate(a) && parseIsoDate(b))) {
|
|
689
|
+
result = compareText(a, b);
|
|
690
|
+
} else {
|
|
691
|
+
result = sortCompareValues(a, b);
|
|
692
|
+
}
|
|
627
693
|
if (result === 0) return 0;
|
|
628
694
|
return direction === "desc" ? -Math.sign(result) : Math.sign(result);
|
|
629
695
|
}
|
|
696
|
+
var isPlainText = (value) => typeof value === "string" && !isFormulaError(value);
|
|
697
|
+
function filterValue(value) {
|
|
698
|
+
if (isBlankValue(value) || isFormulaError(value)) return null;
|
|
699
|
+
if (typeof value === "number") return Number.isFinite(value) ? { value, kind: "number" } : null;
|
|
700
|
+
if (typeof value === "boolean") return { value, kind: "boolean" };
|
|
701
|
+
const num = numericValue(value);
|
|
702
|
+
if (num !== null) return { value: num, kind: "number" };
|
|
703
|
+
const text = String(value);
|
|
704
|
+
return { value: text, kind: parseIsoDate(text) ? "number" : "text" };
|
|
705
|
+
}
|
|
706
|
+
function filterOperand(operand) {
|
|
707
|
+
if (typeof operand !== "string") return filterValue(operand);
|
|
708
|
+
if (operand.trim() === "") return null;
|
|
709
|
+
const parsed = parseCellInput(operand.trim());
|
|
710
|
+
if (parsed.type === "number" || parsed.type === "date" || parsed.type === "boolean") return filterValue(parsed.value);
|
|
711
|
+
return { value: operand, kind: "text" };
|
|
712
|
+
}
|
|
630
713
|
function compareForFilter(value, operand) {
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (a !== null || b !== null) return null;
|
|
636
|
-
return compareText(String(value), String(operand));
|
|
714
|
+
const a = filterValue(value);
|
|
715
|
+
const b = filterOperand(operand);
|
|
716
|
+
if (!a || !b || a.kind !== b.kind) return null;
|
|
717
|
+
return compareValues(a.value, b.value);
|
|
637
718
|
}
|
|
638
719
|
function looselyEquals(value, operand) {
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
if (a !== null && b !== null) return a === b;
|
|
720
|
+
const c = compareForFilter(value, operand);
|
|
721
|
+
if (c !== null) return c === 0;
|
|
642
722
|
return String(value ?? "").trim().toLowerCase() === String(operand ?? "").trim().toLowerCase();
|
|
643
723
|
}
|
|
644
724
|
function matchesFilterRule(value, rule) {
|
|
@@ -713,7 +793,8 @@ function classifyFillValue(raw) {
|
|
|
713
793
|
if (raw.trim() === "") return { kind: "blank" };
|
|
714
794
|
const num = toNumber(raw);
|
|
715
795
|
if (num !== null) return { kind: "number", value: num };
|
|
716
|
-
const
|
|
796
|
+
const typed = parseCellInput(raw);
|
|
797
|
+
const date = ISO_DATE_PATTERN.exec(typed.type === "date" ? String(typed.value) : raw.trim());
|
|
717
798
|
if (date) {
|
|
718
799
|
const [year, month, day] = [Number(date[1]), Number(date[2]), Number(date[3])];
|
|
719
800
|
const time = Date.UTC(year, month - 1, day);
|
|
@@ -1010,11 +1091,16 @@ function parseHtmlTable(html) {
|
|
|
1010
1091
|
return rows.length > 0 ? rows : null;
|
|
1011
1092
|
}
|
|
1012
1093
|
function createSpreadsheetClipboardData(cells, origin) {
|
|
1013
|
-
const
|
|
1014
|
-
const payload = {
|
|
1094
|
+
const texts = cells.map((row) => row.map((cell) => cell.text ?? displayText(cell.value)));
|
|
1095
|
+
const payload = {
|
|
1096
|
+
type: "pixerate-spreadsheet",
|
|
1097
|
+
version: 1,
|
|
1098
|
+
origin,
|
|
1099
|
+
cells: cells.map((row) => row.map(({ raw, value }) => ({ raw, value })))
|
|
1100
|
+
};
|
|
1015
1101
|
return {
|
|
1016
|
-
"text/plain": serializeTsv(
|
|
1017
|
-
"text/html": serializeHtmlTable(
|
|
1102
|
+
"text/plain": serializeTsv(texts),
|
|
1103
|
+
"text/html": serializeHtmlTable(texts),
|
|
1018
1104
|
[SPREADSHEET_CLIPBOARD_MIME]: JSON.stringify(payload)
|
|
1019
1105
|
};
|
|
1020
1106
|
}
|
|
@@ -1228,6 +1314,9 @@ function buildSpreadsheetView(doc, getValue) {
|
|
|
1228
1314
|
|
|
1229
1315
|
// src/spreadsheet/controller.ts
|
|
1230
1316
|
var nextIdSequence = 0;
|
|
1317
|
+
var DEFAULT_MAX_HISTORY_BYTES = 64 * 1024 * 1024;
|
|
1318
|
+
var MAX_SPILL_PASSES = 25;
|
|
1319
|
+
var spillCellId = (rowId, colId) => `${rowId}\0${colId}`;
|
|
1231
1320
|
var SpreadsheetController = class {
|
|
1232
1321
|
document;
|
|
1233
1322
|
activeCell = null;
|
|
@@ -1245,11 +1334,24 @@ var SpreadsheetController = class {
|
|
|
1245
1334
|
undoStack = [];
|
|
1246
1335
|
redoStack = [];
|
|
1247
1336
|
isApplyingHistory = false;
|
|
1337
|
+
maxHistoryBytes;
|
|
1248
1338
|
// Set while importing TSV: history and recalculation are done once at the end.
|
|
1249
1339
|
batching = false;
|
|
1250
1340
|
pendingCommits = [];
|
|
1251
1341
|
// Row being evaluated, so [property] references resolve per row.
|
|
1252
1342
|
evaluatingRowIndex = null;
|
|
1343
|
+
// Cell being evaluated, for implicit intersection (`=@A:A`).
|
|
1344
|
+
evaluatingCell = null;
|
|
1345
|
+
// Dynamic arrays. Spilled values are derived state: they live here, not in
|
|
1346
|
+
// the document, and every recalculation rebuilds them.
|
|
1347
|
+
spills = /* @__PURE__ */ new Map();
|
|
1348
|
+
// anchor key -> spill
|
|
1349
|
+
spilledBy = /* @__PURE__ */ new Map();
|
|
1350
|
+
// spilled cell key -> anchor key
|
|
1351
|
+
spillCells = /* @__PURE__ */ new Map();
|
|
1352
|
+
// spillCellId -> displayed cell
|
|
1353
|
+
// Cells whose spill ownership changed during the current pass.
|
|
1354
|
+
spillChanges = null;
|
|
1253
1355
|
constructor(options = {}) {
|
|
1254
1356
|
const defaultCols = [
|
|
1255
1357
|
{ id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
|
|
@@ -1274,6 +1376,7 @@ var SpreadsheetController = class {
|
|
|
1274
1376
|
...options.document?.table ? { table: options.document.table } : {}
|
|
1275
1377
|
};
|
|
1276
1378
|
this.primaryDataSourceId = options.primaryDataSourceId;
|
|
1379
|
+
this.maxHistoryBytes = Math.max(0, options.maxHistoryBytes ?? DEFAULT_MAX_HISTORY_BYTES);
|
|
1277
1380
|
this.onDocumentChange = options.onDocumentChange;
|
|
1278
1381
|
this.onSelectionChange = options.onSelectionChange;
|
|
1279
1382
|
this.onCellCommit = options.onCellCommit;
|
|
@@ -1283,9 +1386,12 @@ var SpreadsheetController = class {
|
|
|
1283
1386
|
this.evaluator = new FormulaEvaluator({
|
|
1284
1387
|
resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
|
|
1285
1388
|
resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
|
|
1286
|
-
resolveProperty: (prop) => this.resolveCurrentRowProperty(prop)
|
|
1389
|
+
resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),
|
|
1390
|
+
resolveSpill: (colStr, rowNum) => this.resolveSpillByCoord(colStr, rowNum),
|
|
1391
|
+
getFormulaCell: () => this.evaluatingCell
|
|
1287
1392
|
});
|
|
1288
1393
|
this.syncWithDataSource();
|
|
1394
|
+
this.rederiveTypedValues();
|
|
1289
1395
|
this.recalculateAll();
|
|
1290
1396
|
}
|
|
1291
1397
|
loadDocument(document) {
|
|
@@ -1297,9 +1403,63 @@ var SpreadsheetController = class {
|
|
|
1297
1403
|
this.undoStack = [];
|
|
1298
1404
|
this.redoStack = [];
|
|
1299
1405
|
this.syncWithDataSource();
|
|
1406
|
+
this.rederiveTypedValues();
|
|
1300
1407
|
this.recalculateAll();
|
|
1301
1408
|
this.notifyChange();
|
|
1302
1409
|
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Re-parses the value of every stored typed (non-formula) cell from its
|
|
1412
|
+
* `raw` input, so documents saved before typed input was parsed (whose
|
|
1413
|
+
* values are text such as `"120"`) compare and sort as numbers. Cells of
|
|
1414
|
+
* bound columns in record rows keep their value: it belongs to the data
|
|
1415
|
+
* source.
|
|
1416
|
+
*/
|
|
1417
|
+
rederiveTypedValues() {
|
|
1418
|
+
for (const row of this.document.rows) {
|
|
1419
|
+
const cells = this.document.cells[row.id];
|
|
1420
|
+
if (!cells) continue;
|
|
1421
|
+
for (const col of this.document.columns) {
|
|
1422
|
+
const cell = cells[col.id];
|
|
1423
|
+
if (!cell || typeof cell.raw !== "string" || cell.raw.startsWith("=")) continue;
|
|
1424
|
+
if (col.type === "bound" && col.binding && row.recordId) continue;
|
|
1425
|
+
const typed = this.typedCell(cell.raw, cell.format);
|
|
1426
|
+
cell.value = typed.value;
|
|
1427
|
+
if (typed.numberFormat) cell.numberFormat = typed.numberFormat;
|
|
1428
|
+
else delete cell.numberFormat;
|
|
1429
|
+
if (typed.format) cell.format = typed.format;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Value of typed (non-formula) input: a number, boolean, ISO date, error
|
|
1435
|
+
* or text (see `parseCellInput`), its automatic number format, and the
|
|
1436
|
+
* `CellFormat` to use: the automatic one (currency, percentage, date,
|
|
1437
|
+
* number) when `format` is unset or `text` (the default of new columns).
|
|
1438
|
+
*/
|
|
1439
|
+
typedCell(raw, format) {
|
|
1440
|
+
const parsed = parseCellInput(raw);
|
|
1441
|
+
return {
|
|
1442
|
+
value: parsed.value,
|
|
1443
|
+
...parsed.numberFormat ? { numberFormat: parsed.numberFormat } : {},
|
|
1444
|
+
format: parsed.cellFormat && (!format || format === "text") ? parsed.cellFormat : format
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Typed input for a bound field, coerced by the field's declared type:
|
|
1449
|
+
* number, boolean and date fields get a number, boolean or ISO date when
|
|
1450
|
+
* the input parses as one; string and enum fields (and unknown fields)
|
|
1451
|
+
* keep the text exactly as typed.
|
|
1452
|
+
*/
|
|
1453
|
+
boundInputValue(dataSourceId, field, raw) {
|
|
1454
|
+
if (raw.startsWith("=")) return raw;
|
|
1455
|
+
const type = this.binder.getDataSource(dataSourceId)?.getFields().find((f) => f.key === field)?.type;
|
|
1456
|
+
if (type !== "number" && type !== "boolean" && type !== "date") return raw;
|
|
1457
|
+
const parsed = parseCellInput(raw);
|
|
1458
|
+
if (type === "number" && parsed.type === "number") return parsed.value;
|
|
1459
|
+
if (type === "boolean" && parsed.type === "boolean") return parsed.value;
|
|
1460
|
+
if (type === "date" && parsed.type === "date") return parsed.value;
|
|
1461
|
+
return raw;
|
|
1462
|
+
}
|
|
1303
1463
|
registerDataSource(ds) {
|
|
1304
1464
|
this.binder.register(ds);
|
|
1305
1465
|
this.syncWithDataSource();
|
|
@@ -1336,6 +1496,11 @@ var SpreadsheetController = class {
|
|
|
1336
1496
|
}
|
|
1337
1497
|
getCell(rowId, colId) {
|
|
1338
1498
|
const existing = this.document.cells[rowId]?.[colId];
|
|
1499
|
+
if (existing && existing.raw !== "") return existing;
|
|
1500
|
+
if (this.spillCells.size > 0) {
|
|
1501
|
+
const spilled = this.spillCells.get(spillCellId(rowId, colId));
|
|
1502
|
+
if (spilled) return spilled;
|
|
1503
|
+
}
|
|
1339
1504
|
if (existing) return existing;
|
|
1340
1505
|
const col = this.document.columns.find((c) => c.id === colId);
|
|
1341
1506
|
const row = this.document.rows.find((r) => r.id === rowId);
|
|
@@ -1371,21 +1536,20 @@ var SpreadsheetController = class {
|
|
|
1371
1536
|
}
|
|
1372
1537
|
const col = this.document.columns.find((c) => c.id === colId);
|
|
1373
1538
|
const row = this.document.rows.find((r) => r.id === rowId);
|
|
1539
|
+
const isFormula = raw.startsWith("=");
|
|
1540
|
+
let cell;
|
|
1374
1541
|
if (col?.type === "bound" && col.binding && row?.recordId) {
|
|
1375
|
-
this.
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1542
|
+
const value = this.boundInputValue(col.binding.dataSource, col.binding.field, raw);
|
|
1543
|
+
this.binder.commitBoundValue(col.binding.dataSource, row.recordId, col.binding.field, value);
|
|
1544
|
+
cell = { raw, value, format: col.format };
|
|
1545
|
+
} else if (isFormula) {
|
|
1546
|
+
cell = { raw, value: raw, format: col?.format };
|
|
1547
|
+
} else {
|
|
1548
|
+
cell = { raw, ...this.typedCell(raw, col?.format) };
|
|
1381
1549
|
}
|
|
1382
1550
|
const cellKey = this.getCellKey(rowId, colId);
|
|
1383
|
-
this.depGraph.setCellFormula(cellKey,
|
|
1384
|
-
this.document.cells[rowId][colId] =
|
|
1385
|
-
raw,
|
|
1386
|
-
value: raw,
|
|
1387
|
-
format: col?.format
|
|
1388
|
-
};
|
|
1551
|
+
this.depGraph.setCellFormula(cellKey, isFormula ? raw : null);
|
|
1552
|
+
this.document.cells[rowId][colId] = cell;
|
|
1389
1553
|
this.document.updatedAt = Date.now();
|
|
1390
1554
|
if (this.batching) {
|
|
1391
1555
|
this.pendingCommits.push([rowId, colId, raw]);
|
|
@@ -1408,7 +1572,30 @@ var SpreadsheetController = class {
|
|
|
1408
1572
|
*/
|
|
1409
1573
|
recalculateCell(rowId, colId) {
|
|
1410
1574
|
const cellKey = this.getCellKey(rowId, colId);
|
|
1411
|
-
this.
|
|
1575
|
+
const anchor = this.spilledBy.get(cellKey);
|
|
1576
|
+
this.recalculateKeys(anchor ? [cellKey, anchor] : [cellKey], true);
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Recalculates `keys` (and, with `includeDependents`, their dependents) in
|
|
1580
|
+
* dependency order. Spill ranges are only known after evaluating their
|
|
1581
|
+
* anchors, so when a pass changes which cells hold spilled values (and so
|
|
1582
|
+
* the dependency graph), the dependents of those cells are recalculated in
|
|
1583
|
+
* another pass, until nothing changes.
|
|
1584
|
+
*/
|
|
1585
|
+
recalculateKeys(keys, includeDependents) {
|
|
1586
|
+
let pending = Array.from(keys);
|
|
1587
|
+
let withDependents = includeDependents;
|
|
1588
|
+
for (let pass = 0; pass < MAX_SPILL_PASSES && pending.length > 0; pass++) {
|
|
1589
|
+
const changed = /* @__PURE__ */ new Set();
|
|
1590
|
+
this.spillChanges = changed;
|
|
1591
|
+
try {
|
|
1592
|
+
this.runEvaluationPlan(this.depGraph.getEvaluationPlan(pending, { includeDependents: withDependents }));
|
|
1593
|
+
} finally {
|
|
1594
|
+
this.spillChanges = null;
|
|
1595
|
+
}
|
|
1596
|
+
pending = Array.from(changed);
|
|
1597
|
+
withDependents = true;
|
|
1598
|
+
}
|
|
1412
1599
|
}
|
|
1413
1600
|
/**
|
|
1414
1601
|
* Rebuilds the dependency graph from scratch and recalculates every formula
|
|
@@ -1416,6 +1603,9 @@ var SpreadsheetController = class {
|
|
|
1416
1603
|
*/
|
|
1417
1604
|
recalculateAll() {
|
|
1418
1605
|
this.depGraph.clear();
|
|
1606
|
+
this.spills.clear();
|
|
1607
|
+
this.spilledBy.clear();
|
|
1608
|
+
this.spillCells.clear();
|
|
1419
1609
|
const formulaKeys = [];
|
|
1420
1610
|
for (const row of this.document.rows) {
|
|
1421
1611
|
for (const col of this.document.columns) {
|
|
@@ -1427,7 +1617,7 @@ var SpreadsheetController = class {
|
|
|
1427
1617
|
}
|
|
1428
1618
|
}
|
|
1429
1619
|
}
|
|
1430
|
-
this.
|
|
1620
|
+
this.recalculateKeys(formulaKeys, false);
|
|
1431
1621
|
}
|
|
1432
1622
|
runEvaluationPlan(plan) {
|
|
1433
1623
|
for (const key of plan.order) {
|
|
@@ -1454,7 +1644,10 @@ var SpreadsheetController = class {
|
|
|
1454
1644
|
if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
|
|
1455
1645
|
const deps = this.depGraph.getDependencies(cellKey);
|
|
1456
1646
|
const selfReference = deps.includes(cellKey) || deps.includes(`${col.key.toUpperCase()}:`);
|
|
1457
|
-
const
|
|
1647
|
+
const spilledKeys = this.depGraph.getSpilledKeys(cellKey);
|
|
1648
|
+
const ownSpill = deps.find((dep) => spilledKeys.includes(dep));
|
|
1649
|
+
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.";
|
|
1650
|
+
this.clearSpill(cellKey.toUpperCase(), true);
|
|
1458
1651
|
this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!", errorMessage };
|
|
1459
1652
|
}
|
|
1460
1653
|
evalSingleCellByKey(cellKey) {
|
|
@@ -1471,25 +1664,176 @@ var SpreadsheetController = class {
|
|
|
1471
1664
|
cell.error = void 0;
|
|
1472
1665
|
cell.errorMessage = void 0;
|
|
1473
1666
|
}
|
|
1667
|
+
this.clearSpill(`${colName}${rowNum}`);
|
|
1474
1668
|
return;
|
|
1475
1669
|
}
|
|
1476
1670
|
try {
|
|
1477
1671
|
this.evaluatingRowIndex = rowNum - 1;
|
|
1672
|
+
this.evaluatingCell = { col: colName, row: rowNum };
|
|
1478
1673
|
const result = this.evaluator.evaluateWithDiagnostics(cell.raw);
|
|
1479
1674
|
if (!this.document.cells[row.id]) {
|
|
1480
1675
|
this.document.cells[row.id] = {};
|
|
1481
1676
|
}
|
|
1677
|
+
let { value, error, message } = result;
|
|
1678
|
+
if (result.array) {
|
|
1679
|
+
const blocked = this.placeSpill(`${colName}${rowNum}`, rowNum - 1, this.document.columns.indexOf(col), result.array);
|
|
1680
|
+
if (blocked) {
|
|
1681
|
+
value = "#SPILL!";
|
|
1682
|
+
error = "#SPILL!";
|
|
1683
|
+
message = blocked;
|
|
1684
|
+
}
|
|
1685
|
+
} else {
|
|
1686
|
+
this.clearSpill(`${colName}${rowNum}`);
|
|
1687
|
+
}
|
|
1482
1688
|
this.document.cells[row.id][col.id] = {
|
|
1483
1689
|
...cell,
|
|
1484
|
-
value
|
|
1690
|
+
value,
|
|
1485
1691
|
// `#ERROR!` (malformed formula, wrong argument count) keeps its
|
|
1486
1692
|
// message in `error`, as before; other errors store their code.
|
|
1487
|
-
error:
|
|
1488
|
-
errorMessage:
|
|
1693
|
+
error: error === "#ERROR!" ? message : error,
|
|
1694
|
+
errorMessage: message
|
|
1489
1695
|
};
|
|
1490
1696
|
} finally {
|
|
1491
1697
|
this.evaluatingRowIndex = null;
|
|
1698
|
+
this.evaluatingCell = null;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
// --- Dynamic arrays (spilling) ---
|
|
1702
|
+
/**
|
|
1703
|
+
* Spills `array` from the anchor at (row, col) into the cells below and to
|
|
1704
|
+
* the right. Returns why it can't (the anchor then shows `#SPILL!`), or
|
|
1705
|
+
* null once placed. The grid never grows to fit an array, as in Excel.
|
|
1706
|
+
*/
|
|
1707
|
+
placeSpill(anchor, row, col, array) {
|
|
1708
|
+
const { rows, cols } = array;
|
|
1709
|
+
const area = { startRow: row, startCol: col, endRow: row + rows - 1, endCol: col + cols - 1 };
|
|
1710
|
+
const label = `${indexToColName(area.startCol)}${area.startRow + 1}:${indexToColName(area.endCol)}${area.endRow + 1}`;
|
|
1711
|
+
let reason = null;
|
|
1712
|
+
const blockers = [];
|
|
1713
|
+
if (area.endRow >= this.document.rows.length || area.endCol >= this.document.columns.length) {
|
|
1714
|
+
reason = `The ${rows}\xD7${cols} result needs ${label}, which extends past the edge of the sheet.`;
|
|
1715
|
+
} else if (this.getMerges().some((m) => rangesIntersect(m, area))) {
|
|
1716
|
+
reason = `The result can't spill into merged cells (it needs ${label}).`;
|
|
1717
|
+
} else {
|
|
1718
|
+
for (let c = area.startCol; c <= area.endCol; c++) {
|
|
1719
|
+
const column = this.document.columns[c];
|
|
1720
|
+
for (let r = area.startRow; r <= area.endRow; r++) {
|
|
1721
|
+
if (r === row && c === col) continue;
|
|
1722
|
+
const rowObj = this.document.rows[r];
|
|
1723
|
+
const key = `${column.key}${r + 1}`.toUpperCase();
|
|
1724
|
+
const stored = this.document.cells[rowObj.id]?.[column.id];
|
|
1725
|
+
const raw = stored ? stored.raw : this.getDerivedCell(rowObj, column).raw;
|
|
1726
|
+
const owner = this.spilledBy.get(key);
|
|
1727
|
+
if (raw !== "") {
|
|
1728
|
+
blockers.push(key);
|
|
1729
|
+
reason ??= `The result needs ${label}, but ${key} isn't empty.`;
|
|
1730
|
+
} else if (owner && owner !== anchor) {
|
|
1731
|
+
blockers.push(key);
|
|
1732
|
+
reason ??= `The result needs ${label}, but ${key} already holds values spilled from ${owner}.`;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (reason) {
|
|
1738
|
+
this.clearSpill(anchor);
|
|
1739
|
+
this.depGraph.setSpillEdges(anchor, [], blockers);
|
|
1740
|
+
return reason;
|
|
1741
|
+
}
|
|
1742
|
+
const previous = this.spills.get(anchor);
|
|
1743
|
+
const cells = [];
|
|
1744
|
+
for (let c = 0; c < cols; c++) {
|
|
1745
|
+
const column = this.document.columns[col + c];
|
|
1746
|
+
for (let r = 0; r < rows; r++) {
|
|
1747
|
+
if (r === 0 && c === 0) continue;
|
|
1748
|
+
const rowObj = this.document.rows[row + r];
|
|
1749
|
+
const key = `${column.key}${row + r + 1}`.toUpperCase();
|
|
1750
|
+
const value = array.values[c * rows + r] ?? "";
|
|
1751
|
+
const display = { raw: "", value, format: column.format, spillAnchor: anchor };
|
|
1752
|
+
if (isFormulaError(value)) {
|
|
1753
|
+
display.error = value;
|
|
1754
|
+
display.errorMessage = describeFormulaError(value);
|
|
1755
|
+
}
|
|
1756
|
+
this.spillCells.set(spillCellId(rowObj.id, column.id), display);
|
|
1757
|
+
if (this.spilledBy.get(key) !== anchor) {
|
|
1758
|
+
this.spilledBy.set(key, anchor);
|
|
1759
|
+
this.spillChanges?.add(key);
|
|
1760
|
+
}
|
|
1761
|
+
cells.push({ key, rowId: rowObj.id, colId: column.id });
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (previous) {
|
|
1765
|
+
const kept = new Set(cells.map((cell) => cell.key));
|
|
1766
|
+
for (const old of previous.cells) {
|
|
1767
|
+
if (kept.has(old.key)) continue;
|
|
1768
|
+
this.forgetSpilledCell(anchor, old);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
this.spills.set(anchor, { anchor, row, col, rows, cols, values: array.values, cells });
|
|
1772
|
+
this.depGraph.setSpillEdges(anchor, cells.map((cell) => cell.key));
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
/** Removes the array spilled from `anchor`. With `keepEdges`, its dependency edges stay. */
|
|
1776
|
+
clearSpill(anchor, keepEdges = false) {
|
|
1777
|
+
const record = this.spills.get(anchor);
|
|
1778
|
+
if (record) {
|
|
1779
|
+
for (const cell of record.cells) this.forgetSpilledCell(anchor, cell);
|
|
1780
|
+
this.spills.delete(anchor);
|
|
1781
|
+
}
|
|
1782
|
+
if (!keepEdges) this.depGraph.setSpillEdges(anchor, []);
|
|
1783
|
+
}
|
|
1784
|
+
forgetSpilledCell(anchor, cell) {
|
|
1785
|
+
if (this.spilledBy.get(cell.key) !== anchor) return;
|
|
1786
|
+
this.spilledBy.delete(cell.key);
|
|
1787
|
+
this.spillCells.delete(spillCellId(cell.rowId, cell.colId));
|
|
1788
|
+
this.spillChanges?.add(cell.key);
|
|
1789
|
+
}
|
|
1790
|
+
resolveSpillByCoord(colStr, rowNum) {
|
|
1791
|
+
const record = this.spills.get(`${colStr}${rowNum}`.toUpperCase());
|
|
1792
|
+
if (record) return { rows: record.rows, cols: record.cols, values: record.values };
|
|
1793
|
+
const value = this.resolveCellByCoord(colStr, rowNum);
|
|
1794
|
+
return isFormulaError(value) ? value : "#REF!";
|
|
1795
|
+
}
|
|
1796
|
+
toSpill(record) {
|
|
1797
|
+
const row = this.document.rows[record.row];
|
|
1798
|
+
const col = this.document.columns[record.col];
|
|
1799
|
+
return {
|
|
1800
|
+
key: record.anchor,
|
|
1801
|
+
anchor: { row: record.row, col: record.col },
|
|
1802
|
+
range: {
|
|
1803
|
+
startRow: record.row,
|
|
1804
|
+
startCol: record.col,
|
|
1805
|
+
endRow: record.row + record.rows - 1,
|
|
1806
|
+
endCol: record.col + record.cols - 1
|
|
1807
|
+
},
|
|
1808
|
+
formula: row && col ? this.getCell(row.id, col.id).raw : ""
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* The dynamic array covering (row, col), whether it is the anchor (the cell
|
|
1813
|
+
* holding the formula) or a spilled cell; null when there is none.
|
|
1814
|
+
*/
|
|
1815
|
+
getSpillAt(row, col) {
|
|
1816
|
+
if (this.spills.size === 0) return null;
|
|
1817
|
+
const key = `${indexToColName(col)}${row + 1}`;
|
|
1818
|
+
const record = this.spills.get(key) ?? this.spills.get(this.spilledBy.get(key) ?? "");
|
|
1819
|
+
return record ? this.toSpill(record) : null;
|
|
1820
|
+
}
|
|
1821
|
+
/** Every dynamic array currently spilled. */
|
|
1822
|
+
getSpills() {
|
|
1823
|
+
return Array.from(this.spills.values(), (record) => this.toSpill(record));
|
|
1824
|
+
}
|
|
1825
|
+
/** Whether `range` overlaps a spilled array (its anchor or spilled cells). */
|
|
1826
|
+
intersectsSpill(range) {
|
|
1827
|
+
for (const record of this.spills.values()) {
|
|
1828
|
+
const area = {
|
|
1829
|
+
startRow: record.row,
|
|
1830
|
+
startCol: record.col,
|
|
1831
|
+
endRow: record.row + record.rows - 1,
|
|
1832
|
+
endCol: record.col + record.cols - 1
|
|
1833
|
+
};
|
|
1834
|
+
if (rangesIntersect(area, range)) return true;
|
|
1492
1835
|
}
|
|
1836
|
+
return false;
|
|
1493
1837
|
}
|
|
1494
1838
|
resolveCellByCoord(colStr, rowNum) {
|
|
1495
1839
|
const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
|
|
@@ -1721,7 +2065,7 @@ var SpreadsheetController = class {
|
|
|
1721
2065
|
let maxLen = (col.title || "").length + (col.key ? col.key.length + 3 : 0);
|
|
1722
2066
|
for (const row of this.document.rows) {
|
|
1723
2067
|
const cell = this.getCell(row.id, col.id);
|
|
1724
|
-
const valStr =
|
|
2068
|
+
const valStr = formatCellDisplay(cell);
|
|
1725
2069
|
if (valStr.length > maxLen) {
|
|
1726
2070
|
maxLen = valStr.length;
|
|
1727
2071
|
}
|
|
@@ -1745,8 +2089,7 @@ var SpreadsheetController = class {
|
|
|
1745
2089
|
) : this.document.columns;
|
|
1746
2090
|
return rows.map(
|
|
1747
2091
|
(row) => columns.map((col) => {
|
|
1748
|
-
|
|
1749
|
-
return String(cell.value ?? "").replace(/\t/g, " ");
|
|
2092
|
+
return formatCellDisplay(this.getCell(row.id, col.id)).replace(/\t/g, " ");
|
|
1750
2093
|
}).join(" ")
|
|
1751
2094
|
).join("\n");
|
|
1752
2095
|
}
|
|
@@ -1823,7 +2166,7 @@ var SpreadsheetController = class {
|
|
|
1823
2166
|
*/
|
|
1824
2167
|
exportToCsv(options = {}) {
|
|
1825
2168
|
const matrix = this.readMatrix(options.range ?? null).map(
|
|
1826
|
-
(row) => row.map((cell) => options.formulas ? cell.raw :
|
|
2169
|
+
(row) => row.map((cell) => options.formulas ? cell.raw : formatCellDisplay(cell))
|
|
1827
2170
|
);
|
|
1828
2171
|
return serializeCsv(matrix, {
|
|
1829
2172
|
delimiter: options.delimiter,
|
|
@@ -1861,7 +2204,16 @@ var SpreadsheetController = class {
|
|
|
1861
2204
|
*/
|
|
1862
2205
|
copyToClipboardData(range = this.selectedRange) {
|
|
1863
2206
|
const r = range ? normalizeRange(range) : { startRow: 0, startCol: 0, endRow: this.document.rows.length - 1, endCol: this.document.columns.length - 1 };
|
|
1864
|
-
const cells = this.readMatrix(r).map(
|
|
2207
|
+
const cells = this.readMatrix(r).map(
|
|
2208
|
+
(row) => row.map((cell) => {
|
|
2209
|
+
const value = cell.error || cell.value;
|
|
2210
|
+
const text = formatCellDisplay(cell);
|
|
2211
|
+
if (cell.spillAnchor && !this.spillAnchorInRange(cell.spillAnchor, r)) {
|
|
2212
|
+
return { raw: text, value, text };
|
|
2213
|
+
}
|
|
2214
|
+
return { raw: cell.raw, value, text };
|
|
2215
|
+
})
|
|
2216
|
+
);
|
|
1865
2217
|
return createSpreadsheetClipboardData(cells, { row: r.startRow, col: r.startCol });
|
|
1866
2218
|
}
|
|
1867
2219
|
/**
|
|
@@ -1884,6 +2236,10 @@ var SpreadsheetController = class {
|
|
|
1884
2236
|
this.selectRange(r);
|
|
1885
2237
|
return true;
|
|
1886
2238
|
}
|
|
2239
|
+
spillAnchorInRange(anchor, range) {
|
|
2240
|
+
const record = this.spills.get(anchor);
|
|
2241
|
+
return !!record && rangeContains(range, record.row, record.col);
|
|
2242
|
+
}
|
|
1887
2243
|
getSelectionTopLeft() {
|
|
1888
2244
|
if (this.selectedRange) {
|
|
1889
2245
|
const r = normalizeRange(this.selectedRange);
|
|
@@ -1979,6 +2335,7 @@ var SpreadsheetController = class {
|
|
|
1979
2335
|
if (keyCols.length === 0 || keyCols.some((k) => k.index < startCol || k.index > endCol)) return false;
|
|
1980
2336
|
const area = { startRow, startCol, endRow, endCol };
|
|
1981
2337
|
if ((this.document.table?.merges ?? []).some((m) => rangesIntersect(m, area))) return false;
|
|
2338
|
+
if (this.intersectsSpill(area)) return false;
|
|
1982
2339
|
const sourceRows = [];
|
|
1983
2340
|
for (let r = startRow; r <= endRow; r++) sourceRows.push(r);
|
|
1984
2341
|
const sorted = sourceRows.map((r) => ({ r, values: keyCols.map((k) => this.getCellValueAt(r, k.index)) })).sort((a, b) => {
|
|
@@ -2186,6 +2543,7 @@ var SpreadsheetController = class {
|
|
|
2186
2543
|
}
|
|
2187
2544
|
const merges = this.getMerges();
|
|
2188
2545
|
if (merges.some((m) => rangesIntersect(m, r) && !rangeContainsRange(r, m))) return false;
|
|
2546
|
+
if (this.intersectsSpill(r)) return false;
|
|
2189
2547
|
this.applyTableChange(() => {
|
|
2190
2548
|
for (let row = r.startRow; row <= r.endRow; row++) {
|
|
2191
2549
|
const rowId = this.document.rows[row].id;
|
|
@@ -2207,7 +2565,7 @@ var SpreadsheetController = class {
|
|
|
2207
2565
|
if (remaining.length === merges.length) return false;
|
|
2208
2566
|
this.applyTableChange(() => {
|
|
2209
2567
|
this.table.merges = remaining;
|
|
2210
|
-
});
|
|
2568
|
+
}, { recalculate: true });
|
|
2211
2569
|
return true;
|
|
2212
2570
|
}
|
|
2213
2571
|
// --- Autofill ---
|
|
@@ -2328,6 +2686,7 @@ var SpreadsheetController = class {
|
|
|
2328
2686
|
const width = s.endCol - s.startCol + 1;
|
|
2329
2687
|
const t = { startRow: s.startRow, startCol: s.startCol, endRow: s.startRow + width - 1, endCol: s.startCol + height - 1 };
|
|
2330
2688
|
if (this.getMerges().some((m) => rangesIntersect(m, s) || rangesIntersect(m, t))) return false;
|
|
2689
|
+
if (this.intersectsSpill(s) || this.intersectsSpill(t)) return false;
|
|
2331
2690
|
this.applyTableChange(() => {
|
|
2332
2691
|
this.growGrid(t.endRow, t.endCol);
|
|
2333
2692
|
this.remapAllFormulas(({ row, col }) => {
|
|
@@ -2371,6 +2730,28 @@ var SpreadsheetController = class {
|
|
|
2371
2730
|
this.undoStack.push(JSON.stringify(this.document));
|
|
2372
2731
|
if (this.undoStack.length > 50) this.undoStack.shift();
|
|
2373
2732
|
this.redoStack = [];
|
|
2733
|
+
this.trimHistory();
|
|
2734
|
+
}
|
|
2735
|
+
/** Drops the oldest snapshots beyond `maxHistoryBytes`, always keeping the newest. */
|
|
2736
|
+
trimHistory() {
|
|
2737
|
+
if (this.maxHistoryBytes <= 0 || this.undoStack.length <= 1) return;
|
|
2738
|
+
let total = 0;
|
|
2739
|
+
for (const s of this.undoStack) total += s.length * 2;
|
|
2740
|
+
for (const s of this.redoStack) total += s.length * 2;
|
|
2741
|
+
while (total > this.maxHistoryBytes && this.undoStack.length > 1) {
|
|
2742
|
+
total -= this.undoStack.shift().length * 2;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
/** Approximate bytes held by the undo/redo snapshots (UTF-16 size). */
|
|
2746
|
+
getHistoryByteSize() {
|
|
2747
|
+
let total = 0;
|
|
2748
|
+
for (const s of this.undoStack) total += s.length * 2;
|
|
2749
|
+
for (const s of this.redoStack) total += s.length * 2;
|
|
2750
|
+
return total;
|
|
2751
|
+
}
|
|
2752
|
+
/** Number of undo / redo snapshots currently held. */
|
|
2753
|
+
getHistoryDepth() {
|
|
2754
|
+
return { undo: this.undoStack.length, redo: this.redoStack.length };
|
|
2374
2755
|
}
|
|
2375
2756
|
undo() {
|
|
2376
2757
|
if (this.undoStack.length === 0) return;
|
|
@@ -2412,6 +2793,17 @@ function stepInOrder(order, from, delta) {
|
|
|
2412
2793
|
}
|
|
2413
2794
|
|
|
2414
2795
|
// src/spreadsheet/menu.ts
|
|
2796
|
+
function clampMenuPosition(point, size, viewport, margin = 4) {
|
|
2797
|
+
const place = (start, length, limit) => {
|
|
2798
|
+
if (start + length <= limit - margin) return Math.max(margin, start);
|
|
2799
|
+
const flipped = start - length;
|
|
2800
|
+
return Math.max(margin, flipped >= margin ? flipped : limit - margin - length);
|
|
2801
|
+
};
|
|
2802
|
+
return {
|
|
2803
|
+
x: place(point.x, size.width, viewport.width),
|
|
2804
|
+
y: place(point.y, size.height, viewport.height)
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2415
2807
|
var quote = (value) => {
|
|
2416
2808
|
const text = String(value ?? "");
|
|
2417
2809
|
return text.length > 24 ? `"${text.slice(0, 23)}\u2026"` : `"${text}"`;
|
|
@@ -2423,7 +2815,8 @@ function getSpreadsheetMenuItems(controller, cell) {
|
|
|
2423
2815
|
if (!column || !row) return [];
|
|
2424
2816
|
const selection = controller.selectedRange && rangeContains(controller.selectedRange, cell.row, cell.col) ? normalizeRange(controller.selectedRange) : { startRow: cell.row, startCol: cell.col, endRow: cell.row, endCol: cell.col };
|
|
2425
2817
|
const colName = indexToColName(cell.col);
|
|
2426
|
-
const
|
|
2818
|
+
const cellData = controller.getCell(row.id, column.id);
|
|
2819
|
+
const value = cellData.value;
|
|
2427
2820
|
const table = doc.table ?? {};
|
|
2428
2821
|
const filter = controller.getFilterState();
|
|
2429
2822
|
const grouping = table.grouping;
|
|
@@ -2445,7 +2838,7 @@ function getSpreadsheetMenuItems(controller, cell) {
|
|
|
2445
2838
|
},
|
|
2446
2839
|
{
|
|
2447
2840
|
id: "filter-equals",
|
|
2448
|
-
label: `Show only rows where ${colName} = ${quote(
|
|
2841
|
+
label: `Show only rows where ${colName} = ${quote(formatCellDisplay(cellData))}`,
|
|
2449
2842
|
separatorBefore: true,
|
|
2450
2843
|
run: () => controller.addFilterRule({ colId: column.id, operator: "equals", value: String(value ?? "") })
|
|
2451
2844
|
},
|
|
@@ -2694,6 +3087,7 @@ export {
|
|
|
2694
3087
|
computeRowGroups,
|
|
2695
3088
|
buildSpreadsheetView,
|
|
2696
3089
|
SpreadsheetController,
|
|
3090
|
+
clampMenuPosition,
|
|
2697
3091
|
getSpreadsheetMenuItems,
|
|
2698
3092
|
SPREADSHEET_TABLE_ACTIONS,
|
|
2699
3093
|
bindSpreadsheetTableActions,
|
|
@@ -2703,4 +3097,4 @@ export {
|
|
|
2703
3097
|
registerSpreadsheetImportAdapters,
|
|
2704
3098
|
importFileIntoSpreadsheet
|
|
2705
3099
|
};
|
|
2706
|
-
//# sourceMappingURL=chunk-
|
|
3100
|
+
//# sourceMappingURL=chunk-K3R5NG2D.js.map
|