@pixerate/editor 0.21.0 → 0.22.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.
Files changed (70) hide show
  1. package/README.md +20 -11
  2. package/dist/agent/index.cjs +8 -8
  3. package/dist/agent/index.d.cts +2 -2
  4. package/dist/agent/index.d.ts +2 -2
  5. package/dist/agent/index.js +2 -2
  6. package/dist/canvas/index.cjs +1269 -13
  7. package/dist/canvas/index.cjs.map +1 -1
  8. package/dist/canvas/index.d.cts +432 -6
  9. package/dist/canvas/index.d.ts +432 -6
  10. package/dist/canvas/index.js +1264 -8
  11. package/dist/canvas/index.js.map +1 -1
  12. package/dist/{chunk-U6NDQFOK.cjs → chunk-6M3BCTBD.cjs} +427 -138
  13. package/dist/chunk-6M3BCTBD.cjs.map +1 -0
  14. package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
  15. package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
  16. package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
  17. package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
  18. package/dist/{chunk-A7DKIA72.js → chunk-DENSCKFC.js} +642 -69
  19. package/dist/chunk-DENSCKFC.js.map +1 -0
  20. package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
  21. package/dist/chunk-GVSMRPHB.cjs.map +1 -0
  22. package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
  23. package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
  24. package/dist/chunk-MJZBNWG7.js.map +1 -0
  25. package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
  26. package/dist/chunk-QGHGJ56O.js.map +1 -0
  27. package/dist/{chunk-GVW4P7BG.js → chunk-RU52I7QO.js} +305 -16
  28. package/dist/chunk-RU52I7QO.js.map +1 -0
  29. package/dist/{chunk-JYRIOFEN.cjs → chunk-SAZRENEZ.cjs} +647 -74
  30. package/dist/chunk-SAZRENEZ.cjs.map +1 -0
  31. package/dist/{controller-OUdxPY-X.d.ts → controller-B7GIybc4.d.cts} +139 -7
  32. package/dist/{controller-OUdxPY-X.d.cts → controller-B7GIybc4.d.ts} +139 -7
  33. package/dist/extensions/index.cjs +14 -4
  34. package/dist/extensions/index.cjs.map +1 -1
  35. package/dist/extensions/index.d.cts +3 -3
  36. package/dist/extensions/index.d.ts +3 -3
  37. package/dist/extensions/index.js +13 -3
  38. package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
  39. package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
  40. package/dist/index.cjs +250 -16
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +294 -8
  43. package/dist/index.d.ts +294 -8
  44. package/dist/index.js +241 -7
  45. package/dist/index.js.map +1 -1
  46. package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
  47. package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
  48. package/dist/serializers/index.cjs +3 -3
  49. package/dist/serializers/index.d.cts +1 -1
  50. package/dist/serializers/index.d.ts +1 -1
  51. package/dist/serializers/index.js +2 -2
  52. package/dist/spreadsheet/index.cjs +7 -3
  53. package/dist/spreadsheet/index.cjs.map +1 -1
  54. package/dist/spreadsheet/index.d.cts +33 -4
  55. package/dist/spreadsheet/index.d.ts +33 -4
  56. package/dist/spreadsheet/index.js +6 -2
  57. package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
  58. package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
  59. package/dist/{types-DMe0vFB1.d.cts → types-CJ4U-HHI.d.cts} +1 -1
  60. package/dist/{types-DMe0vFB1.d.ts → types-CJ4U-HHI.d.ts} +1 -1
  61. package/package.json +1 -1
  62. package/dist/chunk-3TYMNULT.js.map +0 -1
  63. package/dist/chunk-4QPLEUL2.cjs.map +0 -1
  64. package/dist/chunk-A7DKIA72.js.map +0 -1
  65. package/dist/chunk-CYFJCKC4.js.map +0 -1
  66. package/dist/chunk-GVW4P7BG.js.map +0 -1
  67. package/dist/chunk-JYRIOFEN.cjs.map +0 -1
  68. package/dist/chunk-TVRC4KVT.cjs.map +0 -1
  69. package/dist/chunk-U6NDQFOK.cjs.map +0 -1
  70. /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
