@odoo/o-spreadsheet 16.4.15 → 16.4.17

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.
@@ -301,13 +301,6 @@ const NEWLINE = "\n";
301
301
  const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];
302
302
 
303
303
  //------------------------------------------------------------------------------
304
- /**
305
- * Stringify an object, like JSON.stringify, except that the first level of keys
306
- * is ordered.
307
- */
308
- function stringify(obj) {
309
- return JSON.stringify(obj, Object.keys(obj).sort());
310
- }
311
304
  /**
312
305
  * Remove quotes from a quoted string
313
306
  * ```js
@@ -527,7 +520,7 @@ function isObjectEmptyRecursive(argument) {
527
520
  */
528
521
  function getItemId(item, itemsDic) {
529
522
  for (let [key, value] of Object.entries(itemsDic)) {
530
- if (stringify(value) === stringify(item)) {
523
+ if (deepEquals(value, item)) {
531
524
  return parseInt(key, 10);
532
525
  }
533
526
  }
@@ -2345,8 +2338,10 @@ function convertInternalFormatToFormat(internalFormat) {
2345
2338
  const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
2346
2339
  // Same as above, but matches the exact string (nothing before or after)
2347
2340
  const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
2348
- /** Reference of a column header (eg. A, AB) */
2349
- const colHeader = new RegExp(/^([A-Z]{1,3})+$/, "i");
2341
+ /** Reference of a column header (eg. A, AB, $A) */
2342
+ const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
2343
+ /** Reference of a row header (eg. 1, $1) */
2344
+ const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
2350
2345
  /** Reference of a column (eg. A, $CA, Sheet1!B) */
2351
2346
  const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
2352
2347
  /** Reference of a row (eg. 1, 59, Sheet1!9) */
@@ -2376,6 +2371,9 @@ function isRowReference(xc) {
2376
2371
  function isColHeader(str) {
2377
2372
  return colHeader.test(str);
2378
2373
  }
2374
+ function isRowHeader(str) {
2375
+ return rowHeader.test(str);
2376
+ }
2379
2377
  /**
2380
2378
  * Return true if the given xc is the reference of a single cell,
2381
2379
  * without any specified sheet (e.g. A1)
@@ -3048,18 +3046,10 @@ class RangeImpl {
3048
3046
  if (isFullCol) {
3049
3047
  parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3050
3048
  parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3051
- if (zone.left === zone.right) {
3052
- parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
3053
- parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
3054
- }
3055
3049
  }
3056
3050
  if (isFullRow) {
3057
3051
  parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
3058
3052
  parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
3059
- if (zone.top === zone.bottom) {
3060
- parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3061
- parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3062
- }
3063
3053
  }
3064
3054
  return parts;
3065
3055
  }
@@ -5611,6 +5601,7 @@ const FilterMenuPopoverBuilder = {
5611
5601
  },
5612
5602
  };
5613
5603
 
5604
+ const macRegex = /Mac/i;
5614
5605
  /**
5615
5606
  * Return true if the event was triggered from
5616
5607
  * a child element.
@@ -5648,6 +5639,17 @@ function* iterateChildren(el) {
5648
5639
  }
5649
5640
  function getOpenedMenus() {
5650
5641
  return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
5642
+ }
5643
+ function isMacOS() {
5644
+ return Boolean(macRegex.test(navigator.userAgent));
5645
+ }
5646
+ /**
5647
+ * @param {KeyboardEvent | MouseEvent} ev
5648
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
5649
+ * On Mac, this is the "meta" or "command" key.
5650
+ */
5651
+ function isCtrlKey(ev) {
5652
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
5651
5653
  }
5652
5654
 
5653
5655
  /**
@@ -14704,7 +14706,7 @@ const machine = {
14704
14706
  SPACE: goTo(State.RightRef),
14705
14707
  NUMBER: goTo(State.Found),
14706
14708
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
14707
- SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
14709
+ SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
14708
14710
  },
14709
14711
  [State.RightColumnRef]: {
14710
14712
  SPACE: goTo(State.RightColumnRef),
@@ -14715,6 +14717,7 @@ const machine = {
14715
14717
  SPACE: goTo(State.RightRowRef),
14716
14718
  NUMBER: goTo(State.Found),
14717
14719
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
14720
+ SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
14718
14721
  },
14719
14722
  [State.Found]: {},
14720
14723
  };
@@ -16872,7 +16875,7 @@ function createGaugeChartRuntime(chart, getters) {
16872
16875
  });
16873
16876
  return {
16874
16877
  chartJsConfig: config,
16875
- background: getters.getBackgroundOfSingleCellChart(chart.background, dataRange),
16878
+ background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
16876
16879
  };
16877
16880
  }
16878
16881
 
@@ -17596,7 +17599,7 @@ function createScorecardChartRuntime(chart, getters) {
17596
17599
  };
17597
17600
  baselineCell = getters.getEvaluatedCell(baselinePosition);
17598
17601
  }
17599
- const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue);
17602
+ const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
17600
17603
  const locale = getters.getLocale();
17601
17604
  return {
17602
17605
  title: _t(chart.title),
@@ -17605,7 +17608,7 @@ function createScorecardChartRuntime(chart, getters) {
17605
17608
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
17606
17609
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
17607
17610
  baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
17608
- fontColor: chartFontColor(background),
17611
+ fontColor,
17609
17612
  background,
17610
17613
  baselineStyle: chart.baselineMode !== "percentage" && baseline
17611
17614
  ? getters.getCellStyle({
@@ -20500,10 +20503,10 @@ const arrowMap = {
20500
20503
  function updateSelectionWithArrowKeys(ev, selection) {
20501
20504
  const direction = arrowMap[ev.key];
20502
20505
  if (ev.shiftKey) {
20503
- selection.resizeAnchorZone(direction, ev.ctrlKey ? "end" : 1);
20506
+ selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
20504
20507
  }
20505
20508
  else {
20506
- selection.moveAnchorCell(direction, ev.ctrlKey ? "end" : 1);
20509
+ selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
20507
20510
  }
20508
20511
  }
20509
20512
 
@@ -25548,7 +25551,10 @@ class GridOverlay extends owl.Component {
25548
25551
  return;
25549
25552
  }
25550
25553
  const [col, row] = this.getCartesianCoordinates(ev);
25551
- this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey });
25554
+ this.props.onCellClicked(col, row, {
25555
+ expandZone: ev.shiftKey,
25556
+ addZone: isCtrlKey(ev),
25557
+ });
25552
25558
  }
25553
25559
  onDoubleClick(ev) {
25554
25560
  const [col, row] = this.getCartesianCoordinates(ev);
@@ -25621,6 +25627,7 @@ class AbstractResizer extends owl.Component {
25621
25627
  draggerShadowThickness: 0,
25622
25628
  delta: 0,
25623
25629
  base: 0,
25630
+ position: "before",
25624
25631
  });
25625
25632
  _computeHandleDisplay(ev) {
25626
25633
  const position = this._getEvOffset(ev);
@@ -25745,11 +25752,13 @@ class AbstractResizer extends owl.Component {
25745
25752
  this.state.draggerLinePosition = dimensions.start;
25746
25753
  this.state.draggerShadowPosition = dimensions.start;
25747
25754
  this.state.base = elementIndex;
25755
+ this.state.position = "before";
25748
25756
  }
25749
25757
  else if (this._getSelectedZoneEnd() < elementIndex) {
25750
25758
  this.state.draggerLinePosition = dimensions.end;
25751
25759
  this.state.draggerShadowPosition = dimensions.end - this.state.draggerShadowThickness;
25752
- this.state.base = elementIndex + 1;
25760
+ this.state.base = elementIndex;
25761
+ this.state.position = "after";
25753
25762
  }
25754
25763
  else {
25755
25764
  this.state.draggerLinePosition = startDimensions.start;
@@ -25773,7 +25782,7 @@ class AbstractResizer extends owl.Component {
25773
25782
  this._increaseSelection(index);
25774
25783
  }
25775
25784
  else {
25776
- this._selectElement(index, ev.ctrlKey);
25785
+ this._selectElement(index, isCtrlKey(ev));
25777
25786
  }
25778
25787
  this.lastSelectedElementIndex = index;
25779
25788
  const mouseMoveSelect = (col, row) => {
@@ -25786,7 +25795,7 @@ class AbstractResizer extends owl.Component {
25786
25795
  const mouseUpSelect = () => {
25787
25796
  this.state.isSelecting = false;
25788
25797
  this.lastSelectedElementIndex = null;
25789
- this.env.model.dispatch(ev.ctrlKey ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
25798
+ this.env.model.dispatch(isCtrlKey(ev) ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
25790
25799
  this._computeGrabDisplay(ev);
25791
25800
  };
25792
25801
  dragAndDropBeyondTheViewport(this.env, mouseMoveSelect, mouseUpSelect);
@@ -25932,13 +25941,14 @@ class ColResizer extends AbstractResizer {
25932
25941
  dimension: "COL",
25933
25942
  base: this.state.base,
25934
25943
  elements,
25944
+ position: this.state.position,
25935
25945
  });
25936
25946
  if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
25937
25947
  this.env.raiseError(MergeErrorMessage);
25938
25948
  }
25939
25949
  }
25940
- _selectElement(index, ctrlKey) {
25941
- this.env.model.selection.selectColumn(index, ctrlKey ? "newAnchor" : "overrideSelection");
25950
+ _selectElement(index, addDistinctHeader) {
25951
+ this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
25942
25952
  }
25943
25953
  _increaseSelection(index) {
25944
25954
  this.env.model.selection.selectColumn(index, "updateAnchor");
@@ -26105,13 +26115,14 @@ class RowResizer extends AbstractResizer {
26105
26115
  dimension: "ROW",
26106
26116
  base: this.state.base,
26107
26117
  elements,
26118
+ position: this.state.position,
26108
26119
  });
26109
26120
  if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
26110
26121
  this.env.raiseError(MergeErrorMessage);
26111
26122
  }
26112
26123
  }
26113
- _selectElement(index, ctrlKey) {
26114
- this.env.model.selection.selectRow(index, ctrlKey ? "newAnchor" : "overrideSelection");
26124
+ _selectElement(index, addDistinctHeader) {
26125
+ this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
26115
26126
  }
26116
26127
  _increaseSelection(index) {
26117
26128
  this.env.model.selection.selectRow(index, "updateAnchor");
@@ -26212,8 +26223,8 @@ function useWheelHandler(handler) {
26212
26223
  return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);
26213
26224
  }
26214
26225
  const onMouseWheel = (ev) => {
26215
- const deltaX = normalize(ev.shiftKey ? ev.deltaY : ev.deltaX, ev.deltaMode);
26216
- const deltaY = normalize(ev.shiftKey ? ev.deltaX : ev.deltaY, ev.deltaMode);
26226
+ const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode);
26227
+ const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode);
26217
26228
  handler(deltaX, deltaY);
26218
26229
  };
26219
26230
  return onMouseWheel;
@@ -26917,8 +26928,8 @@ class Grid extends owl.Component {
26917
26928
  // ---------------------------------------------------------------------------
26918
26929
  // Zone selection with mouse
26919
26930
  // ---------------------------------------------------------------------------
26920
- onCellClicked(col, row, { ctrlKey, shiftKey }) {
26921
- if (ctrlKey) {
26931
+ onCellClicked(col, row, { addZone, expandZone }) {
26932
+ if (addZone) {
26922
26933
  this.env.model.dispatch("PREPARE_SELECTION_INPUT_EXPANSION");
26923
26934
  }
26924
26935
  if (this.env.model.getters.hasOpenedPopover()) {
@@ -26927,10 +26938,10 @@ class Grid extends owl.Component {
26927
26938
  if (this.env.model.getters.getEditionMode() === "editing") {
26928
26939
  this.env.model.dispatch("STOP_EDITION");
26929
26940
  }
26930
- if (shiftKey) {
26941
+ if (expandZone) {
26931
26942
  this.env.model.selection.setAnchorCorner(col, row);
26932
26943
  }
26933
- else if (ctrlKey) {
26944
+ else if (addZone) {
26934
26945
  this.env.model.selection.addCellToSelection(col, row);
26935
26946
  }
26936
26947
  else {
@@ -26992,9 +27003,7 @@ class Grid extends owl.Component {
26992
27003
  return;
26993
27004
  }
26994
27005
  let keyDownString = "";
26995
- if (ev.ctrlKey)
26996
- keyDownString += "CTRL+";
26997
- if (ev.metaKey)
27006
+ if (isCtrlKey(ev))
26998
27007
  keyDownString += "CTRL+";
26999
27008
  if (ev.altKey)
27000
27009
  keyDownString += "ALT+";
@@ -31642,7 +31651,7 @@ class BordersPlugin extends CorePlugin {
31642
31651
  */
31643
31652
  function getBorderId(border) {
31644
31653
  for (let [key, value] of Object.entries(borders)) {
31645
- if (stringify(value) === stringify(border)) {
31654
+ if (deepEquals(value, border)) {
31646
31655
  return parseInt(key, 10);
31647
31656
  }
31648
31657
  }
@@ -34551,7 +34560,7 @@ class SheetPlugin extends CorePlugin {
34551
34560
  const elements = cmd.dimension === "COL"
34552
34561
  ? this.getNumberCols(cmd.sheetId)
34553
34562
  : this.getNumberRows(cmd.sheetId);
34554
- if (cmd.base < 0 || cmd.base > elements) {
34563
+ if (cmd.base < 0 || cmd.base >= elements) {
34555
34564
  return 88 /* CommandResult.InvalidHeaderIndex */;
34556
34565
  }
34557
34566
  else if (cmd.quantity <= 0) {
@@ -35492,10 +35501,13 @@ class CompilationParametersBuilder {
35492
35501
  if (evaluatedCell === undefined) {
35493
35502
  return { value: null, format: this.getters.getCell(position)?.format };
35494
35503
  }
35504
+ if (evaluatedCell.type === CellValueType.error) {
35505
+ throw evaluatedCell.error;
35506
+ }
35495
35507
  return evaluatedCell;
35496
35508
  }
35497
35509
  getEvaluatedCellIfNotEmpty(position) {
35498
- const evaluatedCell = this.getEvaluatedCell(position);
35510
+ const evaluatedCell = this.computeCell(position);
35499
35511
  if (evaluatedCell.type === CellValueType.empty) {
35500
35512
  const cell = this.getters.getCell(position);
35501
35513
  if (!cell || (!cell.isFormula && cell.content === "")) {
@@ -35504,13 +35516,6 @@ class CompilationParametersBuilder {
35504
35516
  }
35505
35517
  return evaluatedCell;
35506
35518
  }
35507
- getEvaluatedCell(position) {
35508
- const evaluatedCell = this.computeCell(position);
35509
- if (evaluatedCell.type === CellValueType.error) {
35510
- throw evaluatedCell.error;
35511
- }
35512
- return evaluatedCell;
35513
- }
35514
35519
  /**
35515
35520
  * Return the values of the cell(s) used in reference, but always in the format of a range even
35516
35521
  * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
@@ -35533,7 +35538,11 @@ class CompilationParametersBuilder {
35533
35538
  const { top, left, bottom, right } = zone;
35534
35539
  const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;
35535
35540
  if (cacheKey in this.rangeCache) {
35536
- return this.rangeCache[cacheKey];
35541
+ const result = this.rangeCache[cacheKey];
35542
+ if (result instanceof EvaluationError) {
35543
+ throw result;
35544
+ }
35545
+ return result;
35537
35546
  }
35538
35547
  const height = _zone.bottom - _zone.top + 1;
35539
35548
  const width = _zone.right - _zone.left + 1;
@@ -35546,6 +35555,10 @@ class CompilationParametersBuilder {
35546
35555
  format[colIndex] = new Array(height);
35547
35556
  for (let row = _zone.top; row <= _zone.bottom; row++) {
35548
35557
  const evaluatedCell = this.getEvaluatedCellIfNotEmpty({ sheetId, col, row });
35558
+ if (evaluatedCell?.type === CellValueType.error) {
35559
+ this.rangeCache[cacheKey] = evaluatedCell.error;
35560
+ throw evaluatedCell.error;
35561
+ }
35549
35562
  const rowIndex = row - _zone.top;
35550
35563
  value[colIndex][rowIndex] = evaluatedCell?.value;
35551
35564
  if (evaluatedCell?.format !== undefined) {
@@ -35559,56 +35572,758 @@ class CompilationParametersBuilder {
35559
35572
  }
35560
35573
  }
35561
35574
 
35575
+ function quickselect(arr, k, left, right, compare) {
35576
+ quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
35577
+ }
35578
+
35579
+ function quickselectStep(arr, k, left, right, compare) {
35580
+
35581
+ while (right > left) {
35582
+ if (right - left > 600) {
35583
+ var n = right - left + 1;
35584
+ var m = k - left + 1;
35585
+ var z = Math.log(n);
35586
+ var s = 0.5 * Math.exp(2 * z / 3);
35587
+ var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
35588
+ var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
35589
+ var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
35590
+ quickselectStep(arr, k, newLeft, newRight, compare);
35591
+ }
35592
+
35593
+ var t = arr[k];
35594
+ var i = left;
35595
+ var j = right;
35596
+
35597
+ swap(arr, left, k);
35598
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
35599
+
35600
+ while (i < j) {
35601
+ swap(arr, i, j);
35602
+ i++;
35603
+ j--;
35604
+ while (compare(arr[i], t) < 0) i++;
35605
+ while (compare(arr[j], t) > 0) j--;
35606
+ }
35607
+
35608
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
35609
+ else {
35610
+ j++;
35611
+ swap(arr, j, right);
35612
+ }
35613
+
35614
+ if (j <= k) left = j + 1;
35615
+ if (k <= j) right = j - 1;
35616
+ }
35617
+ }
35618
+
35619
+ function swap(arr, i, j) {
35620
+ var tmp = arr[i];
35621
+ arr[i] = arr[j];
35622
+ arr[j] = tmp;
35623
+ }
35624
+
35625
+ function defaultCompare(a, b) {
35626
+ return a < b ? -1 : a > b ? 1 : 0;
35627
+ }
35628
+
35629
+ class RBush {
35630
+ constructor(maxEntries = 9) {
35631
+ // max entries in a node is 9 by default; min node fill is 40% for best performance
35632
+ this._maxEntries = Math.max(4, maxEntries);
35633
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
35634
+ this.clear();
35635
+ }
35636
+
35637
+ all() {
35638
+ return this._all(this.data, []);
35639
+ }
35640
+
35641
+ search(bbox) {
35642
+ let node = this.data;
35643
+ const result = [];
35644
+
35645
+ if (!intersects(bbox, node)) return result;
35646
+
35647
+ const toBBox = this.toBBox;
35648
+ const nodesToSearch = [];
35649
+
35650
+ while (node) {
35651
+ for (let i = 0; i < node.children.length; i++) {
35652
+ const child = node.children[i];
35653
+ const childBBox = node.leaf ? toBBox(child) : child;
35654
+
35655
+ if (intersects(bbox, childBBox)) {
35656
+ if (node.leaf) result.push(child);
35657
+ else if (contains(bbox, childBBox)) this._all(child, result);
35658
+ else nodesToSearch.push(child);
35659
+ }
35660
+ }
35661
+ node = nodesToSearch.pop();
35662
+ }
35663
+
35664
+ return result;
35665
+ }
35666
+
35667
+ collides(bbox) {
35668
+ let node = this.data;
35669
+
35670
+ if (!intersects(bbox, node)) return false;
35671
+
35672
+ const nodesToSearch = [];
35673
+ while (node) {
35674
+ for (let i = 0; i < node.children.length; i++) {
35675
+ const child = node.children[i];
35676
+ const childBBox = node.leaf ? this.toBBox(child) : child;
35677
+
35678
+ if (intersects(bbox, childBBox)) {
35679
+ if (node.leaf || contains(bbox, childBBox)) return true;
35680
+ nodesToSearch.push(child);
35681
+ }
35682
+ }
35683
+ node = nodesToSearch.pop();
35684
+ }
35685
+
35686
+ return false;
35687
+ }
35688
+
35689
+ load(data) {
35690
+ if (!(data && data.length)) return this;
35691
+
35692
+ if (data.length < this._minEntries) {
35693
+ for (let i = 0; i < data.length; i++) {
35694
+ this.insert(data[i]);
35695
+ }
35696
+ return this;
35697
+ }
35698
+
35699
+ // recursively build the tree with the given data from scratch using OMT algorithm
35700
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
35701
+
35702
+ if (!this.data.children.length) {
35703
+ // save as is if tree is empty
35704
+ this.data = node;
35705
+
35706
+ } else if (this.data.height === node.height) {
35707
+ // split root if trees have the same height
35708
+ this._splitRoot(this.data, node);
35709
+
35710
+ } else {
35711
+ if (this.data.height < node.height) {
35712
+ // swap trees if inserted one is bigger
35713
+ const tmpNode = this.data;
35714
+ this.data = node;
35715
+ node = tmpNode;
35716
+ }
35717
+
35718
+ // insert the small tree into the large tree at appropriate level
35719
+ this._insert(node, this.data.height - node.height - 1, true);
35720
+ }
35721
+
35722
+ return this;
35723
+ }
35724
+
35725
+ insert(item) {
35726
+ if (item) this._insert(item, this.data.height - 1);
35727
+ return this;
35728
+ }
35729
+
35730
+ clear() {
35731
+ this.data = createNode([]);
35732
+ return this;
35733
+ }
35734
+
35735
+ remove(item, equalsFn) {
35736
+ if (!item) return this;
35737
+
35738
+ let node = this.data;
35739
+ const bbox = this.toBBox(item);
35740
+ const path = [];
35741
+ const indexes = [];
35742
+ let i, parent, goingUp;
35743
+
35744
+ // depth-first iterative tree traversal
35745
+ while (node || path.length) {
35746
+
35747
+ if (!node) { // go up
35748
+ node = path.pop();
35749
+ parent = path[path.length - 1];
35750
+ i = indexes.pop();
35751
+ goingUp = true;
35752
+ }
35753
+
35754
+ if (node.leaf) { // check current node
35755
+ const index = findItem(item, node.children, equalsFn);
35756
+
35757
+ if (index !== -1) {
35758
+ // item found, remove the item and condense tree upwards
35759
+ node.children.splice(index, 1);
35760
+ path.push(node);
35761
+ this._condense(path);
35762
+ return this;
35763
+ }
35764
+ }
35765
+
35766
+ if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
35767
+ path.push(node);
35768
+ indexes.push(i);
35769
+ i = 0;
35770
+ parent = node;
35771
+ node = node.children[0];
35772
+
35773
+ } else if (parent) { // go right
35774
+ i++;
35775
+ node = parent.children[i];
35776
+ goingUp = false;
35777
+
35778
+ } else node = null; // nothing found
35779
+ }
35780
+
35781
+ return this;
35782
+ }
35783
+
35784
+ toBBox(item) { return item; }
35785
+
35786
+ compareMinX(a, b) { return a.minX - b.minX; }
35787
+ compareMinY(a, b) { return a.minY - b.minY; }
35788
+
35789
+ toJSON() { return this.data; }
35790
+
35791
+ fromJSON(data) {
35792
+ this.data = data;
35793
+ return this;
35794
+ }
35795
+
35796
+ _all(node, result) {
35797
+ const nodesToSearch = [];
35798
+ while (node) {
35799
+ if (node.leaf) result.push(...node.children);
35800
+ else nodesToSearch.push(...node.children);
35801
+
35802
+ node = nodesToSearch.pop();
35803
+ }
35804
+ return result;
35805
+ }
35806
+
35807
+ _build(items, left, right, height) {
35808
+
35809
+ const N = right - left + 1;
35810
+ let M = this._maxEntries;
35811
+ let node;
35812
+
35813
+ if (N <= M) {
35814
+ // reached leaf level; return leaf
35815
+ node = createNode(items.slice(left, right + 1));
35816
+ calcBBox(node, this.toBBox);
35817
+ return node;
35818
+ }
35819
+
35820
+ if (!height) {
35821
+ // target height of the bulk-loaded tree
35822
+ height = Math.ceil(Math.log(N) / Math.log(M));
35823
+
35824
+ // target number of root entries to maximize storage utilization
35825
+ M = Math.ceil(N / Math.pow(M, height - 1));
35826
+ }
35827
+
35828
+ node = createNode([]);
35829
+ node.leaf = false;
35830
+ node.height = height;
35831
+
35832
+ // split the items into M mostly square tiles
35833
+
35834
+ const N2 = Math.ceil(N / M);
35835
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
35836
+
35837
+ multiSelect(items, left, right, N1, this.compareMinX);
35838
+
35839
+ for (let i = left; i <= right; i += N1) {
35840
+
35841
+ const right2 = Math.min(i + N1 - 1, right);
35842
+
35843
+ multiSelect(items, i, right2, N2, this.compareMinY);
35844
+
35845
+ for (let j = i; j <= right2; j += N2) {
35846
+
35847
+ const right3 = Math.min(j + N2 - 1, right2);
35848
+
35849
+ // pack each entry recursively
35850
+ node.children.push(this._build(items, j, right3, height - 1));
35851
+ }
35852
+ }
35853
+
35854
+ calcBBox(node, this.toBBox);
35855
+
35856
+ return node;
35857
+ }
35858
+
35859
+ _chooseSubtree(bbox, node, level, path) {
35860
+ while (true) {
35861
+ path.push(node);
35862
+
35863
+ if (node.leaf || path.length - 1 === level) break;
35864
+
35865
+ let minArea = Infinity;
35866
+ let minEnlargement = Infinity;
35867
+ let targetNode;
35868
+
35869
+ for (let i = 0; i < node.children.length; i++) {
35870
+ const child = node.children[i];
35871
+ const area = bboxArea(child);
35872
+ const enlargement = enlargedArea(bbox, child) - area;
35873
+
35874
+ // choose entry with the least area enlargement
35875
+ if (enlargement < minEnlargement) {
35876
+ minEnlargement = enlargement;
35877
+ minArea = area < minArea ? area : minArea;
35878
+ targetNode = child;
35879
+
35880
+ } else if (enlargement === minEnlargement) {
35881
+ // otherwise choose one with the smallest area
35882
+ if (area < minArea) {
35883
+ minArea = area;
35884
+ targetNode = child;
35885
+ }
35886
+ }
35887
+ }
35888
+
35889
+ node = targetNode || node.children[0];
35890
+ }
35891
+
35892
+ return node;
35893
+ }
35894
+
35895
+ _insert(item, level, isNode) {
35896
+ const bbox = isNode ? item : this.toBBox(item);
35897
+ const insertPath = [];
35898
+
35899
+ // find the best node for accommodating the item, saving all nodes along the path too
35900
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
35901
+
35902
+ // put the item into the node
35903
+ node.children.push(item);
35904
+ extend(node, bbox);
35905
+
35906
+ // split on node overflow; propagate upwards if necessary
35907
+ while (level >= 0) {
35908
+ if (insertPath[level].children.length > this._maxEntries) {
35909
+ this._split(insertPath, level);
35910
+ level--;
35911
+ } else break;
35912
+ }
35913
+
35914
+ // adjust bboxes along the insertion path
35915
+ this._adjustParentBBoxes(bbox, insertPath, level);
35916
+ }
35917
+
35918
+ // split overflowed node into two
35919
+ _split(insertPath, level) {
35920
+ const node = insertPath[level];
35921
+ const M = node.children.length;
35922
+ const m = this._minEntries;
35923
+
35924
+ this._chooseSplitAxis(node, m, M);
35925
+
35926
+ const splitIndex = this._chooseSplitIndex(node, m, M);
35927
+
35928
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
35929
+ newNode.height = node.height;
35930
+ newNode.leaf = node.leaf;
35931
+
35932
+ calcBBox(node, this.toBBox);
35933
+ calcBBox(newNode, this.toBBox);
35934
+
35935
+ if (level) insertPath[level - 1].children.push(newNode);
35936
+ else this._splitRoot(node, newNode);
35937
+ }
35938
+
35939
+ _splitRoot(node, newNode) {
35940
+ // split root node
35941
+ this.data = createNode([node, newNode]);
35942
+ this.data.height = node.height + 1;
35943
+ this.data.leaf = false;
35944
+ calcBBox(this.data, this.toBBox);
35945
+ }
35946
+
35947
+ _chooseSplitIndex(node, m, M) {
35948
+ let index;
35949
+ let minOverlap = Infinity;
35950
+ let minArea = Infinity;
35951
+
35952
+ for (let i = m; i <= M - m; i++) {
35953
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
35954
+ const bbox2 = distBBox(node, i, M, this.toBBox);
35955
+
35956
+ const overlap = intersectionArea(bbox1, bbox2);
35957
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
35958
+
35959
+ // choose distribution with minimum overlap
35960
+ if (overlap < minOverlap) {
35961
+ minOverlap = overlap;
35962
+ index = i;
35963
+
35964
+ minArea = area < minArea ? area : minArea;
35965
+
35966
+ } else if (overlap === minOverlap) {
35967
+ // otherwise choose distribution with minimum area
35968
+ if (area < minArea) {
35969
+ minArea = area;
35970
+ index = i;
35971
+ }
35972
+ }
35973
+ }
35974
+
35975
+ return index || M - m;
35976
+ }
35977
+
35978
+ // sorts node children by the best axis for split
35979
+ _chooseSplitAxis(node, m, M) {
35980
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
35981
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
35982
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
35983
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
35984
+
35985
+ // if total distributions margin value is minimal for x, sort by minX,
35986
+ // otherwise it's already sorted by minY
35987
+ if (xMargin < yMargin) node.children.sort(compareMinX);
35988
+ }
35989
+
35990
+ // total margin of all possible split distributions where each node is at least m full
35991
+ _allDistMargin(node, m, M, compare) {
35992
+ node.children.sort(compare);
35993
+
35994
+ const toBBox = this.toBBox;
35995
+ const leftBBox = distBBox(node, 0, m, toBBox);
35996
+ const rightBBox = distBBox(node, M - m, M, toBBox);
35997
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
35998
+
35999
+ for (let i = m; i < M - m; i++) {
36000
+ const child = node.children[i];
36001
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
36002
+ margin += bboxMargin(leftBBox);
36003
+ }
36004
+
36005
+ for (let i = M - m - 1; i >= m; i--) {
36006
+ const child = node.children[i];
36007
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
36008
+ margin += bboxMargin(rightBBox);
36009
+ }
36010
+
36011
+ return margin;
36012
+ }
36013
+
36014
+ _adjustParentBBoxes(bbox, path, level) {
36015
+ // adjust bboxes along the given tree path
36016
+ for (let i = level; i >= 0; i--) {
36017
+ extend(path[i], bbox);
36018
+ }
36019
+ }
36020
+
36021
+ _condense(path) {
36022
+ // go through the path, removing empty nodes and updating bboxes
36023
+ for (let i = path.length - 1, siblings; i >= 0; i--) {
36024
+ if (path[i].children.length === 0) {
36025
+ if (i > 0) {
36026
+ siblings = path[i - 1].children;
36027
+ siblings.splice(siblings.indexOf(path[i]), 1);
36028
+
36029
+ } else this.clear();
36030
+
36031
+ } else calcBBox(path[i], this.toBBox);
36032
+ }
36033
+ }
36034
+ }
36035
+
36036
+ function findItem(item, items, equalsFn) {
36037
+ if (!equalsFn) return items.indexOf(item);
36038
+
36039
+ for (let i = 0; i < items.length; i++) {
36040
+ if (equalsFn(item, items[i])) return i;
36041
+ }
36042
+ return -1;
36043
+ }
36044
+
36045
+ // calculate node's bbox from bboxes of its children
36046
+ function calcBBox(node, toBBox) {
36047
+ distBBox(node, 0, node.children.length, toBBox, node);
36048
+ }
36049
+
36050
+ // min bounding rectangle of node children from k to p-1
36051
+ function distBBox(node, k, p, toBBox, destNode) {
36052
+ if (!destNode) destNode = createNode(null);
36053
+ destNode.minX = Infinity;
36054
+ destNode.minY = Infinity;
36055
+ destNode.maxX = -Infinity;
36056
+ destNode.maxY = -Infinity;
36057
+
36058
+ for (let i = k; i < p; i++) {
36059
+ const child = node.children[i];
36060
+ extend(destNode, node.leaf ? toBBox(child) : child);
36061
+ }
36062
+
36063
+ return destNode;
36064
+ }
36065
+
36066
+ function extend(a, b) {
36067
+ a.minX = Math.min(a.minX, b.minX);
36068
+ a.minY = Math.min(a.minY, b.minY);
36069
+ a.maxX = Math.max(a.maxX, b.maxX);
36070
+ a.maxY = Math.max(a.maxY, b.maxY);
36071
+ return a;
36072
+ }
36073
+
36074
+ function compareNodeMinX(a, b) { return a.minX - b.minX; }
36075
+ function compareNodeMinY(a, b) { return a.minY - b.minY; }
36076
+
36077
+ function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
36078
+ function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
36079
+
36080
+ function enlargedArea(a, b) {
36081
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
36082
+ (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
36083
+ }
36084
+
36085
+ function intersectionArea(a, b) {
36086
+ const minX = Math.max(a.minX, b.minX);
36087
+ const minY = Math.max(a.minY, b.minY);
36088
+ const maxX = Math.min(a.maxX, b.maxX);
36089
+ const maxY = Math.min(a.maxY, b.maxY);
36090
+
36091
+ return Math.max(0, maxX - minX) *
36092
+ Math.max(0, maxY - minY);
36093
+ }
36094
+
36095
+ function contains(a, b) {
36096
+ return a.minX <= b.minX &&
36097
+ a.minY <= b.minY &&
36098
+ b.maxX <= a.maxX &&
36099
+ b.maxY <= a.maxY;
36100
+ }
36101
+
36102
+ function intersects(a, b) {
36103
+ return b.minX <= a.maxX &&
36104
+ b.minY <= a.maxY &&
36105
+ b.maxX >= a.minX &&
36106
+ b.maxY >= a.minY;
36107
+ }
36108
+
36109
+ function createNode(children) {
36110
+ return {
36111
+ children,
36112
+ height: 1,
36113
+ leaf: true,
36114
+ minX: Infinity,
36115
+ minY: Infinity,
36116
+ maxX: -Infinity,
36117
+ maxY: -Infinity
36118
+ };
36119
+ }
36120
+
36121
+ // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
36122
+ // combines selection algorithm with binary divide & conquer approach
36123
+
36124
+ function multiSelect(arr, left, right, n, compare) {
36125
+ const stack = [left, right];
36126
+
36127
+ while (stack.length) {
36128
+ right = stack.pop();
36129
+ left = stack.pop();
36130
+
36131
+ if (right - left <= n) continue;
36132
+
36133
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
36134
+ quickselect(arr, mid, left, right, compare);
36135
+
36136
+ stack.push(left, mid, mid, right);
36137
+ }
36138
+ }
36139
+
36140
+ /**
36141
+ * R-Tree Data Structure
36142
+ *
36143
+ * R-Tree is a spatial data structure used for efficient indexing and querying
36144
+ * of multi-dimensional objects, particularly in geometric and spatial applications.
36145
+ *
36146
+ * It organizes objects into a tree hierarchy, grouping nearby objects together
36147
+ * in bounding boxes. Each node in the tree represents a bounding box that
36148
+ * contains its child nodes or leaf objects. This hierarchical structure allows
36149
+ * for faster spatial queries.
36150
+ *
36151
+ * @see https://en.wikipedia.org/wiki/R-tree
36152
+ *
36153
+ * Consider a 2D Space with four zones: A, B, C, D
36154
+ * +--------------------------+
36155
+ * | |
36156
+ * | +---+ +-------+ |
36157
+ * | | A | | B | |
36158
+ * | +---+ +-------+ |
36159
+ * | |
36160
+ * | |
36161
+ * | +---+ |
36162
+ * | | C | |
36163
+ * | +---+ |
36164
+ * | +-----------+ |
36165
+ * | | D | |
36166
+ * | +-----------+ |
36167
+ * | |
36168
+ * +--------------------------+
36169
+ *
36170
+ * It groups together zones that are spatially close into a minimum bounding box.
36171
+ * For example, A and B are grouped together in rectangle R1, and C and D are grouped
36172
+ * in R2.
36173
+ *
36174
+ * R0
36175
+ * +--------------------------+
36176
+ * | R1 |
36177
+ * | +-----------------+ |
36178
+ * | | A | | B | |
36179
+ * | +-----------------+ |
36180
+ * | |
36181
+ * | R2 |
36182
+ * | +---+---+---+ |
36183
+ * | | | C | | |
36184
+ * | | +---+ | |
36185
+ * | +-----------+ |
36186
+ * | | D | |
36187
+ * | +-----------+ |
36188
+ * | |
36189
+ * +--------------------------+
36190
+ *
36191
+ * The tree would look like this:
36192
+ * R0
36193
+ * / \
36194
+ * / \
36195
+ * R1 R2
36196
+ * | |
36197
+ * A,B C,D
36198
+
36199
+ * Choosing how to group the zones is crucial for the performance of the tree.
36200
+ * Key considerations include avoiding excessive empty space coverage and minimizing overlap
36201
+ * to reduce the number of subtrees processed during searches.
36202
+ *
36203
+ * Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
36204
+ * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
36205
+ * the choice cannot be made based on this criterion due to the same enlargement for different groupings,
36206
+ * we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
36207
+ *
36208
+ * This implementation is tailored for spreadsheet use, indexing objects associated
36209
+ * with a zone and a sheet.
36210
+ *
36211
+ * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
36212
+ * @see https://github.com/mourner/rbush
36213
+ */
36214
+ class SpreadsheetRTree {
36215
+ /**
36216
+ * One 2D R-tree per sheet
36217
+ */
36218
+ rTrees = {};
36219
+ /**
36220
+ * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
36221
+ * faster than inserting items one by one. After bulk loading (bulk insertion into
36222
+ * an empty tree), subsequent query performance is also ~20-30% better.
36223
+ */
36224
+ constructor(items = []) {
36225
+ const rangesPerSheet = {};
36226
+ for (const item of items) {
36227
+ const sheetId = item.boundingBox.sheetId;
36228
+ if (!rangesPerSheet[sheetId]) {
36229
+ rangesPerSheet[sheetId] = [];
36230
+ }
36231
+ rangesPerSheet[sheetId].push(item);
36232
+ }
36233
+ for (const sheetId in rangesPerSheet) {
36234
+ this.rTrees[sheetId] = new ZoneRBush();
36235
+ this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
36236
+ }
36237
+ }
36238
+ insert(item) {
36239
+ const sheetId = item.boundingBox.sheetId;
36240
+ if (!this.rTrees[sheetId]) {
36241
+ this.rTrees[sheetId] = new ZoneRBush();
36242
+ }
36243
+ this.rTrees[sheetId].insert(item);
36244
+ }
36245
+ search({ zone, sheetId }) {
36246
+ if (!this.rTrees[sheetId]) {
36247
+ return [];
36248
+ }
36249
+ return this.rTrees[sheetId].search({
36250
+ minX: zone.left,
36251
+ minY: zone.top,
36252
+ maxX: zone.right,
36253
+ maxY: zone.bottom,
36254
+ });
36255
+ }
36256
+ remove(item) {
36257
+ const sheetId = item.boundingBox.sheetId;
36258
+ if (!this.rTrees[sheetId]) {
36259
+ return;
36260
+ }
36261
+ this.rTrees[sheetId].remove(item, deepEquals);
36262
+ }
36263
+ }
36264
+ /**
36265
+ * RBush extension to use zones as bounding boxes
36266
+ */
36267
+ class ZoneRBush extends RBush {
36268
+ toBBox({ boundingBox }) {
36269
+ const zone = boundingBox.zone;
36270
+ return {
36271
+ minX: zone.left,
36272
+ minY: zone.top,
36273
+ maxX: zone.right,
36274
+ maxY: zone.bottom,
36275
+ };
36276
+ }
36277
+ compareMinX(a, b) {
36278
+ return a.boundingBox.zone.left - b.boundingBox.zone.left;
36279
+ }
36280
+ compareMinY(a, b) {
36281
+ return a.boundingBox.zone.top - b.boundingBox.zone.top;
36282
+ }
36283
+ }
36284
+
35562
36285
  /**
35563
- * This class is an implementation of a dependency Graph.
36286
+ * Implementation of a dependency Graph.
35564
36287
  * The graph is used to evaluate the cells in the correct
35565
36288
  * order, and should be updated each time a cell's content is modified
35566
36289
  *
36290
+ * It uses an R-Tree data structure to efficiently find dependent cells.
35567
36291
  */
35568
36292
  class FormulaDependencyGraph {
35569
- /**
35570
- * Internal structure:
35571
- * - key: a cell position (encoded as an integer)
35572
- * - value: a set of cell positions that depends on the key
35573
- *
35574
- * Given
35575
- * - A1:"= B1 + SQRT(B2)"
35576
- * - C1:"= B1";
35577
- * - C2:"= C1"
35578
- *
35579
- * we will have something like:
35580
- * - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
35581
- * - B2 ---> (A1) meaning A1 depends on B2
35582
- * - C1 ---> (C2) meaning C2 depends on C1
35583
- */
35584
- inverseDependencies = new Map();
36293
+ encoder;
35585
36294
  dependencies = new Map();
36295
+ rTree;
36296
+ constructor(encoder, data = []) {
36297
+ this.encoder = encoder;
36298
+ this.rTree = new SpreadsheetRTree(data);
36299
+ }
35586
36300
  removeAllDependencies(formulaPositionId) {
35587
- const dependencies = this.dependencies.get(formulaPositionId);
35588
- if (!dependencies) {
36301
+ const ranges = this.dependencies.get(formulaPositionId);
36302
+ if (!ranges) {
35589
36303
  return;
35590
36304
  }
35591
- for (const dependency of dependencies) {
35592
- this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
36305
+ for (const range of ranges) {
36306
+ this.rTree.remove(range);
35593
36307
  }
35594
36308
  this.dependencies.delete(formulaPositionId);
35595
36309
  }
35596
36310
  addDependencies(formulaPositionId, dependencies) {
35597
- for (const dependency of dependencies) {
35598
- const inverseDependencies = this.inverseDependencies.get(dependency);
35599
- if (inverseDependencies) {
35600
- inverseDependencies.add(formulaPositionId);
35601
- }
35602
- else {
35603
- this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
35604
- }
36311
+ const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
36312
+ data: formulaPositionId,
36313
+ boundingBox: {
36314
+ zone,
36315
+ sheetId,
36316
+ },
36317
+ }));
36318
+ for (const item of rTreeItems) {
36319
+ this.rTree.insert(item);
35605
36320
  }
35606
36321
  const existingDependencies = this.dependencies.get(formulaPositionId);
35607
36322
  if (existingDependencies) {
35608
- existingDependencies.push(...dependencies);
36323
+ existingDependencies.push(...rTreeItems);
35609
36324
  }
35610
36325
  else {
35611
- this.dependencies.set(formulaPositionId, dependencies);
36326
+ this.dependencies.set(formulaPositionId, rTreeItems);
35612
36327
  }
35613
36328
  }
35614
36329
  /**
@@ -35616,20 +36331,20 @@ class FormulaDependencyGraph {
35616
36331
  * in the correct order they should be evaluated.
35617
36332
  * This is called a topological ordering (excluding cycles)
35618
36333
  */
35619
- getCellsDependingOn(positionIds) {
36334
+ getCellsDependingOn(ranges) {
35620
36335
  const visited = new JetSet();
35621
- const queue = Array.from(positionIds).reverse();
36336
+ const queue = Array.from(ranges).reverse();
35622
36337
  while (queue.length > 0) {
35623
- const node = queue.pop();
35624
- visited.add(node);
35625
- const adjacentNodes = this.inverseDependencies.get(node) || new Set();
35626
- for (const adjacentNode of adjacentNodes) {
35627
- if (!visited.has(adjacentNode)) {
35628
- queue.push(adjacentNode);
36338
+ const range = queue.pop();
36339
+ visited.add(...this.encoder.encodeBoundingBox(range));
36340
+ const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
36341
+ for (const positionId of impactedPositionIds) {
36342
+ if (!visited.has(positionId)) {
36343
+ queue.push(this.encoder.decodeToBoundingBox(positionId));
35629
36344
  }
35630
36345
  }
35631
36346
  }
35632
- visited.delete(...positionIds);
36347
+ visited.delete(...ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
35633
36348
  return visited;
35634
36349
  }
35635
36350
  }
@@ -35717,9 +36432,9 @@ class Evaluator {
35717
36432
  context;
35718
36433
  getters;
35719
36434
  compilationParams;
35720
- positionEncoder = new PositionBitsEncoder();
36435
+ encoder = new PositionBitsEncoder();
35721
36436
  evaluatedCells = new Map();
35722
- formulaDependencies = lazy(new FormulaDependencyGraph());
36437
+ formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
35723
36438
  blockedArrayFormulas = new Set();
35724
36439
  spreadingRelations = new SpreadingRelation();
35725
36440
  constructor(context, getters) {
@@ -35728,16 +36443,16 @@ class Evaluator {
35728
36443
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
35729
36444
  }
35730
36445
  getEvaluatedCell(position) {
35731
- return (this.evaluatedCells.get(this.encodePosition(position)) ||
36446
+ return (this.evaluatedCells.get(this.encoder.encode(position)) ||
35732
36447
  createEvaluatedCell("", { locale: this.getters.getLocale() }));
35733
36448
  }
35734
36449
  getArrayFormulaSpreadingOn(position) {
35735
- const positionId = this.encodePosition(position);
36450
+ const positionId = this.encoder.encode(position);
35736
36451
  const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
35737
- return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
36452
+ return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
35738
36453
  }
35739
36454
  getEvaluatedPositions() {
35740
- return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
36455
+ return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
35741
36456
  }
35742
36457
  getArrayFormulaSpreadingOnId(positionId) {
35743
36458
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
@@ -35747,7 +36462,7 @@ class Evaluator {
35747
36462
  return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
35748
36463
  }
35749
36464
  updateDependencies(position) {
35750
- const positionId = this.encodePosition(position);
36465
+ const positionId = this.encoder.encode(position);
35751
36466
  this.formulaDependencies().removeAllDependencies(positionId);
35752
36467
  const dependencies = this.getDirectDependencies(positionId);
35753
36468
  this.formulaDependencies().addDependencies(positionId, dependencies);
@@ -35757,12 +36472,12 @@ class Evaluator {
35757
36472
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
35758
36473
  }
35759
36474
  evaluateCells(positions) {
35760
- const cells = positions.map(this.encodePosition.bind(this));
36475
+ const cells = positions.map((p) => this.encoder.encode(p));
35761
36476
  const cellsToCompute = new JetSet(cells);
35762
- const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
36477
+ const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
35763
36478
  cellsToCompute.add(...this.getCellsDependingOn(cells));
35764
- cellsToCompute.add(...arrayFormulas);
35765
- cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
36479
+ cellsToCompute.add(...arrayFormulasPositionIds);
36480
+ cellsToCompute.add(...this.getCellsDependingOn(arrayFormulasPositionIds));
35766
36481
  this.evaluate(cellsToCompute);
35767
36482
  }
35768
36483
  getArrayFormulasImpactedByChangesOf(positionIds) {
@@ -35785,12 +36500,14 @@ class Evaluator {
35785
36500
  this.blockedArrayFormulas = new Set();
35786
36501
  this.spreadingRelations = new SpreadingRelation();
35787
36502
  this.formulaDependencies = lazy(() => {
35788
- const dependencyGraph = new FormulaDependencyGraph();
35789
- for (const positionId of this.getAllCells()) {
35790
- const dependencies = this.getDirectDependencies(positionId);
35791
- dependencyGraph.addDependencies(positionId, dependencies);
35792
- }
35793
- return dependencyGraph;
36503
+ const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
36504
+ data: positionId,
36505
+ boundingBox: {
36506
+ zone: range.zone,
36507
+ sheetId: range.sheetId,
36508
+ },
36509
+ })));
36510
+ return new FormulaDependencyGraph(this.encoder, dependencies);
35794
36511
  });
35795
36512
  }
35796
36513
  evaluateAllCells() {
@@ -35811,7 +36528,7 @@ class Evaluator {
35811
36528
  for (const sheetId of this.getters.getSheetIds()) {
35812
36529
  const cellIds = this.getters.getCells(sheetId);
35813
36530
  for (const cellId in cellIds) {
35814
- positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
36531
+ positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
35815
36532
  }
35816
36533
  }
35817
36534
  return positionIds;
@@ -35878,7 +36595,7 @@ class Evaluator {
35878
36595
  }
35879
36596
  }
35880
36597
  computeAndSave(position) {
35881
- const positionId = this.encodePosition(position);
36598
+ const positionId = this.encoder.encode(position);
35882
36599
  const evaluatedCell = this.computeCell(positionId);
35883
36600
  if (!this.evaluatedCells.has(positionId)) {
35884
36601
  this.setEvaluatedCell(positionId, evaluatedCell);
@@ -35939,15 +36656,15 @@ class Evaluator {
35939
36656
  throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns and rows."));
35940
36657
  }
35941
36658
  updateSpreadRelation({ sheetId, col, row, }) {
35942
- const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
36659
+ const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
35943
36660
  return (i, j) => {
35944
36661
  const position = { sheetId, col: i + col, row: j + row };
35945
- const resultPositionId = this.encodePosition(position);
36662
+ const resultPositionId = this.encoder.encode(position);
35946
36663
  this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
35947
36664
  };
35948
36665
  }
35949
36666
  checkCollision({ sheetId, col, row }) {
35950
- const formulaPositionId = this.encodePosition({ sheetId, col, row });
36667
+ const formulaPositionId = this.encoder.encode({ sheetId, col, row });
35951
36668
  return (i, j) => {
35952
36669
  const position = { sheetId: sheetId, col: i + col, row: j + row };
35953
36670
  const rawCell = this.getters.getCell(position);
@@ -35969,7 +36686,7 @@ class Evaluator {
35969
36686
  format: format || formatFromPosition(i, j),
35970
36687
  locale: this.getters.getLocale(),
35971
36688
  });
35972
- const positionId = this.encodePosition(position);
36689
+ const positionId = this.encoder.encode(position);
35973
36690
  this.setEvaluatedCell(positionId, evaluatedCell);
35974
36691
  // check if formula dependencies present in the spread zone
35975
36692
  // if so, they need to be recomputed
@@ -36001,29 +36718,17 @@ class Evaluator {
36001
36718
  if (!cell?.isFormula) {
36002
36719
  return [];
36003
36720
  }
36004
- const dependencies = [];
36005
- for (const range of cell.dependencies) {
36006
- if (range.invalidSheetName || range.invalidXc) {
36007
- continue;
36008
- }
36009
- const sheetId = range.sheetId;
36010
- forEachPositionsInZone(range.zone, (col, row) => {
36011
- dependencies.push(this.encodePosition({ sheetId, col, row }));
36012
- });
36013
- }
36014
- return dependencies;
36721
+ return cell.dependencies;
36015
36722
  }
36016
36723
  getCellsDependingOn(positionIds) {
36017
- return this.formulaDependencies().getCellsDependingOn(positionIds);
36724
+ const ranges = [];
36725
+ for (const positionId of positionIds) {
36726
+ ranges.push(this.encoder.decodeToBoundingBox(positionId));
36727
+ }
36728
+ return this.formulaDependencies().getCellsDependingOn(ranges);
36018
36729
  }
36019
36730
  getCell(positionId) {
36020
- return this.getters.getCell(this.decodePosition(positionId));
36021
- }
36022
- encodePosition(position) {
36023
- return this.positionEncoder.encode(position);
36024
- }
36025
- decodePosition(positionId) {
36026
- return this.positionEncoder.decode(positionId);
36731
+ return this.getters.getCell(this.encoder.decode(positionId));
36027
36732
  }
36028
36733
  }
36029
36734
  function forEachSpreadPositionInMatrix(matrix, callback) {
@@ -36098,6 +36803,13 @@ class PositionBitsEncoder {
36098
36803
  encode({ sheetId, col, row }) {
36099
36804
  return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
36100
36805
  }
36806
+ encodeBoundingBox({ sheetId, zone }) {
36807
+ const positions = [];
36808
+ forEachPositionsInZone(zone, (col, row) => {
36809
+ positions.push(this.encode({ sheetId, col, row }));
36810
+ });
36811
+ return positions;
36812
+ }
36101
36813
  decode(id) {
36102
36814
  // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
36103
36815
  const row = Number(id & 2097151n);
@@ -36105,6 +36817,10 @@ class PositionBitsEncoder {
36105
36817
  const sheetId = this.decodeSheet(id >> 42n);
36106
36818
  return { sheetId, col, row };
36107
36819
  }
36820
+ decodeToBoundingBox(id) {
36821
+ const { sheetId, col, row } = this.decode(id);
36822
+ return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
36823
+ }
36108
36824
  encodeSheet(sheetId) {
36109
36825
  const sheetKey = this.sheetMapping[sheetId];
36110
36826
  if (sheetKey === undefined) {
@@ -36219,6 +36935,7 @@ class PositionBitsEncoder {
36219
36935
  class EvaluationPlugin extends UIPlugin {
36220
36936
  static getters = [
36221
36937
  "evaluateFormula",
36938
+ "getCorrespondingFormulaCell",
36222
36939
  "getRangeFormattedValues",
36223
36940
  "getRangeValues",
36224
36941
  "getRangeFormats",
@@ -36272,7 +36989,12 @@ class EvaluationPlugin extends UIPlugin {
36272
36989
  // Getters
36273
36990
  // ---------------------------------------------------------------------------
36274
36991
  evaluateFormula(sheetId, formulaString) {
36275
- return this.evaluator.evaluateFormula(sheetId, formulaString);
36992
+ try {
36993
+ return this.evaluator.evaluateFormula(sheetId, formulaString);
36994
+ }
36995
+ catch (error) {
36996
+ return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
36997
+ }
36276
36998
  }
36277
36999
  /**
36278
37000
  * Return the value of each cell in the range as they are displayed in the grid.
@@ -36535,7 +37257,7 @@ class CustomColorsPlugin extends UIPlugin {
36535
37257
  }
36536
37258
 
36537
37259
  class EvaluationChartPlugin extends UIPlugin {
36538
- static getters = ["getChartRuntime", "getBackgroundOfSingleCellChart"];
37260
+ static getters = ["getChartRuntime", "getStyleOfSingleCellChart"];
36539
37261
  charts = {};
36540
37262
  createRuntimeChart = chartRuntimeFactory(this.getters);
36541
37263
  handle(cmd) {
@@ -36573,25 +37295,26 @@ class EvaluationChartPlugin extends UIPlugin {
36573
37295
  return this.charts[figureId];
36574
37296
  }
36575
37297
  /**
36576
- * Get the background color of a chart based on the color of the first cell of the main range
36577
- * of the chart. In order of priority, it will return :
36578
- *
36579
- * - the chart background color if one is defined
36580
- * - the fill color of the cell if one is defined
36581
- * - the fill color of the cell from conditional formats if one is defined
36582
- * - the default chart color if no other color is defined
37298
+ * Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.
36583
37299
  */
36584
- getBackgroundOfSingleCellChart(chartBackground, mainRange) {
37300
+ getStyleOfSingleCellChart(chartBackground, mainRange) {
36585
37301
  if (chartBackground)
36586
- return chartBackground;
37302
+ return { background: chartBackground, fontColor: chartFontColor(chartBackground) };
36587
37303
  if (!mainRange) {
36588
- return BACKGROUND_CHART_COLOR;
37304
+ return {
37305
+ background: BACKGROUND_CHART_COLOR,
37306
+ fontColor: chartFontColor(BACKGROUND_CHART_COLOR),
37307
+ };
36589
37308
  }
36590
37309
  const col = mainRange.zone.left;
36591
37310
  const row = mainRange.zone.top;
36592
37311
  const sheetId = mainRange.sheetId;
36593
37312
  const style = this.getters.getCellComputedStyle({ sheetId, col, row });
36594
- return style.fillColor || BACKGROUND_CHART_COLOR;
37313
+ const background = style.fillColor || BACKGROUND_CHART_COLOR;
37314
+ return {
37315
+ background,
37316
+ fontColor: style.textColor || chartFontColor(background),
37317
+ };
36595
37318
  }
36596
37319
  }
36597
37320
 
@@ -36661,34 +37384,29 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36661
37384
  getComputedStyles(sheetId) {
36662
37385
  const computedStyle = {};
36663
37386
  for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
36664
- try {
36665
- switch (cf.rule.type) {
36666
- case "ColorScaleRule":
36667
- for (let range of cf.ranges) {
36668
- this.applyColorScale(sheetId, range, cf.rule, computedStyle);
36669
- }
36670
- break;
36671
- case "CellIsRule":
36672
- for (let ref of cf.ranges) {
36673
- const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
36674
- for (let row = zone.top; row <= zone.bottom; row++) {
36675
- for (let col = zone.left; col <= zone.right; col++) {
36676
- const pr = this.rulePredicate[cf.rule.type];
36677
- let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
36678
- if (pr && pr(cell, cf.rule)) {
36679
- if (!computedStyle[col])
36680
- computedStyle[col] = [];
36681
- // we must combine all the properties of all the CF rules applied to the given cell
36682
- computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
36683
- }
37387
+ switch (cf.rule.type) {
37388
+ case "ColorScaleRule":
37389
+ for (let range of cf.ranges) {
37390
+ this.applyColorScale(sheetId, range, cf.rule, computedStyle);
37391
+ }
37392
+ break;
37393
+ case "CellIsRule":
37394
+ for (let ref of cf.ranges) {
37395
+ const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
37396
+ for (let row = zone.top; row <= zone.bottom; row++) {
37397
+ for (let col = zone.left; col <= zone.right; col++) {
37398
+ const pr = this.rulePredicate[cf.rule.type];
37399
+ let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
37400
+ if (pr && pr(cell, cf.rule)) {
37401
+ if (!computedStyle[col])
37402
+ computedStyle[col] = [];
37403
+ // we must combine all the properties of all the CF rules applied to the given cell
37404
+ computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
36684
37405
  }
36685
37406
  }
36686
37407
  }
36687
- break;
36688
- }
36689
- }
36690
- catch (_) {
36691
- // we don't care about the errors within the evaluation of a rule
37408
+ }
37409
+ break;
36692
37410
  }
36693
37411
  }
36694
37412
  return computedStyle;
@@ -36724,7 +37442,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36724
37442
  return percentile(rangeValues, Number(threshold.value) / 100, true);
36725
37443
  case "formula":
36726
37444
  const value = threshold.value && this.getters.evaluateFormula(sheetId, threshold.value);
36727
- return !(value instanceof Promise) ? value : null;
37445
+ return typeof value === "number" ? value : null;
36728
37446
  default:
36729
37447
  return null;
36730
37448
  }
@@ -37290,19 +38008,21 @@ class AutofillPlugin extends UIPlugin {
37290
38008
  let col = zone.left;
37291
38009
  let row = zone.bottom;
37292
38010
  if (col > 0) {
37293
- let left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
37294
- while (left.type !== CellValueType.empty) {
38011
+ let leftPosition = { sheetId, col: col - 1, row };
38012
+ while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
38013
+ this.getters.getCell(leftPosition)?.content) {
37295
38014
  row += 1;
37296
- left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
38015
+ leftPosition = { sheetId, col: col - 1, row };
37297
38016
  }
37298
38017
  }
37299
38018
  if (row === zone.bottom) {
37300
38019
  col = zone.right;
37301
38020
  if (col <= this.getters.getNumberCols(sheetId)) {
37302
- let right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
37303
- while (right.type !== CellValueType.empty) {
38021
+ let rightPosition = { sheetId, col: col + 1, row };
38022
+ while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
38023
+ this.getters.getCell(rightPosition)?.content) {
37304
38024
  row += 1;
37305
- right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
38025
+ rightPosition = { sheetId, col: col + 1, row };
37306
38026
  }
37307
38027
  }
37308
38028
  }
@@ -40220,6 +40940,9 @@ class SelectionInputsManagerPlugin extends UIPlugin {
40220
40940
  // Other
40221
40941
  // ---------------------------------------------------------------------------
40222
40942
  initInput(id, initialRanges, inputHasSingleRange = false) {
40943
+ if (this.inputs[id]) {
40944
+ this.unfocus();
40945
+ }
40223
40946
  this.inputs[id] = new SelectionInputPlugin(this.config, initialRanges, inputHasSingleRange);
40224
40947
  if (initialRanges.length === 0) {
40225
40948
  const input = this.inputs[id];
@@ -42335,6 +43058,10 @@ function setXcToReferenceType(xc, referenceType) {
42335
43058
  return xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
42336
43059
  case "colrow":
42337
43060
  indexOfNumber = xc.search(/[0-9]/);
43061
+ if (indexOfNumber === -1 || indexOfNumber === 0) {
43062
+ // no row number (eg. A) or no column (eg. 1)
43063
+ return "$" + xc;
43064
+ }
42338
43065
  xc = xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
42339
43066
  return "$" + xc;
42340
43067
  case "none":
@@ -43750,7 +44477,7 @@ class GridSelectionPlugin extends UIPlugin {
43750
44477
  sheetId: cmd.sheetId,
43751
44478
  base: cmd.base,
43752
44479
  quantity: thickness,
43753
- position: "before",
44480
+ position: cmd.position,
43754
44481
  });
43755
44482
  const isCol = cmd.dimension === "COL";
43756
44483
  const start = cmd.elements[0];
@@ -43767,12 +44494,13 @@ class GridSelectionPlugin extends UIPlugin {
43767
44494
  },
43768
44495
  ];
43769
44496
  const state = new ClipboardCellsState(target, "CUT", this.getters, this.dispatch, this.selection);
44497
+ const base = isBasedBefore ? cmd.base : cmd.base + 1;
43770
44498
  const pasteTarget = [
43771
44499
  {
43772
- left: isCol ? cmd.base : 0,
43773
- right: isCol ? cmd.base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
43774
- top: !isCol ? cmd.base : 0,
43775
- bottom: !isCol ? cmd.base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
44500
+ left: isCol ? base : 0,
44501
+ right: isCol ? base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
44502
+ top: !isCol ? base : 0,
44503
+ bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
43776
44504
  },
43777
44505
  ];
43778
44506
  state.paste(pasteTarget, { selectTarget: true });
@@ -43809,6 +44537,11 @@ class GridSelectionPlugin extends UIPlugin {
43809
44537
  doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {
43810
44538
  return 2 /* CommandResult.WillRemoveExistingMerge */;
43811
44539
  }
44540
+ const headers = [cmd.base, ...cmd.elements];
44541
+ const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);
44542
+ if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {
44543
+ return 88 /* CommandResult.InvalidHeaderIndex */;
44544
+ }
43812
44545
  return 0 /* CommandResult.Success */;
43813
44546
  }
43814
44547
  //-------------------------------------------
@@ -47034,7 +47767,7 @@ class Spreadsheet extends owl.Component {
47034
47767
  }
47035
47768
  onKeydown(ev) {
47036
47769
  let keyDownString = "";
47037
- if (ev.ctrlKey || ev.metaKey) {
47770
+ if (isCtrlKey(ev)) {
47038
47771
  keyDownString += "CTRL+";
47039
47772
  }
47040
47773
  keyDownString += ev.key.toUpperCase();
@@ -50793,6 +51526,6 @@ exports.setTranslationMethod = setTranslationMethod;
50793
51526
  exports.tokenize = tokenize;
50794
51527
 
50795
51528
 
50796
- __info__.version = '16.4.15';
50797
- __info__.date = '2023-11-24T13:12:26.608Z';
50798
- __info__.hash = '141943d';
51529
+ __info__.version = '16.4.17';
51530
+ __info__.date = '2024-01-04T12:22:53.125Z';
51531
+ __info__.hash = '769f8a3';