@rowsncolumns/rnc-engine 1.5.2 → 1.6.1

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.
@@ -3,7 +3,7 @@
3
3
  import { CellNode, CellRangeNode, isCellCoordinate, isCellNode, makeKey, } from "@rowsncolumns/dag";
4
4
  import { DEFAULT_ACTIVE_CELL, DEFAULT_ARRAY, defaultSpreadsheetTheme, getBandedCells, getInitialActiveSheetId, ICON_SET_COLORS, ICON_SET_GLYPHS, useFocusEmbedOrChart, useFonts, useGetSheetDimensions, useSetTableAutoExpansion, useSetCalculatedColumnFill, } from "@rowsncolumns/spreadsheet";
5
5
  import { applyAdvancedFilterPlan, applySubtotalPlan, CellXfsRegistry, collectPaintMerges, generateDataFromClipboard, getBandedCellFormat, layerBandedFormat, paintSourceForActiveCell, pickFormula, pickValue, planAdvancedFilter, planSubtotals, resolvePasteSpecialOptions, SharedStringRegistry, spreadMergeAnchorFormats, tilePaintMerges, transpose, useOnCopy, previewTableStyle, useOnTraceDependentsPrecedents, useShowError, CIRCULAR_REFERENCE_WARNING_TITLE, CircularReferenceTracker, formatCircularReferenceWarning, getDefaultCellFormat, } from "@rowsncolumns/spreadsheet-state";
6
- import { getFormulaLocale, normalizeExternalUrl, resolveDefaultHorizontalAlignment, } from "@rowsncolumns/utils";
6
+ import { getFormulaLocale, normalizeExternalUrl, resolveDefaultHorizontalAlignment, uuidString, } from "@rowsncolumns/utils";
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
8
  import { createCommandHandlers, rejectionDialog, } from "./command-handlers";
9
9
  import { emptyRenderState, foldCommit, foldCommitStructuralOnly, foldWindow, foldWindows, frozenBandRanges, reconcileSheets, } from "./fold";
@@ -11,10 +11,14 @@ import { RncEngine, } from "./index";
11
11
  import { cellKey, parseCellKey } from "./keys";
12
12
  import { fallbackSheetAfterDelete } from "./sheet-fallback";
13
13
  import { rescopeRulesAfterMove } from "./move-rescope";
14
- import { applyPasteSpecialToTiles, clipboardCellToPasteCell, isSkippedPasteCell, } from "./paste-special";
14
+ import { applyPasteSpecialToTiles, clipboardCellToPasteCell, clipboardFormats, columnWidthCopies, isSkippedPasteCell, noteCopies, pasteLinkCells, reattachClipboardFormats, validationCopies, withoutBorders, } from "./paste-special";
15
15
  import { pastedMerges } from "./paste-merges";
16
+ import { replayPasteEvent, snapshotClipboard } from "./paste-reapply";
16
17
  import { tileClipboardToSelection } from "./paste-tiling";
17
18
  import { windowRectReader } from "./rect-reader";