@@ -2,15 +2,17 @@ import {
2
2
  FormulaEvaluator,
3
3
  WHOLE_COLUMN_END_ROW,
4
4
  colNameToIndex,
5
+ describeFormulaError,
5
6
  getFormulaArgumentSpecIndex,
6
7
  getFormulaFunction,
7
8
  getFormulaSignature,
8
9
  getFormulaSignatureParts,
9
10
  indexToColName,
11
+ isFormulaError,
10
12
  parseFormula,
11
13
  searchFormulaFunctions,
12
14
  tokenizeFormula
13
- } from "./chunk-A7DKIA72.js";
15
+ } from "./chunk-DENSCKFC.js";
14
16
 
15
17
  // src/spreadsheet/formula/dependency-graph.ts
16
18
  var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
@@ -19,6 +21,8 @@ function extractCellDependencies(node) {
19
21
  const visit = (curr) => {
20
22
  if (curr.type === "CellRef") {
21
23
  deps.push({ col: curr.col, row: curr.row });
24
+ } else if (curr.type === "SpillRef") {
25
+ deps.push({ col: curr.col, row: curr.row });
22
26
  } else if (curr.type === "Range" && curr.endRow >= WHOLE_COLUMN_END_ROW) {
23
27
  const startC = colNameToIndex(curr.startCol);
24
28
  const endC = colNameToIndex(curr.endCol);
@@ -54,6 +58,14 @@ var DependencyGraph = class {
54
58
  dependencies = /* @__PURE__ */ new Map();
55
59
  // Map of cellKey (e.g. "A1") -> Set of cellKeys that depend ON this cell (dependents)
56
60
  dependents = /* @__PURE__ */ new Map();
61
+ // Dynamic-array edges, set by the controller after evaluating an anchor:
62
+ // spilled cells depend on their anchor, and a blocked (#SPILL!) anchor
63
+ // depends on the cells blocking it. Kept apart from formula dependencies,
64
+ // so re-entering a formula doesn't drop them.
65
+ spillDependencies = /* @__PURE__ */ new Map();
66
+ spillDependents = /* @__PURE__ */ new Map();
67
+ // anchor -> the edges it registered (so they can be replaced).
68
+ spillEdges = /* @__PURE__ */ new Map();
57
69
  setCellFormula(cellKey, formulaStr) {
58
70
  const key = cellKey.toUpperCase();
59
71
  const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
@@ -84,12 +96,49 @@ var DependencyGraph = class {
84
96
  clear() {
85
97
  this.dependencies.clear();
86
98
  this.dependents.clear();
99
+ this.spillDependencies.clear();
100
+ this.spillDependents.clear();
101
+ this.spillEdges.clear();
87
102
  }
103
+ /**
104
+ * Records the dynamic-array edges of the anchor `anchorKey`, replacing its
105
+ * previous ones: every key in `spilledKeys` (cells holding its spilled
106
+ * values) depends on the anchor, and the anchor depends on every key in
107
+ * `blockingKeys` (cells that stop it spilling). Pass empty lists to remove
108
+ * the anchor's edges.
109
+ */
110
+ setSpillEdges(anchorKey, spilledKeys, blockingKeys = []) {
111
+ const anchor = anchorKey.toUpperCase();
112
+ for (const [from, to] of this.spillEdges.get(anchor) ?? []) {
113
+ this.spillDependencies.get(from)?.delete(to);
114
+ this.spillDependents.get(to)?.delete(from);
115
+ }
116
+ const edges = [];
117
+ for (const key of spilledKeys) edges.push([key.toUpperCase(), anchor]);
118
+ for (const key of blockingKeys) edges.push([anchor, key.toUpperCase()]);
119
+ for (const [from, to] of edges) {
120
+ if (!this.spillDependencies.has(from)) this.spillDependencies.set(from, /* @__PURE__ */ new Set());
121
+ this.spillDependencies.get(from).add(to);
122
+ if (!this.spillDependents.has(to)) this.spillDependents.set(to, /* @__PURE__ */ new Set());
123
+ this.spillDependents.get(to).add(from);
124
+ }
125
+ if (edges.length > 0) this.spillEdges.set(anchor, edges);
126
+ else this.spillEdges.delete(anchor);
127
+ }
128
+ /** The spilled cells registered for `anchorKey` by `setSpillEdges`. */
129
+ getSpilledKeys(anchorKey) {
130
+ const anchor = anchorKey.toUpperCase();
131
+ return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);
132
+ }
133
+ /** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */
88
134
  getDependencies(cellKey) {
89
- return Array.from(this.dependencies.get(cellKey.toUpperCase()) || []);
135
+ const key = cellKey.toUpperCase();
136
+ return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));
90
137
  }
138
+ /** Cells depending on `cellKey`, through formulas or spills. */
91
139
  getDependents(cellKey) {
92
- return Array.from(this.dependents.get(cellKey.toUpperCase()) || []);
140
+ const key = cellKey.toUpperCase();
141
+ return unionOf(this.dependents.get(key), this.spillDependents.get(key));
93
142
  }
94
143
  /**
95
144
  * Detects if there is a cycle containing cellKey.
@@ -101,7 +150,7 @@ var DependencyGraph = class {
101
150
  const dfs = (node) => {
102
151
  visited.add(node);
103
152
  recStack.add(node);
104
- const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
153
+ const deps = this.getDependencies(node);
105
154
  for (const neighbor of deps) {
106
155
  if (!visited.has(neighbor)) {
107
156
  if (dfs(neighbor)) return true;
@@ -180,6 +229,7 @@ var DependencyGraph = class {
180
229
  const node = queue.pop();
181
230
  const direct = [
182
231
  ...this.dependents.get(node) ?? [],
232
+ ...this.spillDependents.get(node) ?? [],
183
233
  ...this.dependents.get(columnDependencyKey(node.replace(/\d+$/, ""))) ?? []
184
234
  ];
185
235
  for (const dep of direct) {
@@ -192,7 +242,7 @@ var DependencyGraph = class {
192
242
  }
193
243
  const neighbors = (node) => {
194
244
  const result = [];
195
- for (const dep of this.dependencies.get(node) ?? []) {
245
+ for (const dep of this.getDependencies(node)) {
196
246
  if (dep.endsWith(":")) {
197
247
  const col = dep.slice(0, -1);
198
248
  for (const candidate of nodes) {
@@ -258,6 +308,11 @@ var DependencyGraph = class {
258
308
  return { order, cyclic };
259
309
  }
260
310
  };
311
+ function unionOf(a, b) {
312
+ if (!b || b.size === 0) return Array.from(a ?? []);
313
+ if (!a || a.size === 0) return Array.from(b);
314
+ return Array.from(/* @__PURE__ */ new Set([...a, ...b]));
315
+ }
261
316
 
262
317
  // src/spreadsheet/formula/references.ts
263
318
  var CELL_PART = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
@@ -335,11 +390,17 @@ function mapFormulaReferences(formula, rewrite) {
335
390
  let cursor = 0;
336
391
  for (let i = 0; i < tokens.length; i++) {
337
392
  const token = tokens[i];
338
- if (token.type !== "CELL_REF" && token.type !== "RANGE") continue;
393
+ if (token.type !== "CELL_REF" && token.type !== "RANGE" && token.type !== "SPILL_REF") continue;
339
394
  if (tokens[i + 1]?.type === "LPAREN") continue;
340
395
  let end = token.position;
341
396
  while (end < formula.length && REF_CHAR.test(formula[end])) end++;
342
397
  const original = formula.slice(token.position, end);
398
+ if (token.type === "SPILL_REF") {
399
+ const rewritten = rewrite(original);
400
+ result += formula.slice(cursor, token.position) + (rewritten === "#REF!" ? rewritten : `${rewritten}#`);
401
+ cursor = end + 1;
402
+ continue;
403
+ }
343
404
  result += formula.slice(cursor, token.position) + rewrite(original);
344
405
  cursor = end;
345
406
  }
@@ -1228,6 +1289,8 @@ function buildSpreadsheetView(doc, getValue) {
1228
1289
 
1229
1290
  // src/spreadsheet/controller.ts
1230
1291
  var nextIdSequence = 0;
1292
+ var MAX_SPILL_PASSES = 25;
1293
+ var spillCellId = (rowId, colId) => `${rowId}\0${colId}`;
1231
1294
  var SpreadsheetController = class {
1232
1295
  document;
1233
1296
  activeCell = null;
@@ -1250,6 +1313,18 @@ var SpreadsheetController = class {
1250
1313
  pendingCommits = [];
1251
1314
  // Row being evaluated, so [property] references resolve per row.
1252
1315
  evaluatingRowIndex = null;
1316
+ // Cell being evaluated, for implicit intersection (`=@A:A`).
1317
+ evaluatingCell = null;
1318
+ // Dynamic arrays. Spilled values are derived state: they live here, not in
1319
+ // the document, and every recalculation rebuilds them.
1320
+ spills = /* @__PURE__ */ new Map();
1321
+ // anchor key -> spill
1322
+ spilledBy = /* @__PURE__ */ new Map();
1323
+ // spilled cell key -> anchor key
1324
+ spillCells = /* @__PURE__ */ new Map();
1325
+ // spillCellId -> displayed cell
1326
+ // Cells whose spill ownership changed during the current pass.
1327
+ spillChanges = null;
1253
1328
  constructor(options = {}) {
1254
1329
  const defaultCols = [
1255
1330
  { id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
@@ -1283,7 +1358,9 @@ var SpreadsheetController = class {
1283
1358
  this.evaluator = new FormulaEvaluator({
1284
1359
  resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),
1285
1360
  resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),
1286
- resolveProperty: (prop) => this.resolveCurrentRowProperty(prop)
1361
+ resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),
1362
+ resolveSpill: (colStr, rowNum) => this.resolveSpillByCoord(colStr, rowNum),
1363
+ getFormulaCell: () => this.evaluatingCell
1287
1364
  });
1288
1365
  this.syncWithDataSource();
1289
1366
  this.recalculateAll();
@@ -1336,6 +1413,11 @@ var SpreadsheetController = class {
1336
1413
  }
1337
1414
  getCell(rowId, colId) {
1338
1415
  const existing = this.document.cells[rowId]?.[colId];
1416
+ if (existing && existing.raw !== "") return existing;
1417
+ if (this.spillCells.size > 0) {
1418
+ const spilled = this.spillCells.get(spillCellId(rowId, colId));
1419
+ if (spilled) return spilled;
1420
+ }
1339
1421
  if (existing) return existing;
1340
1422
  const col = this.document.columns.find((c) => c.id === colId);
1341
1423
  const row = this.document.rows.find((r) => r.id === rowId);
@@ -1408,7 +1490,30 @@ var SpreadsheetController = class {
1408
1490
  */
1409
1491
  recalculateCell(rowId, colId) {
1410
1492
  const cellKey = this.getCellKey(rowId, colId);
1411
- this.runEvaluationPlan(this.depGraph.getEvaluationPlan([cellKey], { includeDependents: true }));
1493
+ const anchor = this.spilledBy.get(cellKey);
1494
+ this.recalculateKeys(anchor ? [cellKey, anchor] : [cellKey], true);
1495
+ }
1496
+ /**
1497
+ * Recalculates `keys` (and, with `includeDependents`, their dependents) in
1498
+ * dependency order. Spill ranges are only known after evaluating their
1499
+ * anchors, so when a pass changes which cells hold spilled values (and so
1500
+ * the dependency graph), the dependents of those cells are recalculated in
1501
+ * another pass, until nothing changes.
1502
+ */
1503
+ recalculateKeys(keys, includeDependents) {
1504
+ let pending = Array.from(keys);
1505
+ let withDependents = includeDependents;
1506
+ for (let pass = 0; pass < MAX_SPILL_PASSES && pending.length > 0; pass++) {
1507
+ const changed = /* @__PURE__ */ new Set();
1508
+ this.spillChanges = changed;
1509
+ try {
1510
+ this.runEvaluationPlan(this.depGraph.getEvaluationPlan(pending, { includeDependents: withDependents }));
1511
+ } finally {
1512
+ this.spillChanges = null;
1513
+ }
1514
+ pending = Array.from(changed);
1515
+ withDependents = true;
1516
+ }
1412
1517
  }
1413
1518
  /**
1414
1519
  * Rebuilds the dependency graph from scratch and recalculates every formula
@@ -1416,6 +1521,9 @@ var SpreadsheetController = class {
1416
1521
  */
1417
1522
  recalculateAll() {
1418
1523
  this.depGraph.clear();
1524
+ this.spills.clear();
1525
+ this.spilledBy.clear();
1526
+ this.spillCells.clear();
1419
1527
  const formulaKeys = [];
1420
1528
  for (const row of this.document.rows) {
1421
1529
  for (const col of this.document.columns) {
@@ -1427,7 +1535,7 @@ var SpreadsheetController = class {
1427
1535
  }
1428
1536
  }
1429
1537
  }
1430
- this.runEvaluationPlan(this.depGraph.getEvaluationPlan(formulaKeys));
1538
+ this.recalculateKeys(formulaKeys, false);
1431
1539
  }
1432
1540
  runEvaluationPlan(plan) {
1433
1541
  for (const key of plan.order) {
@@ -1454,7 +1562,10 @@ var SpreadsheetController = class {
1454
1562
  if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
1455
1563
  const deps = this.depGraph.getDependencies(cellKey);
1456
1564
  const selfReference = deps.includes(cellKey) || deps.includes(`${col.key.toUpperCase()}:`);
1457
- const errorMessage = selfReference ? "This formula can't reference its own cell." : "This formula is part of a circular reference: it depends on its own result through other cells.";
1565
+ const spilledKeys = this.depGraph.getSpilledKeys(cellKey);
1566
+ const ownSpill = deps.find((dep) => spilledKeys.includes(dep));
1567
+ 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.";
1568
+ this.clearSpill(cellKey.toUpperCase(), true);
1458
1569
  this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!", errorMessage };
1459
1570
  }
1460
1571
  evalSingleCellByKey(cellKey) {
@@ -1471,26 +1582,177 @@ var SpreadsheetController = class {
1471
1582
  cell.error = void 0;
1472
1583
  cell.errorMessage = void 0;
1473
1584
  }
1585
+ this.clearSpill(`${colName}${rowNum}`);
1474
1586
  return;
1475
1587
  }
1476
1588
  try {
1477
1589
  this.evaluatingRowIndex = rowNum - 1;
1590
+ this.evaluatingCell = { col: colName, row: rowNum };
1478
1591
  const result = this.evaluator.evaluateWithDiagnostics(cell.raw);
1479
1592
  if (!this.document.cells[row.id]) {
1480
1593
  this.document.cells[row.id] = {};
1481
1594
  }
1595
+ let { value, error, message } = result;
1596
+ if (result.array) {
1597
+ const blocked = this.placeSpill(`${colName}${rowNum}`, rowNum - 1, this.document.columns.indexOf(col), result.array);
1598
+ if (blocked) {
1599
+ value = "#SPILL!";
1600
+ error = "#SPILL!";
1601
+ message = blocked;
1602
+ }
1603
+ } else {
1604
+ this.clearSpill(`${colName}${rowNum}`);
1605
+ }
1482
1606
  this.document.cells[row.id][col.id] = {
1483
1607
  ...cell,
1484
- value: result.value,
1608
+ value,
1485
1609
  // `#ERROR!` (malformed formula, wrong argument count) keeps its
1486
1610
  // message in `error`, as before; other errors store their code.
1487
- error: result.error === "#ERROR!" ? result.message : result.error,
1488
- errorMessage: result.message
1611
+ error: error === "#ERROR!" ? message : error,
1612
+ errorMessage: message
1489
1613
  };
1490
1614
  } finally {
1491
1615
  this.evaluatingRowIndex = null;
1616
+ this.evaluatingCell = null;
1492
1617
  }
1493
1618
  }
1619
+ // --- Dynamic arrays (spilling) ---
1620
+ /**
1621
+ * Spills `array` from the anchor at (row, col) into the cells below and to
1622
+ * the right. Returns why it can't (the anchor then shows `#SPILL!`), or
1623
+ * null once placed. The grid never grows to fit an array, as in Excel.
1624
+ */
1625
+ placeSpill(anchor, row, col, array) {
1626
+ const { rows, cols } = array;
1627
+ const area = { startRow: row, startCol: col, endRow: row + rows - 1, endCol: col + cols - 1 };
1628
+ const label = `${indexToColName(area.startCol)}${area.startRow + 1}:${indexToColName(area.endCol)}${area.endRow + 1}`;
1629
+ let reason = null;
1630
+ const blockers = [];
1631
+ if (area.endRow >= this.document.rows.length || area.endCol >= this.document.columns.length) {
1632
+ reason = `The ${rows}\xD7${cols} result needs ${label}, which extends past the edge of the sheet.`;
1633
+ } else if (this.getMerges().some((m) => rangesIntersect(m, area))) {
1634
+ reason = `The result can't spill into merged cells (it needs ${label}).`;
1635
+ } else {
1636
+ for (let c = area.startCol; c <= area.endCol; c++) {
1637
+ const column = this.document.columns[c];
1638
+ for (let r = area.startRow; r <= area.endRow; r++) {
1639
+ if (r === row && c === col) continue;
1640
+ const rowObj = this.document.rows[r];
1641
+ const key = `${column.key}${r + 1}`.toUpperCase();
1642
+ const stored = this.document.cells[rowObj.id]?.[column.id];
1643
+ const raw = stored ? stored.raw : this.getDerivedCell(rowObj, column).raw;
1644
+ const owner = this.spilledBy.get(key);
1645
+ if (raw !== "") {
1646
+ blockers.push(key);
1647
+ reason ??= `The result needs ${label}, but ${key} isn't empty.`;
1648
+ } else if (owner && owner !== anchor) {
1649
+ blockers.push(key);
1650
+ reason ??= `The result needs ${label}, but ${key} already holds values spilled from ${owner}.`;
1651
+ }
1652
+ }
1653
+ }
1654
+ }
1655
+ if (reason) {
1656
+ this.clearSpill(anchor);
1657
+ this.depGraph.setSpillEdges(anchor, [], blockers);
1658
+ return reason;
1659
+ }
1660
+ const previous = this.spills.get(anchor);
1661
+ const cells = [];
1662
+ for (let c = 0; c < cols; c++) {
1663
+ const column = this.document.columns[col + c];
1664
+ for (let r = 0; r < rows; r++) {
1665
+ if (r === 0 && c === 0) continue;
1666
+ const rowObj = this.document.rows[row + r];
1667
+ const key = `${column.key}${row + r + 1}`.toUpperCase();
1668
+ const value = array.values[c * rows + r] ?? "";
1669
+ const display = { raw: "", value, format: column.format, spillAnchor: anchor };
1670
+ if (isFormulaError(value)) {
1671
+ display.error = value;
1672
+ display.errorMessage = describeFormulaError(value);
1673
+ }
1674
+ this.spillCells.set(spillCellId(rowObj.id, column.id), display);
1675
+ if (this.spilledBy.get(key) !== anchor) {
1676
+ this.spilledBy.set(key, anchor);
1677
+ this.spillChanges?.add(key);
1678
+ }
1679
+ cells.push({ key, rowId: rowObj.id, colId: column.id });
1680
+ }
1681
+ }
1682
+ if (previous) {
1683
+ const kept = new Set(cells.map((cell) => cell.key));
1684
+ for (const old of previous.cells) {
1685
+ if (kept.has(old.key)) continue;
1686
+ this.forgetSpilledCell(anchor, old);
1687
+ }
1688
+ }
1689
+ this.spills.set(anchor, { anchor, row, col, rows, cols, values: array.values, cells });
1690
+ this.depGraph.setSpillEdges(anchor, cells.map((cell) => cell.key));
1691
+ return null;
1692
+ }
1693
+ /** Removes the array spilled from `anchor`. With `keepEdges`, its dependency edges stay. */
1694
+ clearSpill(anchor, keepEdges = false) {
1695
+ const record = this.spills.get(anchor);
1696
+ if (record) {
1697
+ for (const cell of record.cells) this.forgetSpilledCell(anchor, cell);
1698
+ this.spills.delete(anchor);
1699
+ }
1700
+ if (!keepEdges) this.depGraph.setSpillEdges(anchor, []);
1701
+ }
1702
+ forgetSpilledCell(anchor, cell) {
1703
+ if (this.spilledBy.get(cell.key) !== anchor) return;
1704
+ this.spilledBy.delete(cell.key);
1705
+ this.spillCells.delete(spillCellId(cell.rowId, cell.colId));
1706
+ this.spillChanges?.add(cell.key);
1707
+ }
1708
+ resolveSpillByCoord(colStr, rowNum) {
1709
+ const record = this.spills.get(`${colStr}${rowNum}`.toUpperCase());
1710
+ if (record) return { rows: record.rows, cols: record.cols, values: record.values };
1711
+ const value = this.resolveCellByCoord(colStr, rowNum);
1712
+ return isFormulaError(value) ? value : "#REF!";
1713
+ }
1714
+ toSpill(record) {
1715
+ const row = this.document.rows[record.row];
1716
+ const col = this.document.columns[record.col];
1717
+ return {
1718
+ key: record.anchor,
1719
+ anchor: { row: record.row, col: record.col },
1720
+ range: {
1721
+ startRow: record.row,
1722
+ startCol: record.col,
1723
+ endRow: record.row + record.rows - 1,
1724
+ endCol: record.col + record.cols - 1
1725
+ },
1726
+ formula: row && col ? this.getCell(row.id, col.id).raw : ""
1727
+ };
1728
+ }
1729
+ /**
1730
+ * The dynamic array covering (row, col), whether it is the anchor (the cell
1731
+ * holding the formula) or a spilled cell; null when there is none.
1732
+ */
1733
+ getSpillAt(row, col) {
1734
+ if (this.spills.size === 0) return null;
1735
+ const key = `${indexToColName(col)}${row + 1}`;
1736
+ const record = this.spills.get(key) ?? this.spills.get(this.spilledBy.get(key) ?? "");
1737
+ return record ? this.toSpill(record) : null;
1738
+ }
1739
+ /** Every dynamic array currently spilled. */
1740
+ getSpills() {
1741
+ return Array.from(this.spills.values(), (record) => this.toSpill(record));
1742
+ }
1743
+ /** Whether `range` overlaps a spilled array (its anchor or spilled cells). */
1744
+ intersectsSpill(range) {
1745
+ for (const record of this.spills.values()) {
1746
+ const area = {
1747
+ startRow: record.row,
1748
+ startCol: record.col,
1749
+ endRow: record.row + record.rows - 1,
1750
+ endCol: record.col + record.cols - 1
1751
+ };
1752
+ if (rangesIntersect(area, range)) return true;
1753
+ }
1754
+ return false;
1755
+ }
1494
1756
  resolveCellByCoord(colStr, rowNum) {
1495
1757
  const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
1496
1758
  const row = this.document.rows[rowNum - 1];
@@ -1861,7 +2123,15 @@ var SpreadsheetController = class {
1861
2123
  */
1862
2124
  copyToClipboardData(range = this.selectedRange) {
1863
2125
  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((row) => row.map((cell) => ({ raw: cell.raw, value: cell.error || cell.value })));
2126
+ const cells = this.readMatrix(r).map(
2127
+ (row) => row.map((cell) => {
2128
+ const value = cell.error || cell.value;
2129
+ if (cell.spillAnchor && !this.spillAnchorInRange(cell.spillAnchor, r)) {
2130
+ return { raw: value === null || value === void 0 ? "" : String(value), value };
2131
+ }
2132
+ return { raw: cell.raw, value };
2133
+ })
2134
+ );
1865
2135
  return createSpreadsheetClipboardData(cells, { row: r.startRow, col: r.startCol });
1866
2136
  }
1867
2137
  /**
@@ -1884,6 +2154,10 @@ var SpreadsheetController = class {
1884
2154
  this.selectRange(r);
1885
2155
  return true;
1886
2156
  }
2157
+ spillAnchorInRange(anchor, range) {
2158
+ const record = this.spills.get(anchor);
2159
+ return !!record && rangeContains(range, record.row, record.col);
2160
+ }
1887
2161
  getSelectionTopLeft() {
1888
2162
  if (this.selectedRange) {
1889
2163
  const r = normalizeRange(this.selectedRange);
@@ -1979,6 +2253,7 @@ var SpreadsheetController = class {
1979
2253
  if (keyCols.length === 0 || keyCols.some((k) => k.index < startCol || k.index > endCol)) return false;
1980
2254
  const area = { startRow, startCol, endRow, endCol };
1981
2255
  if ((this.document.table?.merges ?? []).some((m) => rangesIntersect(m, area))) return false;
2256
+ if (this.intersectsSpill(area)) return false;
1982
2257
  const sourceRows = [];
1983
2258
  for (let r = startRow; r <= endRow; r++) sourceRows.push(r);
1984
2259
  const sorted = sourceRows.map((r) => ({ r, values: keyCols.map((k) => this.getCellValueAt(r, k.index)) })).sort((a, b) => {
@@ -2186,6 +2461,7 @@ var SpreadsheetController = class {
2186
2461
  }
2187
2462
  const merges = this.getMerges();
2188
2463
  if (merges.some((m) => rangesIntersect(m, r) && !rangeContainsRange(r, m))) return false;
2464
+ if (this.intersectsSpill(r)) return false;
2189
2465
  this.applyTableChange(() => {
2190
2466
  for (let row = r.startRow; row <= r.endRow; row++) {
2191
2467
  const rowId = this.document.rows[row].id;
@@ -2207,7 +2483,7 @@ var SpreadsheetController = class {
2207
2483
  if (remaining.length === merges.length) return false;
2208
2484
  this.applyTableChange(() => {
2209
2485
  this.table.merges = remaining;
2210
- });
2486
+ }, { recalculate: true });
2211
2487
  return true;
2212
2488
  }
2213
2489
  // --- Autofill ---
@@ -2328,6 +2604,7 @@ var SpreadsheetController = class {
2328
2604
  const width = s.endCol - s.startCol + 1;
2329
2605
  const t = { startRow: s.startRow, startCol: s.startCol, endRow: s.startRow + width - 1, endCol: s.startCol + height - 1 };
2330
2606
  if (this.getMerges().some((m) => rangesIntersect(m, s) || rangesIntersect(m, t))) return false;
2607
+ if (this.intersectsSpill(s) || this.intersectsSpill(t)) return false;
2331
2608
  this.applyTableChange(() => {
2332
2609
  this.growGrid(t.endRow, t.endCol);
2333
2610
  this.remapAllFormulas(({ row, col }) => {
@@ -2412,6 +2689,17 @@ function stepInOrder(order, from, delta) {
2412
2689
  }
2413
2690
 
2414
2691
  // src/spreadsheet/menu.ts
2692
+ function clampMenuPosition(point, size, viewport, margin = 4) {
2693
+ const place = (start, length, limit) => {
2694
+ if (start + length <= limit - margin) return Math.max(margin, start);
2695
+ const flipped = start - length;
2696
+ return Math.max(margin, flipped >= margin ? flipped : limit - margin - length);
2697
+ };
2698
+ return {
2699
+ x: place(point.x, size.width, viewport.width),
2700
+ y: place(point.y, size.height, viewport.height)
2701
+ };
2702
+ }
2415
2703
  var quote = (value) => {
2416
2704
  const text = String(value ?? "");
2417
2705
  return text.length > 24 ? `"${text.slice(0, 23)}\u2026"` : `"${text}"`;
@@ -2694,6 +2982,7 @@ export {
2694
2982
  computeRowGroups,
2695
2983
  buildSpreadsheetView,
2696
2984
  SpreadsheetController,
2985
+ clampMenuPosition,
2697
2986
  getSpreadsheetMenuItems,
2698
2987
  SPREADSHEET_TABLE_ACTIONS,
2699
2988
  bindSpreadsheetTableActions,
@@ -2703,4 +2992,4 @@ export {
2703
2992
  registerSpreadsheetImportAdapters,
2704
2993
  importFileIntoSpreadsheet
2705
2994
  };
2706
- //# sourceMappingURL=chunk-GVW4P7BG.js.map
2995
+ //# sourceMappingURL=chunk-RU52I7QO.js.map