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