19
+ /** The width a column at no stored width takes (the grid's default, as `onCopy` reads it). */
20
+ const DEFAULT_PASTE_COLUMN_WIDTH = 100;
21
+ const EMPTY_ALLOW_EDIT_RANGES = [];
18
22
  /** Minimum extra rows/columns to read on EACH side of the visible range so a small scroll reveals
19
23
  * already-loaded cells (no flash) before the next `readWindow`. The buffer actually used scales with
20
24
  * the viewport (see {@link windowBuffer}) and is never smaller than this. The window size stays
@@ -1683,6 +1687,8 @@ export function useEngineCore(options) {
1683
1687
  endColumnIndex: right,
1684
1688
  };
1685
1689
  }, []);
1690
+ // Stable identity: reads through `engineRef`, so the grid's formula-bar effect never re-runs for it.
1691
+ const getArrayFormula = useCallback((sheetId, rowIndex, columnIndex) => engineRef.current?.arrayFormulaAt(sheetId, rowIndex, columnIndex), []);
1686
1692
  const getDataColumnCount = useCallback((sheetId) => {
1687
1693
  const rows = sheetData[String(sheetId)];
1688
1694
  let max = 0;
@@ -2333,6 +2339,7 @@ export function useEngineCore(options) {
2333
2339
  showFirstColumn: t.showFirstColumn,
2334
2340
  showLastColumn: t.showLastColumn,
2335
2341
  totalRow: t.totalRow ?? false,
2342
+ headerRow: t.headerRow !== false,
2336
2343
  range: { ...t.range, sheetId },
2337
2344
  ...t.bandedRange,
2338
2345
  })),
@@ -2571,6 +2578,7 @@ export function useEngineCore(options) {
2571
2578
  paintFacetsRef.current = { bandedRanges: facets.bandedRanges, tables };
2572
2579
  const namedRanges = facets.namedRanges;
2573
2580
  const protectedRanges = facets.protectedRanges;
2581
+ const allowEditRanges = facets.allowEditRanges ?? EMPTY_ALLOW_EDIT_RANGES;
2574
2582
  const embeds = facets.embeds;
2575
2583
  const slicers = facets.slicers;
2576
2584
  // Citations (workbook-level collection; a cell links to one via `citationId`). The grid renders the
@@ -2680,7 +2688,79 @@ export function useEngineCore(options) {
2680
2688
  // cellXfs style-refs to an inline `CellFormat` on each parsed cell, which we forward per cell below;
2681
2689
  // the engine applies it via `set_style` (streams on `styleChanges`). Only a still-unresolved
2682
2690
  // `StyleReference` (no matching cellXfs entry) is skipped, since a bare ref can't cross to the engine.
2683
- const onPaste = useCallback(async (e, sourceSheetId, destinationSheetId, copiedSelections, pasteActiveCell, pasteSelections, _finalSelections, isCutOperation) => {
2691
+ const pasteInto = useCallback(async (e, sourceSheetId, destinationSheetId, copiedSelections, pasteActiveCell, pasteSelections, _finalSelections, isCutOperation) => {
2692
+ // Paste Special. The grid stashes the request under synthetic clipboard keys (same as
2693
+ // `useSpreadsheetState`; `resolvePasteSpecialOptions` reads both the legacy single mode and the
2694
+ // full options).
2695
+ const pasteSpecial = resolvePasteSpecialOptions(e?.clipboardData);
2696
+ // The copied range, kept for Paste Link, the notes / validation / column widths and the cut
2697
+ // source-clear below. A foreign clipboard (another app) has none.
2698
+ const from = copiedSelections?.[0]?.range;
2699
+ const pasteSource = from && sourceSheetId != null
2700
+ ? { sheetId: sourceSheetId, sheetName: getSheetName(sourceSheetId), range: from }
2701
+ : undefined;
2702
+ // Destination top-left = the paste selection's start (else the active cell).
2703
+ const to = pasteSelections?.[pasteSelections.length - 1]?.range;
2704
+ const target = to
2705
+ ? { rowIndex: to.startRowIndex, columnIndex: to.startColumnIndex }
2706
+ : pasteActiveCell;
2707
+ // Excel's Column widths / Keep Source Column Widths: each destination column takes the width
2708
+ // of the copied column it stands for (a source column at the sheet default takes that default).
2709
+ const copySourceColumnWidths = (source) => {
2710
+ const defaultWidth = state.sheets.find((s) => s.sheetId === source.sheetId)?.defaultColumnWidth ??
2711
+ DEFAULT_PASTE_COLUMN_WIDTH;
2712
+ for (const copy of columnWidthCopies(source.range, target.columnIndex)) {
2713
+ onCommand({
2714
+ command: "resize",
2715
+ sheetId: destinationSheetId,
2716
+ indexes: [copy.columnIndex],
2717
+ dimension: getColumnWidth(source.sheetId, copy.sourceColumnIndex) ?? defaultWidth,
2718
+ axis: "y",
2719
+ });
2720
+ }
2721
+ };
2722
+ // Paste Special → Comments and Notes / Validation / Column widths paste no cells: only the
2723
+ // copied range's notes, data-validation rules or column widths travel, in one undo step.
2724
+ // They need a copied range (Excel offers them only after a copy, never after a cut).
2725
+ if (pasteSpecial.notesOnly ||
2726
+ pasteSpecial.validationOnly ||
2727
+ pasteSpecial.columnWidths === "only") {
2728
+ if (!pasteSource || isCutOperation)
2729
+ return;
2730
+ batchHistory(() => {
2731
+ if (pasteSpecial.notesOnly) {
2732
+ const notes = noteCopies(pasteSource.range, target, (r, c) => getNote(pasteSource.sheetId, r, c), pasteSpecial.transpose);
2733
+ for (const { cell, note } of notes) {
2734
+ onCommand({
2735
+ command: "update-note",
2736
+ sheetId: destinationSheetId,
2737
+ activeCell: cell,
2738
+ note,
2739
+ });
2740
+ }
2741
+ }
2742
+ else if (pasteSpecial.validationOnly) {
2743
+ const rules = validationCopies(facets.dataValidations, pasteSource, destinationSheetId, target);
2744
+ for (const rule of rules) {
2745
+ onCommand({
2746
+ command: "create-data-validation",
2747
+ sheetId: destinationSheetId,
2748
+ range: rule.ranges[0],
2749
+ rule: { ...rule, id: uuidString() },
2750
+ });
2751
+ }
2752
+ }
2753
+ else {
2754
+ copySourceColumnWidths(pasteSource);
2755
+ }
2756
+ });
2757
+ return;
2758
+ }
2759
+ // Paste Link refers back to the copied cells, so it needs a copied range — Excel greys it out
2760
+ // after a cut.
2761
+ const pasteLink = Boolean(pasteSpecial.link);
2762
+ if (pasteLink && (!pasteSource || isCutOperation))
2763
+ return;
2684
2764
  // A cut is a MOVE: Excel keeps the moved formulas' text verbatim (`=B1*2` cut from A1 to A5
2685
2765
  // still reads `=B1*2`) and only repoints references INTO the moved block — the engine does
2686
2766
  // that remap (`repoint_formulas_into_moved_block`), so the clipboard parse must not
@@ -2692,7 +2772,7 @@ export function useEngineCore(options) {
2692
2772
  // Pasted image FILES arrive as floating pictures anchored at the active cell (Excel /
2693
2773
  // Google Sheets Ctrl+V of a picture) — the same lossless `create-embed` the Insert → Image
2694
2774
  // dialog emits, one undo entry for all of them, the first one selected like a new chart.
2695
- const pastedEmbeds = parsed?.embeds ?? [];
2775
+ const pastedEmbeds = pasteLink ? [] : parsed?.embeds ?? [];
2696
2776
  if (pastedEmbeds.length) {
2697
2777
  batchHistory(() => {
2698
2778
  for (const spec of pastedEmbeds) {
@@ -2701,35 +2781,42 @@ export function useEngineCore(options) {
2701
2781
  });
2702
2782
  focusEmbedOrChart(pastedEmbeds[0].embedId);
2703
2783
  }
2704
- let results = parsed?.results ?? [];
2784
+ // Paste Link: one reference formula per copied cell instead of the copied cells.
2785
+ let results = pasteLink && pasteSource
2786
+ ? pasteLinkCells(pasteSource, destinationSheetId)
2787
+ : parsed?.results ?? [];
2705
2788
  if (!results.length)
2706
2789
  return;
2707
2790
  // The clipboard's own size and merges, before Paste Special / tiling reshape `results`.
2708
2791
  const clipRows = results.length;
2709
2792
  const clipCols = Math.max(0, ...results.map((row) => row.length));
2710
2793
  const clipboardMerges = parsed?.merges ?? [];
2711
- // Paste Special. The grid stashes the request under synthetic clipboard keys (same as
2712
- // `useSpreadsheetState`; `resolvePasteSpecialOptions` reads both the legacy single mode and the
2713
- // full options). Transpose/Values/Formulas transform the parsed cells UI-side (the engine can't
2714
- // "resolve a formula to its value"); Formats rides a `format_only` flag the engine honors (apply
2715
- // each cell's format, leave destination values intact); Operation / Skip blanks need the
2794
+ // Transpose/Values/Formulas transform the parsed cells UI-side (the engine can't "resolve a
2795
+ // formula to its value"); Formats rides a `format_only` flag the engine honors (apply each
2796
+ // cell's format, leave destination values intact); Operation / Skip blanks need the
2716
2797
  // destination cells and are folded in after tiling (see `applyPasteSpecialToTiles`).
2717
- const pasteSpecial = resolvePasteSpecialOptions(e?.clipboardData);
2718
2798
  if (pasteSpecial.transpose) {
2719
2799
  results = transpose(results);
2720
2800
  }
2801
+ // Values & Number Formatting, Formulas & Number Formatting, Values & Source Formatting: the
2802
+ // source formats are read before the pick, which strips them.
2803
+ const keptFormats = pasteSpecial.numberFormats || pasteSpecial.sourceFormats
2804
+ ? clipboardFormats(results)
2805
+ : undefined;
2721
2806
  if (pasteSpecial.paste === "values") {
2722
2807
  results = pickValue(results, destinationSheetId, initial.current.locale);
2723
2808
  }
2724
2809
  else if (pasteSpecial.paste === "formulas") {
2725
2810
  results = pickFormula(results, destinationSheetId, initial.current.locale);
2726
2811
  }
2812
+ if (keptFormats) {
2813
+ results = reattachClipboardFormats(results, keptFormats, pasteSpecial.sourceFormats ? "sourceFormats" : "numberFormats");
2814
+ }
2815
+ // All except borders (Paste Options ▸ No Borders): the destination keeps its own borders.
2816
+ if (pasteSpecial.noBorders) {
2817
+ results = withoutBorders(results);
2818
+ }
2727
2819
  const formatOnly = pasteSpecial.paste === "formats";
2728
- // Destination top-left = the paste selection's start (else the active cell).
2729
- const to = pasteSelections?.[pasteSelections.length - 1]?.range;
2730
- const target = to
2731
- ? { rowIndex: to.startRowIndex, columnIndex: to.startColumnIndex }
2732
- : pasteActiveCell;
2733
2820
  // A paste selection larger than the clipboard tiles it (copy G1, select G2:G5, paste fills all
2734
2821
  // four), wrapping like `useOnPaste`. Tiling UI-side keeps the engine's `paste` a plain rectangle
2735
2822
  // and the whole paste ONE undo entry; the tiles' formulas are shifted here (see `paste-tiling`).
@@ -2739,8 +2826,6 @@ export function useEngineCore(options) {
2739
2826
  results = tileClipboardToSelection(results, to, pasteActiveCell, destinationSheetId, getSheetName(destinationSheetId), initial.current.locale);
2740
2827
  }
2741
2828
  const tiles = applyPasteSpecialToTiles(results, target, destinationSheetId, pasteSpecial, { getCellData, getUserEnteredFormat }, initial.current.locale);
2742
- // The copied range top-left, kept for the cut source-clear below.
2743
- const from = copiedSelections?.[0]?.range;
2744
2829
  const cutMove = isCutOperation && from && sourceSheetId != null;
2745
2830
  // COPY: `generateDataFromClipboard` has ALREADY re-anchored each relative formula ref to its
2746
2831
  // destination (its `moveFormula(sourceCell → destinationCell)` pass overwrites the cell's `fv`), so
@@ -2759,8 +2844,9 @@ export function useEngineCore(options) {
2759
2844
  // The clipboard rectangle is row-major from the copied range's top-left, so the note of the
2760
2845
  // tile's source cell is looked up by offset; tiled repeats (copy only) reuse the modular offset.
2761
2846
  const noteAt = (i, j) => {
2762
- // A transposed paste no longer maps tiles back to source cells by offset.
2763
- if (!from || sourceSheetId == null || pasteSpecial.transpose)
2847
+ // A transposed paste no longer maps tiles back to source cells by offset; a link carries
2848
+ // only the reference.
2849
+ if (!from || sourceSheetId == null || pasteSpecial.transpose || pasteLink)
2764
2850
  return undefined;
2765
2851
  const height = from.endRowIndex - from.startRowIndex + 1;
2766
2852
  const width = from.endColumnIndex - from.startColumnIndex + 1;
@@ -2790,10 +2876,11 @@ export function useEngineCore(options) {
2790
2876
  // and a cut moves its own merges in the engine (rowsncolumns/spreadsheet#1234).
2791
2877
  const merges = !isCutOperation &&
2792
2878
  !pasteSpecial.transpose &&
2879
+ !pasteLink &&
2793
2880
  (pasteSpecial.paste === "all" || formatOnly)
2794
2881
  ? pastedMerges(clipboardMerges, pasteActiveCell, target, clipRows, clipCols, tiles.length, Math.max(0, ...tiles.map((row) => row.length)))
2795
2882
  : [];
2796
- onCommand({
2883
+ const pasteCommand = {
2797
2884
  command: "paste",
2798
2885
  sheetId: destinationSheetId,
2799
2886
  target,
@@ -2810,17 +2897,99 @@ export function useEngineCore(options) {
2810
2897
  ...(autoCalculatedColumnsRef.current === false
2811
2898
  ? { autoCalculatedColumns: false }
2812
2899
  : {}),
2813
- });
2900
+ };
2901
+ // Keep Source Column Widths: the cells and the widths are one undo step. A transposed block
2902
+ // no longer lines its columns up with the copied ones, so it takes no widths.
2903
+ if (pasteSpecial.columnWidths === "also" && pasteSource && !pasteSpecial.transpose) {
2904
+ batchHistory(() => {
2905
+ onCommand(pasteCommand);
2906
+ copySourceColumnWidths(pasteSource);
2907
+ });
2908
+ }
2909
+ else {
2910
+ onCommand(pasteCommand);
2911
+ }
2814
2912
  }, [
2815
2913
  getSheetName,
2816
2914
  getCellData,
2817
2915
  getUserEnteredFormat,
2818
2916
  getNote,
2917
+ getColumnWidth,
2819
2918
  onCommand,
2820
2919
  batchHistory,
2821
2920
  focusEmbedOrChart,
2822
2921
  getSheetDimensions,
2922
+ facets,
2923
+ state.sheets,
2823
2924
  ]);
2925
+ // The last paste, kept for Excel's Paste Options button (`onReapplyPaste`): its clipboard, its
2926
+ // placement arguments and the undo depth right after it. A cut is a move and has no Paste Options.
2927
+ const lastPasteRef = useRef(null);
2928
+ const onPaste = useCallback(async (e, sourceSheetId, destinationSheetId, copiedSelections, pasteActiveCell, pasteSelections, finalSelections, isCutOperation) => {
2929
+ // Read the clipboard before the first await: a browser paste event's data is gone after it.
2930
+ const entries = isCutOperation ? null : snapshotClipboard(e?.clipboardData);
2931
+ const depthBefore = engineRef.current?.undoDepth() ?? 0;
2932
+ lastPasteRef.current = null;
2933
+ await pasteInto(e, sourceSheetId, destinationSheetId, copiedSelections, pasteActiveCell, pasteSelections, finalSelections, isCutOperation);
2934
+ const depth = engineRef.current?.undoDepth() ?? 0;
2935
+ if (entries && depth > depthBefore) {
2936
+ lastPasteRef.current = {
2937
+ entries,
2938
+ args: [
2939
+ sourceSheetId,
2940
+ destinationSheetId,
2941
+ copiedSelections,
2942
+ pasteActiveCell,
2943
+ pasteSelections,
2944
+ finalSelections,
2945
+ ],
2946
+ depth,
2947
+ };
2948
+ }
2949
+ }, [pasteInto]);
2950
+ // Paste Options: undo the last paste and paste its clipboard again as `options`, at the same place.
2951
+ // The undone paste only moves to the redo stack, and the new paste clears it, so the result is ONE
2952
+ // undo step. Refused (false) once anything else has been written since the paste.
2953
+ const onReapplyPaste = useCallback(async (options) => {
2954
+ const last = lastPasteRef.current;
2955
+ const engine = engineRef.current;
2956
+ if (!last || !engine || engine.undoDepth() !== last.depth) {
2957
+ lastPasteRef.current = null;
2958
+ return false;
2959
+ }
2960
+ // The pasted range stays selected, as after the paste (the undo alone would restore the
2961
+ // selection from before it).
2962
+ const view = viewRef.current;
2963
+ undo();
2964
+ await onPaste(replayPasteEvent(last.entries, options), ...last.args, false);
2965
+ if (view)
2966
+ applyView(view);
2967
+ return lastPasteRef.current !== null;
2968
+ }, [onPaste, undo, applyView]);
2969
+ // Insert-copy. Two commits, the insert first: the engine refuses a band that would tear a merge /
2970
+ // table / array or push data off the sheet, and a refused insert must not let the paste overwrite
2971
+ // the destination (a `batch` runs every member whatever the one before it refused).
2972
+ const onInsertCopiedCells = useCallback(async (e, sheetId, from, to, shift) => {
2973
+ const direction = shift ??
2974
+ (to.range.startRowIndex === from.range.startRowIndex ? "right" : "down");
2975
+ const target = {
2976
+ rowIndex: to.range.startRowIndex,
2977
+ columnIndex: to.range.startColumnIndex,
2978
+ };
2979
+ const commit = onCommand({
2980
+ command: direction === "down"
2981
+ ? "insert-cells-shift-down"
2982
+ : "insert-cells-shift-right",
2983
+ sheetId,
2984
+ activeCell: target,
2985
+ selections: [to],
2986
+ });
2987
+ if (!commit || commit.rejections?.length)
2988
+ return;
2989
+ // Opening the band slides a source that sits wholly in it; the paste reads the source's
2990
+ // notes from where they now are (the cells themselves already ride on the clipboard).
2991
+ await pasteInto(e, sheetId, sheetId, [{ ...from, range: rangeAfterInsertBand(from.range, to.range, direction) }], target, [to], [to], false);
2992
+ }, [onCommand, pasteInto]);
2824
2993
  return useMemo(() => ({
2825
2994
  ...state,
2826
2995
  ...handlers,
@@ -2890,6 +3059,7 @@ export function useEngineCore(options) {
2890
3059
  getSheetColumnCount,
2891
3060
  getDataRowCount,
2892
3061
  getDataRegion,
3062
+ getArrayFormula,
2893
3063
  findAll,
2894
3064
  getSeriesValuesFromRange,
2895
3065
  getDomainValuesFromRange,
@@ -2929,6 +3099,7 @@ export function useEngineCore(options) {
2929
3099
  tables,
2930
3100
  namedRanges,
2931
3101
  protectedRanges,
3102
+ allowEditRanges,
2932
3103
  customTableStyles: facets.customTableStyles,
2933
3104
  embeds,
2934
3105
  slicers,
@@ -2988,6 +3159,8 @@ export function useEngineCore(options) {
2988
3159
  onDeleteDataValidationRules,
2989
3160
  onCopy,
2990
3161
  onPaste,
3162
+ onInsertCopiedCells,
3163
+ onReapplyPaste,
2991
3164
  }), [
2992
3165
  formulaLocale,
2993
3166
  state,
@@ -3051,6 +3224,7 @@ export function useEngineCore(options) {
3051
3224
  getSheetColumnCount,
3052
3225
  getDataRowCount,
3053
3226
  getDataRegion,
3227
+ getArrayFormula,
3054
3228
  findAll,
3055
3229
  getSeriesValuesFromRange,
3056
3230
  getDomainValuesFromRange,
@@ -3074,6 +3248,7 @@ export function useEngineCore(options) {
3074
3248
  tables,
3075
3249
  namedRanges,
3076
3250
  protectedRanges,
3251
+ allowEditRanges,
3077
3252
  facets.customTableStyles,
3078
3253
  embeds,
3079
3254
  slicers,
@@ -3102,5 +3277,33 @@ export function useEngineCore(options) {
3102
3277
  onClearHistory,
3103
3278
  onCopy,
3104
3279
  onPaste,
3280
+ onInsertCopiedCells,
3281
+ onReapplyPaste,
3105
3282
  ]);
3106
3283
  }
3284
+ /** Where `range` sits once cells open at `band` and shift `direction` (a range wholly in the band's
3285
+ * lanes, at or past its first line, slides by the band's size; anything else stays put). */
3286
+ export const rangeAfterInsertBand = (range, band, direction) => {
3287
+ if (direction === "down") {
3288
+ const inLanes = range.startColumnIndex >= band.startColumnIndex &&
3289
+ range.endColumnIndex <= band.endColumnIndex;
3290
+ if (!inLanes || range.startRowIndex < band.startRowIndex)
3291
+ return range;
3292
+ const size = band.endRowIndex - band.startRowIndex + 1;
3293
+ return {
3294
+ ...range,
3295
+ startRowIndex: range.startRowIndex + size,
3296
+ endRowIndex: range.endRowIndex + size,
3297
+ };
3298
+ }
3299
+ const inLanes = range.startRowIndex >= band.startRowIndex &&
3300
+ range.endRowIndex <= band.endRowIndex;
3301
+ if (!inLanes || range.startColumnIndex < band.startColumnIndex)
3302
+ return range;
3303
+ const size = band.endColumnIndex - band.startColumnIndex + 1;
3304
+ return {
3305
+ ...range,
3306
+ startColumnIndex: range.startColumnIndex + size,
3307
+ endColumnIndex: range.endColumnIndex + size,
3308
+ };
3309
+ };
@@ -17,6 +17,11 @@ export class CollabSpreadsheetEngine {
17
17
  * `syncRemote()` to rebuild the engine and get a `CommitResult` for the UI.
18
18
  */
19
19
  applyUpdate(update: Uint8Array): void;
20
+ /**
21
+ * The legacy Ctrl+Shift+Enter array covering a 1-based cell (see the single-user engine's
22
+ * `arrayFormulaAt`), or `null`.
23
+ */
24
+ arrayFormulaAt(sheet_id: number, row: number, col: number): string;
20
25
  canRedo(): boolean;
21
26
  canUndo(): boolean;
22
27
  clearHistory(): void;
@@ -131,6 +136,13 @@ export class SpreadsheetEngine {
131
136
  * Apply a `commands.ts` JSON command; returns `CommitResult` JSON.
132
137
  */
133
138
  applyCommand(command_json: string): string;
139
+ /**
140
+ * The legacy Ctrl+Shift+Enter array covering a 1-based cell, as JSON
141
+ * `{"range":{startRowIndex,endRowIndex,startColumnIndex,endColumnIndex},"formula":"=…"}` —
142
+ * the formula every cell of the array shows (Excel's formula bar wraps it in `{…}`) and the
143
+ * range a re-entry replaces — or `null` outside every CSE array.
144
+ */
145
+ arrayFormulaAt(sheet_id: number, row: number, col: number): string;
134
146
  canRedo(): boolean;
135
147
  /**
136
148
  * Whether an undo / redo is currently available (for enabling toolbar buttons).
@@ -350,9 +362,47 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
350
362
 
351
363
  export interface InitOutput {
352
364
  readonly memory: WebAssembly.Memory;
365
+ readonly __wbg_collabspreadsheetengine_free: (a: number, b: number) => void;
366
+ readonly collabspreadsheetengine_addSheet: (a: number, b: number, c: number, d: number) => void;
367
+ readonly collabspreadsheetengine_applyCommand: (a: number, b: number, c: number) => [number, number, number, number];
368
+ readonly collabspreadsheetengine_applyUpdate: (a: number, b: number, c: number) => [number, number];
369
+ readonly collabspreadsheetengine_arrayFormulaAt: (a: number, b: number, c: number, d: number) => [number, number];
370
+ readonly collabspreadsheetengine_canRedo: (a: number) => number;
371
+ readonly collabspreadsheetengine_canUndo: (a: number) => number;
372
+ readonly collabspreadsheetengine_clearHistory: (a: number) => void;
373
+ readonly collabspreadsheetengine_dataRegion: (a: number, b: number, c: number, d: number) => [number, number, number, number];
374
+ readonly collabspreadsheetengine_dependents: (a: number, b: number, c: number, d: number) => [number, number, number, number];
375
+ readonly collabspreadsheetengine_documentFacets: (a: number) => [number, number];
376
+ readonly collabspreadsheetengine_encodeDiffV1: (a: number, b: number, c: number) => [number, number, number, number];
377
+ readonly collabspreadsheetengine_encodeStateAsUpdate: (a: number) => [number, number];
378
+ readonly collabspreadsheetengine_encodeStateVector: (a: number) => [number, number];
379
+ readonly collabspreadsheetengine_exportCsv: (a: number, b: number) => [number, number];
380
+ readonly collabspreadsheetengine_exportXlsx: (a: number) => [number, number, number, number];
381
+ readonly collabspreadsheetengine_hostOwnedCells: (a: number, b: number) => [number, number, number, number];
382
+ readonly collabspreadsheetengine_initGrid: (a: number, b: number, c: number, d: number) => void;
383
+ readonly collabspreadsheetengine_loadDocument: (a: number, b: number, c: number) => [number, number, number, number];
384
+ readonly collabspreadsheetengine_new: () => number;
385
+ readonly collabspreadsheetengine_newWithClientId: (a: number) => number;
386
+ readonly collabspreadsheetengine_precedents: (a: number, b: number, c: number, d: number) => [number, number, number, number];
387
+ readonly collabspreadsheetengine_rangeStats: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
388
+ readonly collabspreadsheetengine_readWindow: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
389
+ readonly collabspreadsheetengine_redo: (a: number) => [number, number, number, number];
390
+ readonly collabspreadsheetengine_redoDepth: (a: number) => number;
391
+ readonly collabspreadsheetengine_registerCustomFunction: (a: number, b: number, c: number) => void;
392
+ readonly collabspreadsheetengine_setWindowed: (a: number, b: number) => void;
393
+ readonly collabspreadsheetengine_sheetExtents: (a: number) => [number, number, number, number];
394
+ readonly collabspreadsheetengine_sheetIds: (a: number) => [number, number];
395
+ readonly collabspreadsheetengine_sheets: (a: number) => [number, number];
396
+ readonly collabspreadsheetengine_streamBegin: (a: number) => void;
397
+ readonly collabspreadsheetengine_streamEnd: (a: number) => void;
398
+ readonly collabspreadsheetengine_streamWrite: (a: number, b: number, c: number) => void;
399
+ readonly collabspreadsheetengine_syncRemote: (a: number) => [number, number, number, number];
400
+ readonly collabspreadsheetengine_undo: (a: number) => [number, number, number, number];
401
+ readonly collabspreadsheetengine_undoDepth: (a: number) => number;
353
402
  readonly __wbg_spreadsheetengine_free: (a: number, b: number) => void;
354
403
  readonly spreadsheetengine_addSheet: (a: number, b: number, c: number, d: number) => void;
355
404
  readonly spreadsheetengine_applyCommand: (a: number, b: number, c: number) => [number, number, number, number];
405
+ readonly spreadsheetengine_arrayFormulaAt: (a: number, b: number, c: number, d: number) => [number, number];
356
406
  readonly spreadsheetengine_canRedo: (a: number) => number;
357
407
  readonly spreadsheetengine_canUndo: (a: number) => number;
358
408
  readonly spreadsheetengine_clearHistory: (a: number) => void;
@@ -393,42 +443,6 @@ export interface InitOutput {
393
443
  readonly spreadsheetengine_validationSourceValues: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
394
444
  readonly spreadsheetengine_wasmMemoryBytes: (a: number) => number;
395
445
  readonly transformRangeThroughOps: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
396
- readonly __wbg_collabspreadsheetengine_free: (a: number, b: number) => void;
397
- readonly collabspreadsheetengine_addSheet: (a: number, b: number, c: number, d: number) => void;
398
- readonly collabspreadsheetengine_applyCommand: (a: number, b: number, c: number) => [number, number, number, number];
399
- readonly collabspreadsheetengine_applyUpdate: (a: number, b: number, c: number) => [number, number];
400
- readonly collabspreadsheetengine_canRedo: (a: number) => number;
401
- readonly collabspreadsheetengine_canUndo: (a: number) => number;
402
- readonly collabspreadsheetengine_clearHistory: (a: number) => void;
403
- readonly collabspreadsheetengine_dataRegion: (a: number, b: number, c: number, d: number) => [number, number, number, number];
404
- readonly collabspreadsheetengine_dependents: (a: number, b: number, c: number, d: number) => [number, number, number, number];
405
- readonly collabspreadsheetengine_documentFacets: (a: number) => [number, number];
406
- readonly collabspreadsheetengine_encodeDiffV1: (a: number, b: number, c: number) => [number, number, number, number];
407
- readonly collabspreadsheetengine_encodeStateAsUpdate: (a: number) => [number, number];
408
- readonly collabspreadsheetengine_encodeStateVector: (a: number) => [number, number];
409
- readonly collabspreadsheetengine_exportCsv: (a: number, b: number) => [number, number];
410
- readonly collabspreadsheetengine_exportXlsx: (a: number) => [number, number, number, number];
411
- readonly collabspreadsheetengine_hostOwnedCells: (a: number, b: number) => [number, number, number, number];
412
- readonly collabspreadsheetengine_initGrid: (a: number, b: number, c: number, d: number) => void;
413
- readonly collabspreadsheetengine_loadDocument: (a: number, b: number, c: number) => [number, number, number, number];
414
- readonly collabspreadsheetengine_new: () => number;
415
- readonly collabspreadsheetengine_newWithClientId: (a: number) => number;
416
- readonly collabspreadsheetengine_precedents: (a: number, b: number, c: number, d: number) => [number, number, number, number];
417
- readonly collabspreadsheetengine_rangeStats: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
418
- readonly collabspreadsheetengine_readWindow: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
419
- readonly collabspreadsheetengine_redo: (a: number) => [number, number, number, number];
420
- readonly collabspreadsheetengine_redoDepth: (a: number) => number;
421
- readonly collabspreadsheetengine_registerCustomFunction: (a: number, b: number, c: number) => void;
422
- readonly collabspreadsheetengine_setWindowed: (a: number, b: number) => void;
423
- readonly collabspreadsheetengine_sheetExtents: (a: number) => [number, number, number, number];
424
- readonly collabspreadsheetengine_sheetIds: (a: number) => [number, number];
425
- readonly collabspreadsheetengine_sheets: (a: number) => [number, number];
426
- readonly collabspreadsheetengine_streamBegin: (a: number) => void;
427
- readonly collabspreadsheetengine_streamEnd: (a: number) => void;
428
- readonly collabspreadsheetengine_streamWrite: (a: number, b: number, c: number) => void;
429
- readonly collabspreadsheetengine_syncRemote: (a: number) => [number, number, number, number];
430
- readonly collabspreadsheetengine_undo: (a: number) => [number, number, number, number];
431
- readonly collabspreadsheetengine_undoDepth: (a: number) => number;
432
446
  readonly __wbindgen_free: (a: number, b: number, c: number) => void;
433
447
  readonly __wbindgen_exn_store: (a: number) => void;
434
448
  readonly __externref_table_alloc: () => number;
@@ -69,6 +69,26 @@ export class CollabSpreadsheetEngine {
69
69
  throw takeFromExternrefTable0(ret[0]);
70
70
  }
71
71
  }
72
+ /**
73
+ * The legacy Ctrl+Shift+Enter array covering a 1-based cell (see the single-user engine's
74
+ * `arrayFormulaAt`), or `null`.
75
+ * @param {number} sheet_id
76
+ * @param {number} row
77
+ * @param {number} col
78
+ * @returns {string}
79
+ */
80
+ arrayFormulaAt(sheet_id, row, col) {
81
+ let deferred1_0;
82
+ let deferred1_1;
83
+ try {
84
+ const ret = wasm.collabspreadsheetengine_arrayFormulaAt(this.__wbg_ptr, sheet_id, row, col);
85
+ deferred1_0 = ret[0];
86
+ deferred1_1 = ret[1];
87
+ return getStringFromWasm0(ret[0], ret[1]);
88
+ } finally {
89
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
90
+ }
91
+ }
72
92
  /**
73
93
  * @returns {boolean}
74
94
  */
@@ -592,6 +612,28 @@ export class SpreadsheetEngine {
592
612
  wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
593
613
  }
594
614
  }
615
+ /**
616
+ * The legacy Ctrl+Shift+Enter array covering a 1-based cell, as JSON
617
+ * `{"range":{startRowIndex,endRowIndex,startColumnIndex,endColumnIndex},"formula":"=…"}` —
618
+ * the formula every cell of the array shows (Excel's formula bar wraps it in `{…}`) and the
619
+ * range a re-entry replaces — or `null` outside every CSE array.
620
+ * @param {number} sheet_id
621
+ * @param {number} row
622
+ * @param {number} col
623
+ * @returns {string}
624
+ */
625
+ arrayFormulaAt(sheet_id, row, col) {
626
+ let deferred1_0;
627
+ let deferred1_1;
628
+ try {
629
+ const ret = wasm.spreadsheetengine_arrayFormulaAt(this.__wbg_ptr, sheet_id, row, col);
630
+ deferred1_0 = ret[0];
631
+ deferred1_1 = ret[1];
632
+ return getStringFromWasm0(ret[0], ret[1]);
633
+ } finally {
634
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
635
+ }
636
+ }
595
637
  /**
596
638
  * @returns {boolean}
597
639
  */
Binary file