@ricsam/react-spreadsheets 0.0.8 → 0.0.10

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/react-spreadsheets",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/lib.mjs CHANGED
@@ -1364,12 +1364,37 @@ var alignScrollOffset = (current, itemStart, itemEnd, viewportSize, align) => {
1364
1364
  return itemStart;
1365
1365
  if (align === "end")
1366
1366
  return itemEnd - viewportSize;
1367
+ if (itemEnd - itemStart > viewportSize) {
1368
+ if (itemEnd <= current)
1369
+ return itemEnd - viewportSize;
1370
+ if (itemStart >= current + viewportSize)
1371
+ return itemStart;
1372
+ return current;
1373
+ }
1367
1374
  if (itemStart < current)
1368
1375
  return itemStart;
1369
1376
  if (itemEnd > current + viewportSize)
1370
1377
  return itemEnd - viewportSize;
1371
1378
  return current;
1372
1379
  };
1380
+ var normalizeScrollRangeAxis = (start, end, finiteCount) => {
1381
+ if (!Number.isFinite(start) || end.type === "number" && !Number.isFinite(end.value)) {
1382
+ return;
1383
+ }
1384
+ const normalizedStart = Math.max(0, Math.floor(start));
1385
+ const normalizedEnd = end.type === "infinity" ? finiteCount === undefined ? normalizedStart : Math.max(0, finiteCount - 1) : Math.max(0, Math.floor(end.value));
1386
+ const lower = Math.min(normalizedStart, normalizedEnd);
1387
+ const upper = Math.max(normalizedStart, normalizedEnd);
1388
+ if (finiteCount === undefined)
1389
+ return { start: lower, end: upper };
1390
+ if (finiteCount === 0)
1391
+ return;
1392
+ const lastIndex = finiteCount - 1;
1393
+ return {
1394
+ start: Math.min(lower, lastIndex),
1395
+ end: Math.min(upper, lastIndex)
1396
+ };
1397
+ };
1373
1398
  var hasCellData = (value) => value !== undefined && value !== "";
