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