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