@odoo/o-spreadsheet 17.1.0-alpha.5 → 17.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 17.1.0-alpha.5
6
- * @date 2023-12-05T09:51:40.034Z
7
- * @hash c2823eb
5
+ * @version 17.1.0-alpha.6
6
+ * @date 2024-01-04T12:22:45.921Z
7
+ * @hash 56dbdc9
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -329,13 +329,6 @@
329
329
  //------------------------------------------------------------------------------
330
330
  // Miscellaneous
331
331
  //------------------------------------------------------------------------------
332
- /**
333
- * Stringify an object, like JSON.stringify, except that the first level of keys
334
- * is ordered.
335
- */
336
- function stringify(obj) {
337
- return JSON.stringify(obj, Object.keys(obj).sort());
338
- }
339
332
  /**
340
333
  * Remove quotes from a quoted string
341
334
  * ```js
@@ -558,7 +551,7 @@
558
551
  */
559
552
  function getItemId(item, itemsDic) {
560
553
  for (let [key, value] of Object.entries(itemsDic)) {
561
- if (stringify(value) === stringify(item)) {
554
+ if (deepEquals(value, item)) {
562
555
  return parseInt(key, 10);
563
556
  }
564
557
  }
@@ -2061,6 +2054,8 @@
2061
2054
  CommandResult["NoChanges"] = "NoChanges";
2062
2055
  })(exports.CommandResult || (exports.CommandResult = {}));
2063
2056
 