1374
1399
  var getSparseUsedRange = (cellData, rowCount, columnCount) => {
1375
1400
  let minRow = Infinity;
@@ -2126,22 +2151,20 @@ var Spreadsheet = forwardRef(({
2126
2151
  scrollY: Math.min(Math.max(0, scrollY), maxScrollY)
2127
2152
  };
2128
2153
  }, [columnWidths, finiteColumnCount, finiteRowCount, height, rowHeights, width]);
2129
- const revealCell = useCallback3((cell, options = {}) => {
2154
+ const revealRange = useCallback3((range, options = {}) => {
2130
2155
  if (finiteRowCount === 0 || finiteColumnCount === 0)
2131
2156
  return;
2132
- if (!Number.isFinite(cell.row) || !Number.isFinite(cell.col))
2157
+ const rows = normalizeScrollRangeAxis(range.start.row, range.end.row, finiteRowCount);
2158
+ const columns = normalizeScrollRangeAxis(range.start.col, range.end.col, finiteColumnCount);
2159
+ if (!rows || !columns)
2133
2160
  return;
2134
- const requestedRow = Math.max(0, Math.floor(cell.row));
2135
- const requestedCol = Math.max(0, Math.floor(cell.col));
2136
- const row = finiteRowCount === undefined ? requestedRow : Math.min(requestedRow, finiteRowCount - 1);
2137
- const col = finiteColumnCount === undefined ? requestedCol : Math.min(requestedCol, finiteColumnCount - 1);
2138
2161
  setViewport((previous) => {
2139
2162
  const bodyWidth = Math.max(0, width - HEADER_WIDTH * previous.zoom);
2140
2163
  const bodyHeight = Math.max(0, height - HEADER_HEIGHT * previous.zoom);
2141
- const left = getColumnEdgePosition(col, columnWidths) * previous.zoom;
2142
- const right = getColumnEdgePosition(col + 1, columnWidths) * previous.zoom;
2143
- const top = getRowEdgePosition(row, rowHeights) * previous.zoom;
2144
- const bottom = getRowEdgePosition(row + 1, rowHeights) * previous.zoom;
2164
+ const left = getColumnEdgePosition(columns.start, columnWidths) * previous.zoom;
2165
+ const right = getColumnEdgePosition(columns.end + 1, columnWidths) * previous.zoom;
2166
+ const top = getRowEdgePosition(rows.start, rowHeights) * previous.zoom;
2167
+ const bottom = getRowEdgePosition(rows.end + 1, rowHeights) * previous.zoom;
2145
2168
  const desired = clampViewportOffsets(alignScrollOffset(previous.scrollX, left, right, bodyWidth, options.columnAlign ?? "nearest"), alignScrollOffset(previous.scrollY, top, bottom, bodyHeight, options.rowAlign ?? "nearest"), previous.zoom);
2146
2169
  if (desired.scrollX === previous.scrollX && desired.scrollY === previous.scrollY) {
2147
2170
  return previous;
@@ -2157,17 +2180,34 @@ var Spreadsheet = forwardRef(({
2157
2180
  rowHeights,
2158
2181
  width
2159
2182
  ]);
2183
+ const revealCell = useCallback3((cell, options = {}) => {
2184
+ revealRange({
2185
+ start: { row: cell.row, col: cell.col },
2186
+ end: {
2187
+ row: { type: "number", value: cell.row },
2188
+ col: { type: "number", value: cell.col }
2189
+ }
2190
+ }, options);
2191
+ }, [revealRange]);
2160
2192
  const scrollToCell = useCallback3((cell, options = {}) => {
2161
2193
  const align = options.align ?? "nearest";
2162
2194
  revealCell(cell, { rowAlign: align, columnAlign: align });
2163
2195
  }, [revealCell]);
2196
+ const scrollToRange = useCallback3((range, options = {}) => {
2197
+ const align = options.align ?? "nearest";
2198
+ revealRange(range, { rowAlign: align, columnAlign: align });
2199
+ }, [revealRange]);
2164
2200
  useEffect3(() => selectionManager.listenToViewportRequest((request) => {
2201
+ if (request.type === "reveal-range") {
2202
+ scrollToRange(request.range, { align: request.align });
2203
+ return;
2204
+ }
2165
2205
  const isVertical = request.direction === "up" || request.direction === "down";
2166
2206
  revealCell(request.cell, {
2167
2207
  rowAlign: isVertical ? request.align : "nearest",
2168
2208
  columnAlign: isVertical ? "nearest" : request.align
2169
2209
  });
2170
- }), [revealCell, selectionManager]);
2210
+ }), [revealCell, scrollToRange, selectionManager]);
2171
2211
  useEffect3(() => {
2172
2212
  setViewport((previous) => {
2173
2213
  const next = clampViewportOffsets(previous.scrollX, previous.scrollY, previous.zoom);
@@ -2802,7 +2842,7 @@ var Spreadsheet = forwardRef(({
2802
2842
  visibleCells
2803
2843
  ]);
2804
2844
  React2.useEffect(() => {
2805
- if (!containerDivRef || !hasFocus || !parentSelected) {
2845
+ if (!containerDivRef || !parentSelected) {
2806
2846
  return;
2807
2847
  }
2808
2848
  containerDivRef.addEventListener("wheel", handleWheel, {
@@ -2811,7 +2851,7 @@ var Spreadsheet = forwardRef(({
2811
2851
  return () => {
2812
2852
  containerDivRef.removeEventListener("wheel", handleWheel);
2813
2853
  };
2814
- }, [containerDivRef, handleWheel, hasFocus, parentSelected]);
2854
+ }, [containerDivRef, handleWheel, parentSelected]);
2815
2855
  useImperativeHandle(ref, () => ({
2816
2856
  focus: () => {
2817
2857
  selectionManager.focus();
@@ -2819,8 +2859,9 @@ var Spreadsheet = forwardRef(({
2819
2859
  blur: () => {
2820
2860
  selectionManager.blur();
2821
2861
  },
2822
- scrollToCell
2823
- }), [scrollToCell, selectionManager]);
2862
+ scrollToCell,
2863
+ scrollToRange
2864
+ }), [scrollToCell, scrollToRange, selectionManager]);
2824
2865
  return /* @__PURE__ */ jsx3("div", {
2825
2866
  className: cn("rsp-root", className),
2826
2867
  "data-rsp-scheme": colorScheme,
@@ -3038,7 +3079,7 @@ Spreadsheet.displayName = "Spreadsheet";
3038
3079
  // src/workbook/workbook.tsx
3039
3080
  import {
3040
3081
  parseCellReference as parseCellReference2,
3041
- getCellReference as getCellReference3
3082
+ getCellReference as getCellReference2
3042
3083
  } from "@ricsam/formula-engine";
3043
3084
  import React3, { useCallback as useCallback4, useMemo as useMemo2, useRef as useRef4, useState as useState6 } from "react";
3044
3085
 
@@ -3166,8 +3207,24 @@ import { indexToColumn as indexToColumn2 } from "@ricsam/formula-engine";
3166
3207
  class WorkbookSelectionManager {
3167
3208
  managers = new Map;
3168
3209
  lastFocusedManager = null;
3210
+ lastSelections = [];
3211
+ selectionsByView = new Map;
3169
3212
  listeners = new Set;
3170
3213
  cleanupFunctions = new Map;
3214
+ pendingRangeFocus = null;
3215
+ applyingRangeFocus = new Set;
3216
+ getViewKey(context) {
3217
+ return JSON.stringify([context.workbookName, context.sheetName, context.viewId ?? null]);
3218
+ }
3219
+ cloneSmArea(area) {
3220
+ return {
3221
+ start: { row: area.start.row, col: area.start.col },
3222
+ end: {
3223
+ row: area.end.row.type === "infinity" ? { type: "infinity" } : { type: "number", value: area.end.row.value },
3224
+ col: area.end.col.type === "infinity" ? { type: "infinity" } : { type: "number", value: area.end.col.value }
3225
+ }
3226
+ };
3227
+ }
3171
3228
  smAreaToSpreadsheetRange(area) {
3172
3229
  const rowEnd = area.end.row.type === "infinity" ? { type: "infinity", sign: "positive" } : { type: "number", value: area.end.row.value };
3173
3230
  const colEnd = area.end.col.type === "infinity" ? { type: "infinity", sign: "positive" } : { type: "number", value: area.end.col.value };
@@ -3182,18 +3239,85 @@ class WorkbookSelectionManager {
3182
3239
  }
3183
3240
  };
3184
3241
  }
3242
+ spreadsheetRangeToSmArea(range) {
3243
+ return {
3244
+ start: { row: range.start.row, col: range.start.col },
3245
+ end: {
3246
+ row: range.end.row.type === "infinity" ? { type: "infinity" } : { type: "number", value: range.end.row.value },
3247
+ col: range.end.col.type === "infinity" ? { type: "infinity" } : { type: "number", value: range.end.col.value }
3248
+ }
3249
+ };
3250
+ }
3251
+ cloneRangeAddress(address) {
3252
+ return {
3253
+ workbookName: address.workbookName,
3254
+ sheetName: address.sheetName,
3255
+ range: this.smAreaToSpreadsheetRange(this.spreadsheetRangeToSmArea(address.range))
3256
+ };
3257
+ }
3258
+ areasToRangeAddresses(areas, context) {
3259
+ return areas.map((area) => ({
3260
+ workbookName: context.workbookName,
3261
+ sheetName: context.sheetName,
3262
+ range: this.smAreaToSpreadsheetRange(area)
3263
+ }));
3264
+ }
3265
+ rememberSelections(manager, context, areas = manager.getState().selections) {
3266
+ const cachedAreas = areas.map((area) => this.cloneSmArea(area));
3267
+ this.selectionsByView.set(this.getViewKey(context), cachedAreas);
3268
+ this.lastFocusedManager = { sm: manager, context };
3269
+ this.lastSelections = this.areasToRangeAddresses(cachedAreas, context);
3270
+ }
3271
+ matchesContext(address, context, viewId) {
3272
+ return address.workbookName === context.workbookName && address.sheetName === context.sheetName && (viewId === undefined || context.viewId === viewId);
3273
+ }
3274
+ applyRangeFocus(manager, context, request) {
3275
+ const selection = this.spreadsheetRangeToSmArea(request.address.range);
3276
+ this.applyingRangeFocus.add(manager);
3277
+ try {
3278
+ manager.replaceSelections([selection]);
3279
+ manager.focus();
3280
+ } finally {
3281
+ this.applyingRangeFocus.delete(manager);
3282
+ }
3283
+ manager.revealRange(selection, request.options);
3284
+ this.rememberSelections(manager, context, [selection]);
3285
+ this.emitChange();
3286
+ }
3287
+ applyPendingRangeFocusOnMount(manager, context) {
3288
+ const pending = this.pendingRangeFocus;
3289
+ if (!pending || !this.matchesContext(pending.address, context, pending.options.viewId))
3290
+ return;
3291
+ queueMicrotask(() => {
3292
+ if (this.pendingRangeFocus !== pending)
3293
+ return;
3294
+ const mountedContext = this.managers.get(manager);
3295
+ if (!mountedContext || !this.matchesContext(pending.address, mountedContext, pending.options.viewId)) {
3296
+ return;
3297
+ }
3298
+ this.pendingRangeFocus = null;
3299
+ this.applyRangeFocus(manager, mountedContext, pending);
3300
+ });
3301
+ }
3185
3302
  add(manager, context) {
3186
- this.managers.set(manager, context);
3303
+ const registeredContext = { ...context };
3304
+ this.managers.set(manager, registeredContext);
3305
+ const cachedSelections = this.selectionsByView.get(this.getViewKey(registeredContext));
3306
+ if (cachedSelections && manager.getState().selections.length === 0) {
3307
+ manager.replaceSelections(cachedSelections);
3308
+ }
3187
3309
  const focusCleanup = manager.observeStateChange((state) => state.hasFocus, (hasFocus) => {
3188
- if (hasFocus) {
3189
- this.lastFocusedManager = { sm: manager, context };
3310
+ if (hasFocus && !this.applyingRangeFocus.has(manager)) {
3311
+ this.rememberSelections(manager, registeredContext);
3190
3312
  this.emitChange();
3191
3313
  }
3192
3314
  });
3193
3315
  const selectionCleanup = manager.observeStateChange((state) => JSON.stringify(state.selections), (selectionsJson) => {
3316
+ if (this.applyingRangeFocus.has(manager))
3317
+ return;
3194
3318
  const selections = JSON.parse(selectionsJson);
3195
3319
  if (selections && selections.length > 0) {
3196
- this.lastFocusedManager = { sm: manager, context };
3320
+ this.rememberSelections(manager, registeredContext, selections);
3197
3321
  this.emitChange();
3198
3322
  }
3199
3323
  });
@@ -3203,26 +3327,19 @@ class WorkbookSelectionManager {
3203
3327
  this.managers.delete(manager);
3204
3328
  this.cleanupFunctions.delete(manager);
3205
3329
  if (this.lastFocusedManager?.sm === manager) {
3330
+ this.rememberSelections(manager, registeredContext);
3206
3331
  this.lastFocusedManager = null;
3207
3332
  }
3208
3333
  };
3209
3334
  this.cleanupFunctions.set(manager, cleanup);
3335
+ this.applyPendingRangeFocusOnMount(manager, registeredContext);
3210
3336
  return cleanup;
3211
3337
  }
3212
3338
  getLastFocusedSelectionManager() {
3213
3339
  return this.lastFocusedManager;
3214
3340
  }
3215
3341
  getSelections() {
3216
- if (!this.lastFocusedManager) {
3217
- return [];
3218
- }
3219
- const { sm, context } = this.lastFocusedManager;
3220
- const state = sm.getState();
3221
- return state.selections.map((area) => ({
3222
- workbookName: context.workbookName,
3223
- sheetName: context.sheetName,
3224
- range: this.smAreaToSpreadsheetRange(area)
3225
- }));
3342
+ return this.lastSelections.map((selection) => this.cloneRangeAddress(selection));
3226
3343
  }
3227
3344
  getLastSelection() {
3228
3345
  const selections = this.getSelections();
@@ -3271,53 +3388,67 @@ class WorkbookSelectionManager {
3271
3388
  return `[${workbookName}]${sheetRef}!${rangeStr}`;
3272
3389
  }
3273
3390
  blur() {
3274
- this.managers.forEach((context, manager) => {
3391
+ this.managers.forEach((_context, manager) => {
3275
3392
  manager.clearSelections();
3276
3393
  manager.blur();
3277
3394
  });
3395
+ this.selectionsByView.clear();
3278
3396
  this.lastFocusedManager = null;
3397
+ this.lastSelections = [];
3279
3398
  this.emitChange();
3280
3399
  }
3281
- focusWorkbook(workbookName) {
3400
+ focusWorkbook(workbookName, viewId) {
3282
3401
  for (const [manager, context] of this.managers) {
3283
- if (context.workbookName === workbookName) {
3402
+ if (context.workbookName === workbookName && (viewId === undefined || context.viewId === viewId)) {
3284
3403
  manager.focus();
3285
- this.lastFocusedManager = { sm: manager, context };
3404
+ this.rememberSelections(manager, context);
3286
3405
  this.emitChange();
3287
3406
  return;
3288
3407
  }
3289
3408
  }
3290
3409
  }
3291
- focusSheet(workbookName, sheetName) {
3410
+ focusSheet(workbookName, sheetName, viewId) {
3292
3411
  for (const [manager, context] of this.managers) {
3293
- if (context.workbookName === workbookName && context.sheetName === sheetName) {
3412
+ if (context.workbookName === workbookName && context.sheetName === sheetName && (viewId === undefined || context.viewId === viewId)) {
3294
3413
  manager.focus();
3295
- this.lastFocusedManager = { sm: manager, context };
3414
+ this.rememberSelections(manager, context);
3296
3415
  this.emitChange();
3297
3416
  return true;
3298
3417
  }
3299
3418
  }
3300
3419
  return false;
3301
3420
  }
3302
- focusCell(workbookName, sheetName, row, col) {
3303
- for (const [manager, context] of this.managers) {
3304
- if (context.workbookName === workbookName && context.sheetName === sheetName) {
3305
- const selection = {
3306
- start: { row, col },
3307
- end: {
3308
- row: { type: "number", value: row },
3309
- col: { type: "number", value: col }
3310
- }
3311
- };
3312
- manager.setState({ selections: [selection] });
3313
- manager.focus();
3314
- this.lastFocusedManager = { sm: manager, context };
3315
- this.emitChange();
3316
- return true;
3421
+ focusCell(workbookName, sheetName, row, col, options = {}) {
3422
+ return this.focusRange({
3423
+ workbookName,
3424
+ sheetName,
3425
+ range: {
3426
+ start: { row, col },
3427
+ end: {
3428
+ row: { type: "number", value: row },
3429
+ col: { type: "number", value: col }
3430
+ }
3317
3431
  }
3432
+ }, options);
3433
+ }
3434
+ focusRange(address, options = {}) {
3435
+ const request = {
3436
+ address: this.cloneRangeAddress(address),
3437
+ options: { ...options }
3438
+ };
3439
+ for (const [manager, context] of this.managers) {
3440
+ if (!this.matchesContext(request.address, context, request.options.viewId))
3441
+ continue;
3442
+ this.pendingRangeFocus = null;
3443
+ this.applyRangeFocus(manager, context, request);
3444
+ return true;
3318
3445
  }
3446
+ this.pendingRangeFocus = request;
3319
3447
  return false;
3320
3448
  }
3449
+ cancelPendingFocusRange() {
3450
+ this.pendingRangeFocus = null;
3451
+ }
3321
3452
  emitChange() {
3322
3453
  const currentSelection = this.getLastSelection();
3323
3454
  const selections = currentSelection ? [currentSelection] : [];
@@ -3496,7 +3627,7 @@ class WorkbookClipboardManager extends ClipboardUtils {
3496
3627
  };
3497
3628
  this.copiedCells.push(cellAddress);
3498
3629
  const value = this.engine.getCellValue(cellAddress, false);
3499
- const formula = cellData.get(getCellReference3(cellAddress));
3630
+ const formula = cellData.get(getCellReference2(cellAddress));
3500
3631
  valueExportGrid[relative.rowIndex][relative.columnIndex] = getCellDisplayValue(value);
3501
3632
  formulaExportGrid[relative.rowIndex][relative.columnIndex] = formula;
3502
3633
  });
@@ -3544,6 +3675,7 @@ function FormulaSheet({
3544
3675
  verboseErrors = false,
3545
3676
  selection,
3546
3677
  selectionManager,
3678
+ viewId,
3547
3679
  customCellStyle,
3548
3680
  customCellRenderer,
3549
3681
  onCellDataChangeError = queueCellEditError,
@@ -3715,7 +3847,8 @@ function FormulaSheet({
3715
3847
  if (selectionManager) {
3716
3848
  const workbookManagerCleanup = selectionManager.add(spreadsheetSelectionManager, {
3717
3849
  workbookName,
3718
- sheetName
3850
+ sheetName,
3851
+ ...viewId === undefined ? {} : { viewId }
3719
3852
  });
3720
3853
  cleanups.push(workbookManagerCleanup);
3721
3854
  }
@@ -3736,6 +3869,7 @@ function FormulaSheet({
3736
3869
  selectionManager,
3737
3870
  sheetName,
3738
3871
  showFormulas,
3872
+ viewId,
3739
3873
  workbookName
3740
3874
  ]);
3741
3875
  if (!sheet) {
@@ -3875,7 +4009,7 @@ function FormulaSheet({
3875
4009
  return style;
3876
4010
  },
3877
4011
  customCellRenderer: (cell) => {
3878
- const cellRef = getCellReference3({
4012
+ const cellRef = getCellReference2({
3879
4013
  colIndex: cell.colIndex,
3880
4014
  rowIndex: cell.rowIndex
3881
4015
  });
@@ -3969,6 +4103,7 @@ function FormulaWorkbook({
3969
4103
  onActiveSheetChange,
3970
4104
  verboseErrors = false,
3971
4105
  selectionManager,
4106
+ viewId,
3972
4107
  components,
3973
4108
  sheetOverlayChildren,
3974
4109
  sheetSelectedOverlayIds,
@@ -4136,6 +4271,7 @@ function FormulaWorkbook({
4136
4271
  engine,
4137
4272
  verboseErrors,
4138
4273
  selectionManager,
4274
+ viewId,
4139
4275
  ...rest,
4140
4276
  components,
4141
4277
  overlayChildren: activeSheetOverlayChildren,
@@ -4363,4 +4499,4 @@ export {
4363
4499
  useResizeObserver
4364
4500
  };
4365
4501
 
4366
- //# debugId=22D97CA826E5512F64756E2164756E21
4502
+ //# debugId=27095BB366C4630864756E2164756E21