@rowsncolumns/rnc-engine 0.11.9 → 1.1.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/dist/esm/command-handlers.d.ts +57 -13
- package/dist/esm/command-handlers.d.ts.map +1 -1
- package/dist/esm/command-handlers.js +153 -14
- package/dist/esm/fold.d.ts.map +1 -1
- package/dist/esm/fold.js +7 -1
- package/dist/esm/index.d.ts +37 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/use-engine-core.d.ts +38 -3
- package/dist/esm/use-engine-core.d.ts.map +1 -1
- package/dist/esm/use-engine-core.js +249 -40
- package/dist/esm/use-spreadsheet-engine.d.ts +10 -1
- package/dist/esm/use-spreadsheet-engine.d.ts.map +1 -1
- package/dist/esm/use-spreadsheet-engine.js +67 -1
- package/dist/esm/use-spreadsheet-ui.d.ts +2 -2
- package/dist/esm/use-spreadsheet-ui.d.ts.map +1 -1
- package/dist/esm/use-spreadsheet-ui.js +19 -0
- package/dist/esm/wasm/rnc_wasm_bg.wasm +0 -0
- package/package.json +12 -10
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Lightweight dependency-graph NODE classes (data holders — NOT the JS calculator) used to shape the
|
|
2
2
|
// real `getPrecedents`/`getDependents` results, which the engine now backs with actual graph data.
|
|
3
3
|
import { CellNode, CellRangeNode, isCellCoordinate, isCellNode, makeKey, } from "@rowsncolumns/dag";
|
|
4
|
-
import { DEFAULT_ACTIVE_CELL, DEFAULT_ARRAY, defaultSpreadsheetTheme, getBandedCells, getInitialActiveSheetId, ICON_SET_COLORS, ICON_SET_GLYPHS, useFonts, } from "@rowsncolumns/spreadsheet";
|
|
5
|
-
import { applyAdvancedFilterPlan, applySubtotalPlan, CellXfsRegistry, collectPaintMerges, generateDataFromClipboard, getBandedCellFormat, isStyleReference, layerBandedFormat, paintSourceForActiveCell, pickFormula, pickValue, planAdvancedFilter, planSubtotals, resolvePasteSpecialOptions, SharedStringRegistry, spreadMergeAnchorFormats,
|
|
6
|
-
import { getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, normalizeExternalUrl, resolveDefaultHorizontalAlignment, } from "@rowsncolumns/utils";
|
|
4
|
+
import { DEFAULT_ACTIVE_CELL, DEFAULT_ARRAY, defaultSpreadsheetTheme, getBandedCells, getInitialActiveSheetId, ICON_SET_COLORS, ICON_SET_GLYPHS, useFocusEmbedOrChart, useFonts, useSetTableAutoExpansion, useSetCalculatedColumnFill, } from "@rowsncolumns/spreadsheet";
|
|
5
|
+
import { applyAdvancedFilterPlan, applySubtotalPlan, CellXfsRegistry, collectPaintMerges, generateDataFromClipboard, getBandedCellFormat, isStyleReference, layerBandedFormat, paintSourceForActiveCell, pickFormula, pickValue, planAdvancedFilter, planSubtotals, resolvePasteSpecialOptions, SharedStringRegistry, spreadMergeAnchorFormats, tilePaintMerges, transpose, useOnCopy, previewTableStyle, useOnTraceDependentsPrecedents, useShowError, } from "@rowsncolumns/spreadsheet-state";
|
|
6
|
+
import { getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, getFormulaLocale, normalizeExternalUrl, resolveDefaultHorizontalAlignment, } from "@rowsncolumns/utils";
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
-
import { createCommandHandlers, } from "./command-handlers";
|
|
8
|
+
import { createCommandHandlers, rejectionDialog, } from "./command-handlers";
|
|
9
9
|
import { emptyRenderState, foldCommit, foldCommitStructuralOnly, foldWindow, foldWindows, frozenBandRanges, reconcileSheets, } from "./fold";
|
|
10
10
|
import { RncEngine, } from "./index";
|
|
11
11
|
import { cellKey, parseCellKey } from "./keys";
|
|
@@ -140,6 +140,7 @@ const EMPTY_FACETS = {
|
|
|
140
140
|
embeds: [],
|
|
141
141
|
slicers: [],
|
|
142
142
|
bandedRanges: [],
|
|
143
|
+
customTableStyles: [],
|
|
143
144
|
protectedRanges: [],
|
|
144
145
|
basicFilters: [],
|
|
145
146
|
citations: [],
|
|
@@ -252,9 +253,6 @@ function parseInternalLocation(location) {
|
|
|
252
253
|
},
|
|
253
254
|
};
|
|
254
255
|
}
|
|
255
|
-
/** The rnc engine's structural-edit refusals (`off-sheet-push`, `torn-merge`, `torn-table`,
|
|
256
|
-
* `torn-array`) — the codes the legacy engine's `constants.ts` defines, surfaced as a dialog. */
|
|
257
|
-
const isStructuralRejection = (rejection) => rejection.code in STRUCTURAL_REFUSAL_MESSAGES;
|
|
258
256
|
// `useEngineCore` is the engine-only surface (data / calc / commands / accessors) — NOT a standalone
|
|
259
257
|
// drop-in for `useSpreadsheetState`. The chrome (dialogs + host intents) lives in `useSpreadsheetUI`;
|
|
260
258
|
// the drop-in parity lock is asserted on their composition, `useSpreadsheetEngine`.
|
|
@@ -284,6 +282,7 @@ export function useEngineCore(options) {
|
|
|
284
282
|
const onCustomEvalCancelRef = useRef(options.onCustomEvalCancel);
|
|
285
283
|
onCustomEvalCancelRef.current = options.onCustomEvalCancel;
|
|
286
284
|
const showError = useShowError();
|
|
285
|
+
const focusEmbedOrChart = useFocusEmbedOrChart();
|
|
287
286
|
// Latest-refs for the streaming batch-write observers (identities may change every render).
|
|
288
287
|
const onStreamStartRef = useRef(options.onStreamStart);
|
|
289
288
|
onStreamStartRef.current = options.onStreamStart;
|
|
@@ -313,6 +312,16 @@ export function useEngineCore(options) {
|
|
|
313
312
|
stateSheetsRef.current = state.sheets;
|
|
314
313
|
const customAutofillListsRef = useRef(options.customAutofillLists);
|
|
315
314
|
customAutofillListsRef.current = options.customAutofillLists;
|
|
315
|
+
const autoExpandTablesRef = useRef(options.autoExpandTables);
|
|
316
|
+
autoExpandTablesRef.current = options.autoExpandTables;
|
|
317
|
+
const autoCalculatedColumnsRef = useRef(options.autoCalculatedColumns);
|
|
318
|
+
autoCalculatedColumnsRef.current = options.autoCalculatedColumns;
|
|
319
|
+
// Excel's AutoCorrect smart tag after a table auto-expanded — armed from the commit's
|
|
320
|
+
// `tableAutoExpansion` record when a value-writing command grew a table; `CanvasGrid` renders it.
|
|
321
|
+
const setTableAutoExpansion = useSetTableAutoExpansion();
|
|
322
|
+
// Excel's AutoCorrect smart tag after a formula typed into a table column — armed from the
|
|
323
|
+
// commit's `calculatedColumnFill` record; `CanvasGrid` renders it.
|
|
324
|
+
const setCalculatedColumnFill = useSetCalculatedColumnFill();
|
|
316
325
|
// One windowed read = the buffered window + the sheet's frozen bands + the sheet's active cell
|
|
317
326
|
// (pinned so the formula bar's active-cell read survives scrolling it out of the window — see
|
|
318
327
|
// `readWindowWithFrozenBands`). Frozen counts come from the host's getter when provided (see
|
|
@@ -412,6 +421,7 @@ export function useEngineCore(options) {
|
|
|
412
421
|
// The initial sheets/document are creation-time inputs; capture them so the effect runs once and
|
|
413
422
|
// doesn't tear the engine down on every render.
|
|
414
423
|
const initial = useRef(options);
|
|
424
|
+
const formulaLocale = useMemo(() => getFormulaLocale(options.formulaLocale ?? options.locale), [options.formulaLocale, options.locale]);
|
|
415
425
|
useEffect(() => {
|
|
416
426
|
let alive = true;
|
|
417
427
|
let unsubscribe = () => { };
|
|
@@ -652,6 +662,12 @@ export function useEngineCore(options) {
|
|
|
652
662
|
const preview = streamPreviewRef.current;
|
|
653
663
|
if (!preview?.size)
|
|
654
664
|
return;
|
|
665
|
+
// A batch retires what each of its members covers.
|
|
666
|
+
if (command.command === "batch") {
|
|
667
|
+
for (const member of command.commands)
|
|
668
|
+
retireStreamPreviewForCommand(member);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
655
671
|
const cmd = command;
|
|
656
672
|
if (cmd.command === "change-theme")
|
|
657
673
|
return;
|
|
@@ -686,10 +702,16 @@ export function useEngineCore(options) {
|
|
|
686
702
|
sheetDataRef.current = state.sheetData;
|
|
687
703
|
const committedFvAt = useCallback((sheetId, rowIndex, columnIndex) => sheetDataRef.current?.[sheetId]?.[rowIndex]?.values?.[columnIndex]?.fv ??
|
|
688
704
|
undefined, []);
|
|
705
|
+
// Commands issued while a `batchHistory` action runs, applied together when it returns.
|
|
706
|
+
const batchCommandsRef = useRef(null);
|
|
689
707
|
const onCommand = useCallback((command) => {
|
|
690
708
|
const engine = engineRef.current;
|
|
691
709
|
if (!engine)
|
|
692
710
|
return;
|
|
711
|
+
if (batchCommandsRef.current) {
|
|
712
|
+
batchCommandsRef.current.push(command);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
693
715
|
// The host's apply (or any other local write) is the authority — retire the preview for the cells
|
|
694
716
|
// it covers in this same render, so the committed value replaces the previewed one with no blank
|
|
695
717
|
// frame in between.
|
|
@@ -704,12 +726,22 @@ export function useEngineCore(options) {
|
|
|
704
726
|
// Snapshot the view BEFORE applying — that's where undo should return the user.
|
|
705
727
|
const before = viewRef.current;
|
|
706
728
|
const commit = engine.applyCommand(command);
|
|
707
|
-
//
|
|
708
|
-
// the
|
|
709
|
-
//
|
|
729
|
+
// A value-writing command grew a table over the written cells: arm the AutoCorrect smart tag
|
|
730
|
+
// with the engine's record of the growth (rowsncolumns/spreadsheet#668 T-12) — "Undo Table
|
|
731
|
+
// AutoExpansion" resizes back and clears the cells the growth wrote (`writes`, T-89).
|
|
732
|
+
if (commit.tableAutoExpansion) {
|
|
733
|
+
setTableAutoExpansion(commit.tableAutoExpansion);
|
|
734
|
+
}
|
|
735
|
+
if (commit.calculatedColumnFill) {
|
|
736
|
+
setCalculatedColumnFill(commit.calculatedColumnFill);
|
|
737
|
+
}
|
|
738
|
+
// A refusal wrote nothing — show the user Excel's dialog text through the same channel the
|
|
739
|
+
// legacy engine's hooks use (`useShowError`), once: `createCommandHandlers` skips the
|
|
740
|
+
// refusals that have a dialog here. The rest keep riding `commit.rejections` to it.
|
|
710
741
|
for (const rejection of commit.rejections ?? []) {
|
|
711
|
-
|
|
712
|
-
|
|
742
|
+
const dialog = rejectionDialog(rejection);
|
|
743
|
+
if (dialog) {
|
|
744
|
+
showError(dialog);
|
|
713
745
|
}
|
|
714
746
|
}
|
|
715
747
|
// Only record a view snapshot if the command actually pushed a document undo entry (skips
|
|
@@ -719,6 +751,12 @@ export function useEngineCore(options) {
|
|
|
719
751
|
viewRedo.current = [];
|
|
720
752
|
}
|
|
721
753
|
lastUndoDepth.current = engine.undoDepth();
|
|
754
|
+
// Excel selects a freshly inserted chart; a batch ends up on the last one it inserted.
|
|
755
|
+
for (const member of command.command === "batch" ? command.commands : [command]) {
|
|
756
|
+
if (member.command === "create-chart") {
|
|
757
|
+
focusEmbedOrChart(member.spec.chartId);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
722
760
|
// The engine's move-range relocates cells but never re-scopes range-scoped CF/DV rules —
|
|
723
761
|
// carry the touched rules to the destination right after the move (Excel/Sheets behavior).
|
|
724
762
|
if (command.command === "move-range") {
|
|
@@ -728,7 +766,25 @@ export function useEngineCore(options) {
|
|
|
728
766
|
}
|
|
729
767
|
// Handed back so the command handlers can surface an engine refusal (`commit.rejections`).
|
|
730
768
|
return commit;
|
|
731
|
-
}, [retireStreamPreviewForCommand, showError]);
|
|
769
|
+
}, [retireStreamPreviewForCommand, showError, focusEmbedOrChart, setTableAutoExpansion, setCalculatedColumnFill]);
|
|
770
|
+
const batchHistory = useCallback((action) => {
|
|
771
|
+
if (batchCommandsRef.current) {
|
|
772
|
+
return action();
|
|
773
|
+
}
|
|
774
|
+
const commands = [];
|
|
775
|
+
batchCommandsRef.current = commands;
|
|
776
|
+
let result;
|
|
777
|
+
try {
|
|
778
|
+
result = action();
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
batchCommandsRef.current = null;
|
|
782
|
+
}
|
|
783
|
+
if (commands.length) {
|
|
784
|
+
onCommand({ command: "batch", commands });
|
|
785
|
+
}
|
|
786
|
+
return result;
|
|
787
|
+
}, [onCommand]);
|
|
732
788
|
// "Calculate Now" (F9): force a full recalculation so volatile functions (NOW/TODAY/RAND/…) refresh.
|
|
733
789
|
// The engine re-evaluates + streams the changed cells through the normal commit (folded like any edit);
|
|
734
790
|
// it mutates no document state, so it isn't undoable. The reference's `options` (reset-graph /
|
|
@@ -797,6 +853,19 @@ export function useEngineCore(options) {
|
|
|
797
853
|
customFnsRef.current.set(name, impl);
|
|
798
854
|
engineRef.current?.registerCustomFunction(name);
|
|
799
855
|
}, []);
|
|
856
|
+
// Chart selection after an undo/redo step (Excel: the chart an undo brings back, or a redo
|
|
857
|
+
// re-creates / re-positions, ends up selected; one whose step removed it is deselected). The
|
|
858
|
+
// engine's chart deltas are the signal — same as the legacy history's `selectedEmbedOrChart`.
|
|
859
|
+
const restoreChartSelection = useCallback((commit) => {
|
|
860
|
+
for (const change of commit.chartChanges) {
|
|
861
|
+
if (change.chart) {
|
|
862
|
+
focusEmbedOrChart(change.chartId);
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
focusEmbedOrChart((prev) => prev === change.chartId ? undefined : prev);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}, [focusEmbedOrChart]);
|
|
800
869
|
// Undo AND redo of an edit both return the cursor to the EDIT location — mirroring the reference's
|
|
801
870
|
// `{ activeCell: { undo: coords, redo: coords } }`. So when an entry crosses between the undo/redo
|
|
802
871
|
// stacks we move the SAME stored view, NOT the live cursor (pushing the live cursor would make redo
|
|
@@ -806,7 +875,7 @@ export function useEngineCore(options) {
|
|
|
806
875
|
if (!engine)
|
|
807
876
|
return;
|
|
808
877
|
const prevDepth = engine.undoDepth();
|
|
809
|
-
engine.undo();
|
|
878
|
+
const commit = engine.undo();
|
|
810
879
|
if (engine.undoDepth() < prevDepth) {
|
|
811
880
|
const restore = viewUndo.current.pop();
|
|
812
881
|
if (restore) {
|
|
@@ -815,13 +884,14 @@ export function useEngineCore(options) {
|
|
|
815
884
|
}
|
|
816
885
|
}
|
|
817
886
|
lastUndoDepth.current = engine.undoDepth();
|
|
818
|
-
|
|
887
|
+
restoreChartSelection(commit);
|
|
888
|
+
}, [applyView, restoreChartSelection]);
|
|
819
889
|
const redo = useCallback(() => {
|
|
820
890
|
const engine = engineRef.current;
|
|
821
891
|
if (!engine)
|
|
822
892
|
return;
|
|
823
893
|
const prevRedoDepth = engine.redoDepth();
|
|
824
|
-
engine.redo();
|
|
894
|
+
const commit = engine.redo();
|
|
825
895
|
if (engine.redoDepth() < prevRedoDepth) {
|
|
826
896
|
const restore = viewRedo.current.pop();
|
|
827
897
|
if (restore) {
|
|
@@ -830,7 +900,8 @@ export function useEngineCore(options) {
|
|
|
830
900
|
}
|
|
831
901
|
}
|
|
832
902
|
lastUndoDepth.current = engine.undoDepth();
|
|
833
|
-
|
|
903
|
+
restoreChartSelection(commit);
|
|
904
|
+
}, [applyView, restoreChartSelection]);
|
|
834
905
|
const load = useCallback((document) => {
|
|
835
906
|
engineRef.current?.load(document);
|
|
836
907
|
}, []);
|
|
@@ -859,6 +930,9 @@ export function useEngineCore(options) {
|
|
|
859
930
|
hidden: es.hidden,
|
|
860
931
|
tabColor: es.tabColor,
|
|
861
932
|
defaultRowHeight: es.defaultRowHeight,
|
|
933
|
+
defaultColumnWidth: es.defaultColumnWidth,
|
|
934
|
+
type: es.type,
|
|
935
|
+
chartId: es.chartId,
|
|
862
936
|
}));
|
|
863
937
|
const firstId = engineSheets[0]?.sheetId ?? viewRef.current.activeSheetId;
|
|
864
938
|
windowRef.current = windowedRef.current
|
|
@@ -1757,6 +1831,8 @@ export function useEngineCore(options) {
|
|
|
1757
1831
|
// handler→command mapping; nothing here touches the document directly.
|
|
1758
1832
|
const handlers = useMemo(() => createCommandHandlers(onCommand, {
|
|
1759
1833
|
getCustomAutofillLists: () => customAutofillListsRef.current,
|
|
1834
|
+
getAutoExpandTables: () => autoExpandTablesRef.current,
|
|
1835
|
+
getAutoCalculatedColumns: () => autoCalculatedColumnsRef.current,
|
|
1760
1836
|
}), [onCommand]);
|
|
1761
1837
|
// Data tools composed over the streamed commands. The planners need the WHOLE list range (a
|
|
1762
1838
|
// windowed host holds only the viewport), so they read it straight from the engine in one
|
|
@@ -1776,15 +1852,18 @@ export function useEngineCore(options) {
|
|
|
1776
1852
|
});
|
|
1777
1853
|
if (!plan)
|
|
1778
1854
|
return undefined;
|
|
1779
|
-
|
|
1855
|
+
// Excel undoes a Subtotal in ONE step: the plan's row deletes / inserts, total-row writes and
|
|
1856
|
+
// outline groups apply as one `batch` — one commit to fold, one undo entry on either history
|
|
1857
|
+
// backend, one view snapshot — instead of ten Ctrl+Z presses.
|
|
1858
|
+
batchHistory(() => applySubtotalPlan(sheetId, plan, {
|
|
1780
1859
|
onDeleteRow: handlers.onDeleteRow,
|
|
1781
1860
|
onInsertRow: handlers.onInsertRow,
|
|
1782
1861
|
onChangeBatch: (s, range, values) => handlers.onChangeBatch(s, range, values),
|
|
1783
1862
|
onGroupRows: handlers.onGroupRows,
|
|
1784
1863
|
onUngroupRows: handlers.onUngroupRows,
|
|
1785
|
-
});
|
|
1864
|
+
}));
|
|
1786
1865
|
return plan;
|
|
1787
|
-
}, [handlers, readRect, dimMeta]);
|
|
1866
|
+
}, [batchHistory, handlers, readRect, dimMeta]);
|
|
1788
1867
|
const onAdvancedFilter = useCallback((sheetId, options) => {
|
|
1789
1868
|
const list = readRect(sheetId, options.listRange, "raw");
|
|
1790
1869
|
const criteria = readRect(sheetId, options.criteriaRange, "raw");
|
|
@@ -1812,15 +1891,46 @@ export function useEngineCore(options) {
|
|
|
1812
1891
|
// (the live active sheet) instead of closing over `activeSheetId`, so identity never changes. The
|
|
1813
1892
|
// grid omits sheetId from chart move/resize (charts live on the active sheet) and passes just the rule
|
|
1814
1893
|
// to create-conditional-format.
|
|
1815
|
-
const onMoveChart = useCallback((chartId, anchorCell, offsetXPixels, offsetYPixels) => onCommand({
|
|
1894
|
+
const onMoveChart = useCallback((chartId, anchorCell, offsetXPixels, offsetYPixels, toAnchor) => onCommand({
|
|
1816
1895
|
command: "move-chart",
|
|
1817
1896
|
chartId,
|
|
1818
1897
|
sheetId: viewRef.current.activeSheetId,
|
|
1819
1898
|
anchorCell,
|
|
1820
1899
|
offsetXPixels,
|
|
1821
1900
|
offsetYPixels,
|
|
1901
|
+
...toAnchor,
|
|
1822
1902
|
}), [onCommand]);
|
|
1823
|
-
|
|
1903
|
+
// Excel's "Move Chart…" (#668 C-17): the engine relocates the chart (minting / deleting the
|
|
1904
|
+
// chartsheet); the view follows the chart to its destination sheet and keeps it selected.
|
|
1905
|
+
const onMoveChartToSheet = useCallback((chart, target) => {
|
|
1906
|
+
const engine = engineRef.current;
|
|
1907
|
+
if (!engine)
|
|
1908
|
+
return undefined;
|
|
1909
|
+
const sourceSheetId = chart.position.sheetId;
|
|
1910
|
+
if (target.kind === "sheet" && target.sheetId === sourceSheetId) {
|
|
1911
|
+
return undefined;
|
|
1912
|
+
}
|
|
1913
|
+
onCommand({
|
|
1914
|
+
command: "move-chart-to-sheet",
|
|
1915
|
+
chartId: chart.chartId,
|
|
1916
|
+
...(target.kind === "sheet"
|
|
1917
|
+
? { sheetId: target.sheetId }
|
|
1918
|
+
: { title: target.title }),
|
|
1919
|
+
});
|
|
1920
|
+
const destination = target.kind === "sheet"
|
|
1921
|
+
? target.sheetId
|
|
1922
|
+
: engine
|
|
1923
|
+
.sheets?.()
|
|
1924
|
+
.find((sheet) => sheet.type === "chartsheet" &&
|
|
1925
|
+
String(sheet.chartId) === String(chart.chartId))?.sheetId;
|
|
1926
|
+
if (destination === undefined || destination === sourceSheetId) {
|
|
1927
|
+
return undefined;
|
|
1928
|
+
}
|
|
1929
|
+
onChangeActiveSheet(destination);
|
|
1930
|
+
focusEmbedOrChart(chart.chartId);
|
|
1931
|
+
return destination;
|
|
1932
|
+
}, [onCommand, onChangeActiveSheet, focusEmbedOrChart]);
|
|
1933
|
+
const onResizeChart = useCallback((chartId, anchorCell, offsetXPixels, offsetYPixels, width, height, toAnchor) => onCommand({
|
|
1824
1934
|
command: "resize-chart",
|
|
1825
1935
|
chartId,
|
|
1826
1936
|
sheetId: viewRef.current.activeSheetId,
|
|
@@ -1829,6 +1939,7 @@ export function useEngineCore(options) {
|
|
|
1829
1939
|
offsetYPixels,
|
|
1830
1940
|
width,
|
|
1831
1941
|
height,
|
|
1942
|
+
...toAnchor,
|
|
1832
1943
|
}), [onCommand]);
|
|
1833
1944
|
const onCreateConditionalFormattingRule = useCallback((rule) => onCommand({
|
|
1834
1945
|
command: "create-conditional-format",
|
|
@@ -1854,6 +1965,9 @@ export function useEngineCore(options) {
|
|
|
1854
1965
|
.filter((t) => t.sheetId === sheetId && t.bandedRange != null)
|
|
1855
1966
|
.map((t) => ({
|
|
1856
1967
|
bandedRangeId: `${t.id}-${t.theme}`,
|
|
1968
|
+
showFirstColumn: t.showFirstColumn,
|
|
1969
|
+
showLastColumn: t.showLastColumn,
|
|
1970
|
+
totalRow: t.totalRow ?? false,
|
|
1857
1971
|
range: { ...t.range, sheetId },
|
|
1858
1972
|
...t.bandedRange,
|
|
1859
1973
|
})),
|
|
@@ -1903,6 +2017,44 @@ export function useEngineCore(options) {
|
|
|
1903
2017
|
}
|
|
1904
2018
|
}
|
|
1905
2019
|
}, [onCommand]);
|
|
2020
|
+
// Convert to Range keeps the table's look (Excel bakes the table style into the cell formats):
|
|
2021
|
+
// the banded formats the grid painted under the table become static cell formats — the same
|
|
2022
|
+
// capture the format painter makes (#620) — replayed over the table's range in one batch with
|
|
2023
|
+
// the removal, so one undo restores the table and the unbaked cells together.
|
|
2024
|
+
const onRemoveTable = useCallback((tableView) => {
|
|
2025
|
+
const { sheetId, range } = tableView;
|
|
2026
|
+
const banded = paintBandedCells(sheetId);
|
|
2027
|
+
batchHistory(() => {
|
|
2028
|
+
if (banded?.size) {
|
|
2029
|
+
let baked = false;
|
|
2030
|
+
const grid = [];
|
|
2031
|
+
for (let r = range.startRowIndex; r <= range.endRowIndex; r++) {
|
|
2032
|
+
const row = [];
|
|
2033
|
+
for (let c = range.startColumnIndex; c <= range.endColumnIndex; c++) {
|
|
2034
|
+
// The hook's own formats are consolidated `CellFormat`s (never a style reference).
|
|
2035
|
+
const own = getUserEnteredFormat(sheetId, r, c);
|
|
2036
|
+
const band = getBandedCellFormat(banded, r, c);
|
|
2037
|
+
if (band)
|
|
2038
|
+
baked = true;
|
|
2039
|
+
// `replace` re-writes every cell of the range: a cell without a band keeps its own format.
|
|
2040
|
+
row.push((band ? layerBandedFormat(own, band) : own) ?? null);
|
|
2041
|
+
}
|
|
2042
|
+
grid.push(row);
|
|
2043
|
+
}
|
|
2044
|
+
if (baked) {
|
|
2045
|
+
onCommand({
|
|
2046
|
+
command: "change-formatting",
|
|
2047
|
+
sheetId,
|
|
2048
|
+
activeCell: { rowIndex: range.startRowIndex, columnIndex: range.startColumnIndex },
|
|
2049
|
+
selections: [{ range }],
|
|
2050
|
+
cellFormat: grid,
|
|
2051
|
+
options: { replace: true },
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
handlers.onRemoveTable(tableView);
|
|
2056
|
+
});
|
|
2057
|
+
}, [paintBandedCells, getUserEnteredFormat, batchHistory, onCommand, handlers]);
|
|
1906
2058
|
const onSavePaintFormat = useCallback((sheetId, activeCell, selections) => {
|
|
1907
2059
|
// A plain click on a merged cell arms the painter with the anchor and NO selection (the
|
|
1908
2060
|
// grid clears the list) — Excel treats that click as the whole merged area.
|
|
@@ -2053,11 +2205,22 @@ export function useEngineCore(options) {
|
|
|
2053
2205
|
// style is materialized on its facet entry as `bandedRange` (never as per-cell format), so derive
|
|
2054
2206
|
// those here exactly like `useSpreadsheetState` — this single array is what the grid paints and the
|
|
2055
2207
|
// copy path serializes table styling from.
|
|
2208
|
+
// The gallery's hovered style paints over the table's own while the pointer rests on the
|
|
2209
|
+
// swatch — UI state only, never a command.
|
|
2210
|
+
const [tableStylePreview, setTableStylePreview] = useState(null);
|
|
2211
|
+
const onPreviewTableStyle = useCallback((preview) => setTableStylePreview(preview), []);
|
|
2056
2212
|
const bandedRanges = useMemo(() => {
|
|
2057
2213
|
const tableBands = (tables ?? [])
|
|
2058
|
-
.filter((t) => t.sheetId === activeSheetId
|
|
2214
|
+
.filter((t) => t.sheetId === activeSheetId)
|
|
2215
|
+
.map((t) => tableStylePreview?.sheetId === t.sheetId && tableStylePreview.tableId === t.id
|
|
2216
|
+
? previewTableStyle(t, tableStylePreview)
|
|
2217
|
+
: t)
|
|
2218
|
+
.filter((t) => t.bandedRange != null)
|
|
2059
2219
|
.map((t) => ({
|
|
2060
2220
|
bandedRangeId: `${t.id}-${t.theme}`,
|
|
2221
|
+
showFirstColumn: t.showFirstColumn,
|
|
2222
|
+
showLastColumn: t.showLastColumn,
|
|
2223
|
+
totalRow: t.totalRow ?? false,
|
|
2061
2224
|
range: { ...t.range, sheetId: activeSheetId },
|
|
2062
2225
|
...t.bandedRange,
|
|
2063
2226
|
}));
|
|
@@ -2065,7 +2228,7 @@ export function useEngineCore(options) {
|
|
|
2065
2228
|
...facets.bandedRanges.filter((b) => b.range?.sheetId === activeSheetId),
|
|
2066
2229
|
...tableBands,
|
|
2067
2230
|
];
|
|
2068
|
-
}, [facets, tables, activeSheetId]);
|
|
2231
|
+
}, [facets, tables, activeSheetId, tableStylePreview]);
|
|
2069
2232
|
const basicFilter = useMemo(() => facets.basicFilters.find((f) => f.sheetId === activeSheetId)?.filter, [facets, activeSheetId]);
|
|
2070
2233
|
// Body rows the engine hid for the active sheet (basic filter OR a table-column filter), as compact
|
|
2071
2234
|
// [start,end] 1-based inclusive ranges. CanvasGrid derives `visibleRowRanges` from these — the
|
|
@@ -2141,7 +2304,11 @@ export function useEngineCore(options) {
|
|
|
2141
2304
|
// the engine applies it via `set_style` (streams on `styleChanges`). Only a still-unresolved
|
|
2142
2305
|
// `StyleReference` (no matching cellXfs entry) is skipped, since a bare ref can't cross to the engine.
|
|
2143
2306
|
const onPaste = useCallback(async (e, sourceSheetId, destinationSheetId, copiedSelections, pasteActiveCell, pasteSelections, _finalSelections, isCutOperation) => {
|
|
2144
|
-
|
|
2307
|
+
// A cut is a MOVE: Excel keeps the moved formulas' text verbatim (`=B1*2` cut from A1 to A5
|
|
2308
|
+
// still reads `=B1*2`) and only repoints references INTO the moved block — the engine does
|
|
2309
|
+
// that remap (`repoint_formulas_into_moved_block`), so the clipboard parse must not
|
|
2310
|
+
// relativize cut formulas the way it does copied ones.
|
|
2311
|
+
const parsed = await generateDataFromClipboard(e, pasteActiveCell, destinationSheetId, getSheetName(destinationSheetId), true, initial.current.locale, undefined, undefined, undefined, { relocateFormulas: !isCutOperation });
|
|
2145
2312
|
let results = parsed?.results ?? [];
|
|
2146
2313
|
if (!results.length)
|
|
2147
2314
|
return;
|
|
@@ -2170,19 +2337,39 @@ export function useEngineCore(options) {
|
|
|
2170
2337
|
// A paste selection larger than the clipboard tiles it (copy G1, select G2:G5, paste fills all
|
|
2171
2338
|
// four), wrapping like `useOnPaste`. Tiling UI-side keeps the engine's `paste` a plain rectangle
|
|
2172
2339
|
// and the whole paste ONE undo entry; the tiles' formulas are shifted here (see `paste-tiling`).
|
|
2173
|
-
|
|
2340
|
+
// A cut block is placed exactly once — Excel never tiles a moved range over a larger
|
|
2341
|
+
// selection (its source is cleared once).
|
|
2342
|
+
if (to && !isCutOperation) {
|
|
2174
2343
|
results = tileClipboardToSelection(results, to, pasteActiveCell, destinationSheetId, getSheetName(destinationSheetId), initial.current.locale);
|
|
2175
2344
|
}
|
|
2176
2345
|
const tiles = applyPasteSpecialToTiles(results, target, destinationSheetId, pasteSpecial, { getCellData, getUserEnteredFormat }, initial.current.locale);
|
|
2177
2346
|
// The copied range top-left, kept for the cut source-clear below.
|
|
2178
2347
|
const from = copiedSelections?.[0]?.range;
|
|
2179
|
-
|
|
2180
|
-
//
|
|
2181
|
-
//
|
|
2182
|
-
//
|
|
2183
|
-
//
|
|
2184
|
-
//
|
|
2185
|
-
|
|
2348
|
+
const cutMove = isCutOperation && from && sourceSheetId != null;
|
|
2349
|
+
// COPY: `generateDataFromClipboard` has ALREADY re-anchored each relative formula ref to its
|
|
2350
|
+
// destination (its `moveFormula(sourceCell → destinationCell)` pass overwrites the cell's `fv`), so
|
|
2351
|
+
// the formula text in `cells` is destination-shifted on arrival. The engine's paste then shifts
|
|
2352
|
+
// relative refs by (target − source); passing the real copy origin as `source` would apply that
|
|
2353
|
+
// shift a SECOND time and DOUBLE the offset (`=A1` from B1 pasted to B2 → `=A3` instead of `=A2`).
|
|
2354
|
+
// Pin `source` to `target` so the engine's shift is a zero-delta no-op.
|
|
2355
|
+
// CUT: the formulas arrive verbatim (see `relocateFormulas` above) and the engine needs the REAL
|
|
2356
|
+
// cut origin — (target − source) is the delta it uses to repoint every formula that reads the
|
|
2357
|
+
// moved cells (`C1 =A1` → `=E1` after cut A1 → paste E1; `=SUM(A1:A3)` → `=SUM(D1:D3)`, other
|
|
2358
|
+
// sheets included). Pinning it to `target` zeroed that delta, so cut + paste never repointed.
|
|
2359
|
+
const source = cutMove
|
|
2360
|
+
? { rowIndex: from.startRowIndex, columnIndex: from.startColumnIndex }
|
|
2361
|
+
: target;
|
|
2362
|
+
// Notes ride with the cells they annotate (Excel carries a note on copy and moves it on cut).
|
|
2363
|
+
// The clipboard rectangle is row-major from the copied range's top-left, so the note of the
|
|
2364
|
+
// tile's source cell is looked up by offset; tiled repeats (copy only) reuse the modular offset.
|
|
2365
|
+
const noteAt = (i, j) => {
|
|
2366
|
+
// A transposed paste no longer maps tiles back to source cells by offset.
|
|
2367
|
+
if (!from || sourceSheetId == null || pasteSpecial.transpose)
|
|
2368
|
+
return undefined;
|
|
2369
|
+
const height = from.endRowIndex - from.startRowIndex + 1;
|
|
2370
|
+
const width = from.endColumnIndex - from.startColumnIndex + 1;
|
|
2371
|
+
return getNote(sourceSheetId, from.startRowIndex + (i % height), from.startColumnIndex + (j % width));
|
|
2372
|
+
};
|
|
2186
2373
|
const toPasteCell = (cell) => {
|
|
2187
2374
|
if (!cell)
|
|
2188
2375
|
return null;
|
|
@@ -2216,12 +2403,16 @@ export function useEngineCore(options) {
|
|
|
2216
2403
|
: hl.location;
|
|
2217
2404
|
return { value, format, hyperlink };
|
|
2218
2405
|
};
|
|
2219
|
-
const cells = tiles.map((row) => row.map((cell) =>
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2406
|
+
const cells = tiles.map((row, i) => row.map((cell, j) => {
|
|
2407
|
+
// Skip blanks: the engine leaves a `skip` cell untouched (the snapshot is only read by
|
|
2408
|
+
// an engine build predating `skip`, which writes it back instead of clearing).
|
|
2409
|
+
if (isSkippedPasteCell(cell)) {
|
|
2410
|
+
return { skip: true, value: null, ...(toPasteCell(cell.fallback) ?? {}) };
|
|
2411
|
+
}
|
|
2412
|
+
const pasteCell = toPasteCell(cell);
|
|
2413
|
+
const note = noteAt(i, j);
|
|
2414
|
+
return pasteCell && note != null ? { ...pasteCell, note } : pasteCell;
|
|
2415
|
+
}));
|
|
2225
2416
|
onCommand({
|
|
2226
2417
|
command: "paste",
|
|
2227
2418
|
sheetId: destinationSheetId,
|
|
@@ -2232,21 +2423,28 @@ export function useEngineCore(options) {
|
|
|
2232
2423
|
? { sheetId: sourceSheetId, range: from }
|
|
2233
2424
|
: null,
|
|
2234
2425
|
formatOnly,
|
|
2426
|
+
...(autoExpandTablesRef.current === false ? { autoExpandTables: false } : {}),
|
|
2427
|
+
...(autoCalculatedColumnsRef.current === false
|
|
2428
|
+
? { autoCalculatedColumns: false }
|
|
2429
|
+
: {}),
|
|
2235
2430
|
});
|
|
2236
|
-
}, [getSheetName, getCellData, getUserEnteredFormat, onCommand]);
|
|
2431
|
+
}, [getSheetName, getCellData, getUserEnteredFormat, getNote, onCommand]);
|
|
2237
2432
|
return useMemo(() => ({
|
|
2238
2433
|
...state,
|
|
2239
2434
|
...handlers,
|
|
2435
|
+
onRemoveTable,
|
|
2240
2436
|
ready,
|
|
2241
2437
|
engine: engineRef.current,
|
|
2242
2438
|
canUndo: hist.canUndo,
|
|
2243
2439
|
canRedo: hist.canRedo,
|
|
2440
|
+
formulaLocale,
|
|
2244
2441
|
onCommand,
|
|
2245
2442
|
undo,
|
|
2246
2443
|
redo,
|
|
2247
2444
|
onUndo: undo,
|
|
2248
2445
|
onRedo: redo,
|
|
2249
2446
|
onMoveChart,
|
|
2447
|
+
onMoveChartToSheet,
|
|
2250
2448
|
onResizeChart,
|
|
2251
2449
|
onCreateConditionalFormattingRule,
|
|
2252
2450
|
// Repeat last formatting (the action itself is recorded in `onCommand`).
|
|
@@ -2321,8 +2519,11 @@ export function useEngineCore(options) {
|
|
|
2321
2519
|
// Undefined when the file didn't specify — CanvasGrid falls back to its own default.
|
|
2322
2520
|
defaultRowHeight: state.sheets.find((s) => s.sheetId === activeSheetId)
|
|
2323
2521
|
?.defaultRowHeight ?? undefined,
|
|
2522
|
+
defaultColumnWidth: state.sheets.find((s) => s.sheetId === activeSheetId)
|
|
2523
|
+
?.defaultColumnWidth ?? undefined,
|
|
2324
2524
|
isDarkMode: false,
|
|
2325
2525
|
bandedRanges,
|
|
2526
|
+
onPreviewTableStyle,
|
|
2326
2527
|
basicFilter,
|
|
2327
2528
|
hostHiddenRowRanges,
|
|
2328
2529
|
arrows,
|
|
@@ -2330,6 +2531,7 @@ export function useEngineCore(options) {
|
|
|
2330
2531
|
tables,
|
|
2331
2532
|
namedRanges,
|
|
2332
2533
|
protectedRanges,
|
|
2534
|
+
customTableStyles: facets.customTableStyles,
|
|
2333
2535
|
embeds,
|
|
2334
2536
|
slicers,
|
|
2335
2537
|
getDataValidation,
|
|
@@ -2366,6 +2568,7 @@ export function useEngineCore(options) {
|
|
|
2366
2568
|
applyPatch,
|
|
2367
2569
|
generateStatePatches,
|
|
2368
2570
|
createHistory,
|
|
2571
|
+
batchHistory,
|
|
2369
2572
|
onClearHistory,
|
|
2370
2573
|
getFormattingFromRange,
|
|
2371
2574
|
getFormulasFromRange,
|
|
@@ -2382,14 +2585,18 @@ export function useEngineCore(options) {
|
|
|
2382
2585
|
onCopy,
|
|
2383
2586
|
onPaste,
|
|
2384
2587
|
}), [
|
|
2588
|
+
formulaLocale,
|
|
2385
2589
|
state,
|
|
2386
2590
|
handlers,
|
|
2591
|
+
onRemoveTable,
|
|
2387
2592
|
ready,
|
|
2388
2593
|
hist,
|
|
2389
2594
|
onCommand,
|
|
2390
2595
|
undo,
|
|
2391
2596
|
redo,
|
|
2597
|
+
batchHistory,
|
|
2392
2598
|
onMoveChart,
|
|
2599
|
+
onMoveChartToSheet,
|
|
2393
2600
|
onResizeChart,
|
|
2394
2601
|
onCreateConditionalFormattingRule,
|
|
2395
2602
|
onRepeatFormatting,
|
|
@@ -2452,6 +2659,7 @@ export function useEngineCore(options) {
|
|
|
2452
2659
|
onSelectNextSheet,
|
|
2453
2660
|
onSelectPreviousSheet,
|
|
2454
2661
|
bandedRanges,
|
|
2662
|
+
onPreviewTableStyle,
|
|
2455
2663
|
basicFilter,
|
|
2456
2664
|
hostHiddenRowRanges,
|
|
2457
2665
|
arrows,
|
|
@@ -2459,6 +2667,7 @@ export function useEngineCore(options) {
|
|
|
2459
2667
|
tables,
|
|
2460
2668
|
namedRanges,
|
|
2461
2669
|
protectedRanges,
|
|
2670
|
+
facets.customTableStyles,
|
|
2462
2671
|
embeds,
|
|
2463
2672
|
slicers,
|
|
2464
2673
|
getDataValidation,
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useSpreadsheetEngine` — the batteries-included **Option B** hook: the engine-backed data/calc core
|
|
3
|
+
* ({@link useEngineCore}) composed with the backend-agnostic chrome ({@link useSpreadsheetUI}). It's
|
|
4
|
+
* the Option-B peer of `useSpreadsheetState` — one hook a component drives the whole grid + dialogs
|
|
5
|
+
* from. The core owns the engine surface (data/calc/commands/accessors); the chrome owns the dialogs +
|
|
6
|
+
* host intents (clipboard/citations/comments/…). Their UNION is what makes this a structural drop-in
|
|
7
|
+
* for `useSpreadsheetState` — the parity lock below is asserted here, not on either layer alone.
|
|
8
|
+
*/
|
|
9
|
+
import { type SparklineGroupHandlers } from "@rowsncolumns/spreadsheet-state";
|
|
1
10
|
import { type EngineSpreadsheet, type UseEngineSpreadsheetOptions } from "./use-engine-core";
|
|
2
11
|
import { type SpreadsheetUI } from "./use-spreadsheet-ui";
|
|
3
12
|
/** The composed surface: the engine core (data/calc/commands/accessors) + the chrome (dialogs + host
|
|
4
13
|
* intents). This — not `useEngineCore` alone — is the drop-in replacement for `useSpreadsheetState`. */
|
|
5
|
-
export type SpreadsheetEngine = EngineSpreadsheet & SpreadsheetUI;
|
|
14
|
+
export type SpreadsheetEngine = EngineSpreadsheet & SpreadsheetUI & SparklineGroupHandlers;
|
|
6
15
|
export declare function useSpreadsheetEngine(options: UseEngineSpreadsheetOptions): SpreadsheetEngine;
|
|
7
16
|
//# sourceMappingURL=use-spreadsheet-engine.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-spreadsheet-engine.d.ts","sourceRoot":"","sources":["../../src/use-spreadsheet-engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-spreadsheet-engine.d.ts","sourceRoot":"","sources":["../../src/use-spreadsheet-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,KAAK,sBAAsB,EAO5B,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,2BAA2B,EACjC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAE5E;wGACwG;AACxG,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAC/C,aAAa,GACb,sBAAsB,CAAC;AAazB,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,2BAA2B,GACnC,iBAAiB,CAwGnB"}
|