2057
+ const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
2058
+
2064
2059
  const DEFAULT_LOCALES = [
2065
2060
  {
2066
2061
  name: "English (US)",
@@ -2824,6 +2819,9 @@
2824
2819
  * Formats a cell value with its format.
2825
2820
  */
2826
2821
  function formatValue(value, { format, locale }) {
2822
+ if (format === PLAIN_TEXT_FORMAT) {
2823
+ return toString(value) || "";
2824
+ }
2827
2825
  switch (typeof value) {
2828
2826
  case "string":
2829
2827
  return value;
@@ -5295,7 +5293,7 @@
5295
5293
  };
5296
5294
  baselineCell = getters.getEvaluatedCell(baselinePosition);
5297
5295
  }
5298
- const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue);
5296
+ const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
5299
5297
  const locale = getters.getLocale();
5300
5298
  return {
5301
5299
  title: _t(chart.title),
@@ -5304,7 +5302,7 @@
5304
5302
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
5305
5303
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
5306
5304
  baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
5307
- fontColor: chartFontColor(background),
5305
+ fontColor,
5308
5306
  background,
5309
5307
  baselineStyle: chart.baselineMode !== "percentage" && baseline
5310
5308
  ? getters.getCellStyle({
@@ -5730,6 +5728,9 @@
5730
5728
  }
5731
5729
 
5732
5730
  function evaluateLiteral(content, localeFormat) {
5731
+ if (localeFormat.format === PLAIN_TEXT_FORMAT) {
5732
+ return textCell(content || "", localeFormat);
5733
+ }
5733
5734
  return createEvaluatedCell(parseLiteral(content || "", localeFormat.locale), localeFormat);
5734
5735
  }
5735
5736
  function parseLiteral(content, locale) {
@@ -5764,6 +5765,9 @@
5764
5765
  }
5765
5766
  function _createEvaluatedCell(value, localeFormat) {
5766
5767
  try {
5768
+ if (localeFormat.format === PLAIN_TEXT_FORMAT) {
5769
+ return textCell(toString(value), localeFormat);
5770
+ }
5767
5771
  for (const builder of builders) {
5768
5772
  const evaluateCell = builder(value, localeFormat);
5769
5773
  if (evaluateCell) {
@@ -6599,6 +6603,7 @@
6599
6603
  },
6600
6604
  };
6601
6605
 
6606
+ const macRegex = /Mac/i;
6602
6607
  /**
6603
6608
  * Return true if the event was triggered from
6604
6609
  * a child element.
@@ -6650,7 +6655,7 @@
6650
6655
  */
6651
6656
  function keyboardEventToShortcutString(ev, mode = "key") {
6652
6657
  let keyDownString = "";
6653
- if (ev.ctrlKey && ev.key !== "Ctrl")
6658
+ if (isCtrlKey(ev) && ev.key !== "Ctrl")
6654
6659
  keyDownString += "Ctrl+";
6655
6660
  if (ev.metaKey)
6656
6661
  keyDownString += "Ctrl+";
@@ -6663,7 +6668,15 @@
6663
6668
  return keyDownString;
6664
6669
  }
6665
6670
  function isMacOS() {
6666
- return navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
6671
+ return Boolean(macRegex.test(navigator.userAgent));
6672
+ }
6673
+ /**
6674
+ * @param {KeyboardEvent | MouseEvent} ev
6675
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
6676
+ * On Mac, this is the "meta" or "command" key.
6677
+ */
6678
+ function isCtrlKey(ev) {
6679
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
6667
6680
  }
6668
6681
 
6669
6682
  /**
@@ -8902,7 +8915,7 @@
8902
8915
  });
8903
8916
  return {
8904
8917
  chartJsConfig: config,
8905
- background: getters.getBackgroundOfSingleCellChart(chart.background, dataRange),
8918
+ background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
8906
8919
  };
8907
8920
  }
8908
8921
 
@@ -8983,7 +8996,7 @@
8983
8996
  else if (format.includes("h") || format.includes("H")) {
8984
8997
  return "hour";
8985
8998
  }
8986
- else if (format.includes("D")) {
8999
+ else if (format.includes("d")) {
8987
9000
  return "day";
8988
9001
  }
8989
9002
  else if (format.includes("M")) {
@@ -9495,6 +9508,27 @@
9495
9508
  const percentage = (dataset[dataIndex] / total) * 100;
9496
9509
  return percentage.toFixed(2);
9497
9510
  }
9511
+ function filterNegativeValues(labels, datasets) {
9512
+ const dataPointsIndexes = labels.reduce((indexes, label, i) => {
9513
+ const shouldKeep = datasets.some((dataset) => {
9514
+ const dataPoint = dataset.data[i];
9515
+ return typeof dataPoint !== "number" || dataPoint >= 0;
9516
+ });
9517
+ if (shouldKeep) {
9518
+ indexes.push(i);
9519
+ }
9520
+ return indexes;
9521
+ }, []);
9522
+ const filteredLabels = dataPointsIndexes.map((i) => labels[i] || "");
9523
+ const filteredDatasets = datasets.map((dataset) => ({
9524
+ ...dataset,
9525
+ data: dataPointsIndexes.map((i) => {
9526
+ const dataPoint = dataset.data[i];
9527
+ return typeof dataPoint !== "number" || dataPoint >= 0 ? dataPoint : 0;
9528
+ }),
9529
+ }));
9530
+ return { labels: filteredLabels, dataSetsValues: filteredDatasets };
9531
+ }
9498
9532
  function createPieChartRuntime(chart, getters) {
9499
9533
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
9500
9534
  let labels = labelValues.formattedValues;
@@ -9508,6 +9542,7 @@
9508
9542
  if (chart.aggregated) {
9509
9543
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
9510
9544
  }
9545
+ ({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));
9511
9546
  const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
9512
9547
  const locale = getters.getLocale();
9513
9548
  const config = getPieConfiguration(chart, labels, { format: dataSetFormat, locale });
@@ -10345,7 +10380,7 @@
10345
10380
  // Simple actions
10346
10381
  //------------------------------------------------------------------------------
10347
10382
  const PASTE_ACTION = async (env) => paste$1(env);
10348
- const PASTE_VALUE_ACTION = async (env) => paste$1(env, "onlyValue");
10383
+ const PASTE_AS_VALUE_ACTION = async (env) => paste$1(env, "asValue");
10349
10384
  async function paste$1(env, pasteOption) {
10350
10385
  const spreadsheetClipboard = env.model.getters.getClipboardTextContent();
10351
10386
  const osClipboard = await env.clipboard.readText();
@@ -10358,7 +10393,7 @@
10358
10393
  else {
10359
10394
  interactivePaste(env, target, pasteOption);
10360
10395
  }
10361
- if (env.model.getters.isCutOperation() && pasteOption !== "onlyValue") {
10396
+ if (env.model.getters.isCutOperation() && pasteOption !== "asValue") {
10362
10397
  await env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
10363
10398
  }
10364
10399
  break;
@@ -10773,9 +10808,9 @@
10773
10808
  icon: "o-spreadsheet-Icon.PASTE",
10774
10809
  };
10775
10810
  const pasteSpecialValue = {
10776
- name: _t("Paste value only"),
10811
+ name: _t("Paste as value"),
10777
10812
  description: "Ctrl+Shift+V",
10778
- execute: PASTE_VALUE_ACTION,
10813
+ execute: PASTE_AS_VALUE_ACTION,
10779
10814
  };
10780
10815
  const pasteSpecialFormat = {
10781
10816
  name: _t("Paste format only"),
@@ -20014,6 +20049,11 @@
20014
20049
  execute: (env) => setFormatter(env, ""),
20015
20050
  isActive: (env) => isAutomaticFormatSelected(env),
20016
20051
  };
20052
+ const formatNumberPlainText = {
20053
+ name: _t("Plain text"),
20054
+ execute: (env) => setFormatter(env, PLAIN_TEXT_FORMAT),
20055
+ isActive: (env) => isFormatSelected(env, PLAIN_TEXT_FORMAT),
20056
+ };
20017
20057
  const formatNumberNumber = createFormatActionSpec({
20018
20058
  name: _t("Number"),
20019
20059
  descriptionValue: 1000.12,
@@ -20373,6 +20413,7 @@
20373
20413
  formatNumberFullWeekDayAndMonth: formatNumberFullWeekDayAndMonth,
20374
20414
  formatNumberNumber: formatNumberNumber,
20375
20415
  formatNumberPercent: formatNumberPercent,
20416
+ formatNumberPlainText: formatNumberPlainText,
20376
20417
  formatNumberShortMonth: formatNumberShortMonth,
20377
20418
  formatNumberShortWeekDay: formatNumberShortWeekDay,
20378
20419
  formatNumberTime: formatNumberTime,
@@ -20803,6 +20844,10 @@
20803
20844
  .add("format_number_automatic", {
20804
20845
  ...formatNumberAutomatic,
20805
20846
  sequence: 10,
20847
+ })
20848
+ .add("format_number_plain_text", {
20849
+ ...formatNumberPlainText,
20850
+ sequence: 15,
20806
20851
  separator: true,
20807
20852
  })
20808
20853
  .add("format_number_number", {
@@ -21401,10 +21446,10 @@
21401
21446
  function updateSelectionWithArrowKeys(ev, selection) {
21402
21447
  const direction = arrowMap[ev.key];
21403
21448
  if (ev.shiftKey) {
21404
- selection.resizeAnchorZone(direction, ev.ctrlKey ? "end" : 1);
21449
+ selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
21405
21450
  }
21406
21451
  else {
21407
- selection.moveAnchorCell(direction, ev.ctrlKey ? "end" : 1);
21452
+ selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
21408
21453
  }
21409
21454
  }
21410
21455
 
@@ -24013,6 +24058,12 @@
24013
24058
  padding: 4px 0 4px 4px;
24014
24059
  }
24015
24060
  }
24061
+
24062
+ .o-matches-count div {
24063
+ text-overflow: ellipsis;
24064
+ overflow: hidden;
24065
+ white-space: nowrap;
24066
+ }
24016
24067
  }
24017
24068
  `;
24018
24069
  class FindAndReplacePanel extends owl.Component {
@@ -28443,7 +28494,10 @@
28443
28494
  return;
28444
28495
  }
28445
28496
  const [col, row] = this.getCartesianCoordinates(ev);
28446
- this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey });
28497
+ this.props.onCellClicked(col, row, {
28498
+ expandZone: ev.shiftKey,
28499
+ addZone: isCtrlKey(ev),
28500
+ });
28447
28501
  }
28448
28502
  onDoubleClick(ev) {
28449
28503
  const [col, row] = this.getCartesianCoordinates(ev);
@@ -28673,7 +28727,7 @@
28673
28727
  this._increaseSelection(index);
28674
28728
  }
28675
28729
  else {
28676
- this._selectElement(index, ev.ctrlKey);
28730
+ this._selectElement(index, isCtrlKey(ev));
28677
28731
  }
28678
28732
  this.lastSelectedElementIndex = index;
28679
28733
  const mouseMoveSelect = (col, row) => {
@@ -28830,8 +28884,8 @@
28830
28884
  this.env.raiseError(MergeErrorMessage);
28831
28885
  }
28832
28886
  }
28833
- _selectElement(index, ctrlKey) {
28834
- this.env.model.selection.selectColumn(index, ctrlKey ? "newAnchor" : "overrideSelection");
28887
+ _selectElement(index, addDistinctHeader) {
28888
+ this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
28835
28889
  }
28836
28890
  _increaseSelection(index) {
28837
28891
  this.env.model.selection.selectColumn(index, "updateAnchor");
@@ -28995,8 +29049,8 @@
28995
29049
  this.env.raiseError(MergeErrorMessage);
28996
29050
  }
28997
29051
  }
28998
- _selectElement(index, ctrlKey) {
28999
- this.env.model.selection.selectRow(index, ctrlKey ? "newAnchor" : "overrideSelection");
29052
+ _selectElement(index, addDistinctHeader) {
29053
+ this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
29000
29054
  }
29001
29055
  _increaseSelection(index) {
29002
29056
  this.env.model.selection.selectRow(index, "updateAnchor");
@@ -29689,7 +29743,7 @@
29689
29743
  "Ctrl+Shift+E": () => this.setHorizontalAlign("center"),
29690
29744
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
29691
29745
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
29692
- "Ctrl+Shift+V": () => PASTE_VALUE_ACTION(this.env),
29746
+ "Ctrl+Shift+V": () => PASTE_AS_VALUE_ACTION(this.env),
29693
29747
  "Ctrl+Shift+<": () => this.clearFormatting(),
29694
29748
  "Ctrl+<": () => this.clearFormatting(),
29695
29749
  "Ctrl+Shift+ ": () => {
@@ -29797,17 +29851,17 @@
29797
29851
  // ---------------------------------------------------------------------------
29798
29852
  // Zone selection with mouse
29799
29853
  // ---------------------------------------------------------------------------
29800
- onCellClicked(col, row, { ctrlKey, shiftKey }) {
29854
+ onCellClicked(col, row, { addZone, expandZone }) {
29801
29855
  if (this.env.model.getters.hasOpenedPopover()) {
29802
29856
  this.closeOpenedPopover();
29803
29857
  }
29804
29858
  if (this.env.model.getters.getEditionMode() === "editing") {
29805
29859
  interactiveStopEdition(this.env);
29806
29860
  }
29807
- if (shiftKey) {
29861
+ if (expandZone) {
29808
29862
  this.env.model.selection.setAnchorCorner(col, row);
29809
29863
  }
29810
- else if (ctrlKey) {
29864
+ else if (addZone) {
29811
29865
  this.env.model.selection.addCellToSelection(col, row);
29812
29866
  }
29813
29867
  else {
@@ -30610,7 +30664,7 @@
30610
30664
  46: "hhhh:mm:ss",
30611
30665
  47: "hhhh:mm:ss",
30612
30666
  48: undefined,
30613
- 49: undefined,
30667
+ 49: PLAIN_TEXT_FORMAT,
30614
30668
  };
30615
30669
  /**
30616
30670
  * Mapping format index to format defined by default
@@ -34743,7 +34797,7 @@
34743
34797
  */
34744
34798
  function getBorderId(border) {
34745
34799
  for (let [key, value] of Object.entries(borders)) {
34746
- if (stringify(value) === stringify(border)) {
34800
+ if (deepEquals(value, border)) {
34747
34801
  return parseInt(key, 10);
34748
34802
  }
34749
34803
  }
@@ -35914,7 +35968,9 @@
35914
35968
  }
35915
35969
  createLiteralCell(id, content, format, style) {
35916
35970
  const locale = this.getters.getLocale();
35917
- content = parseLiteral(content, locale).toString();
35971
+ if (format !== PLAIN_TEXT_FORMAT) {
35972
+ content = toString(parseLiteral(content, locale));
35973
+ }
35918
35974
  return {
35919
35975
  id,
35920
35976
  content,
@@ -40061,56 +40117,758 @@
40061
40117
  }
40062
40118
  }
40063
40119
 
40120
+ function quickselect(arr, k, left, right, compare) {
40121
+ quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
40122
+ }
40123
+
40124
+ function quickselectStep(arr, k, left, right, compare) {
40125
+
40126
+ while (right > left) {
40127
+ if (right - left > 600) {
40128
+ var n = right - left + 1;
40129
+ var m = k - left + 1;
40130
+ var z = Math.log(n);
40131
+ var s = 0.5 * Math.exp(2 * z / 3);
40132
+ var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
40133
+ var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
40134
+ var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
40135
+ quickselectStep(arr, k, newLeft, newRight, compare);
40136
+ }
40137
+
40138
+ var t = arr[k];
40139
+ var i = left;
40140
+ var j = right;
40141
+
40142
+ swap(arr, left, k);
40143
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
40144
+
40145
+ while (i < j) {
40146
+ swap(arr, i, j);
40147
+ i++;
40148
+ j--;
40149
+ while (compare(arr[i], t) < 0) i++;
40150
+ while (compare(arr[j], t) > 0) j--;
40151
+ }
40152
+
40153
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
40154
+ else {
40155
+ j++;
40156
+ swap(arr, j, right);
40157
+ }
40158
+
40159
+ if (j <= k) left = j + 1;
40160
+ if (k <= j) right = j - 1;
40161
+ }
40162
+ }
40163
+
40164
+ function swap(arr, i, j) {
40165
+ var tmp = arr[i];
40166
+ arr[i] = arr[j];
40167
+ arr[j] = tmp;
40168
+ }
40169
+
40170
+ function defaultCompare(a, b) {
40171
+ return a < b ? -1 : a > b ? 1 : 0;
40172
+ }
40173
+
40174
+ class RBush {
40175
+ constructor(maxEntries = 9) {
40176
+ // max entries in a node is 9 by default; min node fill is 40% for best performance
40177
+ this._maxEntries = Math.max(4, maxEntries);
40178
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
40179
+ this.clear();
40180
+ }
40181
+
40182
+ all() {
40183
+ return this._all(this.data, []);
40184
+ }
40185
+
40186
+ search(bbox) {
40187
+ let node = this.data;
40188
+ const result = [];
40189
+
40190
+ if (!intersects(bbox, node)) return result;
40191
+
40192
+ const toBBox = this.toBBox;
40193
+ const nodesToSearch = [];
40194
+
40195
+ while (node) {
40196
+ for (let i = 0; i < node.children.length; i++) {
40197
+ const child = node.children[i];
40198
+ const childBBox = node.leaf ? toBBox(child) : child;
40199
+
40200
+ if (intersects(bbox, childBBox)) {
40201
+ if (node.leaf) result.push(child);
40202
+ else if (contains(bbox, childBBox)) this._all(child, result);
40203
+ else nodesToSearch.push(child);
40204
+ }
40205
+ }
40206
+ node = nodesToSearch.pop();
40207
+ }
40208
+
40209
+ return result;
40210
+ }
40211
+
40212
+ collides(bbox) {
40213
+ let node = this.data;
40214
+
40215
+ if (!intersects(bbox, node)) return false;
40216
+
40217
+ const nodesToSearch = [];
40218
+ while (node) {
40219
+ for (let i = 0; i < node.children.length; i++) {
40220
+ const child = node.children[i];
40221
+ const childBBox = node.leaf ? this.toBBox(child) : child;
40222
+
40223
+ if (intersects(bbox, childBBox)) {
40224
+ if (node.leaf || contains(bbox, childBBox)) return true;
40225
+ nodesToSearch.push(child);
40226
+ }
40227
+ }
40228
+ node = nodesToSearch.pop();
40229
+ }
40230
+
40231
+ return false;
40232
+ }
40233
+
40234
+ load(data) {
40235
+ if (!(data && data.length)) return this;
40236
+
40237
+ if (data.length < this._minEntries) {
40238
+ for (let i = 0; i < data.length; i++) {
40239
+ this.insert(data[i]);
40240
+ }
40241
+ return this;
40242
+ }
40243
+
40244
+ // recursively build the tree with the given data from scratch using OMT algorithm
40245
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
40246
+
40247
+ if (!this.data.children.length) {
40248
+ // save as is if tree is empty
40249
+ this.data = node;
40250
+
40251
+ } else if (this.data.height === node.height) {
40252
+ // split root if trees have the same height
40253
+ this._splitRoot(this.data, node);
40254
+
40255
+ } else {
40256
+ if (this.data.height < node.height) {
40257
+ // swap trees if inserted one is bigger
40258
+ const tmpNode = this.data;
40259
+ this.data = node;
40260
+ node = tmpNode;
40261
+ }
40262
+
40263
+ // insert the small tree into the large tree at appropriate level
40264
+ this._insert(node, this.data.height - node.height - 1, true);
40265
+ }
40266
+
40267
+ return this;
40268
+ }
40269
+
40270
+ insert(item) {
40271
+ if (item) this._insert(item, this.data.height - 1);
40272
+ return this;
40273
+ }
40274
+
40275
+ clear() {
40276
+ this.data = createNode([]);
40277
+ return this;
40278
+ }
40279
+
40280
+ remove(item, equalsFn) {
40281
+ if (!item) return this;
40282
+
40283
+ let node = this.data;
40284
+ const bbox = this.toBBox(item);
40285
+ const path = [];
40286
+ const indexes = [];
40287
+ let i, parent, goingUp;
40288
+
40289
+ // depth-first iterative tree traversal
40290
+ while (node || path.length) {
40291
+
40292
+ if (!node) { // go up
40293
+ node = path.pop();
40294
+ parent = path[path.length - 1];
40295
+ i = indexes.pop();
40296
+ goingUp = true;
40297
+ }
40298
+
40299
+ if (node.leaf) { // check current node
40300
+ const index = findItem(item, node.children, equalsFn);
40301
+
40302
+ if (index !== -1) {
40303
+ // item found, remove the item and condense tree upwards
40304
+ node.children.splice(index, 1);
40305
+ path.push(node);
40306
+ this._condense(path);
40307
+ return this;
40308
+ }
40309
+ }
40310
+
40311
+ if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
40312
+ path.push(node);
40313
+ indexes.push(i);
40314
+ i = 0;
40315
+ parent = node;
40316
+ node = node.children[0];
40317
+
40318
+ } else if (parent) { // go right
40319
+ i++;
40320
+ node = parent.children[i];
40321
+ goingUp = false;
40322
+
40323
+ } else node = null; // nothing found
40324
+ }
40325
+
40326
+ return this;
40327
+ }
40328
+
40329
+ toBBox(item) { return item; }
40330
+
40331
+ compareMinX(a, b) { return a.minX - b.minX; }
40332
+ compareMinY(a, b) { return a.minY - b.minY; }
40333
+
40334
+ toJSON() { return this.data; }
40335
+
40336
+ fromJSON(data) {
40337
+ this.data = data;
40338
+ return this;
40339
+ }
40340
+
40341
+ _all(node, result) {
40342
+ const nodesToSearch = [];
40343
+ while (node) {
40344
+ if (node.leaf) result.push(...node.children);
40345
+ else nodesToSearch.push(...node.children);
40346
+
40347
+ node = nodesToSearch.pop();
40348
+ }
40349
+ return result;
40350
+ }
40351
+
40352
+ _build(items, left, right, height) {
40353
+
40354
+ const N = right - left + 1;
40355
+ let M = this._maxEntries;
40356
+ let node;
40357
+
40358
+ if (N <= M) {
40359
+ // reached leaf level; return leaf
40360
+ node = createNode(items.slice(left, right + 1));
40361
+ calcBBox(node, this.toBBox);
40362
+ return node;
40363
+ }
40364
+
40365
+ if (!height) {
40366
+ // target height of the bulk-loaded tree
40367
+ height = Math.ceil(Math.log(N) / Math.log(M));
40368
+
40369
+ // target number of root entries to maximize storage utilization
40370
+ M = Math.ceil(N / Math.pow(M, height - 1));
40371
+ }
40372
+
40373
+ node = createNode([]);
40374
+ node.leaf = false;
40375
+ node.height = height;
40376
+
40377
+ // split the items into M mostly square tiles
40378
+
40379
+ const N2 = Math.ceil(N / M);
40380
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
40381
+
40382
+ multiSelect(items, left, right, N1, this.compareMinX);
40383
+
40384
+ for (let i = left; i <= right; i += N1) {
40385
+
40386
+ const right2 = Math.min(i + N1 - 1, right);
40387
+
40388
+ multiSelect(items, i, right2, N2, this.compareMinY);
40389
+
40390
+ for (let j = i; j <= right2; j += N2) {
40391
+
40392
+ const right3 = Math.min(j + N2 - 1, right2);
40393
+
40394
+ // pack each entry recursively
40395
+ node.children.push(this._build(items, j, right3, height - 1));
40396
+ }
40397
+ }
40398
+
40399
+ calcBBox(node, this.toBBox);
40400
+
40401
+ return node;
40402
+ }
40403
+
40404
+ _chooseSubtree(bbox, node, level, path) {
40405
+ while (true) {
40406
+ path.push(node);
40407
+
40408
+ if (node.leaf || path.length - 1 === level) break;
40409
+
40410
+ let minArea = Infinity;
40411
+ let minEnlargement = Infinity;
40412
+ let targetNode;
40413
+
40414
+ for (let i = 0; i < node.children.length; i++) {
40415
+ const child = node.children[i];
40416
+ const area = bboxArea(child);
40417
+ const enlargement = enlargedArea(bbox, child) - area;
40418
+
40419
+ // choose entry with the least area enlargement
40420
+ if (enlargement < minEnlargement) {
40421
+ minEnlargement = enlargement;
40422
+ minArea = area < minArea ? area : minArea;
40423
+ targetNode = child;
40424
+
40425
+ } else if (enlargement === minEnlargement) {
40426
+ // otherwise choose one with the smallest area
40427
+ if (area < minArea) {
40428
+ minArea = area;
40429
+ targetNode = child;
40430
+ }
40431
+ }
40432
+ }
40433
+
40434
+ node = targetNode || node.children[0];
40435
+ }
40436
+
40437
+ return node;
40438
+ }
40439
+
40440
+ _insert(item, level, isNode) {
40441
+ const bbox = isNode ? item : this.toBBox(item);
40442
+ const insertPath = [];
40443
+
40444
+ // find the best node for accommodating the item, saving all nodes along the path too
40445
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
40446
+
40447
+ // put the item into the node
40448
+ node.children.push(item);
40449
+ extend(node, bbox);
40450
+
40451
+ // split on node overflow; propagate upwards if necessary
40452
+ while (level >= 0) {
40453
+ if (insertPath[level].children.length > this._maxEntries) {
40454
+ this._split(insertPath, level);
40455
+ level--;
40456
+ } else break;
40457
+ }
40458
+
40459
+ // adjust bboxes along the insertion path
40460
+ this._adjustParentBBoxes(bbox, insertPath, level);
40461
+ }
40462
+
40463
+ // split overflowed node into two
40464
+ _split(insertPath, level) {
40465
+ const node = insertPath[level];
40466
+ const M = node.children.length;
40467
+ const m = this._minEntries;
40468
+
40469
+ this._chooseSplitAxis(node, m, M);
40470
+
40471
+ const splitIndex = this._chooseSplitIndex(node, m, M);
40472
+
40473
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
40474
+ newNode.height = node.height;
40475
+ newNode.leaf = node.leaf;
40476
+
40477
+ calcBBox(node, this.toBBox);
40478
+ calcBBox(newNode, this.toBBox);
40479
+
40480
+ if (level) insertPath[level - 1].children.push(newNode);
40481
+ else this._splitRoot(node, newNode);
40482
+ }
40483
+
40484
+ _splitRoot(node, newNode) {
40485
+ // split root node
40486
+ this.data = createNode([node, newNode]);
40487
+ this.data.height = node.height + 1;
40488
+ this.data.leaf = false;
40489
+ calcBBox(this.data, this.toBBox);
40490
+ }
40491
+
40492
+ _chooseSplitIndex(node, m, M) {
40493
+ let index;
40494
+ let minOverlap = Infinity;
40495
+ let minArea = Infinity;
40496
+
40497
+ for (let i = m; i <= M - m; i++) {
40498
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
40499
+ const bbox2 = distBBox(node, i, M, this.toBBox);
40500
+
40501
+ const overlap = intersectionArea(bbox1, bbox2);
40502
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
40503
+
40504
+ // choose distribution with minimum overlap
40505
+ if (overlap < minOverlap) {
40506
+ minOverlap = overlap;
40507
+ index = i;
40508
+
40509
+ minArea = area < minArea ? area : minArea;
40510
+
40511
+ } else if (overlap === minOverlap) {
40512
+ // otherwise choose distribution with minimum area
40513
+ if (area < minArea) {
40514
+ minArea = area;
40515
+ index = i;
40516
+ }
40517
+ }
40518
+ }
40519
+
40520
+ return index || M - m;
40521
+ }
40522
+
40523
+ // sorts node children by the best axis for split
40524
+ _chooseSplitAxis(node, m, M) {
40525
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
40526
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
40527
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
40528
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
40529
+
40530
+ // if total distributions margin value is minimal for x, sort by minX,
40531
+ // otherwise it's already sorted by minY
40532
+ if (xMargin < yMargin) node.children.sort(compareMinX);
40533
+ }
40534
+
40535
+ // total margin of all possible split distributions where each node is at least m full
40536
+ _allDistMargin(node, m, M, compare) {
40537
+ node.children.sort(compare);
40538
+
40539
+ const toBBox = this.toBBox;
40540
+ const leftBBox = distBBox(node, 0, m, toBBox);
40541
+ const rightBBox = distBBox(node, M - m, M, toBBox);
40542
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
40543
+
40544
+ for (let i = m; i < M - m; i++) {
40545
+ const child = node.children[i];
40546
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
40547
+ margin += bboxMargin(leftBBox);
40548
+ }
40549
+
40550
+ for (let i = M - m - 1; i >= m; i--) {
40551
+ const child = node.children[i];
40552
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
40553
+ margin += bboxMargin(rightBBox);
40554
+ }
40555
+
40556
+ return margin;
40557
+ }
40558
+
40559
+ _adjustParentBBoxes(bbox, path, level) {
40560
+ // adjust bboxes along the given tree path
40561
+ for (let i = level; i >= 0; i--) {
40562
+ extend(path[i], bbox);
40563
+ }
40564
+ }
40565
+
40566
+ _condense(path) {
40567
+ // go through the path, removing empty nodes and updating bboxes
40568
+ for (let i = path.length - 1, siblings; i >= 0; i--) {
40569
+ if (path[i].children.length === 0) {
40570
+ if (i > 0) {
40571
+ siblings = path[i - 1].children;
40572
+ siblings.splice(siblings.indexOf(path[i]), 1);
40573
+
40574
+ } else this.clear();
40575
+
40576
+ } else calcBBox(path[i], this.toBBox);
40577
+ }
40578
+ }
40579
+ }
40580
+
40581
+ function findItem(item, items, equalsFn) {
40582
+ if (!equalsFn) return items.indexOf(item);
40583
+
40584
+ for (let i = 0; i < items.length; i++) {
40585
+ if (equalsFn(item, items[i])) return i;
40586
+ }
40587
+ return -1;
40588
+ }
40589
+
40590
+ // calculate node's bbox from bboxes of its children
40591
+ function calcBBox(node, toBBox) {
40592
+ distBBox(node, 0, node.children.length, toBBox, node);
40593
+ }
40594
+
40595
+ // min bounding rectangle of node children from k to p-1
40596
+ function distBBox(node, k, p, toBBox, destNode) {
40597
+ if (!destNode) destNode = createNode(null);
40598
+ destNode.minX = Infinity;
40599
+ destNode.minY = Infinity;
40600
+ destNode.maxX = -Infinity;
40601
+ destNode.maxY = -Infinity;
40602
+
40603
+ for (let i = k; i < p; i++) {
40604
+ const child = node.children[i];
40605
+ extend(destNode, node.leaf ? toBBox(child) : child);
40606
+ }
40607
+
40608
+ return destNode;
40609
+ }
40610
+
40611
+ function extend(a, b) {
40612
+ a.minX = Math.min(a.minX, b.minX);
40613
+ a.minY = Math.min(a.minY, b.minY);
40614
+ a.maxX = Math.max(a.maxX, b.maxX);
40615
+ a.maxY = Math.max(a.maxY, b.maxY);
40616
+ return a;
40617
+ }
40618
+
40619
+ function compareNodeMinX(a, b) { return a.minX - b.minX; }
40620
+ function compareNodeMinY(a, b) { return a.minY - b.minY; }
40621
+
40622
+ function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
40623
+ function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
40624
+
40625
+ function enlargedArea(a, b) {
40626
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
40627
+ (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
40628
+ }
40629
+
40630
+ function intersectionArea(a, b) {
40631
+ const minX = Math.max(a.minX, b.minX);
40632
+ const minY = Math.max(a.minY, b.minY);
40633
+ const maxX = Math.min(a.maxX, b.maxX);
40634
+ const maxY = Math.min(a.maxY, b.maxY);
40635
+
40636
+ return Math.max(0, maxX - minX) *
40637
+ Math.max(0, maxY - minY);
40638
+ }
40639
+
40640
+ function contains(a, b) {
40641
+ return a.minX <= b.minX &&
40642
+ a.minY <= b.minY &&
40643
+ b.maxX <= a.maxX &&
40644
+ b.maxY <= a.maxY;
40645
+ }
40646
+
40647
+ function intersects(a, b) {
40648
+ return b.minX <= a.maxX &&
40649
+ b.minY <= a.maxY &&
40650
+ b.maxX >= a.minX &&
40651
+ b.maxY >= a.minY;
40652
+ }
40653
+
40654
+ function createNode(children) {
40655
+ return {
40656
+ children,
40657
+ height: 1,
40658
+ leaf: true,
40659
+ minX: Infinity,
40660
+ minY: Infinity,
40661
+ maxX: -Infinity,
40662
+ maxY: -Infinity
40663
+ };
40664
+ }
40665
+
40666
+ // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
40667
+ // combines selection algorithm with binary divide & conquer approach
40668
+
40669
+ function multiSelect(arr, left, right, n, compare) {
40670
+ const stack = [left, right];
40671
+
40672
+ while (stack.length) {
40673
+ right = stack.pop();
40674
+ left = stack.pop();
40675
+
40676
+ if (right - left <= n) continue;
40677
+
40678
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
40679
+ quickselect(arr, mid, left, right, compare);
40680
+
40681
+ stack.push(left, mid, mid, right);
40682
+ }
40683
+ }
40684
+
40685
+ /**
40686
+ * R-Tree Data Structure
40687
+ *
40688
+ * R-Tree is a spatial data structure used for efficient indexing and querying
40689
+ * of multi-dimensional objects, particularly in geometric and spatial applications.
40690
+ *
40691
+ * It organizes objects into a tree hierarchy, grouping nearby objects together
40692
+ * in bounding boxes. Each node in the tree represents a bounding box that
40693
+ * contains its child nodes or leaf objects. This hierarchical structure allows
40694
+ * for faster spatial queries.
40695
+ *
40696
+ * @see https://en.wikipedia.org/wiki/R-tree
40697
+ *
40698
+ * Consider a 2D Space with four zones: A, B, C, D
40699
+ * +--------------------------+
40700
+ * | |
40701
+ * | +---+ +-------+ |
40702
+ * | | A | | B | |
40703
+ * | +---+ +-------+ |
40704
+ * | |
40705
+ * | |
40706
+ * | +---+ |
40707
+ * | | C | |
40708
+ * | +---+ |
40709
+ * | +-----------+ |
40710
+ * | | D | |
40711
+ * | +-----------+ |
40712
+ * | |
40713
+ * +--------------------------+
40714
+ *
40715
+ * It groups together zones that are spatially close into a minimum bounding box.
40716
+ * For example, A and B are grouped together in rectangle R1, and C and D are grouped
40717
+ * in R2.
40718
+ *
40719
+ * R0
40720
+ * +--------------------------+
40721
+ * | R1 |
40722
+ * | +-----------------+ |
40723
+ * | | A | | B | |
40724
+ * | +-----------------+ |
40725
+ * | |
40726
+ * | R2 |
40727
+ * | +---+---+---+ |
40728
+ * | | | C | | |
40729
+ * | | +---+ | |
40730
+ * | +-----------+ |
40731
+ * | | D | |
40732
+ * | +-----------+ |
40733
+ * | |
40734
+ * +--------------------------+
40735
+ *
40736
+ * The tree would look like this:
40737
+ * R0
40738
+ * / \
40739
+ * / \
40740
+ * R1 R2
40741
+ * | |
40742
+ * A,B C,D
40743
+
40744
+ * Choosing how to group the zones is crucial for the performance of the tree.
40745
+ * Key considerations include avoiding excessive empty space coverage and minimizing overlap
40746
+ * to reduce the number of subtrees processed during searches.
40747
+ *
40748
+ * Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
40749
+ * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
40750
+ * the choice cannot be made based on this criterion due to the same enlargement for different groupings,
40751
+ * we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
40752
+ *
40753
+ * This implementation is tailored for spreadsheet use, indexing objects associated
40754
+ * with a zone and a sheet.
40755
+ *
40756
+ * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
40757
+ * @see https://github.com/mourner/rbush
40758
+ */
40759
+ class SpreadsheetRTree {
40760
+ /**
40761
+ * One 2D R-tree per sheet
40762
+ */
40763
+ rTrees = {};
40764
+ /**
40765
+ * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
40766
+ * faster than inserting items one by one. After bulk loading (bulk insertion into
40767
+ * an empty tree), subsequent query performance is also ~20-30% better.
40768
+ */
40769
+ constructor(items = []) {
40770
+ const rangesPerSheet = {};
40771
+ for (const item of items) {
40772
+ const sheetId = item.boundingBox.sheetId;
40773
+ if (!rangesPerSheet[sheetId]) {
40774
+ rangesPerSheet[sheetId] = [];
40775
+ }
40776
+ rangesPerSheet[sheetId].push(item);
40777
+ }
40778
+ for (const sheetId in rangesPerSheet) {
40779
+ this.rTrees[sheetId] = new ZoneRBush();
40780
+ this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
40781
+ }
40782
+ }
40783
+ insert(item) {
40784
+ const sheetId = item.boundingBox.sheetId;
40785
+ if (!this.rTrees[sheetId]) {
40786
+ this.rTrees[sheetId] = new ZoneRBush();
40787
+ }
40788
+ this.rTrees[sheetId].insert(item);
40789
+ }
40790
+ search({ zone, sheetId }) {
40791
+ if (!this.rTrees[sheetId]) {
40792
+ return [];
40793
+ }
40794
+ return this.rTrees[sheetId].search({
40795
+ minX: zone.left,
40796
+ minY: zone.top,
40797
+ maxX: zone.right,
40798
+ maxY: zone.bottom,
40799
+ });
40800
+ }
40801
+ remove(item) {
40802
+ const sheetId = item.boundingBox.sheetId;
40803
+ if (!this.rTrees[sheetId]) {
40804
+ return;
40805
+ }
40806
+ this.rTrees[sheetId].remove(item, deepEquals);
40807
+ }
40808
+ }
40809
+ /**
40810
+ * RBush extension to use zones as bounding boxes
40811
+ */
40812
+ class ZoneRBush extends RBush {
40813
+ toBBox({ boundingBox }) {
40814
+ const zone = boundingBox.zone;
40815
+ return {
40816
+ minX: zone.left,
40817
+ minY: zone.top,
40818
+ maxX: zone.right,
40819
+ maxY: zone.bottom,
40820
+ };
40821
+ }
40822
+ compareMinX(a, b) {
40823
+ return a.boundingBox.zone.left - b.boundingBox.zone.left;
40824
+ }
40825
+ compareMinY(a, b) {
40826
+ return a.boundingBox.zone.top - b.boundingBox.zone.top;
40827
+ }
40828
+ }
40829
+
40064
40830
  /**
40065
- * This class is an implementation of a dependency Graph.
40831
+ * Implementation of a dependency Graph.
40066
40832
  * The graph is used to evaluate the cells in the correct
40067
40833
  * order, and should be updated each time a cell's content is modified
40068
40834
  *
40835
+ * It uses an R-Tree data structure to efficiently find dependent cells.
40069
40836
  */
40070
40837
  class FormulaDependencyGraph {
40071
- /**
40072
- * Internal structure:
40073
- * - key: a cell position (encoded as an integer)
40074
- * - value: a set of cell positions that depends on the key
40075
- *
40076
- * Given
40077
- * - A1:"= B1 + SQRT(B2)"
40078
- * - C1:"= B1";
40079
- * - C2:"= C1"
40080
- *
40081
- * we will have something like:
40082
- * - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
40083
- * - B2 ---> (A1) meaning A1 depends on B2
40084
- * - C1 ---> (C2) meaning C2 depends on C1
40085
- */
40086
- inverseDependencies = new Map();
40838
+ encoder;
40087
40839
  dependencies = new Map();
40840
+ rTree;
40841
+ constructor(encoder, data = []) {
40842
+ this.encoder = encoder;
40843
+ this.rTree = new SpreadsheetRTree(data);
40844
+ }
40088
40845
  removeAllDependencies(formulaPositionId) {
40089
- const dependencies = this.dependencies.get(formulaPositionId);
40090
- if (!dependencies) {
40846
+ const ranges = this.dependencies.get(formulaPositionId);
40847
+ if (!ranges) {
40091
40848
  return;
40092
40849
  }
40093
- for (const dependency of dependencies) {
40094
- this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
40850
+ for (const range of ranges) {
40851
+ this.rTree.remove(range);
40095
40852
  }
40096
40853
  this.dependencies.delete(formulaPositionId);
40097
40854
  }
40098
40855
  addDependencies(formulaPositionId, dependencies) {
40099
- for (const dependency of dependencies) {
40100
- const inverseDependencies = this.inverseDependencies.get(dependency);
40101
- if (inverseDependencies) {
40102
- inverseDependencies.add(formulaPositionId);
40103
- }
40104
- else {
40105
- this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
40106
- }
40856
+ const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
40857
+ data: formulaPositionId,
40858
+ boundingBox: {
40859
+ zone,
40860
+ sheetId,
40861
+ },
40862
+ }));
40863
+ for (const item of rTreeItems) {
40864
+ this.rTree.insert(item);
40107
40865
  }
40108
40866
  const existingDependencies = this.dependencies.get(formulaPositionId);
40109
40867
  if (existingDependencies) {
40110
- existingDependencies.push(...dependencies);
40868
+ existingDependencies.push(...rTreeItems);
40111
40869
  }
40112
40870
  else {
40113
- this.dependencies.set(formulaPositionId, dependencies);
40871
+ this.dependencies.set(formulaPositionId, rTreeItems);
40114
40872
  }
40115
40873
  }
40116
40874
  /**
@@ -40118,20 +40876,20 @@
40118
40876
  * in the correct order they should be evaluated.
40119
40877
  * This is called a topological ordering (excluding cycles)
40120
40878
  */
40121
- getCellsDependingOn(positionIds) {
40879
+ getCellsDependingOn(ranges) {
40122
40880
  const visited = new JetSet();
40123
- const queue = Array.from(positionIds).reverse();
40881
+ const queue = Array.from(ranges).reverse();
40124
40882
  while (queue.length > 0) {
40125
- const node = queue.pop();
40126
- visited.add(node);
40127
- const adjacentNodes = this.inverseDependencies.get(node) || new Set();
40128
- for (const adjacentNode of adjacentNodes) {
40129
- if (!visited.has(adjacentNode)) {
40130
- queue.push(adjacentNode);
40883
+ const range = queue.pop();
40884
+ visited.add(...this.encoder.encodeBoundingBox(range));
40885
+ const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
40886
+ for (const positionId of impactedPositionIds) {
40887
+ if (!visited.has(positionId)) {
40888
+ queue.push(this.encoder.decodeToBoundingBox(positionId));
40131
40889
  }
40132
40890
  }
40133
40891
  }
40134
- visited.delete(...positionIds);
40892
+ visited.delete(...ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
40135
40893
  return visited;
40136
40894
  }
40137
40895
  }
@@ -40219,9 +40977,9 @@
40219
40977
  context;
40220
40978
  getters;
40221
40979
  compilationParams;
40222
- positionEncoder = new PositionBitsEncoder();
40980
+ encoder = new PositionBitsEncoder();
40223
40981
  evaluatedCells = new Map();
40224
- formulaDependencies = lazy(new FormulaDependencyGraph());
40982
+ formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
40225
40983
  blockedArrayFormulas = new Set();
40226
40984
  spreadingRelations = new SpreadingRelation();
40227
40985
  constructor(context, getters) {
@@ -40230,23 +40988,23 @@
40230
40988
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
40231
40989
  }
40232
40990
  getEvaluatedCell(position) {
40233
- return (this.evaluatedCells.get(this.encodePosition(position)) ||
40991
+ return (this.evaluatedCells.get(this.encoder.encode(position)) ||
40234
40992
  createEvaluatedCell("", { locale: this.getters.getLocale() }));
40235
40993
  }
40236
40994
  getSpreadPositionsOf(position) {
40237
- const positionId = this.encodePosition(position);
40995
+ const positionId = this.encoder.encode(position);
40238
40996
  if (!this.spreadingRelations.isArrayFormula(positionId)) {
40239
40997
  return [];
40240
40998
  }
40241
- return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map(this.decodePosition.bind(this));
40999
+ return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map((positionId) => this.encoder.decode(positionId));
40242
41000
  }
40243
41001
  getArrayFormulaSpreadingOn(position) {
40244
- const positionId = this.encodePosition(position);
41002
+ const positionId = this.encoder.encode(position);
40245
41003
  const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
40246
- return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
41004
+ return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
40247
41005
  }
40248
41006
  getEvaluatedPositions() {
40249
- return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
41007
+ return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
40250
41008
  }
40251
41009
  getArrayFormulaSpreadingOnId(positionId) {
40252
41010
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
@@ -40256,7 +41014,7 @@
40256
41014
  return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
40257
41015
  }
40258
41016
  updateDependencies(position) {
40259
- const positionId = this.encodePosition(position);
41017
+ const positionId = this.encoder.encode(position);
40260
41018
  this.formulaDependencies().removeAllDependencies(positionId);
40261
41019
  const dependencies = this.getDirectDependencies(positionId);
40262
41020
  this.formulaDependencies().addDependencies(positionId, dependencies);
@@ -40266,12 +41024,12 @@
40266
41024
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
40267
41025
  }
40268
41026
  evaluateCells(positions) {
40269
- const cells = positions.map(this.encodePosition.bind(this));
41027
+ const cells = positions.map((p) => this.encoder.encode(p));
40270
41028
  const cellsToCompute = new JetSet(cells);
40271
- const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
41029
+ const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
40272
41030
  cellsToCompute.add(...this.getCellsDependingOn(cells));
40273
- cellsToCompute.add(...arrayFormulas);
40274
- cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
41031
+ cellsToCompute.add(...arrayFormulasPositionIds);
41032
+ cellsToCompute.add(...this.getCellsDependingOn(arrayFormulasPositionIds));
40275
41033
  this.evaluate(cellsToCompute);
40276
41034
  }
40277
41035
  getArrayFormulasImpactedByChangesOf(positionIds) {
@@ -40294,12 +41052,14 @@
40294
41052
  this.blockedArrayFormulas = new Set();
40295
41053
  this.spreadingRelations = new SpreadingRelation();
40296
41054
  this.formulaDependencies = lazy(() => {
40297
- const dependencyGraph = new FormulaDependencyGraph();
40298
- for (const positionId of this.getAllCells()) {
40299
- const dependencies = this.getDirectDependencies(positionId);
40300
- dependencyGraph.addDependencies(positionId, dependencies);
40301
- }
40302
- return dependencyGraph;
41055
+ const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
41056
+ data: positionId,
41057
+ boundingBox: {
41058
+ zone: range.zone,
41059
+ sheetId: range.sheetId,
41060
+ },
41061
+ })));
41062
+ return new FormulaDependencyGraph(this.encoder, dependencies);
40303
41063
  });
40304
41064
  }
40305
41065
  evaluateAllCells() {
@@ -40321,7 +41081,7 @@
40321
41081
  for (const sheetId of this.getters.getSheetIds()) {
40322
41082
  const cellIds = this.getters.getCells(sheetId);
40323
41083
  for (const cellId in cellIds) {
40324
- positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
41084
+ positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
40325
41085
  }
40326
41086
  }
40327
41087
  return positionIds;
@@ -40366,7 +41126,7 @@
40366
41126
  if (!this.blockedArrayFormulas.has(positionId)) {
40367
41127
  this.invalidateSpreading(positionId);
40368
41128
  }
40369
- const cellPosition = this.decodePosition(positionId);
41129
+ const cellPosition = this.encoder.decode(positionId);
40370
41130
  const cell = this.getters.getCell(cellPosition);
40371
41131
  if (cell === undefined) {
40372
41132
  return createEvaluatedCell("", { locale: this.getters.getLocale() });
@@ -40389,7 +41149,7 @@
40389
41149
  }
40390
41150
  }
40391
41151
  computeAndSave(position) {
40392
- const positionId = this.encodePosition(position);
41152
+ const positionId = this.encoder.encode(position);
40393
41153
  const evaluatedCell = this.computeCell(positionId);
40394
41154
  if (!this.evaluatedCells.has(positionId)) {
40395
41155
  this.setEvaluatedCell(positionId, evaluatedCell);
@@ -40444,15 +41204,15 @@
40444
41204
  throw new Error(_t("Result couldn't be automatically expanded. Please insert more columns and rows."));
40445
41205
  }
40446
41206
  updateSpreadRelation({ sheetId, col, row, }) {
40447
- const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
41207
+ const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
40448
41208
  return (i, j) => {
40449
41209
  const position = { sheetId, col: i + col, row: j + row };
40450
- const resultPositionId = this.encodePosition(position);
41210
+ const resultPositionId = this.encoder.encode(position);
40451
41211
  this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
40452
41212
  };
40453
41213
  }
40454
41214
  checkCollision({ sheetId, col, row }) {
40455
- const formulaPositionId = this.encodePosition({ sheetId, col, row });
41215
+ const formulaPositionId = this.encoder.encode({ sheetId, col, row });
40456
41216
  return (i, j) => {
40457
41217
  const position = { sheetId: sheetId, col: i + col, row: j + row };
40458
41218
  const rawCell = this.getters.getCell(position);
@@ -40473,7 +41233,7 @@
40473
41233
  format: format || matrixResult[i][j]?.format,
40474
41234
  locale: this.getters.getLocale(),
40475
41235
  });
40476
- const positionId = this.encodePosition(position);
41236
+ const positionId = this.encoder.encode(position);
40477
41237
  this.setEvaluatedCell(positionId, evaluatedCell);
40478
41238
  // check if formula dependencies present in the spread zone
40479
41239
  // if so, they need to be recomputed
@@ -40505,29 +41265,17 @@
40505
41265
  if (!cell?.isFormula) {
40506
41266
  return [];
40507
41267
  }
40508
- const dependencies = [];
40509
- for (const range of cell.compiledFormula.dependencies) {
40510
- if (range.invalidSheetName || range.invalidXc) {
40511
- continue;
40512
- }
40513
- const sheetId = range.sheetId;
40514
- forEachPositionsInZone(range.zone, (col, row) => {
40515
- dependencies.push(this.encodePosition({ sheetId, col, row }));
40516
- });
40517
- }
40518
- return dependencies;
41268
+ return cell.compiledFormula.dependencies;
40519
41269
  }
40520
41270
  getCellsDependingOn(positionIds) {
40521
- return this.formulaDependencies().getCellsDependingOn(positionIds);
41271
+ const ranges = [];
41272
+ for (const positionId of positionIds) {
41273
+ ranges.push(this.encoder.decodeToBoundingBox(positionId));
41274
+ }
41275
+ return this.formulaDependencies().getCellsDependingOn(ranges);
40522
41276
  }
40523
41277
  getCell(positionId) {
40524
- return this.getters.getCell(this.decodePosition(positionId));
40525
- }
40526
- encodePosition(position) {
40527
- return this.positionEncoder.encode(position);
40528
- }
40529
- decodePosition(positionId) {
40530
- return this.positionEncoder.decode(positionId);
41278
+ return this.getters.getCell(this.encoder.decode(positionId));
40531
41279
  }
40532
41280
  }
40533
41281
  function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
@@ -40583,6 +41331,13 @@
40583
41331
  encode({ sheetId, col, row }) {
40584
41332
  return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
40585
41333
  }
41334
+ encodeBoundingBox({ sheetId, zone }) {
41335
+ const positions = [];
41336
+ forEachPositionsInZone(zone, (col, row) => {
41337
+ positions.push(this.encode({ sheetId, col, row }));
41338
+ });
41339
+ return positions;
41340
+ }
40586
41341
  decode(id) {
40587
41342
  // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
40588
41343
  const row = Number(id & 2097151n);
@@ -40590,6 +41345,10 @@
40590
41345
  const sheetId = this.decodeSheet(id >> 42n);
40591
41346
  return { sheetId, col, row };
40592
41347
  }
41348
+ decodeToBoundingBox(id) {
41349
+ const { sheetId, col, row } = this.decode(id);
41350
+ return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
41351
+ }
40593
41352
  encodeSheet(sheetId) {
40594
41353
  const sheetKey = this.sheetMapping[sheetId];
40595
41354
  if (sheetKey === undefined) {
@@ -40772,7 +41531,12 @@
40772
41531
  // Getters
40773
41532
  // ---------------------------------------------------------------------------
40774
41533
  evaluateFormula(sheetId, formulaString) {
40775
- return this.evaluator.evaluateFormula(sheetId, formulaString);
41534
+ try {
41535
+ return this.evaluator.evaluateFormula(sheetId, formulaString);
41536
+ }
41537
+ catch (error) {
41538
+ return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
41539
+ }
40776
41540
  }
40777
41541
  /**
40778
41542
  * Return the value of each cell in the range as they are displayed in the grid.
@@ -41041,7 +41805,7 @@
41041
41805
  }
41042
41806
 
41043
41807
  class EvaluationChartPlugin extends UIPlugin {
41044
- static getters = ["getChartRuntime", "getBackgroundOfSingleCellChart"];
41808
+ static getters = ["getChartRuntime", "getStyleOfSingleCellChart"];
41045
41809
  charts = {};
41046
41810
  createRuntimeChart = chartRuntimeFactory(this.getters);
41047
41811
  handle(cmd) {
@@ -41079,25 +41843,26 @@
41079
41843
  return this.charts[figureId];
41080
41844
  }
41081
41845
  /**
41082
- * Get the background color of a chart based on the color of the first cell of the main range
41083
- * of the chart. In order of priority, it will return :
41084
- *
41085
- * - the chart background color if one is defined
41086
- * - the fill color of the cell if one is defined
41087
- * - the fill color of the cell from conditional formats if one is defined
41088
- * - the default chart color if no other color is defined
41846
+ * Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.
41089
41847
  */
41090
- getBackgroundOfSingleCellChart(chartBackground, mainRange) {
41848
+ getStyleOfSingleCellChart(chartBackground, mainRange) {
41091
41849
  if (chartBackground)
41092
- return chartBackground;
41850
+ return { background: chartBackground, fontColor: chartFontColor(chartBackground) };
41093
41851
  if (!mainRange) {
41094
- return BACKGROUND_CHART_COLOR;
41852
+ return {
41853
+ background: BACKGROUND_CHART_COLOR,
41854
+ fontColor: chartFontColor(BACKGROUND_CHART_COLOR),
41855
+ };
41095
41856
  }
41096
41857
  const col = mainRange.zone.left;
41097
41858
  const row = mainRange.zone.top;
41098
41859
  const sheetId = mainRange.sheetId;
41099
41860
  const style = this.getters.getCellComputedStyle({ sheetId, col, row });
41100
- return style.fillColor || BACKGROUND_CHART_COLOR;
41861
+ const background = style.fillColor || BACKGROUND_CHART_COLOR;
41862
+ return {
41863
+ background,
41864
+ fontColor: style.textColor || chartFontColor(background),
41865
+ };
41101
41866
  }
41102
41867
  exportForExcel(data) {
41103
41868
  for (const sheet of data.sheets) {
@@ -41208,45 +41973,40 @@
41208
41973
  getComputedStyles(sheetId) {
41209
41974
  const computedStyle = {};
41210
41975
  for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
41211
- try {
41212
- switch (cf.rule.type) {
41213
- case "ColorScaleRule":
41214
- for (let range of cf.ranges) {
41215
- this.applyColorScale(sheetId, range, cf.rule, computedStyle);
41216
- }
41217
- break;
41218
- case "CellIsRule":
41219
- const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
41220
- for (let ref of cf.ranges) {
41221
- const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
41222
- for (let row = zone.top; row <= zone.bottom; row++) {
41223
- for (let col = zone.left; col <= zone.right; col++) {
41224
- const predicate = this.rulePredicate[cf.rule.type];
41225
- const target = { sheetId, col, row };
41226
- const values = cf.rule.values.map((value, i) => {
41227
- const compiledFormula = formulas[i];
41228
- if (compiledFormula) {
41229
- return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
41230
- ...compiledFormula,
41231
- dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
41232
- });
41233
- }
41234
- return value;
41235
- });
41236
- if (predicate && predicate(target, { ...cf.rule, values })) {
41237
- if (!computedStyle[col])
41238
- computedStyle[col] = [];
41239
- // we must combine all the properties of all the CF rules applied to the given cell
41240
- computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
41976
+ switch (cf.rule.type) {
41977
+ case "ColorScaleRule":
41978
+ for (let range of cf.ranges) {
41979
+ this.applyColorScale(sheetId, range, cf.rule, computedStyle);
41980
+ }
41981
+ break;
41982
+ case "CellIsRule":
41983
+ const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
41984
+ for (let ref of cf.ranges) {
41985
+ const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
41986
+ for (let row = zone.top; row <= zone.bottom; row++) {
41987
+ for (let col = zone.left; col <= zone.right; col++) {
41988
+ const predicate = this.rulePredicate[cf.rule.type];
41989
+ const target = { sheetId, col, row };
41990
+ const values = cf.rule.values.map((value, i) => {
41991
+ const compiledFormula = formulas[i];
41992
+ if (compiledFormula) {
41993
+ return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
41994
+ ...compiledFormula,
41995
+ dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
41996
+ });
41241
41997
  }
41998
+ return value;
41999
+ });
42000
+ if (predicate && predicate(target, { ...cf.rule, values })) {
42001
+ if (!computedStyle[col])
42002
+ computedStyle[col] = [];
42003
+ // we must combine all the properties of all the CF rules applied to the given cell
42004
+ computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
41242
42005
  }
41243
42006
  }
41244
42007
  }
41245
- break;
41246
- }
41247
- }
41248
- catch (_) {
41249
- // we don't care about the errors within the evaluation of a rule
42008
+ }
42009
+ break;
41250
42010
  }
41251
42011
  }
41252
42012
  return computedStyle;
@@ -41857,11 +42617,6 @@
41857
42617
  return "Success" /* CommandResult.Success */;
41858
42618
  }
41859
42619
  return "InvalidAutofillSelection" /* CommandResult.InvalidAutofillSelection */;
41860
- case "AUTOFILL_AUTO":
41861
- const zone = this.getters.getSelectedZone();
41862
- return zone.top === zone.bottom
41863
- ? "Success" /* CommandResult.Success */
41864
- : "CancelledForUnknownReason" /* CommandResult.CancelledForUnknownReason */;
41865
42620
  }
41866
42621
  return "Success" /* CommandResult.Success */;
41867
42622
  }
@@ -42018,7 +42773,7 @@
42018
42773
  let row = zone.bottom;
42019
42774
  if (col > 0) {
42020
42775
  let leftPosition = { sheetId, col: col - 1, row };
42021
- while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty ||
42776
+ while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
42022
42777
  this.getters.getCell(leftPosition)?.content) {
42023
42778
  row += 1;
42024
42779
  leftPosition = { sheetId, col: col - 1, row };
@@ -42028,7 +42783,7 @@
42028
42783
  col = zone.right;
42029
42784
  if (col <= this.getters.getNumberCols(sheetId)) {
42030
42785
  let rightPosition = { sheetId, col: col + 1, row };
42031
- while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty ||
42786
+ while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
42032
42787
  this.getters.getCell(rightPosition)?.content) {
42033
42788
  row += 1;
42034
42789
  rightPosition = { sheetId, col: col + 1, row };
@@ -43623,7 +44378,7 @@
43623
44378
  isPasteAllowed(target, clipboardOption) {
43624
44379
  const sheetId = this.getters.getActiveSheetId();
43625
44380
  if (this.operation === "CUT" && clipboardOption?.pasteOption !== undefined) {
43626
- // cannot paste only format or only value if the previous operation is a CUT
44381
+ // cannot paste only format or as value if the previous operation is a CUT
43627
44382
  return "WrongPasteOption" /* CommandResult.WrongPasteOption */;
43628
44383
  }
43629
44384
  if (target.length > 1) {
@@ -43805,7 +44560,7 @@
43805
44560
  // This condition is used to determine if we have to paste the CF or not.
43806
44561
  // We have to do it when the command handled is "PASTE", not "INSERT_CELL"
43807
44562
  // or "DELETE_CELL". So, the state should be the local state
43808
- const shouldPasteCF = clipboardOptions?.pasteOption !== "onlyValue" && clipboardOptions?.shouldPasteCF;
44563
+ const shouldPasteCF = clipboardOptions?.pasteOption !== "asValue" && clipboardOptions?.shouldPasteCF;
43809
44564
  const shouldPasteDV = !clipboardOptions?.pasteOption;
43810
44565
  const sheetId = this.getters.getActiveSheetId();
43811
44566
  // first, add missing cols/rows if needed
@@ -43841,10 +44596,11 @@
43841
44596
  pasteCell(origin, target, operation, clipboardOption) {
43842
44597
  const { sheetId, col, row } = target;
43843
44598
  const targetCell = this.getters.getEvaluatedCell(target);
43844
- if (clipboardOption?.pasteOption === "onlyValue") {
44599
+ const originFormat = origin.cell?.format ?? origin.evaluatedCell.format;
44600
+ if (clipboardOption?.pasteOption === "asValue") {
43845
44601
  const locale = this.getters.getLocale();
43846
44602
  const content = formatValue(origin.evaluatedCell.value, { locale });
43847
- this.dispatch("UPDATE_CELL", { ...target, content });
44603
+ this.dispatch("UPDATE_CELL", { ...target, content, format: originFormat });
43848
44604
  return;
43849
44605
  }
43850
44606
  const targetBorders = this.getters.getCellBorder(target);
@@ -43860,7 +44616,7 @@
43860
44616
  this.dispatch("UPDATE_CELL", {
43861
44617
  ...target,
43862
44618
  style: origin.cell?.style ?? null,
43863
- format: origin.cell?.format ?? origin.evaluatedCell.format ?? targetCell.format,
44619
+ format: originFormat ?? targetCell.format,
43864
44620
  });
43865
44621
  return;
43866
44622
  }
@@ -52308,7 +53064,7 @@
52308
53064
  }
52309
53065
  onKeydown(ev) {
52310
53066
  let keyDownString = "";
52311
- if (ev.ctrlKey || ev.metaKey) {
53067
+ if (isCtrlKey(ev)) {
52312
53068
  keyDownString += "CTRL+";
52313
53069
  }
52314
53070
  keyDownString += ev.key.toUpperCase();
@@ -55045,7 +55801,8 @@
55045
55801
  }
55046
55802
  else if (cell.content && cell.content !== "") {
55047
55803
  const isTableHeader = isCellTableHeader(c, r, sheet);
55048
- ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader));
55804
+ const isPlainText = !!(cell.format && data.formats[cell.format] === PLAIN_TEXT_FORMAT);
55805
+ ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isPlainText));
55049
55806
  }
55050
55807
  attributes.push(...additionalAttrs);
55051
55808
  cellNodes.push(escapeXml /*xml*/ `
@@ -55974,6 +56731,7 @@
55974
56731
  colorToRGBA,
55975
56732
  positionToZone,
55976
56733
  isDefined: isDefined$1,
56734
+ isMatrix,
55977
56735
  lazy,
55978
56736
  genericRepeat,
55979
56737
  createAction,
@@ -56058,9 +56816,9 @@
56058
56816
  exports.tokenize = tokenize;
56059
56817
 
56060
56818
 
56061
- __info__.version = "17.1.0-alpha.5";
56062
- __info__.date = "2023-12-05T09:51:40.034Z";
56063
- __info__.hash = "c2823eb";
56819
+ __info__.version = "17.1.0-alpha.6";
56820
+ __info__.date = "2024-01-04T12:22:45.921Z";
56821
+ __info__.hash = "56dbdc9";
56064
56822
 
56065
56823
 
56066
56824
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);