@odoo/o-spreadsheet 17.0.4 → 17.0.6

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.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 17.0.4
6
- * @date 2023-11-24T13:13:42.575Z
7
- * @hash 7346d0a
5
+ * @version 17.0.6
6
+ * @date 2024-01-04T12:23:10.357Z
7
+ * @hash 310ca57
8
8
  */
9
9
 
10
10
  import { Component, useRef, onMounted, useEffect, onWillPatch, useState, onWillUpdateProps, onPatched, useComponent, useExternalListener, onWillUnmount, onWillStart, xml, useChildSubEnv, useSubEnv, markRaw } from '@odoo/owl';
@@ -329,13 +329,6 @@ const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];
329
329
  //------------------------------------------------------------------------------
330
330
  // Miscellaneous
331
331
  //------------------------------------------------------------------------------
332
- /**
333
- * Stringify an object, like JSON.stringify, except that the first level of keys
334
- * is ordered.
335
- */
336
- function stringify(obj) {
337
- return JSON.stringify(obj, Object.keys(obj).sort());
338
- }
339
332
  /**
340
333
  * Remove quotes from a quoted string
341
334
  * ```js
@@ -558,7 +551,7 @@ function isObjectEmptyRecursive(argument) {
558
551
  */
559
552
  function getItemId(item, itemsDic) {
560
553
  for (let [key, value] of Object.entries(itemsDic)) {
561
- if (stringify(value) === stringify(item)) {
554
+ if (deepEquals(value, item)) {
562
555
  return parseInt(key, 10);
563
556
  }
564
557
  }
@@ -3387,8 +3380,10 @@ function convertInternalFormatToFormat(internalFormat) {
3387
3380
  const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
3388
3381
  // Same as above, but matches the exact string (nothing before or after)
3389
3382
  const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
3390
- /** Reference of a column header (eg. A, AB) */
3391
- const colHeader = new RegExp(/^([A-Z]{1,3})+$/, "i");
3383
+ /** Reference of a column header (eg. A, AB, $A) */
3384
+ const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
3385
+ /** Reference of a row header (eg. 1, $1) */
3386
+ const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
3392
3387
  /** Reference of a column (eg. A, $CA, Sheet1!B) */
3393
3388
  const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
3394
3389
  /** Reference of a row (eg. 1, 59, Sheet1!9) */
@@ -3418,6 +3413,9 @@ function isRowReference(xc) {
3418
3413
  function isColHeader(str) {
3419
3414
  return colHeader.test(str);
3420
3415
  }
3416
+ function isRowHeader(str) {
3417
+ return rowHeader.test(str);
3418
+ }
3421
3419
  /**
3422
3420
  * Return true if the given xc is the reference of a single cell,
3423
3421
  * without any specified sheet (e.g. A1)
@@ -4101,18 +4099,10 @@ class RangeImpl {
4101
4099
  if (isFullCol) {
4102
4100
  parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
4103
4101
  parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
4104
- if (zone.left === zone.right) {
4105
- parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
4106
- parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
4107
- }
4108
4102
  }
4109
4103
  if (isFullRow) {
4110
4104
  parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
4111
4105
  parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
4112
- if (zone.top === zone.bottom) {
4113
- parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
4114
- parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
4115
- }
4116
4106
  }
4117
4107
  return parts;
4118
4108
  }
@@ -4354,7 +4344,7 @@ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
4354
4344
  function getDefaultCellHeight(ctx, cell, colSize) {
4355
4345
  if (!cell || !cell.content)
4356
4346
  return DEFAULT_CELL_HEIGHT;
4357
- const maxWidth = cell.style?.wrapping ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
4347
+ const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
4358
4348
  const numberOfLines = cell.isFormula
4359
4349
  ? 1
4360
4350
  : splitTextToWidth(ctx, cell.content, cell.style, maxWidth).length;
@@ -5289,7 +5279,7 @@ function createScorecardChartRuntime(chart, getters) {
5289
5279
  };
5290
5280
  baselineCell = getters.getEvaluatedCell(baselinePosition);
5291
5281
  }
5292
- const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue);
5282
+ const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
5293
5283
  const locale = getters.getLocale();
5294
5284
  return {
5295
5285
  title: _t(chart.title),
@@ -5298,7 +5288,7 @@ function createScorecardChartRuntime(chart, getters) {
5298
5288
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
5299
5289
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
5300
5290
  baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
5301
- fontColor: chartFontColor(background),
5291
+ fontColor,
5302
5292
  background,
5303
5293
  baselineStyle: chart.baselineMode !== "percentage" && baseline
5304
5294
  ? getters.getCellStyle({
@@ -6249,10 +6239,6 @@ css /* scss */ `
6249
6239
 
6250
6240
  .o-error-tooltip-message {
6251
6241
  overflow: hidden;
6252
- display: -webkit-box; /* Limit to 3 lines */
6253
- -webkit-line-clamp: 3;
6254
- line-clamp: 3;
6255
- -webkit-box-orient: vertical;
6256
6242
  }
6257
6243
  }
6258
6244
  `;
@@ -6597,6 +6583,7 @@ const FilterMenuPopoverBuilder = {
6597
6583
  },
6598
6584
  };
6599
6585
 
6586
+ const macRegex = /Mac/i;
6600
6587
  /**
6601
6588
  * Return true if the event was triggered from
6602
6589
  * a child element.
@@ -6635,6 +6622,17 @@ function* iterateChildren(el) {
6635
6622
  function getOpenedMenus() {
6636
6623
  return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
6637
6624
  }
6625
+ function isMacOS() {
6626
+ return Boolean(macRegex.test(navigator.userAgent));
6627
+ }
6628
+ /**
6629
+ * @param {KeyboardEvent | MouseEvent} ev
6630
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
6631
+ * On Mac, this is the "meta" or "command" key.
6632
+ */
6633
+ function isCtrlKey(ev) {
6634
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
6635
+ }
6638
6636
 
6639
6637
  /**
6640
6638
  * Return the o-spreadsheet element position relative
@@ -8882,7 +8880,7 @@ function createGaugeChartRuntime(chart, getters) {
8882
8880
  });
8883
8881
  return {
8884
8882
  chartJsConfig: config,
8885
- background: getters.getBackgroundOfSingleCellChart(chart.background, dataRange),
8883
+ background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
8886
8884
  };
8887
8885
  }
8888
8886
 
@@ -8963,7 +8961,7 @@ function getFormatMinDisplayUnit(format) {
8963
8961
  else if (format.includes("h") || format.includes("H")) {
8964
8962
  return "hour";
8965
8963
  }
8966
- else if (format.includes("D")) {
8964
+ else if (format.includes("d")) {
8967
8965
  return "day";
8968
8966
  }
8969
8967
  else if (format.includes("M")) {
@@ -21216,10 +21214,10 @@ const arrowMap = {
21216
21214
  function updateSelectionWithArrowKeys(ev, selection) {
21217
21215
  const direction = arrowMap[ev.key];
21218
21216
  if (ev.shiftKey) {
21219
- selection.resizeAnchorZone(direction, ev.ctrlKey ? "end" : 1);
21217
+ selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
21220
21218
  }
21221
21219
  else {
21222
- selection.moveAnchorCell(direction, ev.ctrlKey ? "end" : 1);
21220
+ selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
21223
21221
  }
21224
21222
  }
21225
21223
 
@@ -28204,7 +28202,10 @@ class GridOverlay extends Component {
28204
28202
  return;
28205
28203
  }
28206
28204
  const [col, row] = this.getCartesianCoordinates(ev);
28207
- this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey });
28205
+ this.props.onCellClicked(col, row, {
28206
+ expandZone: ev.shiftKey,
28207
+ addZone: isCtrlKey(ev),
28208
+ });
28208
28209
  }
28209
28210
  onDoubleClick(ev) {
28210
28211
  const [col, row] = this.getCartesianCoordinates(ev);
@@ -28279,6 +28280,7 @@ class AbstractResizer extends Component {
28279
28280
  draggerShadowThickness: 0,
28280
28281
  delta: 0,
28281
28282
  base: 0,
28283
+ position: "before",
28282
28284
  });
28283
28285
  _computeHandleDisplay(ev) {
28284
28286
  const position = this._getEvOffset(ev);
@@ -28403,11 +28405,13 @@ class AbstractResizer extends Component {
28403
28405
  this.state.draggerLinePosition = dimensions.start;
28404
28406
  this.state.draggerShadowPosition = dimensions.start;
28405
28407
  this.state.base = elementIndex;
28408
+ this.state.position = "before";
28406
28409
  }
28407
28410
  else if (this._getSelectedZoneEnd() < elementIndex) {
28408
28411
  this.state.draggerLinePosition = dimensions.end;
28409
28412
  this.state.draggerShadowPosition = dimensions.end - this.state.draggerShadowThickness;
28410
- this.state.base = elementIndex + 1;
28413
+ this.state.base = elementIndex;
28414
+ this.state.position = "after";
28411
28415
  }
28412
28416
  else {
28413
28417
  this.state.draggerLinePosition = startDimensions.start;
@@ -28431,7 +28435,7 @@ class AbstractResizer extends Component {
28431
28435
  this._increaseSelection(index);
28432
28436
  }
28433
28437
  else {
28434
- this._selectElement(index, ev.ctrlKey);
28438
+ this._selectElement(index, isCtrlKey(ev));
28435
28439
  }
28436
28440
  this.lastSelectedElementIndex = index;
28437
28441
  const mouseMoveSelect = (col, row) => {
@@ -28589,13 +28593,14 @@ class ColResizer extends AbstractResizer {
28589
28593
  dimension: "COL",
28590
28594
  base: this.state.base,
28591
28595
  elements,
28596
+ position: this.state.position,
28592
28597
  });
28593
28598
  if (!result.isSuccessful && result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
28594
28599
  this.env.raiseError(MergeErrorMessage);
28595
28600
  }
28596
28601
  }
28597
- _selectElement(index, ctrlKey) {
28598
- this.env.model.selection.selectColumn(index, ctrlKey ? "newAnchor" : "overrideSelection");
28602
+ _selectElement(index, addDistinctHeader) {
28603
+ this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
28599
28604
  }
28600
28605
  _increaseSelection(index) {
28601
28606
  this.env.model.selection.selectColumn(index, "updateAnchor");
@@ -28762,13 +28767,14 @@ class RowResizer extends AbstractResizer {
28762
28767
  dimension: "ROW",
28763
28768
  base: this.state.base,
28764
28769
  elements,
28770
+ position: this.state.position,
28765
28771
  });
28766
28772
  if (!result.isSuccessful && result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
28767
28773
  this.env.raiseError(MergeErrorMessage);
28768
28774
  }
28769
28775
  }
28770
- _selectElement(index, ctrlKey) {
28771
- this.env.model.selection.selectRow(index, ctrlKey ? "newAnchor" : "overrideSelection");
28776
+ _selectElement(index, addDistinctHeader) {
28777
+ this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
28772
28778
  }
28773
28779
  _increaseSelection(index) {
28774
28780
  this.env.model.selection.selectRow(index, "updateAnchor");
@@ -28869,8 +28875,8 @@ function useWheelHandler(handler) {
28869
28875
  return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);
28870
28876
  }
28871
28877
  const onMouseWheel = (ev) => {
28872
- const deltaX = normalize(ev.shiftKey ? ev.deltaY : ev.deltaX, ev.deltaMode);
28873
- const deltaY = normalize(ev.shiftKey ? ev.deltaX : ev.deltaY, ev.deltaMode);
28878
+ const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode);
28879
+ const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode);
28874
28880
  handler(deltaX, deltaY);
28875
28881
  };
28876
28882
  return onMouseWheel;
@@ -29570,17 +29576,17 @@ class Grid extends Component {
29570
29576
  // ---------------------------------------------------------------------------
29571
29577
  // Zone selection with mouse
29572
29578
  // ---------------------------------------------------------------------------
29573
- onCellClicked(col, row, { ctrlKey, shiftKey }) {
29579
+ onCellClicked(col, row, { addZone, expandZone }) {
29574
29580
  if (this.env.model.getters.hasOpenedPopover()) {
29575
29581
  this.closeOpenedPopover();
29576
29582
  }
29577
29583
  if (this.env.model.getters.getEditionMode() === "editing") {
29578
29584
  interactiveStopEdition(this.env);
29579
29585
  }
29580
- if (shiftKey) {
29586
+ if (expandZone) {
29581
29587
  this.env.model.selection.setAnchorCorner(col, row);
29582
29588
  }
29583
- else if (ctrlKey) {
29589
+ else if (addZone) {
29584
29590
  this.env.model.selection.addCellToSelection(col, row);
29585
29591
  }
29586
29592
  else {
@@ -29641,7 +29647,7 @@ class Grid extends Component {
29641
29647
  onKeydown(ev) {
29642
29648
  const eventKey = ev.key.toUpperCase();
29643
29649
  let keyDownString = "";
29644
- if (ev.ctrlKey && eventKey !== "CTRL")
29650
+ if (isCtrlKey(ev) && eventKey !== "CTRL")
29645
29651
  keyDownString += "CTRL+";
29646
29652
  if (ev.metaKey)
29647
29653
  keyDownString += "CTRL+";
@@ -33324,7 +33330,7 @@ const machine = {
33324
33330
  SPACE: goTo(State.RightRef),
33325
33331
  NUMBER: goTo(State.Found),
33326
33332
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
33327
- SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
33333
+ SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
33328
33334
  },
33329
33335
  [State.RightColumnRef]: {
33330
33336
  SPACE: goTo(State.RightColumnRef),
@@ -33335,6 +33341,7 @@ const machine = {
33335
33341
  SPACE: goTo(State.RightRowRef),
33336
33342
  NUMBER: goTo(State.Found),
33337
33343
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
33344
+ SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
33338
33345
  },
33339
33346
  [State.Found]: {},
33340
33347
  };
@@ -34531,7 +34538,7 @@ class BordersPlugin extends CorePlugin {
34531
34538
  */
34532
34539
  function getBorderId(border) {
34533
34540
  for (let [key, value] of Object.entries(borders)) {
34534
- if (stringify(value) === stringify(border)) {
34541
+ if (deepEquals(value, border)) {
34535
34542
  return parseInt(key, 10);
34536
34543
  }
34537
34544
  }
@@ -37316,7 +37323,16 @@ class HeaderVisibilityPlugin extends CorePlugin {
37316
37323
  return consecutiveIndexes;
37317
37324
  }
37318
37325
  getAllVisibleHeaders(sheetId, dimension) {
37319
- return range(0, this.hiddenHeaders[sheetId][dimension].length).filter((i) => !this.hiddenHeaders[sheetId][dimension][i]);
37326
+ const headers = range(0, this.getters.getNumberHeaders(sheetId, dimension));
37327
+ const foldedHeaders = [];
37328
+ this.getters.getHeaderGroups(sheetId, dimension).forEach((group) => {
37329
+ if (group.isFolded) {
37330
+ foldedHeaders.push(...range(group.start, group.end + 1));
37331
+ }
37332
+ });
37333
+ return headers.filter((i) => {
37334
+ return !this.hiddenHeaders[sheetId][dimension][i] && !foldedHeaders.includes(i);
37335
+ });
37320
37336
  }
37321
37337
  import(data) {
37322
37338
  for (let sheet of data.sheets) {
@@ -39776,10 +39792,13 @@ class CompilationParametersBuilder {
39776
39792
  if (evaluatedCell === undefined) {
39777
39793
  return { value: null, format: this.getters.getCell(position)?.format };
39778
39794
  }
39795
+ if (evaluatedCell.type === CellValueType.error) {
39796
+ throw evaluatedCell.error;
39797
+ }
39779
39798
  return evaluatedCell;
39780
39799
  }
39781
39800
  getEvaluatedCellIfNotEmpty(position) {
39782
- const evaluatedCell = this.getEvaluatedCell(position);
39801
+ const evaluatedCell = this.computeCell(position);
39783
39802
  if (evaluatedCell.type === CellValueType.empty) {
39784
39803
  const cell = this.getters.getCell(position);
39785
39804
  if (!cell || (!cell.isFormula && cell.content === "")) {
@@ -39788,13 +39807,6 @@ class CompilationParametersBuilder {
39788
39807
  }
39789
39808
  return evaluatedCell;
39790
39809
  }
39791
- getEvaluatedCell(position) {
39792
- const evaluatedCell = this.computeCell(position);
39793
- if (evaluatedCell.type === CellValueType.error) {
39794
- throw evaluatedCell.error;
39795
- }
39796
- return evaluatedCell;
39797
- }
39798
39810
  /**
39799
39811
  * Return the values of the cell(s) used in reference, but always in the format of a range even
39800
39812
  * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
@@ -39817,7 +39829,11 @@ class CompilationParametersBuilder {
39817
39829
  const { top, left, bottom, right } = zone;
39818
39830
  const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;
39819
39831
  if (cacheKey in this.rangeCache) {
39820
- return this.rangeCache[cacheKey];
39832
+ const result = this.rangeCache[cacheKey];
39833
+ if (result instanceof EvaluationError) {
39834
+ throw result;
39835
+ }
39836
+ return result;
39821
39837
  }
39822
39838
  const height = _zone.bottom - _zone.top + 1;
39823
39839
  const width = _zone.right - _zone.left + 1;
@@ -39827,6 +39843,11 @@ class CompilationParametersBuilder {
39827
39843
  const colIndex = col - _zone.left;
39828
39844
  matrix[colIndex] = new Array(height);
39829
39845
  for (let row = _zone.top; row <= _zone.bottom; row++) {
39846
+ const evaluatedCell = this.getEvaluatedCellIfNotEmpty({ sheetId, col, row });
39847
+ if (evaluatedCell?.type === CellValueType.error) {
39848
+ this.rangeCache[cacheKey] = evaluatedCell.error;
39849
+ throw evaluatedCell.error;
39850
+ }
39830
39851
  const rowIndex = row - _zone.top;
39831
39852
  matrix[colIndex][rowIndex] = this.readCell({ sheetId, col, row });
39832
39853
  }
@@ -39836,56 +39857,758 @@ class CompilationParametersBuilder {
39836
39857
  }
39837
39858
  }
39838
39859
 
39860
+ function quickselect(arr, k, left, right, compare) {
39861
+ quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
39862
+ }
39863
+
39864
+ function quickselectStep(arr, k, left, right, compare) {
39865
+
39866
+ while (right > left) {
39867
+ if (right - left > 600) {
39868
+ var n = right - left + 1;
39869
+ var m = k - left + 1;
39870
+ var z = Math.log(n);
39871
+ var s = 0.5 * Math.exp(2 * z / 3);
39872
+ var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
39873
+ var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
39874
+ var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
39875
+ quickselectStep(arr, k, newLeft, newRight, compare);
39876
+ }
39877
+
39878
+ var t = arr[k];
39879
+ var i = left;
39880
+ var j = right;
39881
+
39882
+ swap(arr, left, k);
39883
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
39884
+
39885
+ while (i < j) {
39886
+ swap(arr, i, j);
39887
+ i++;
39888
+ j--;
39889
+ while (compare(arr[i], t) < 0) i++;
39890
+ while (compare(arr[j], t) > 0) j--;
39891
+ }
39892
+
39893
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
39894
+ else {
39895
+ j++;
39896
+ swap(arr, j, right);
39897
+ }
39898
+
39899
+ if (j <= k) left = j + 1;
39900
+ if (k <= j) right = j - 1;
39901
+ }
39902
+ }
39903
+
39904
+ function swap(arr, i, j) {
39905
+ var tmp = arr[i];
39906
+ arr[i] = arr[j];
39907
+ arr[j] = tmp;
39908
+ }
39909
+
39910
+ function defaultCompare(a, b) {
39911
+ return a < b ? -1 : a > b ? 1 : 0;
39912
+ }
39913
+
39914
+ class RBush {
39915
+ constructor(maxEntries = 9) {
39916
+ // max entries in a node is 9 by default; min node fill is 40% for best performance
39917
+ this._maxEntries = Math.max(4, maxEntries);
39918
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
39919
+ this.clear();
39920
+ }
39921
+
39922
+ all() {
39923
+ return this._all(this.data, []);
39924
+ }
39925
+
39926
+ search(bbox) {
39927
+ let node = this.data;
39928
+ const result = [];
39929
+
39930
+ if (!intersects(bbox, node)) return result;
39931
+
39932
+ const toBBox = this.toBBox;
39933
+ const nodesToSearch = [];
39934
+
39935
+ while (node) {
39936
+ for (let i = 0; i < node.children.length; i++) {
39937
+ const child = node.children[i];
39938
+ const childBBox = node.leaf ? toBBox(child) : child;
39939
+
39940
+ if (intersects(bbox, childBBox)) {
39941
+ if (node.leaf) result.push(child);
39942
+ else if (contains(bbox, childBBox)) this._all(child, result);
39943
+ else nodesToSearch.push(child);
39944
+ }
39945
+ }
39946
+ node = nodesToSearch.pop();
39947
+ }
39948
+
39949
+ return result;
39950
+ }
39951
+
39952
+ collides(bbox) {
39953
+ let node = this.data;
39954
+
39955
+ if (!intersects(bbox, node)) return false;
39956
+
39957
+ const nodesToSearch = [];
39958
+ while (node) {
39959
+ for (let i = 0; i < node.children.length; i++) {
39960
+ const child = node.children[i];
39961
+ const childBBox = node.leaf ? this.toBBox(child) : child;
39962
+
39963
+ if (intersects(bbox, childBBox)) {
39964
+ if (node.leaf || contains(bbox, childBBox)) return true;
39965
+ nodesToSearch.push(child);
39966
+ }
39967
+ }
39968
+ node = nodesToSearch.pop();
39969
+ }
39970
+
39971
+ return false;
39972
+ }
39973
+
39974
+ load(data) {
39975
+ if (!(data && data.length)) return this;
39976
+
39977
+ if (data.length < this._minEntries) {
39978
+ for (let i = 0; i < data.length; i++) {
39979
+ this.insert(data[i]);
39980
+ }
39981
+ return this;
39982
+ }
39983
+
39984
+ // recursively build the tree with the given data from scratch using OMT algorithm
39985
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
39986
+
39987
+ if (!this.data.children.length) {
39988
+ // save as is if tree is empty
39989
+ this.data = node;
39990
+
39991
+ } else if (this.data.height === node.height) {
39992
+ // split root if trees have the same height
39993
+ this._splitRoot(this.data, node);
39994
+
39995
+ } else {
39996
+ if (this.data.height < node.height) {
39997
+ // swap trees if inserted one is bigger
39998
+ const tmpNode = this.data;
39999
+ this.data = node;
40000
+ node = tmpNode;
40001
+ }
40002
+
40003
+ // insert the small tree into the large tree at appropriate level
40004
+ this._insert(node, this.data.height - node.height - 1, true);
40005
+ }
40006
+
40007
+ return this;
40008
+ }
40009
+
40010
+ insert(item) {
40011
+ if (item) this._insert(item, this.data.height - 1);
40012
+ return this;
40013
+ }
40014
+
40015
+ clear() {
40016
+ this.data = createNode([]);
40017
+ return this;
40018
+ }
40019
+
40020
+ remove(item, equalsFn) {
40021
+ if (!item) return this;
40022
+
40023
+ let node = this.data;
40024
+ const bbox = this.toBBox(item);
40025
+ const path = [];
40026
+ const indexes = [];
40027
+ let i, parent, goingUp;
40028
+
40029
+ // depth-first iterative tree traversal
40030
+ while (node || path.length) {
40031
+
40032
+ if (!node) { // go up
40033
+ node = path.pop();
40034
+ parent = path[path.length - 1];
40035
+ i = indexes.pop();
40036
+ goingUp = true;
40037
+ }
40038
+
40039
+ if (node.leaf) { // check current node
40040
+ const index = findItem(item, node.children, equalsFn);
40041
+
40042
+ if (index !== -1) {
40043
+ // item found, remove the item and condense tree upwards
40044
+ node.children.splice(index, 1);
40045
+ path.push(node);
40046
+ this._condense(path);
40047
+ return this;
40048
+ }
40049
+ }
40050
+
40051
+ if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
40052
+ path.push(node);
40053
+ indexes.push(i);
40054
+ i = 0;
40055
+ parent = node;
40056
+ node = node.children[0];
40057
+
40058
+ } else if (parent) { // go right
40059
+ i++;
40060
+ node = parent.children[i];
40061
+ goingUp = false;
40062
+
40063
+ } else node = null; // nothing found
40064
+ }
40065
+
40066
+ return this;
40067
+ }
40068
+
40069
+ toBBox(item) { return item; }
40070
+
40071
+ compareMinX(a, b) { return a.minX - b.minX; }
40072
+ compareMinY(a, b) { return a.minY - b.minY; }
40073
+
40074
+ toJSON() { return this.data; }
40075
+
40076
+ fromJSON(data) {
40077
+ this.data = data;
40078
+ return this;
40079
+ }
40080
+
40081
+ _all(node, result) {
40082
+ const nodesToSearch = [];
40083
+ while (node) {
40084
+ if (node.leaf) result.push(...node.children);
40085
+ else nodesToSearch.push(...node.children);
40086
+
40087
+ node = nodesToSearch.pop();
40088
+ }
40089
+ return result;
40090
+ }
40091
+
40092
+ _build(items, left, right, height) {
40093
+
40094
+ const N = right - left + 1;
40095
+ let M = this._maxEntries;
40096
+ let node;
40097
+
40098
+ if (N <= M) {
40099
+ // reached leaf level; return leaf
40100
+ node = createNode(items.slice(left, right + 1));
40101
+ calcBBox(node, this.toBBox);
40102
+ return node;
40103
+ }
40104
+
40105
+ if (!height) {
40106
+ // target height of the bulk-loaded tree
40107
+ height = Math.ceil(Math.log(N) / Math.log(M));
40108
+
40109
+ // target number of root entries to maximize storage utilization
40110
+ M = Math.ceil(N / Math.pow(M, height - 1));
40111
+ }
40112
+
40113
+ node = createNode([]);
40114
+ node.leaf = false;
40115
+ node.height = height;
40116
+
40117
+ // split the items into M mostly square tiles
40118
+
40119
+ const N2 = Math.ceil(N / M);
40120
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
40121
+
40122
+ multiSelect(items, left, right, N1, this.compareMinX);
40123
+
40124
+ for (let i = left; i <= right; i += N1) {
40125
+
40126
+ const right2 = Math.min(i + N1 - 1, right);
40127
+
40128
+ multiSelect(items, i, right2, N2, this.compareMinY);
40129
+
40130
+ for (let j = i; j <= right2; j += N2) {
40131
+
40132
+ const right3 = Math.min(j + N2 - 1, right2);
40133
+
40134
+ // pack each entry recursively
40135
+ node.children.push(this._build(items, j, right3, height - 1));
40136
+ }
40137
+ }
40138
+
40139
+ calcBBox(node, this.toBBox);
40140
+
40141
+ return node;
40142
+ }
40143
+
40144
+ _chooseSubtree(bbox, node, level, path) {
40145
+ while (true) {
40146
+ path.push(node);
40147
+
40148
+ if (node.leaf || path.length - 1 === level) break;
40149
+
40150
+ let minArea = Infinity;
40151
+ let minEnlargement = Infinity;
40152
+ let targetNode;
40153
+
40154
+ for (let i = 0; i < node.children.length; i++) {
40155
+ const child = node.children[i];
40156
+ const area = bboxArea(child);
40157
+ const enlargement = enlargedArea(bbox, child) - area;
40158
+
40159
+ // choose entry with the least area enlargement
40160
+ if (enlargement < minEnlargement) {
40161
+ minEnlargement = enlargement;
40162
+ minArea = area < minArea ? area : minArea;
40163
+ targetNode = child;
40164
+
40165
+ } else if (enlargement === minEnlargement) {
40166
+ // otherwise choose one with the smallest area
40167
+ if (area < minArea) {
40168
+ minArea = area;
40169
+ targetNode = child;
40170
+ }
40171
+ }
40172
+ }
40173
+
40174
+ node = targetNode || node.children[0];
40175
+ }
40176
+
40177
+ return node;
40178
+ }
40179
+
40180
+ _insert(item, level, isNode) {
40181
+ const bbox = isNode ? item : this.toBBox(item);
40182
+ const insertPath = [];
40183
+
40184
+ // find the best node for accommodating the item, saving all nodes along the path too
40185
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
40186
+
40187
+ // put the item into the node
40188
+ node.children.push(item);
40189
+ extend(node, bbox);
40190
+
40191
+ // split on node overflow; propagate upwards if necessary
40192
+ while (level >= 0) {
40193
+ if (insertPath[level].children.length > this._maxEntries) {
40194
+ this._split(insertPath, level);
40195
+ level--;
40196
+ } else break;
40197
+ }
40198
+
40199
+ // adjust bboxes along the insertion path
40200
+ this._adjustParentBBoxes(bbox, insertPath, level);
40201
+ }
40202
+
40203
+ // split overflowed node into two
40204
+ _split(insertPath, level) {
40205
+ const node = insertPath[level];
40206
+ const M = node.children.length;
40207
+ const m = this._minEntries;
40208
+
40209
+ this._chooseSplitAxis(node, m, M);
40210
+
40211
+ const splitIndex = this._chooseSplitIndex(node, m, M);
40212
+
40213
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
40214
+ newNode.height = node.height;
40215
+ newNode.leaf = node.leaf;
40216
+
40217
+ calcBBox(node, this.toBBox);
40218
+ calcBBox(newNode, this.toBBox);
40219
+
40220
+ if (level) insertPath[level - 1].children.push(newNode);
40221
+ else this._splitRoot(node, newNode);
40222
+ }
40223
+
40224
+ _splitRoot(node, newNode) {
40225
+ // split root node
40226
+ this.data = createNode([node, newNode]);
40227
+ this.data.height = node.height + 1;
40228
+ this.data.leaf = false;
40229
+ calcBBox(this.data, this.toBBox);
40230
+ }
40231
+
40232
+ _chooseSplitIndex(node, m, M) {
40233
+ let index;
40234
+ let minOverlap = Infinity;
40235
+ let minArea = Infinity;
40236
+
40237
+ for (let i = m; i <= M - m; i++) {
40238
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
40239
+ const bbox2 = distBBox(node, i, M, this.toBBox);
40240
+
40241
+ const overlap = intersectionArea(bbox1, bbox2);
40242
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
40243
+
40244
+ // choose distribution with minimum overlap
40245
+ if (overlap < minOverlap) {
40246
+ minOverlap = overlap;
40247
+ index = i;
40248
+
40249
+ minArea = area < minArea ? area : minArea;
40250
+
40251
+ } else if (overlap === minOverlap) {
40252
+ // otherwise choose distribution with minimum area
40253
+ if (area < minArea) {
40254
+ minArea = area;
40255
+ index = i;
40256
+ }
40257
+ }
40258
+ }
40259
+
40260
+ return index || M - m;
40261
+ }
40262
+
40263
+ // sorts node children by the best axis for split
40264
+ _chooseSplitAxis(node, m, M) {
40265
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
40266
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
40267
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
40268
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
40269
+
40270
+ // if total distributions margin value is minimal for x, sort by minX,
40271
+ // otherwise it's already sorted by minY
40272
+ if (xMargin < yMargin) node.children.sort(compareMinX);
40273
+ }
40274
+
40275
+ // total margin of all possible split distributions where each node is at least m full
40276
+ _allDistMargin(node, m, M, compare) {
40277
+ node.children.sort(compare);
40278
+
40279
+ const toBBox = this.toBBox;
40280
+ const leftBBox = distBBox(node, 0, m, toBBox);
40281
+ const rightBBox = distBBox(node, M - m, M, toBBox);
40282
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
40283
+
40284
+ for (let i = m; i < M - m; i++) {
40285
+ const child = node.children[i];
40286
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
40287
+ margin += bboxMargin(leftBBox);
40288
+ }
40289
+
40290
+ for (let i = M - m - 1; i >= m; i--) {
40291
+ const child = node.children[i];
40292
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
40293
+ margin += bboxMargin(rightBBox);
40294
+ }
40295
+
40296
+ return margin;
40297
+ }
40298
+
40299
+ _adjustParentBBoxes(bbox, path, level) {
40300
+ // adjust bboxes along the given tree path
40301
+ for (let i = level; i >= 0; i--) {
40302
+ extend(path[i], bbox);
40303
+ }
40304
+ }
40305
+
40306
+ _condense(path) {
40307
+ // go through the path, removing empty nodes and updating bboxes
40308
+ for (let i = path.length - 1, siblings; i >= 0; i--) {
40309
+ if (path[i].children.length === 0) {
40310
+ if (i > 0) {
40311
+ siblings = path[i - 1].children;
40312
+ siblings.splice(siblings.indexOf(path[i]), 1);
40313
+
40314
+ } else this.clear();
40315
+
40316
+ } else calcBBox(path[i], this.toBBox);
40317
+ }
40318
+ }
40319
+ }
40320
+
40321
+ function findItem(item, items, equalsFn) {
40322
+ if (!equalsFn) return items.indexOf(item);
40323
+
40324
+ for (let i = 0; i < items.length; i++) {
40325
+ if (equalsFn(item, items[i])) return i;
40326
+ }
40327
+ return -1;
40328
+ }
40329
+
40330
+ // calculate node's bbox from bboxes of its children
40331
+ function calcBBox(node, toBBox) {
40332
+ distBBox(node, 0, node.children.length, toBBox, node);
40333
+ }
40334
+
40335
+ // min bounding rectangle of node children from k to p-1
40336
+ function distBBox(node, k, p, toBBox, destNode) {
40337
+ if (!destNode) destNode = createNode(null);
40338
+ destNode.minX = Infinity;
40339
+ destNode.minY = Infinity;
40340
+ destNode.maxX = -Infinity;
40341
+ destNode.maxY = -Infinity;
40342
+
40343
+ for (let i = k; i < p; i++) {
40344
+ const child = node.children[i];
40345
+ extend(destNode, node.leaf ? toBBox(child) : child);
40346
+ }
40347
+
40348
+ return destNode;
40349
+ }
40350
+
40351
+ function extend(a, b) {
40352
+ a.minX = Math.min(a.minX, b.minX);
40353
+ a.minY = Math.min(a.minY, b.minY);
40354
+ a.maxX = Math.max(a.maxX, b.maxX);
40355
+ a.maxY = Math.max(a.maxY, b.maxY);
40356
+ return a;
40357
+ }
40358
+
40359
+ function compareNodeMinX(a, b) { return a.minX - b.minX; }
40360
+ function compareNodeMinY(a, b) { return a.minY - b.minY; }
40361
+
40362
+ function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
40363
+ function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
40364
+
40365
+ function enlargedArea(a, b) {
40366
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
40367
+ (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
40368
+ }
40369
+
40370
+ function intersectionArea(a, b) {
40371
+ const minX = Math.max(a.minX, b.minX);
40372
+ const minY = Math.max(a.minY, b.minY);
40373
+ const maxX = Math.min(a.maxX, b.maxX);
40374
+ const maxY = Math.min(a.maxY, b.maxY);
40375
+
40376
+ return Math.max(0, maxX - minX) *
40377
+ Math.max(0, maxY - minY);
40378
+ }
40379
+
40380
+ function contains(a, b) {
40381
+ return a.minX <= b.minX &&
40382
+ a.minY <= b.minY &&
40383
+ b.maxX <= a.maxX &&
40384
+ b.maxY <= a.maxY;
40385
+ }
40386
+
40387
+ function intersects(a, b) {
40388
+ return b.minX <= a.maxX &&
40389
+ b.minY <= a.maxY &&
40390
+ b.maxX >= a.minX &&
40391
+ b.maxY >= a.minY;
40392
+ }
40393
+
40394
+ function createNode(children) {
40395
+ return {
40396
+ children,
40397
+ height: 1,
40398
+ leaf: true,
40399
+ minX: Infinity,
40400
+ minY: Infinity,
40401
+ maxX: -Infinity,
40402
+ maxY: -Infinity
40403
+ };
40404
+ }
40405
+
40406
+ // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
40407
+ // combines selection algorithm with binary divide & conquer approach
40408
+
40409
+ function multiSelect(arr, left, right, n, compare) {
40410
+ const stack = [left, right];
40411
+
40412
+ while (stack.length) {
40413
+ right = stack.pop();
40414
+ left = stack.pop();
40415
+
40416
+ if (right - left <= n) continue;
40417
+
40418
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
40419
+ quickselect(arr, mid, left, right, compare);
40420
+
40421
+ stack.push(left, mid, mid, right);
40422
+ }
40423
+ }
40424
+
40425
+ /**
40426
+ * R-Tree Data Structure
40427
+ *
40428
+ * R-Tree is a spatial data structure used for efficient indexing and querying
40429
+ * of multi-dimensional objects, particularly in geometric and spatial applications.
40430
+ *
40431
+ * It organizes objects into a tree hierarchy, grouping nearby objects together
40432
+ * in bounding boxes. Each node in the tree represents a bounding box that
40433
+ * contains its child nodes or leaf objects. This hierarchical structure allows
40434
+ * for faster spatial queries.
40435
+ *
40436
+ * @see https://en.wikipedia.org/wiki/R-tree
40437
+ *
40438
+ * Consider a 2D Space with four zones: A, B, C, D
40439
+ * +--------------------------+
40440
+ * | |
40441
+ * | +---+ +-------+ |
40442
+ * | | A | | B | |
40443
+ * | +---+ +-------+ |
40444
+ * | |
40445
+ * | |
40446
+ * | +---+ |
40447
+ * | | C | |
40448
+ * | +---+ |
40449
+ * | +-----------+ |
40450
+ * | | D | |
40451
+ * | +-----------+ |
40452
+ * | |
40453
+ * +--------------------------+
40454
+ *
40455
+ * It groups together zones that are spatially close into a minimum bounding box.
40456
+ * For example, A and B are grouped together in rectangle R1, and C and D are grouped
40457
+ * in R2.
40458
+ *
40459
+ * R0
40460
+ * +--------------------------+
40461
+ * | R1 |
40462
+ * | +-----------------+ |
40463
+ * | | A | | B | |
40464
+ * | +-----------------+ |
40465
+ * | |
40466
+ * | R2 |
40467
+ * | +---+---+---+ |
40468
+ * | | | C | | |
40469
+ * | | +---+ | |
40470
+ * | +-----------+ |
40471
+ * | | D | |
40472
+ * | +-----------+ |
40473
+ * | |
40474
+ * +--------------------------+
40475
+ *
40476
+ * The tree would look like this:
40477
+ * R0
40478
+ * / \
40479
+ * / \
40480
+ * R1 R2
40481
+ * | |
40482
+ * A,B C,D
40483
+
40484
+ * Choosing how to group the zones is crucial for the performance of the tree.
40485
+ * Key considerations include avoiding excessive empty space coverage and minimizing overlap
40486
+ * to reduce the number of subtrees processed during searches.
40487
+ *
40488
+ * Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
40489
+ * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
40490
+ * the choice cannot be made based on this criterion due to the same enlargement for different groupings,
40491
+ * we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
40492
+ *
40493
+ * This implementation is tailored for spreadsheet use, indexing objects associated
40494
+ * with a zone and a sheet.
40495
+ *
40496
+ * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
40497
+ * @see https://github.com/mourner/rbush
40498
+ */
40499
+ class SpreadsheetRTree {
40500
+ /**
40501
+ * One 2D R-tree per sheet
40502
+ */
40503
+ rTrees = {};
40504
+ /**
40505
+ * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
40506
+ * faster than inserting items one by one. After bulk loading (bulk insertion into
40507
+ * an empty tree), subsequent query performance is also ~20-30% better.
40508
+ */
40509
+ constructor(items = []) {
40510
+ const rangesPerSheet = {};
40511
+ for (const item of items) {
40512
+ const sheetId = item.boundingBox.sheetId;
40513
+ if (!rangesPerSheet[sheetId]) {
40514
+ rangesPerSheet[sheetId] = [];
40515
+ }
40516
+ rangesPerSheet[sheetId].push(item);
40517
+ }
40518
+ for (const sheetId in rangesPerSheet) {
40519
+ this.rTrees[sheetId] = new ZoneRBush();
40520
+ this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
40521
+ }
40522
+ }
40523
+ insert(item) {
40524
+ const sheetId = item.boundingBox.sheetId;
40525
+ if (!this.rTrees[sheetId]) {
40526
+ this.rTrees[sheetId] = new ZoneRBush();
40527
+ }
40528
+ this.rTrees[sheetId].insert(item);
40529
+ }
40530
+ search({ zone, sheetId }) {
40531
+ if (!this.rTrees[sheetId]) {
40532
+ return [];
40533
+ }
40534
+ return this.rTrees[sheetId].search({
40535
+ minX: zone.left,
40536
+ minY: zone.top,
40537
+ maxX: zone.right,
40538
+ maxY: zone.bottom,
40539
+ });
40540
+ }
40541
+ remove(item) {
40542
+ const sheetId = item.boundingBox.sheetId;
40543
+ if (!this.rTrees[sheetId]) {
40544
+ return;
40545
+ }
40546
+ this.rTrees[sheetId].remove(item, deepEquals);
40547
+ }
40548
+ }
40549
+ /**
40550
+ * RBush extension to use zones as bounding boxes
40551
+ */
40552
+ class ZoneRBush extends RBush {
40553
+ toBBox({ boundingBox }) {
40554
+ const zone = boundingBox.zone;
40555
+ return {
40556
+ minX: zone.left,
40557
+ minY: zone.top,
40558
+ maxX: zone.right,
40559
+ maxY: zone.bottom,
40560
+ };
40561
+ }
40562
+ compareMinX(a, b) {
40563
+ return a.boundingBox.zone.left - b.boundingBox.zone.left;
40564
+ }
40565
+ compareMinY(a, b) {
40566
+ return a.boundingBox.zone.top - b.boundingBox.zone.top;
40567
+ }
40568
+ }
40569
+
39839
40570
  /**
39840
- * This class is an implementation of a dependency Graph.
40571
+ * Implementation of a dependency Graph.
39841
40572
  * The graph is used to evaluate the cells in the correct
39842
40573
  * order, and should be updated each time a cell's content is modified
39843
40574
  *
40575
+ * It uses an R-Tree data structure to efficiently find dependent cells.
39844
40576
  */
39845
40577
  class FormulaDependencyGraph {
39846
- /**
39847
- * Internal structure:
39848
- * - key: a cell position (encoded as an integer)
39849
- * - value: a set of cell positions that depends on the key
39850
- *
39851
- * Given
39852
- * - A1:"= B1 + SQRT(B2)"
39853
- * - C1:"= B1";
39854
- * - C2:"= C1"
39855
- *
39856
- * we will have something like:
39857
- * - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
39858
- * - B2 ---> (A1) meaning A1 depends on B2
39859
- * - C1 ---> (C2) meaning C2 depends on C1
39860
- */
39861
- inverseDependencies = new Map();
40578
+ encoder;
39862
40579
  dependencies = new Map();
40580
+ rTree;
40581
+ constructor(encoder, data = []) {
40582
+ this.encoder = encoder;
40583
+ this.rTree = new SpreadsheetRTree(data);
40584
+ }
39863
40585
  removeAllDependencies(formulaPositionId) {
39864
- const dependencies = this.dependencies.get(formulaPositionId);
39865
- if (!dependencies) {
40586
+ const ranges = this.dependencies.get(formulaPositionId);
40587
+ if (!ranges) {
39866
40588
  return;
39867
40589
  }
39868
- for (const dependency of dependencies) {
39869
- this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
40590
+ for (const range of ranges) {
40591
+ this.rTree.remove(range);
39870
40592
  }
39871
40593
  this.dependencies.delete(formulaPositionId);
39872
40594
  }
39873
40595
  addDependencies(formulaPositionId, dependencies) {
39874
- for (const dependency of dependencies) {
39875
- const inverseDependencies = this.inverseDependencies.get(dependency);
39876
- if (inverseDependencies) {
39877
- inverseDependencies.add(formulaPositionId);
39878
- }
39879
- else {
39880
- this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
39881
- }
40596
+ const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
40597
+ data: formulaPositionId,
40598
+ boundingBox: {
40599
+ zone,
40600
+ sheetId,
40601
+ },
40602
+ }));
40603
+ for (const item of rTreeItems) {
40604
+ this.rTree.insert(item);
39882
40605
  }
39883
40606
  const existingDependencies = this.dependencies.get(formulaPositionId);
39884
40607
  if (existingDependencies) {
39885
- existingDependencies.push(...dependencies);
40608
+ existingDependencies.push(...rTreeItems);
39886
40609
  }
39887
40610
  else {
39888
- this.dependencies.set(formulaPositionId, dependencies);
40611
+ this.dependencies.set(formulaPositionId, rTreeItems);
39889
40612
  }
39890
40613
  }
39891
40614
  /**
@@ -39893,20 +40616,20 @@ class FormulaDependencyGraph {
39893
40616
  * in the correct order they should be evaluated.
39894
40617
  * This is called a topological ordering (excluding cycles)
39895
40618
  */
39896
- getCellsDependingOn(positionIds) {
40619
+ getCellsDependingOn(ranges) {
39897
40620
  const visited = new JetSet();
39898
- const queue = Array.from(positionIds).reverse();
40621
+ const queue = Array.from(ranges).reverse();
39899
40622
  while (queue.length > 0) {
39900
- const node = queue.pop();
39901
- visited.add(node);
39902
- const adjacentNodes = this.inverseDependencies.get(node) || new Set();
39903
- for (const adjacentNode of adjacentNodes) {
39904
- if (!visited.has(adjacentNode)) {
39905
- queue.push(adjacentNode);
40623
+ const range = queue.pop();
40624
+ visited.add(...this.encoder.encodeBoundingBox(range));
40625
+ const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
40626
+ for (const positionId of impactedPositionIds) {
40627
+ if (!visited.has(positionId)) {
40628
+ queue.push(this.encoder.decodeToBoundingBox(positionId));
39906
40629
  }
39907
40630
  }
39908
40631
  }
39909
- visited.delete(...positionIds);
40632
+ visited.delete(...ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
39910
40633
  return visited;
39911
40634
  }
39912
40635
  }
@@ -39994,9 +40717,9 @@ class Evaluator {
39994
40717
  context;
39995
40718
  getters;
39996
40719
  compilationParams;
39997
- positionEncoder = new PositionBitsEncoder();
40720
+ encoder = new PositionBitsEncoder();
39998
40721
  evaluatedCells = new Map();
39999
- formulaDependencies = lazy(new FormulaDependencyGraph());
40722
+ formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
40000
40723
  blockedArrayFormulas = new Set();
40001
40724
  spreadingRelations = new SpreadingRelation();
40002
40725
  constructor(context, getters) {
@@ -40005,23 +40728,23 @@ class Evaluator {
40005
40728
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
40006
40729
  }
40007
40730
  getEvaluatedCell(position) {
40008
- return (this.evaluatedCells.get(this.encodePosition(position)) ||
40731
+ return (this.evaluatedCells.get(this.encoder.encode(position)) ||
40009
40732
  createEvaluatedCell("", { locale: this.getters.getLocale() }));
40010
40733
  }
40011
40734
  getSpreadPositionsOf(position) {
40012
- const positionId = this.encodePosition(position);
40735
+ const positionId = this.encoder.encode(position);
40013
40736
  if (!this.spreadingRelations.isArrayFormula(positionId)) {
40014
40737
  return [];
40015
40738
  }
40016
- return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map(this.decodePosition.bind(this));
40739
+ return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map((positionId) => this.encoder.decode(positionId));
40017
40740
  }
40018
40741
  getArrayFormulaSpreadingOn(position) {
40019
- const positionId = this.encodePosition(position);
40742
+ const positionId = this.encoder.encode(position);
40020
40743
  const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
40021
- return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
40744
+ return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
40022
40745
  }
40023
40746
  getEvaluatedPositions() {
40024
- return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
40747
+ return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
40025
40748
  }
40026
40749
  getArrayFormulaSpreadingOnId(positionId) {
40027
40750
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
@@ -40031,7 +40754,7 @@ class Evaluator {
40031
40754
  return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
40032
40755
  }
40033
40756
  updateDependencies(position) {
40034
- const positionId = this.encodePosition(position);
40757
+ const positionId = this.encoder.encode(position);
40035
40758
  this.formulaDependencies().removeAllDependencies(positionId);
40036
40759
  const dependencies = this.getDirectDependencies(positionId);
40037
40760
  this.formulaDependencies().addDependencies(positionId, dependencies);
@@ -40041,12 +40764,12 @@ class Evaluator {
40041
40764
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
40042
40765
  }
40043
40766
  evaluateCells(positions) {
40044
- const cells = positions.map(this.encodePosition.bind(this));
40767
+ const cells = positions.map((p) => this.encoder.encode(p));
40045
40768
  const cellsToCompute = new JetSet(cells);
40046
- const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
40769
+ const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
40047
40770
  cellsToCompute.add(...this.getCellsDependingOn(cells));
40048
- cellsToCompute.add(...arrayFormulas);
40049
- cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
40771
+ cellsToCompute.add(...arrayFormulasPositionIds);
40772
+ cellsToCompute.add(...this.getCellsDependingOn(arrayFormulasPositionIds));
40050
40773
  this.evaluate(cellsToCompute);
40051
40774
  }
40052
40775
  getArrayFormulasImpactedByChangesOf(positionIds) {
@@ -40069,12 +40792,14 @@ class Evaluator {
40069
40792
  this.blockedArrayFormulas = new Set();
40070
40793
  this.spreadingRelations = new SpreadingRelation();
40071
40794
  this.formulaDependencies = lazy(() => {
40072
- const dependencyGraph = new FormulaDependencyGraph();
40073
- for (const positionId of this.getAllCells()) {
40074
- const dependencies = this.getDirectDependencies(positionId);
40075
- dependencyGraph.addDependencies(positionId, dependencies);
40076
- }
40077
- return dependencyGraph;
40795
+ const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
40796
+ data: positionId,
40797
+ boundingBox: {
40798
+ zone: range.zone,
40799
+ sheetId: range.sheetId,
40800
+ },
40801
+ })));
40802
+ return new FormulaDependencyGraph(this.encoder, dependencies);
40078
40803
  });
40079
40804
  }
40080
40805
  evaluateAllCells() {
@@ -40096,7 +40821,7 @@ class Evaluator {
40096
40821
  for (const sheetId of this.getters.getSheetIds()) {
40097
40822
  const cellIds = this.getters.getCells(sheetId);
40098
40823
  for (const cellId in cellIds) {
40099
- positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
40824
+ positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
40100
40825
  }
40101
40826
  }
40102
40827
  return positionIds;
@@ -40163,7 +40888,7 @@ class Evaluator {
40163
40888
  }
40164
40889
  }
40165
40890
  computeAndSave(position) {
40166
- const positionId = this.encodePosition(position);
40891
+ const positionId = this.encoder.encode(position);
40167
40892
  const evaluatedCell = this.computeCell(positionId);
40168
40893
  if (!this.evaluatedCells.has(positionId)) {
40169
40894
  this.setEvaluatedCell(positionId, evaluatedCell);
@@ -40223,15 +40948,15 @@ class Evaluator {
40223
40948
  throw new Error(_t("Result couldn't be automatically expanded. Please insert more columns and rows."));
40224
40949
  }
40225
40950
  updateSpreadRelation({ sheetId, col, row, }) {
40226
- const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
40951
+ const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
40227
40952
  return (i, j) => {
40228
40953
  const position = { sheetId, col: i + col, row: j + row };
40229
- const resultPositionId = this.encodePosition(position);
40954
+ const resultPositionId = this.encoder.encode(position);
40230
40955
  this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
40231
40956
  };
40232
40957
  }
40233
40958
  checkCollision({ sheetId, col, row }) {
40234
- const formulaPositionId = this.encodePosition({ sheetId, col, row });
40959
+ const formulaPositionId = this.encoder.encode({ sheetId, col, row });
40235
40960
  return (i, j) => {
40236
40961
  const position = { sheetId: sheetId, col: i + col, row: j + row };
40237
40962
  const rawCell = this.getters.getCell(position);
@@ -40252,7 +40977,7 @@ class Evaluator {
40252
40977
  format: format || matrixResult[i][j]?.format,
40253
40978
  locale: this.getters.getLocale(),
40254
40979
  });
40255
- const positionId = this.encodePosition(position);
40980
+ const positionId = this.encoder.encode(position);
40256
40981
  this.setEvaluatedCell(positionId, evaluatedCell);
40257
40982
  // check if formula dependencies present in the spread zone
40258
40983
  // if so, they need to be recomputed
@@ -40284,29 +41009,17 @@ class Evaluator {
40284
41009
  if (!cell?.isFormula) {
40285
41010
  return [];
40286
41011
  }
40287
- const dependencies = [];
40288
- for (const range of cell.compiledFormula.dependencies) {
40289
- if (range.invalidSheetName || range.invalidXc) {
40290
- continue;
40291
- }
40292
- const sheetId = range.sheetId;
40293
- forEachPositionsInZone(range.zone, (col, row) => {
40294
- dependencies.push(this.encodePosition({ sheetId, col, row }));
40295
- });
40296
- }
40297
- return dependencies;
41012
+ return cell.compiledFormula.dependencies;
40298
41013
  }
40299
41014
  getCellsDependingOn(positionIds) {
40300
- return this.formulaDependencies().getCellsDependingOn(positionIds);
41015
+ const ranges = [];
41016
+ for (const positionId of positionIds) {
41017
+ ranges.push(this.encoder.decodeToBoundingBox(positionId));
41018
+ }
41019
+ return this.formulaDependencies().getCellsDependingOn(ranges);
40301
41020
  }
40302
41021
  getCell(positionId) {
40303
- return this.getters.getCell(this.decodePosition(positionId));
40304
- }
40305
- encodePosition(position) {
40306
- return this.positionEncoder.encode(position);
40307
- }
40308
- decodePosition(positionId) {
40309
- return this.positionEncoder.decode(positionId);
41022
+ return this.getters.getCell(this.encoder.decode(positionId));
40310
41023
  }
40311
41024
  }
40312
41025
  function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
@@ -40362,6 +41075,13 @@ class PositionBitsEncoder {
40362
41075
  encode({ sheetId, col, row }) {
40363
41076
  return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
40364
41077
  }
41078
+ encodeBoundingBox({ sheetId, zone }) {
41079
+ const positions = [];
41080
+ forEachPositionsInZone(zone, (col, row) => {
41081
+ positions.push(this.encode({ sheetId, col, row }));
41082
+ });
41083
+ return positions;
41084
+ }
40365
41085
  decode(id) {
40366
41086
  // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
40367
41087
  const row = Number(id & 2097151n);
@@ -40369,6 +41089,10 @@ class PositionBitsEncoder {
40369
41089
  const sheetId = this.decodeSheet(id >> 42n);
40370
41090
  return { sheetId, col, row };
40371
41091
  }
41092
+ decodeToBoundingBox(id) {
41093
+ const { sheetId, col, row } = this.decode(id);
41094
+ return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
41095
+ }
40372
41096
  encodeSheet(sheetId) {
40373
41097
  const sheetKey = this.sheetMapping[sheetId];
40374
41098
  if (sheetKey === undefined) {
@@ -40539,7 +41263,12 @@ class EvaluationPlugin extends UIPlugin {
40539
41263
  // Getters
40540
41264
  // ---------------------------------------------------------------------------
40541
41265
  evaluateFormula(sheetId, formulaString) {
40542
- return this.evaluator.evaluateFormula(sheetId, formulaString);
41266
+ try {
41267
+ return this.evaluator.evaluateFormula(sheetId, formulaString);
41268
+ }
41269
+ catch (error) {
41270
+ return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
41271
+ }
40543
41272
  }
40544
41273
  /**
40545
41274
  * Return the value of each cell in the range as they are displayed in the grid.
@@ -40808,7 +41537,7 @@ class CustomColorsPlugin extends UIPlugin {
40808
41537
  }
40809
41538
 
40810
41539
  class EvaluationChartPlugin extends UIPlugin {
40811
- static getters = ["getChartRuntime", "getBackgroundOfSingleCellChart"];
41540
+ static getters = ["getChartRuntime", "getStyleOfSingleCellChart"];
40812
41541
  charts = {};
40813
41542
  createRuntimeChart = chartRuntimeFactory(this.getters);
40814
41543
  handle(cmd) {
@@ -40846,25 +41575,26 @@ class EvaluationChartPlugin extends UIPlugin {
40846
41575
  return this.charts[figureId];
40847
41576
  }
40848
41577
  /**
40849
- * Get the background color of a chart based on the color of the first cell of the main range
40850
- * of the chart. In order of priority, it will return :
40851
- *
40852
- * - the chart background color if one is defined
40853
- * - the fill color of the cell if one is defined
40854
- * - the fill color of the cell from conditional formats if one is defined
40855
- * - the default chart color if no other color is defined
41578
+ * Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.
40856
41579
  */
40857
- getBackgroundOfSingleCellChart(chartBackground, mainRange) {
41580
+ getStyleOfSingleCellChart(chartBackground, mainRange) {
40858
41581
  if (chartBackground)
40859
- return chartBackground;
41582
+ return { background: chartBackground, fontColor: chartFontColor(chartBackground) };
40860
41583
  if (!mainRange) {
40861
- return BACKGROUND_CHART_COLOR;
41584
+ return {
41585
+ background: BACKGROUND_CHART_COLOR,
41586
+ fontColor: chartFontColor(BACKGROUND_CHART_COLOR),
41587
+ };
40862
41588
  }
40863
41589
  const col = mainRange.zone.left;
40864
41590
  const row = mainRange.zone.top;
40865
41591
  const sheetId = mainRange.sheetId;
40866
41592
  const style = this.getters.getCellComputedStyle({ sheetId, col, row });
40867
- return style.fillColor || BACKGROUND_CHART_COLOR;
41593
+ const background = style.fillColor || BACKGROUND_CHART_COLOR;
41594
+ return {
41595
+ background,
41596
+ fontColor: style.textColor || chartFontColor(background),
41597
+ };
40868
41598
  }
40869
41599
  exportForExcel(data) {
40870
41600
  for (const sheet of data.sheets) {
@@ -40975,45 +41705,40 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
40975
41705
  getComputedStyles(sheetId) {
40976
41706
  const computedStyle = {};
40977
41707
  for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
40978
- try {
40979
- switch (cf.rule.type) {
40980
- case "ColorScaleRule":
40981
- for (let range of cf.ranges) {
40982
- this.applyColorScale(sheetId, range, cf.rule, computedStyle);
40983
- }
40984
- break;
40985
- case "CellIsRule":
40986
- const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
40987
- for (let ref of cf.ranges) {
40988
- const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
40989
- for (let row = zone.top; row <= zone.bottom; row++) {
40990
- for (let col = zone.left; col <= zone.right; col++) {
40991
- const predicate = this.rulePredicate[cf.rule.type];
40992
- const target = { sheetId, col, row };
40993
- const values = cf.rule.values.map((value, i) => {
40994
- const compiledFormula = formulas[i];
40995
- if (compiledFormula) {
40996
- return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
40997
- ...compiledFormula,
40998
- dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
40999
- });
41000
- }
41001
- return value;
41002
- });
41003
- if (predicate && predicate(target, { ...cf.rule, values })) {
41004
- if (!computedStyle[col])
41005
- computedStyle[col] = [];
41006
- // we must combine all the properties of all the CF rules applied to the given cell
41007
- computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
41708
+ switch (cf.rule.type) {
41709
+ case "ColorScaleRule":
41710
+ for (let range of cf.ranges) {
41711
+ this.applyColorScale(sheetId, range, cf.rule, computedStyle);
41712
+ }
41713
+ break;
41714
+ case "CellIsRule":
41715
+ const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
41716
+ for (let ref of cf.ranges) {
41717
+ const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
41718
+ for (let row = zone.top; row <= zone.bottom; row++) {
41719
+ for (let col = zone.left; col <= zone.right; col++) {
41720
+ const predicate = this.rulePredicate[cf.rule.type];
41721
+ const target = { sheetId, col, row };
41722
+ const values = cf.rule.values.map((value, i) => {
41723
+ const compiledFormula = formulas[i];
41724
+ if (compiledFormula) {
41725
+ return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
41726
+ ...compiledFormula,
41727
+ dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
41728
+ });
41008
41729
  }
41730
+ return value;
41731
+ });
41732
+ if (predicate && predicate(target, { ...cf.rule, values })) {
41733
+ if (!computedStyle[col])
41734
+ computedStyle[col] = [];
41735
+ // we must combine all the properties of all the CF rules applied to the given cell
41736
+ computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
41009
41737
  }
41010
41738
  }
41011
41739
  }
41012
- break;
41013
- }
41014
- }
41015
- catch (_) {
41016
- // we don't care about the errors within the evaluation of a rule
41740
+ }
41741
+ break;
41017
41742
  }
41018
41743
  }
41019
41744
  return computedStyle;
@@ -41784,19 +42509,21 @@ class AutofillPlugin extends UIPlugin {
41784
42509
  let col = zone.left;
41785
42510
  let row = zone.bottom;
41786
42511
  if (col > 0) {
41787
- let left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
41788
- while (left.type !== CellValueType.empty) {
42512
+ let leftPosition = { sheetId, col: col - 1, row };
42513
+ while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
42514
+ this.getters.getCell(leftPosition)?.content) {
41789
42515
  row += 1;
41790
- left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
42516
+ leftPosition = { sheetId, col: col - 1, row };
41791
42517
  }
41792
42518
  }
41793
42519
  if (row === zone.bottom) {
41794
42520
  col = zone.right;
41795
42521
  if (col <= this.getters.getNumberCols(sheetId)) {
41796
- let right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
41797
- while (right.type !== CellValueType.empty) {
42522
+ let rightPosition = { sheetId, col: col + 1, row };
42523
+ while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
42524
+ this.getters.getCell(rightPosition)?.content) {
41798
42525
  row += 1;
41799
- right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
42526
+ rightPosition = { sheetId, col: col + 1, row };
41800
42527
  }
41801
42528
  }
41802
42529
  }
@@ -42788,6 +43515,7 @@ class Session extends EventBus {
42788
43515
  isReplayingInitialRevisions = false;
42789
43516
  processedRevisions = new Set();
42790
43517
  uuidGenerator = new UuidGenerator();
43518
+ lastLocalOperation;
42791
43519
  /**
42792
43520
  * Manages the collaboration between multiple users on the same spreadsheet.
42793
43521
  * It can forward local state changes to other users to ensure they all eventually
@@ -42820,6 +43548,11 @@ class Session extends EventBus {
42820
43548
  return;
42821
43549
  const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, rootCommand, changes, Date.now());
42822
43550
  this.revisions.append(revision.id, revision);
43551
+ // REQUEST_REDO just repeats the last operation, the
43552
+ // last operation is still the same and should not change.
43553
+ if (rootCommand.type !== "REQUEST_REDO") {
43554
+ this.lastLocalOperation = revision;
43555
+ }
42823
43556
  this.trigger("new-local-state-update", { id: revision.id });
42824
43557
  this.sendUpdateMessage({
42825
43558
  type: "REMOTE_REVISION",
@@ -42916,19 +43649,10 @@ class Session extends EventBus {
42916
43649
  return this.pendingMessages.length === 0;
42917
43650
  }
42918
43651
  /**
42919
- * Get the last local revision whose root command isn't in the given list of ignored commands
43652
+ * Get the last local revision
42920
43653
  * */
42921
- getLastLocalNonEmptyRevision(ignoredRootCommands) {
42922
- const revisions = this.revisions.getRevertedExecution();
42923
- for (const rev of revisions) {
42924
- if (rev.rootCommand === "SNAPSHOT")
42925
- return undefined;
42926
- if (!rev.rootCommand || rev.rootCommand === "REMOTE")
42927
- continue;
42928
- if (!ignoredRootCommands.includes(rev.rootCommand?.type) && rev.commands.length)
42929
- return rev;
42930
- }
42931
- return undefined;
43654
+ getLastLocalNonEmptyRevision() {
43655
+ return this.lastLocalOperation;
42932
43656
  }
42933
43657
  _move(position) {
42934
43658
  // this method is debounced and might be called after the client
@@ -42988,7 +43712,7 @@ class Session extends EventBus {
42988
43712
  break;
42989
43713
  case "REMOTE_REVISION":
42990
43714
  const { clientId, commands, timestamp } = message;
42991
- const revision = new Revision(message.nextRevisionId, clientId, commands, "REMOTE", undefined, timestamp);
43715
+ const revision = new Revision(message.nextRevisionId, clientId, commands, undefined, undefined, timestamp);
42992
43716
  if (revision.clientId !== this.clientId) {
42993
43717
  this.revisions.insert(revision.id, revision, message.serverRevisionId);
42994
43718
  const pendingCommands = this.pendingMessages
@@ -43001,10 +43725,11 @@ class Session extends EventBus {
43001
43725
  }
43002
43726
  break;
43003
43727
  case "SNAPSHOT_CREATED": {
43004
- const revision = new Revision(message.nextRevisionId, "server", [], "SNAPSHOT", undefined, Date.now());
43728
+ const revision = new Revision(message.nextRevisionId, "server", [], undefined, undefined, Date.now());
43005
43729
  this.revisions.insert(revision.id, revision, message.serverRevisionId);
43006
43730
  this.dropPendingHistoryMessages();
43007
43731
  this.trigger("snapshot");
43732
+ this.lastLocalOperation = undefined;
43008
43733
  break;
43009
43734
  }
43010
43735
  }
@@ -45559,6 +46284,9 @@ class SelectionInputsManagerPlugin extends UIPlugin {
45559
46284
  // Other
45560
46285
  // ---------------------------------------------------------------------------
45561
46286
  initInput(id, initialRanges, inputHasSingleRange = false) {
46287
+ if (this.inputs[id]) {
46288
+ this.unfocus();
46289
+ }
45562
46290
  this.inputs[id] = new SelectionInputPlugin(this.config, initialRanges, inputHasSingleRange);
45563
46291
  if (initialRanges.length === 0) {
45564
46292
  const input = this.inputs[id];
@@ -46357,7 +47085,7 @@ class HistoryPlugin extends UIPlugin {
46357
47085
  * Ignore standard undo/redo revisions (that are empty)
46358
47086
  */
46359
47087
  getPossibleRevisionToRepeat() {
46360
- return this.session.getLastLocalNonEmptyRevision(["REQUEST_REDO"]);
47088
+ return this.session.getLastLocalNonEmptyRevision();
46361
47089
  }
46362
47090
  }
46363
47091
 
@@ -47142,6 +47870,10 @@ function setXcToReferenceType(xc, referenceType) {
47142
47870
  return xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
47143
47871
  case "colrow":
47144
47872
  indexOfNumber = xc.search(/[0-9]/);
47873
+ if (indexOfNumber === -1 || indexOfNumber === 0) {
47874
+ // no row number (eg. A) or no column (eg. 1)
47875
+ return "$" + xc;
47876
+ }
47145
47877
  xc = xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
47146
47878
  return "$" + xc;
47147
47879
  case "none":
@@ -47912,6 +48644,12 @@ class FilterEvaluationPlugin extends UIPlugin {
47912
48644
  break;
47913
48645
  case "HIDE_COLUMNS_ROWS":
47914
48646
  case "UNHIDE_COLUMNS_ROWS":
48647
+ case "GROUP_HEADERS":
48648
+ case "UNGROUP_HEADERS":
48649
+ case "FOLD_HEADER_GROUP":
48650
+ case "UNFOLD_HEADER_GROUP":
48651
+ case "FOLD_ALL_HEADER_GROUPS":
48652
+ case "UNFOLD_ALL_HEADER_GROUPS":
47915
48653
  this.updateHiddenRows();
47916
48654
  break;
47917
48655
  case "UPDATE_FILTER":
@@ -48613,7 +49351,7 @@ class GridSelectionPlugin extends UIPlugin {
48613
49351
  sheetId: cmd.sheetId,
48614
49352
  base: cmd.base,
48615
49353
  quantity: thickness,
48616
- position: "before",
49354
+ position: cmd.position,
48617
49355
  });
48618
49356
  const isCol = cmd.dimension === "COL";
48619
49357
  const start = cmd.elements[0];
@@ -48630,12 +49368,13 @@ class GridSelectionPlugin extends UIPlugin {
48630
49368
  },
48631
49369
  ];
48632
49370
  const state = new ClipboardCellsState(target, "CUT", this.getters, this.dispatch, this.selection);
49371
+ const base = isBasedBefore ? cmd.base : cmd.base + 1;
48633
49372
  const pasteTarget = [
48634
49373
  {
48635
- left: isCol ? cmd.base : 0,
48636
- right: isCol ? cmd.base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
48637
- top: !isCol ? cmd.base : 0,
48638
- bottom: !isCol ? cmd.base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
49374
+ left: isCol ? base : 0,
49375
+ right: isCol ? base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
49376
+ top: !isCol ? base : 0,
49377
+ bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
48639
49378
  },
48640
49379
  ];
48641
49380
  state.paste(pasteTarget, { selectTarget: true });
@@ -48670,6 +49409,11 @@ class GridSelectionPlugin extends UIPlugin {
48670
49409
  doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {
48671
49410
  return "WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */;
48672
49411
  }
49412
+ const headers = [cmd.base, ...cmd.elements];
49413
+ const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);
49414
+ if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {
49415
+ return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
49416
+ }
48673
49417
  return "Success" /* CommandResult.Success */;
48674
49418
  }
48675
49419
  //-------------------------------------------
@@ -49715,10 +50459,13 @@ class HeaderPositionsUIPlugin extends UIPlugin {
49715
50459
  }
49716
50460
  break;
49717
50461
  case "UPDATE_CELL":
49718
- if ("content" in cmd || "format" in cmd || cmd.style?.fontSize !== undefined) {
50462
+ if ("content" in cmd || "format" in cmd) {
49719
50463
  this.headerPositions = {};
49720
50464
  this.isDirty = true;
49721
50465
  }
50466
+ else {
50467
+ this.headerPositions[cmd.sheetId] = this.computeHeaderPositionsOfSheet(cmd.sheetId);
50468
+ }
49722
50469
  break;
49723
50470
  case "UPDATE_FILTER":
49724
50471
  case "REMOVE_FILTER_TABLE":
@@ -51875,6 +52622,8 @@ css /* scss */ `
51875
52622
  *:before,
51876
52623
  *:after {
51877
52624
  box-sizing: content-box;
52625
+ /** rtl not supported ATM */
52626
+ direction: ltr;
51878
52627
  }
51879
52628
  .o-separator {
51880
52629
  border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
@@ -52152,7 +52901,7 @@ class Spreadsheet extends Component {
52152
52901
  }
52153
52902
  onKeydown(ev) {
52154
52903
  let keyDownString = "";
52155
- if (ev.ctrlKey || ev.metaKey) {
52904
+ if (isCtrlKey(ev)) {
52156
52905
  keyDownString += "CTRL+";
52157
52906
  }
52158
52907
  keyDownString += ev.key.toUpperCase();
@@ -55864,6 +56613,7 @@ const helpers = {
55864
56613
  colorToRGBA,
55865
56614
  positionToZone,
55866
56615
  isDefined: isDefined$1,
56616
+ isMatrix,
55867
56617
  lazy,
55868
56618
  genericRepeat,
55869
56619
  createAction,
@@ -55912,6 +56662,6 @@ const constants = {
55912
56662
  export { AbstractChart, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
55913
56663
 
55914
56664
 
55915
- __info__.version = "17.0.4";
55916
- __info__.date = "2023-11-24T13:13:42.575Z";
55917
- __info__.hash = "7346d0a";
56665
+ __info__.version = "17.0.6";
56666
+ __info__.date = "2024-01-04T12:23:10.357Z";
56667
+ __info__.hash = "310ca57";