@odoo/o-spreadsheet 19.0.5 → 19.0.7

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 19.0.5
6
- * @date 2025-10-07T10:04:06.400Z
7
- * @hash 86fc442
5
+ * @version 19.0.7
6
+ * @date 2025-10-23T08:19:01.764Z
7
+ * @hash 1c1d1ec
8
8
  */
9
9
 
10
10
  'use strict';
@@ -1893,6 +1893,29 @@ profilesStartingPosition, profiles, zones, toRemove = false) {
1893
1893
  removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
1894
1894
  }
1895
1895
  }
1896
+ function profilesContainsZone(profilesStartingPosition, profiles, zone) {
1897
+ const leftValue = zone.left;
1898
+ const rightValue = zone.right;
1899
+ const topValue = zone.top;
1900
+ const bottomValue = zone.bottom + 1;
1901
+ const leftIndex = binaryPredecessorSearch(profilesStartingPosition, leftValue, 0);
1902
+ const rightIndex = binaryPredecessorSearch(profilesStartingPosition, rightValue, leftIndex);
1903
+ if (leftIndex === -1 || rightIndex === -1) {
1904
+ return false;
1905
+ }
1906
+ for (let i = leftIndex; i <= rightIndex; i++) {
1907
+ const profile = profiles.get(profilesStartingPosition[i]);
1908
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, true);
1909
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, true);
1910
+ if (topPredIndex === -1 || topPredIndex % 2 !== 0) {
1911
+ return false;
1912
+ }
1913
+ if (topValue < profile[topPredIndex] || bottomValue > profile[bottomSuccIndex]) {
1914
+ return false;
1915
+ }
1916
+ }
1917
+ return true;
1918
+ }
1896
1919
  function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
1897
1920
  if (value === undefined) {
1898
1921
  // this is only the case when the value correspond to a bottom value that could be undefined
@@ -1977,7 +2000,18 @@ function modifyProfile(profile, zone, toRemove = false) {
1977
2000
  }
1978
2001
  // add the top and bottom value to the profile and
1979
2002
  // remove all information between the top and bottom index
1980
- profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
2003
+ const toDelete = bottomSuccIndex - topPredIndex - 1;
2004
+ const toInsert = newPoints.length;
2005
+ const start = topPredIndex + 1;
2006
+ // fast path and slow path
2007
+ if (start === profile.length - 1 && toDelete === 1 && toInsert === 1) {
2008
+ // fast path: we just need to replace the last element
2009
+ profile[start] = newPoints[0] ?? newPoints[1];
2010
+ }
2011
+ else {
2012
+ // equivalent but slower and with memory allocation
2013
+ profile.splice(start, toDelete, ...newPoints);
2014
+ }
1981
2015
  }
1982
2016
  function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
1983
2017
  const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
@@ -2016,8 +2050,10 @@ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
2016
2050
  left,
2017
2051
  bottom,
2018
2052
  right,
2019
- hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
2020
2053
  };
2054
+ if ((bottom === undefined && top !== 0) || (right === undefined && left !== 0)) {
2055
+ profileZone.hasHeader = true;
2056
+ }
2021
2057
  let findCorrespondingZone = false;
2022
2058
  for (let j = pendingZones.length - 1; j >= 0; j--) {
2023
2059
  const pendingZone = pendingZones[j];
@@ -2502,17 +2538,6 @@ function excludeTopLeft(zone) {
2502
2538
  }
2503
2539
  return [leftColumnZone, rightPartZone];
2504
2540
  }
2505
- function aggregatePositionsToZones(positions) {
2506
- const result = {};
2507
- for (const position of positions) {
2508
- result[position.sheetId] ??= [];
2509
- result[position.sheetId].push(positionToZone(position));
2510
- }
2511
- for (const sheetId in result) {
2512
- result[sheetId] = recomputeZones(result[sheetId]);
2513
- }
2514
- return result;
2515
- }
2516
2541
  /**
2517
2542
  * Array of all positions in the zone.
2518
2543
  */
@@ -4515,7 +4540,17 @@ function toNumberMatrix(data, argName) {
4515
4540
  return toMatrix(data).map((row) => {
4516
4541
  return row.map((cell) => {
4517
4542
  if (typeof cell.value !== "number") {
4518
- throw new EvaluationError(_t("Function [[FUNCTION_NAME]] expects number values for %s, but got a %s.", argName, typeof cell.value));
4543
+ let message = "";
4544
+ if (typeof cell === "object") {
4545
+ message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got an empty value.", argName);
4546
+ }
4547
+ else if (typeof cell === "string") {
4548
+ message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a string.", argName);
4549
+ }
4550
+ else if (typeof cell === "boolean") {
4551
+ message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a boolean.", argName);
4552
+ }
4553
+ throw new EvaluationError(message);
4519
4554
  }
4520
4555
  return cell.value;
4521
4556
  });
@@ -6934,6 +6969,10 @@ function getRangeParts(xc, zone) {
6934
6969
  }
6935
6970
  return parts;
6936
6971
  }
6972
+ function positionToBoundedRange(position) {
6973
+ const zone = { left: position.col, top: position.row, right: position.col, bottom: position.row };
6974
+ return { sheetId: position.sheetId, zone };
6975
+ }
6937
6976
  /**
6938
6977
  * Check that a zone is valid regarding the order of top-bottom and left-right.
6939
6978
  * Left should be smaller than right, top should be smaller than bottom.
@@ -9530,7 +9569,7 @@ class CellClipboardHandler extends AbstractCellClipboardHandler {
9530
9569
  pasteCell(origin, target, clipboardOption) {
9531
9570
  const { sheetId, col, row } = target;
9532
9571
  const targetCell = this.getters.getEvaluatedCell(target);
9533
- const originFormat = origin?.format ?? origin.evaluatedCell.format;
9572
+ const originFormat = origin?.format || origin.evaluatedCell.format;
9534
9573
  if (clipboardOption?.pasteOption === "asValue") {
9535
9574
  this.dispatch("UPDATE_CELL", {
9536
9575
  ...target,
@@ -13903,7 +13942,7 @@ const GROWTH = {
13903
13942
  if (knownDataY.length === 0 || knownDataY[0].length === 0) {
13904
13943
  return new EvaluationError(emptyDataErrorMessage("known_data_y"));
13905
13944
  }
13906
- return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, "the first argument (known_data_y)")), toNumberMatrix(knownDataX, "the second argument (known_data_x)"), toNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b)));
13945
+ return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, "known_data_y")), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b)));
13907
13946
  },
13908
13947
  };
13909
13948
  // -----------------------------------------------------------------------------
@@ -13976,7 +14015,7 @@ const LINEST = {
13976
14015
  if (dataY.length === 0 || dataY[0].length === 0) {
13977
14016
  return new EvaluationError(emptyDataErrorMessage("data_y"));
13978
14017
  }
13979
- return fullLinearRegression(toNumberMatrix(dataX, "the first argument (data_y)"), toNumberMatrix(dataY, "the second argument (data_x)"), toBoolean(calculateB), toBoolean(verbose));
14018
+ return fullLinearRegression(toNumberMatrix(dataX, "data_x"), toNumberMatrix(dataY, "data_y"), toBoolean(calculateB), toBoolean(verbose));
13980
14019
  },
13981
14020
  isExported: true,
13982
14021
  };
@@ -13995,7 +14034,7 @@ const LOGEST = {
13995
14034
  if (dataY.length === 0 || dataY[0].length === 0) {
13996
14035
  return new EvaluationError(emptyDataErrorMessage("data_y"));
13997
14036
  }
13998
- const coeffs = fullLinearRegression(toNumberMatrix(dataX, "the second argument (data_x)"), logM(toNumberMatrix(dataY, "the first argument (data_y)")), toBoolean(calculateB), toBoolean(verbose));
14037
+ const coeffs = fullLinearRegression(toNumberMatrix(dataX, "data_x"), logM(toNumberMatrix(dataY, "data_y")), toBoolean(calculateB), toBoolean(verbose));
13999
14038
  for (let i = 0; i < coeffs.length; i++) {
14000
14039
  coeffs[i][0] = Math.exp(coeffs[i][0]);
14001
14040
  }
@@ -14616,7 +14655,7 @@ const TREND = {
14616
14655
  if (knownDataY.length === 0 || knownDataY[0].length === 0) {
14617
14656
  return new EvaluationError(emptyDataErrorMessage("known_data_y"));
14618
14657
  }
14619
- return predictLinearValues(toNumberMatrix(knownDataY, "the first argument (known_data_y)"), toNumberMatrix(knownDataX, "the second argument (known_data_x)"), toNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b));
14658
+ return predictLinearValues(toNumberMatrix(knownDataY, "known_data_y"), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b));
14620
14659
  },
14621
14660
  };
14622
14661
  // -----------------------------------------------------------------------------
@@ -23137,6 +23176,10 @@ const chartShowValuesPlugin = {
23137
23176
  }
23138
23177
  const ctx = chart.ctx;
23139
23178
  ctx.save();
23179
+ const { left, top, height, width } = chart.chartArea;
23180
+ ctx.beginPath();
23181
+ ctx.rect(left, top, width, height);
23182
+ ctx.clip();
23140
23183
  ctx.textAlign = "center";
23141
23184
  ctx.textBaseline = "middle";
23142
23185
  ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
@@ -24180,7 +24223,7 @@ class ChartJsComponent extends owl.Component {
24180
24223
  this.chart.update();
24181
24224
  }
24182
24225
  hasChartDataChanged() {
24183
- return !deepEquals(this.currentRuntime.chartJsConfig.data, this.chartRuntime.chartJsConfig.data);
24226
+ return !deepEquals(this.getChartDataInRuntime(this.currentRuntime), this.getChartDataInRuntime(this.chartRuntime));
24184
24227
  }
24185
24228
  enableAnimationInChartData(chartData) {
24186
24229
  return {
@@ -24188,6 +24231,17 @@ class ChartJsComponent extends owl.Component {
24188
24231
  options: { ...chartData.options, animation: { animateRotate: true } },
24189
24232
  };
24190
24233
  }
24234
+ getChartDataInRuntime(runtime) {
24235
+ const data = runtime.chartJsConfig.data;
24236
+ return {
24237
+ labels: data.labels,
24238
+ dataset: data.datasets.map((dataset) => ({
24239
+ data: dataset.data,
24240
+ label: dataset.label,
24241
+ tree: dataset.tree,
24242
+ })),
24243
+ };
24244
+ }
24191
24245
  get animationChartId() {
24192
24246
  return this.props.isFullScreen ? this.props.chartId + "-fullscreen" : this.props.chartId;
24193
24247
  }
@@ -25611,6 +25665,7 @@ function getChartTimeOptions(labels, labelFormat, locale) {
25611
25665
  parser: luxonFormat,
25612
25666
  displayFormats,
25613
25667
  unit: timeUnit ?? false,
25668
+ tooltipFormat: luxonFormat,
25614
25669
  };
25615
25670
  }
25616
25671
  /**
@@ -26679,6 +26734,7 @@ function getLineChartScales(definition, args) {
26679
26734
  };
26680
26735
  Object.assign(scales.x, axis);
26681
26736
  scales.x.ticks.maxTicksLimit = 15;
26737
+ delete scales?.x?.ticks?.callback;
26682
26738
  }
26683
26739
  else if (axisType === "linear") {
26684
26740
  scales.x.type = "linear";
@@ -32364,7 +32420,7 @@ class ChartDashboardMenu extends owl.Component {
32364
32420
  }
32365
32421
  openContextMenu(ev) {
32366
32422
  this.menuState.isOpen = true;
32367
- this.menuState.anchorRect = { x: ev.clientX, y: ev.clientY, width: 0, height: 0 };
32423
+ this.menuState.anchorRect = getBoundingRectAsPOJO(ev.currentTarget);
32368
32424
  const figureId = this.env.model.getters.getFigureIdFromChartId(this.props.chartId);
32369
32425
  this.menuState.menuItems = getChartMenuActions(figureId, () => { }, this.env);
32370
32426
  }
@@ -32396,6 +32452,7 @@ class CarouselFigure extends owl.Component {
32396
32452
  onFigureDeleted: Function,
32397
32453
  editFigureStyle: { type: Function, optional: true },
32398
32454
  isFullScreen: { type: Boolean, optional: true },
32455
+ openContextMenu: { type: Function, optional: true },
32399
32456
  };
32400
32457
  static components = { ChartDashboardMenu, MenuPopover };
32401
32458
  carouselTabsRef = owl.useRef("carouselTabs");
@@ -32529,6 +32586,12 @@ class CarouselFigure extends owl.Component {
32529
32586
  get visibleCarouselItems() {
32530
32587
  return this.carousel.items.filter((item) => item.type === "carouselDataView" && this.props.isFullScreen ? false : true);
32531
32588
  }
32589
+ openContextMenu(event) {
32590
+ const target = event.currentTarget;
32591
+ if (target) {
32592
+ this.props.openContextMenu?.(getBoundingRectAsPOJO(target));
32593
+ }
32594
+ }
32532
32595
  }
32533
32596
 
32534
32597
  class ChartFigure extends owl.Component {
@@ -32538,6 +32601,7 @@ class ChartFigure extends owl.Component {
32538
32601
  onFigureDeleted: Function,
32539
32602
  editFigureStyle: { type: Function, optional: true },
32540
32603
  isFullScreen: { type: Boolean, optional: true },
32604
+ openContextMenu: { type: Function, optional: true },
32541
32605
  };
32542
32606
  static components = { ChartDashboardMenu };
32543
32607
  onDoubleClick() {
@@ -32570,6 +32634,7 @@ class ImageFigure extends owl.Component {
32570
32634
  figureUI: Object,
32571
32635
  onFigureDeleted: Function,
32572
32636
  editFigureStyle: { type: Function, optional: true },
32637
+ openContextMenu: { type: Function, optional: true },
32573
32638
  };
32574
32639
  static components = {};
32575
32640
  // ---------------------------------------------------------------------------
@@ -34621,8 +34686,11 @@ class Composer extends owl.Component {
34621
34686
  }
34622
34687
  const newSelection = this.contentHelper.getCurrentSelection();
34623
34688
  this.props.composerStore.stopComposerRangeSelection();
34624
- this.props.onComposerContentFocused();
34625
- this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
34689
+ const isCurrentlyInactive = this.props.composerStore.editionMode === "inactive";
34690
+ this.props.onComposerContentFocused(newSelection);
34691
+ if (!isCurrentlyInactive) {
34692
+ this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
34693
+ }
34626
34694
  this.processTokenAtCursor();
34627
34695
  }
34628
34696
  onDblClick() {
@@ -35117,13 +35185,6 @@ class AbstractComposerStore extends SpreadsheetStore {
35117
35185
  }
35118
35186
  }
35119
35187
  startEdition(text, selection) {
35120
- if (selection) {
35121
- const content = text || this.getComposerContent(this.getters.getActivePosition());
35122
- const validSelection = this.isSelectionValid(content.length, selection.start, selection.end);
35123
- if (!validSelection) {
35124
- return;
35125
- }
35126
- }
35127
35188
  const { col, row } = this.getters.getActivePosition();
35128
35189
  this.model.dispatch("SELECT_FIGURE", { figureId: null });
35129
35190
  this.model.dispatch("SCROLL_TO_CELL", { col, row });
@@ -35180,7 +35241,7 @@ class AbstractComposerStore extends SpreadsheetStore {
35180
35241
  // ---------------------------------------------------------------------------
35181
35242
  get currentContent() {
35182
35243
  if (this.editionMode === "inactive") {
35183
- return this.getComposerContent(this.getters.getActivePosition());
35244
+ return this.getComposerContent(this.getters.getActivePosition()).text;
35184
35245
  }
35185
35246
  return this._currentContent;
35186
35247
  }
@@ -35379,8 +35440,9 @@ class AbstractComposerStore extends SpreadsheetStore {
35379
35440
  this.sheetId = sheetId;
35380
35441
  this.row = row;
35381
35442
  this.editionMode = "editing";
35382
- this.initialContent = this.getComposerContent({ sheetId, col, row });
35383
- this.setContent(str || this.initialContent, selection);
35443
+ const { text, adjustedSelection } = this.getComposerContent({ sheetId, col, row }, selection);
35444
+ this.initialContent = text;
35445
+ this.setContent(str || this.initialContent, adjustedSelection ?? selection);
35384
35446
  this.colorIndexByRange = {};
35385
35447
  const zone = positionToZone({ col: this.col, row: this.row });
35386
35448
  this.captureSelection(zone, col, row);
@@ -35857,7 +35919,7 @@ class StandaloneComposerStore extends AbstractComposerStore {
35857
35919
  constructor(get, args) {
35858
35920
  super(get);
35859
35921
  this.args = args;
35860
- this._currentContent = this.getComposerContent();
35922
+ this._currentContent = this.getComposerContent().text;
35861
35923
  }
35862
35924
  getAutoCompleteProviders() {
35863
35925
  const providersDefinitions = super.getAutoCompleteProviders();
@@ -35894,7 +35956,7 @@ class StandaloneComposerStore extends AbstractComposerStore {
35894
35956
  })
35895
35957
  .join("");
35896
35958
  }
35897
- return localizeContent(content, this.getters.getLocale());
35959
+ return { text: localizeContent(content, this.getters.getLocale()) };
35898
35960
  }
35899
35961
  stopEdition() {
35900
35962
  this._stopEdition();
@@ -38664,12 +38726,23 @@ cellPopoverRegistry
38664
38726
  .add("LinkEditor", LinkEditorPopoverBuilder)
38665
38727
  .add("FilterMenu", FilterMenuPopoverBuilder);
38666
38728
 
38667
- const CHART_LIMITS = {
38668
- MAX_PIE_CATEGORIES: 7,
38669
- MAX_PIE_CATEGORIES_NO_TITLE: 6,
38670
- MIN_RADAR_CATEGORIES: 3,
38671
- MAX_RADAR_CATEGORIES: 12,
38672
- PERCENTAGE_THRESHOLD: 100,
38729
+ const DEFAULT_BAR_CHART_CONFIG = {
38730
+ type: "bar",
38731
+ title: {},
38732
+ dataSets: [],
38733
+ legendPosition: "none",
38734
+ dataSetsHaveTitle: false,
38735
+ stacked: false,
38736
+ };
38737
+ const DEFAULT_LINE_CHART_CONFIG = {
38738
+ type: "line",
38739
+ title: {},
38740
+ dataSets: [],
38741
+ legendPosition: "none",
38742
+ dataSetsHaveTitle: false,
38743
+ stacked: false,
38744
+ cumulative: false,
38745
+ labelsAsText: false,
38673
38746
  };
38674
38747
  function getUnboundRange(getters, zone) {
38675
38748
  return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
@@ -38708,43 +38781,19 @@ function detectColumnType(cells) {
38708
38781
  return detectedType;
38709
38782
  }
38710
38783
  function categorizeColumns(zones, getters) {
38711
- const columns = {
38712
- number: [],
38713
- text: [],
38714
- date: [],
38715
- };
38784
+ const columns = [];
38716
38785
  for (const zone of getZonesByColumns(zones)) {
38717
38786
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
38718
- const type = detectColumnType(cells);
38719
- if (type !== "empty") {
38720
- const targetType = type === "percentage" ? "number" : type;
38721
- columns[targetType].push({ zone, type });
38722
- }
38787
+ columns.push({ zone, type: detectColumnType(cells) });
38723
38788
  }
38724
38789
  return columns;
38725
38790
  }
38726
38791
  function getCellStats(getters, zone) {
38727
38792
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
38728
- const uniqueValues = new Set();
38729
- let totalCount = 0;
38730
- let percentageSum = 0;
38731
- for (let i = 0; i < cells.length; i++) {
38732
- const { value } = cells[i];
38733
- const str = value?.toString().trim();
38734
- if (!str) {
38735
- continue;
38736
- }
38737
- uniqueValues.add(str);
38738
- totalCount++;
38739
- const num = Number(value);
38740
- if (!isNaN(num)) {
38741
- percentageSum += Math.abs(num) * 100;
38742
- }
38743
- }
38793
+ const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
38744
38794
  return {
38745
- uniqueCount: uniqueValues.size,
38746
- totalCount,
38747
- percentageSum,
38795
+ uniqueCount: new Set(values).size,
38796
+ totalCount: values.length,
38748
38797
  };
38749
38798
  }
38750
38799
  function isDatasetTitled(getters, column) {
@@ -38755,167 +38804,191 @@ function isDatasetTitled(getters, column) {
38755
38804
  });
38756
38805
  return ![CellValueType.number, CellValueType.empty].includes(titleCell.type);
38757
38806
  }
38758
- function createBaseChart(type, dataSets, options = {}) {
38759
- return {
38760
- type,
38761
- title: {},
38762
- dataSets,
38763
- legendPosition: "none",
38764
- ...options,
38765
- };
38766
- }
38807
+ /**
38808
+ * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
38809
+ * - If the column contains a single cell, create a scorecard.
38810
+ * - If the column type is "percentage", create a pie chart.
38811
+ * - If the column type is "text", create a pie chart
38812
+ * - If the column type is "date", create a line chart.
38813
+ * - Otherwise, create a bar chart.
38814
+ */
38767
38815
  function buildSingleColumnChart(column, getters) {
38768
38816
  const { type, zone } = column;
38769
38817
  const sheetId = getters.getActiveSheetId();
38770
38818
  const dataSetsHaveTitle = isDatasetTitled(getters, column);
38771
38819
  const dataRange = getUnboundRange(getters, zone);
38772
38820
  const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
38821
+ if (getZoneArea(zone) === 1) {
38822
+ return buildScorecard(zone, getters);
38823
+ }
38773
38824
  switch (type) {
38774
38825
  case "percentage":
38775
- const { percentageSum } = getCellStats(getters, zone);
38776
- return createBaseChart("pie", [{ dataRange }], {
38826
+ return {
38827
+ type: "pie",
38777
38828
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38829
+ dataSets: [{ dataRange }],
38830
+ legendPosition: "none",
38778
38831
  dataSetsHaveTitle,
38779
- isDoughnut: percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
38780
- });
38832
+ };
38781
38833
  case "text":
38782
38834
  const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
38783
38835
  const titleCount = cells.reduce((count, cell) => (cell.value === titleCell.value ? count + 1 : count), 0);
38784
38836
  const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
38785
- return createBaseChart("pie", [{ dataRange }], {
38837
+ return {
38838
+ type: "pie",
38786
38839
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
38840
+ dataSets: [{ dataRange }],
38787
38841
  labelRange: dataRange,
38788
38842
  dataSetsHaveTitle: hasUniqueTitle,
38789
- isDoughnut: false,
38790
38843
  aggregated: true,
38791
38844
  legendPosition: "top",
38792
- });
38793
- // TODO: Handle date column with matrix chart when matrix chart is supported
38845
+ };
38794
38846
  case "date":
38795
- return createBaseChart("line", [{ dataRange }], {
38796
- labelRange: dataRange,
38847
+ return {
38848
+ ...DEFAULT_LINE_CHART_CONFIG,
38849
+ type: "line",
38850
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38851
+ dataSets: [{ dataRange }],
38797
38852
  dataSetsHaveTitle,
38798
- cumulative: false,
38799
- labelsAsText: false,
38800
- });
38853
+ };
38801
38854
  }
38802
- return createBaseChart("bar", [{ dataRange }], { dataSetsHaveTitle });
38855
+ return {
38856
+ ...DEFAULT_BAR_CHART_CONFIG,
38857
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38858
+ dataSets: [{ dataRange }],
38859
+ dataSetsHaveTitle,
38860
+ };
38803
38861
  }
38862
+ /**
38863
+ * Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
38864
+ * columns left to right, and is as follows:
38865
+ * - any type + percentage columns: pie chart
38866
+ * - number + number columns: scatter chart
38867
+ * - date + number columns: line chart
38868
+ * - text + number columns: treemap if repetition in labels
38869
+ * - any other combination: bar chart
38870
+ */
38804
38871
  function buildTwoColumnChart(columns, getters) {
38805
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38806
- if (numberColumns.length === 2) {
38807
- return createBaseChart("scatter", [{ dataRange: getUnboundRange(getters, numberColumns[1].zone) }], {
38808
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
38809
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[1]),
38810
- labelsAsText: false,
38811
- });
38872
+ if (columns.length !== 2) {
38873
+ throw new Error("buildTwoColumnChart expects exactly two columns");
38812
38874
  }
38813
- // TODO: Handle date + number with matrix chart when matrix chart is supported
38814
- if (dateColumns.length === 1 && numberColumns.length === 1) {
38815
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, numberColumns[0].zone) }], {
38816
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
38817
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[0]),
38818
- aggregated: false,
38819
- cumulative: false,
38875
+ if (columns[1].type === "percentage") {
38876
+ return {
38877
+ type: "pie",
38878
+ title: {},
38879
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38880
+ labelRange: getUnboundRange(getters, columns[0].zone),
38881
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38882
+ aggregated: true,
38883
+ legendPosition: "none",
38884
+ };
38885
+ }
38886
+ if (columns[0].type === "number" && columns[1].type === "number") {
38887
+ return {
38888
+ type: "scatter",
38889
+ title: {},
38890
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38891
+ labelRange: getUnboundRange(getters, columns[0].zone),
38892
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38820
38893
  labelsAsText: false,
38821
- });
38894
+ legendPosition: "none",
38895
+ };
38896
+ }
38897
+ // TODO: Handle date + number with calendar chart when implemented (and change the docstring)
38898
+ if (columns[0].type === "date" && columns[1].type === "number") {
38899
+ return {
38900
+ ...DEFAULT_LINE_CHART_CONFIG,
38901
+ type: "line",
38902
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38903
+ labelRange: getUnboundRange(getters, columns[0].zone),
38904
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[0]),
38905
+ };
38822
38906
  }
38823
- if (textColumns.length === 1 && numberColumns.length === 1) {
38824
- const [textColumn] = textColumns;
38825
- const [numberColumn] = numberColumns;
38907
+ if (columns[0].type === "text" && columns[1].type === "number") {
38908
+ const textColumn = columns[0];
38909
+ const numberColumn = columns[1];
38826
38910
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
38827
38911
  const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
38828
- const maxCategories = dataSetsHaveTitle
38829
- ? CHART_LIMITS.MAX_PIE_CATEGORIES
38830
- : CHART_LIMITS.MAX_PIE_CATEGORIES_NO_TITLE;
38831
- const labelRange = getUnboundRange(getters, textColumn.zone);
38832
- const dataRange = getUnboundRange(getters, numberColumn.zone);
38833
- if (uniqueCount <= maxCategories) {
38834
- const { percentageSum } = getCellStats(getters, numberColumn.zone);
38835
- return createBaseChart("pie", [{ dataRange }], {
38836
- labelRange,
38837
- dataSetsHaveTitle,
38838
- isDoughnut: numberColumn.type === "percentage" && percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
38839
- aggregated: true,
38840
- legendPosition: "top",
38841
- });
38842
- }
38843
- // Use treemap when categories repeat, as pie chart would be cluttered
38844
38912
  if (uniqueCount !== totalCount) {
38845
- return createBaseChart("treemap", [{ dataRange: labelRange }], {
38846
- labelRange: dataRange,
38913
+ return {
38914
+ type: "treemap",
38915
+ title: {},
38916
+ dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }],
38917
+ labelRange: getUnboundRange(getters, numberColumn.zone),
38847
38918
  dataSetsHaveTitle,
38848
- });
38919
+ legendPosition: "none",
38920
+ };
38849
38921
  }
38850
- return createBaseChart("bar", [{ dataRange }], {
38851
- labelRange,
38852
- dataSetsHaveTitle,
38853
- });
38854
38922
  }
38855
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
38856
- const dataColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
38857
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, dataColumn.zone) }], {
38858
- labelRange: getUnboundRange(getters, labelColumn.zone),
38859
- dataSetsHaveTitle: isDatasetTitled(getters, dataColumn),
38860
- cumulative: false,
38861
- labelsAsText: true,
38862
- });
38923
+ return {
38924
+ ...DEFAULT_BAR_CHART_CONFIG,
38925
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38926
+ labelRange: getUnboundRange(getters, columns[0].zone),
38927
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38928
+ };
38863
38929
  }
38930
+ /**
38931
+ * Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
38932
+ * the columns left to right, and is as follows:
38933
+ * - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
38934
+ * - any type + multiple percentage columns: pie chart
38935
+ * - date + multiple number columns: line chart
38936
+ * - any other combination: bar chart
38937
+ */
38864
38938
  function buildMultiColumnChart(columns, getters) {
38865
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38866
- const dataSetsHaveTitle = numberColumns.some((col) => isDatasetTitled(getters, col));
38867
- if (textColumns.length >= 2 && numberColumns.length === 1) {
38868
- const sortedTextColumns = textColumns.sort((colA, colB) => getCellStats(getters, colA.zone).uniqueCount - getCellStats(getters, colB.zone).uniqueCount);
38869
- const dataSets = sortedTextColumns.map(({ zone }) => ({
38939
+ if (columns.length < 3) {
38940
+ throw new Error("buildMultiColumnChart expects at least three columns");
38941
+ }
38942
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
38943
+ const lastColumn = columns[columns.length - 1];
38944
+ const columnsExceptLast = columns.slice(0, columns.length - 1);
38945
+ if ((lastColumn.type === "percentage" || lastColumn.type === "number") &&
38946
+ columnsExceptLast.every((col) => col.type === "text")) {
38947
+ const dataSets = columnsExceptLast.map(({ zone }) => ({
38870
38948
  dataRange: getUnboundRange(getters, zone),
38871
38949
  }));
38872
- return createBaseChart(textColumns.length >= 3 ? "sunburst" : "treemap", dataSets, {
38873
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
38950
+ return {
38951
+ type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
38952
+ title: {},
38953
+ dataSets,
38954
+ labelRange: getUnboundRange(getters, lastColumn.zone),
38874
38955
  dataSetsHaveTitle,
38875
- });
38956
+ legendPosition: "none",
38957
+ };
38876
38958
  }
38877
- const dataSets = recomputeZones(numberColumns.map((col) => col.zone)).map((zone) => ({
38959
+ const firstColumn = columns[0];
38960
+ const columnsExceptFirst = columns.slice(1);
38961
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({
38878
38962
  dataRange: getUnboundRange(getters, zone),
38879
38963
  }));
38880
- if (dateColumns.length === 1 && numberColumns.length > 1) {
38881
- return createBaseChart("line", dataSets, {
38882
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
38964
+ if (columnsExceptFirst.every((col) => col.type === "percentage")) {
38965
+ return {
38966
+ type: "pie",
38967
+ title: {},
38968
+ dataSets: rangesOfColumnsExceptFirst,
38969
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38883
38970
  dataSetsHaveTitle,
38884
- cumulative: false,
38885
- labelsAsText: false,
38971
+ aggregated: false,
38886
38972
  legendPosition: "top",
38887
- });
38973
+ };
38888
38974
  }
38889
- if (textColumns.length === 1 && numberColumns.length >= 2) {
38890
- const [textColumn] = textColumns;
38891
- const firstCell = getters.getEvaluatedCell({
38892
- sheetId: getters.getActiveSheetId(),
38893
- row: textColumn.zone.top,
38894
- col: textColumn.zone.left,
38895
- });
38896
- const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
38897
- const categoryCount = dataSetsHaveTitle && firstCell.value ? uniqueCount - 1 : uniqueCount;
38898
- const expectedDataCount = categoryCount * numberColumns.length + (dataSetsHaveTitle ? numberColumns.length : 0);
38899
- const actualDataCount = numberColumns.reduce((sum, dataCol) => sum + getCellStats(getters, dataCol.zone).totalCount, 0);
38900
- if (uniqueCount === totalCount &&
38901
- uniqueCount >= CHART_LIMITS.MIN_RADAR_CATEGORIES &&
38902
- uniqueCount <= CHART_LIMITS.MAX_RADAR_CATEGORIES &&
38903
- expectedDataCount === actualDataCount) {
38904
- return createBaseChart("radar", dataSets, {
38905
- title: dataSetsHaveTitle && firstCell.value ? { text: String(firstCell.value) } : {},
38906
- labelRange: getUnboundRange(getters, textColumn.zone),
38907
- dataSetsHaveTitle,
38908
- legendPosition: "top",
38909
- });
38910
- }
38975
+ if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
38976
+ return {
38977
+ ...DEFAULT_LINE_CHART_CONFIG,
38978
+ type: "line",
38979
+ dataSets: rangesOfColumnsExceptFirst,
38980
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38981
+ dataSetsHaveTitle,
38982
+ legendPosition: "top",
38983
+ };
38911
38984
  }
38912
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
38913
- return createBaseChart("bar", dataSets, {
38914
- labelRange: dataSets.length ? getUnboundRange(getters, labelColumn.zone) : "",
38985
+ return {
38986
+ ...DEFAULT_BAR_CHART_CONFIG,
38987
+ dataSets: rangesOfColumnsExceptFirst,
38988
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38915
38989
  dataSetsHaveTitle,
38916
- aggregated: true,
38917
38990
  legendPosition: "top",
38918
- });
38991
+ };
38919
38992
  }
38920
38993
  function buildScorecard(zone, getters) {
38921
38994
  const cell = getters.getCell({
@@ -38938,22 +39011,18 @@ function buildScorecard(zone, getters) {
38938
39011
  */
38939
39012
  function getSmartChartDefinition(zones, getters) {
38940
39013
  const columns = categorizeColumns(zones, getters);
38941
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38942
- const columnCount = numberColumns.length + textColumns.length + dateColumns.length;
38943
- switch (columnCount) {
38944
- case 0:
38945
- return createBaseChart("bar", [{ dataRange: getUnboundRange(getters, zones[0]) }], {
38946
- dataSetsHaveTitle: false,
38947
- });
39014
+ if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
39015
+ const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) }));
39016
+ return { ...DEFAULT_BAR_CHART_CONFIG, dataSets };
39017
+ }
39018
+ const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
39019
+ switch (nonEmptyColumns.length) {
38948
39020
  case 1:
38949
- const singleColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
38950
- return getZoneArea(singleColumn.zone) === 1
38951
- ? buildScorecard(singleColumn.zone, getters)
38952
- : buildSingleColumnChart(singleColumn, getters);
39021
+ return buildSingleColumnChart(nonEmptyColumns[0], getters);
38953
39022
  case 2:
38954
- return buildTwoColumnChart(columns, getters);
39023
+ return buildTwoColumnChart(nonEmptyColumns, getters);
38955
39024
  default:
38956
- return buildMultiColumnChart(columns, getters);
39025
+ return buildMultiColumnChart(nonEmptyColumns, getters);
38957
39026
  }
38958
39027
  }
38959
39028
 
@@ -39572,6 +39641,74 @@ function getPath2D(svgPath) {
39572
39641
  return path2D;
39573
39642
  }
39574
39643
 
39644
+ /**
39645
+ * Get the relative path between two files
39646
+ *
39647
+ * Eg.:
39648
+ * from "folder1/file1.txt" to "folder2/file2.txt" => "../folder2/file2.txt"
39649
+ */
39650
+ function getRelativePath(from, to) {
39651
+ const fromPathParts = from.split("/");
39652
+ const toPathParts = to.split("/");
39653
+ let relPath = "";
39654
+ let startIndex = 0;
39655
+ for (let i = 0; i < fromPathParts.length - 1; i++) {
39656
+ if (fromPathParts[i] === toPathParts[i]) {
39657
+ startIndex++;
39658
+ }
39659
+ else {
39660
+ relPath += "../";
39661
+ }
39662
+ }
39663
+ relPath += toPathParts.slice(startIndex).join("/");
39664
+ return relPath;
39665
+ }
39666
+ /**
39667
+ * Convert an array of element into an object where the objects keys were the elements position in the array.
39668
+ * Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.
39669
+ *
39670
+ * eg. : ["a", "b"] => {0:"a", 1:"b"}
39671
+ */
39672
+ function arrayToObject(array, indexOffset = 0) {
39673
+ const obj = {};
39674
+ for (let i = 0; i < array.length; i++) {
39675
+ if (array[i]) {
39676
+ obj[i + indexOffset] = array[i];
39677
+ }
39678
+ }
39679
+ return obj;
39680
+ }
39681
+ /**
39682
+ * In xlsx we can have string with unicode characters with the format _x00fa_.
39683
+ * Replace with characters understandable by JS
39684
+ */
39685
+ function fixXlsxUnicode(str) {
39686
+ return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {
39687
+ return String.fromCharCode(parseInt(code, 16));
39688
+ });
39689
+ }
39690
+ /** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
39691
+ function getSheetDataHeader(sheetData, dimension, index) {
39692
+ if (dimension === "COL") {
39693
+ if (!sheetData.cols[index]) {
39694
+ sheetData.cols[index] = {};
39695
+ }
39696
+ return sheetData.cols[index];
39697
+ }
39698
+ if (!sheetData.rows[index]) {
39699
+ sheetData.rows[index] = {};
39700
+ }
39701
+ return sheetData.rows[index];
39702
+ }
39703
+ /** Prefix the string by "=" if the string looks like a formula */
39704
+ function prefixFormulaWithEqual(formula) {
39705
+ if (formula[0] === "=") {
39706
+ return formula;
39707
+ }
39708
+ const tokens = tokenize(formula);
39709
+ return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
39710
+ }
39711
+
39575
39712
  /**
39576
39713
  * Map of the different types of conversions warnings and their name in error messages
39577
39714
  */
@@ -40094,66 +40231,6 @@ function hexaToInt(hex) {
40094
40231
  */
40095
40232
  const DEFAULT_SYSTEM_COLOR = "FF000000";
40096
40233
 
40097
- /**
40098
- * Get the relative path between two files
40099
- *
40100
- * Eg.:
40101
- * from "folder1/file1.txt" to "folder2/file2.txt" => "../folder2/file2.txt"
40102
- */
40103
- function getRelativePath(from, to) {
40104
- const fromPathParts = from.split("/");
40105
- const toPathParts = to.split("/");
40106
- let relPath = "";
40107
- let startIndex = 0;
40108
- for (let i = 0; i < fromPathParts.length - 1; i++) {
40109
- if (fromPathParts[i] === toPathParts[i]) {
40110
- startIndex++;
40111
- }
40112
- else {
40113
- relPath += "../";
40114
- }
40115
- }
40116
- relPath += toPathParts.slice(startIndex).join("/");
40117
- return relPath;
40118
- }
40119
- /**
40120
- * Convert an array of element into an object where the objects keys were the elements position in the array.
40121
- * Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.
40122
- *
40123
- * eg. : ["a", "b"] => {0:"a", 1:"b"}
40124
- */
40125
- function arrayToObject(array, indexOffset = 0) {
40126
- const obj = {};
40127
- for (let i = 0; i < array.length; i++) {
40128
- if (array[i]) {
40129
- obj[i + indexOffset] = array[i];
40130
- }
40131
- }
40132
- return obj;
40133
- }
40134
- /**
40135
- * In xlsx we can have string with unicode characters with the format _x00fa_.
40136
- * Replace with characters understandable by JS
40137
- */
40138
- function fixXlsxUnicode(str) {
40139
- return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {
40140
- return String.fromCharCode(parseInt(code, 16));
40141
- });
40142
- }
40143
- /** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
40144
- function getSheetDataHeader(sheetData, dimension, index) {
40145
- if (dimension === "COL") {
40146
- if (!sheetData.cols[index]) {
40147
- sheetData.cols[index] = {};
40148
- }
40149
- return sheetData.cols[index];
40150
- }
40151
- if (!sheetData.rows[index]) {
40152
- sheetData.rows[index] = {};
40153
- }
40154
- return sheetData.rows[index];
40155
- }
40156
-
40157
40234
  const XLSX_DATE_FORMAT_REGEX = /^(yy|yyyy|m{1,5}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/m|\s|-|\/|\.|:)+$/i;
40158
40235
  /**
40159
40236
  * Convert excel format to o_spreadsheet format
@@ -40368,9 +40445,9 @@ function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
40368
40445
  if (!rule.operator || !rule.formula || rule.formula.length === 0)
40369
40446
  continue;
40370
40447
  operator = CF_OPERATOR_TYPE_CONVERSION_MAP[rule.operator];
40371
- values.push(prefixFormula(rule.formula[0]));
40448
+ values.push(prefixFormulaWithEqual(rule.formula[0]));
40372
40449
  if (rule.formula.length === 2) {
40373
- values.push(prefixFormula(rule.formula[1]));
40450
+ values.push(prefixFormulaWithEqual(rule.formula[1]));
40374
40451
  }
40375
40452
  break;
40376
40453
  }
@@ -40528,11 +40605,6 @@ function convertIcons(xlsxIconSet, index) {
40528
40605
  ? ICON_SETS[iconSet].neutral
40529
40606
  : ICON_SETS[iconSet].good;
40530
40607
  }
40531
- /** Prefix the string by "=" if the string looks like a formula */
40532
- function prefixFormula(formula) {
40533
- const tokens = tokenize(formula);
40534
- return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
40535
- }
40536
40608
  // ---------------------------------------------------------------------------
40537
40609
  // Warnings
40538
40610
  // ---------------------------------------------------------------------------
@@ -41008,7 +41080,7 @@ function convertDataValidationRules(xlsxDataValidations, warningManager) {
41008
41080
  dvRules.push(decimalRule);
41009
41081
  break;
41010
41082
  case "list":
41011
- const listRule = convertListrule(dvId++, dv);
41083
+ const listRule = convertListRule(dvId++, dv);
41012
41084
  dvRules.push(listRule);
41013
41085
  break;
41014
41086
  case "date":
@@ -41028,9 +41100,9 @@ function convertDataValidationRules(xlsxDataValidations, warningManager) {
41028
41100
  return dvRules;
41029
41101
  }
41030
41102
  function convertDecimalRule(id, dv) {
41031
- const values = [dv.formula1.toString()];
41103
+ const values = [prefixFormulaWithEqual(dv.formula1.toString())];
41032
41104
  if (dv.formula2) {
41033
- values.push(dv.formula2.toString());
41105
+ values.push(prefixFormulaWithEqual(dv.formula2.toString()));
41034
41106
  }
41035
41107
  return {
41036
41108
  id: id.toString(),
@@ -41042,7 +41114,7 @@ function convertDecimalRule(id, dv) {
41042
41114
  },
41043
41115
  };
41044
41116
  }
41045
- function convertListrule(id, dv) {
41117
+ function convertListRule(id, dv) {
41046
41118
  const formula1 = dv.formula1.toString();
41047
41119
  const isRangeRule = rangeReference.test(formula1);
41048
41120
  return {
@@ -41058,9 +41130,9 @@ function convertListrule(id, dv) {
41058
41130
  }
41059
41131
  function convertDateRule(id, dv) {
41060
41132
  let criterion;
41061
- const values = [dv.formula1.toString()];
41133
+ const values = [prefixFormulaWithEqual(dv.formula1.toString())];
41062
41134
  if (dv.formula2) {
41063
- values.push(dv.formula2.toString());
41135
+ values.push(prefixFormulaWithEqual(dv.formula2.toString()));
41064
41136
  criterion = {
41065
41137
  type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
41066
41138
  values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
@@ -41087,7 +41159,7 @@ function convertCustomRule(id, dv) {
41087
41159
  isBlocking: dv.errorStyle !== "warning",
41088
41160
  criterion: {
41089
41161
  type: "customFormula",
41090
- values: [`=${dv.formula1.toString()}`],
41162
+ values: [prefixFormulaWithEqual(dv.formula1.toString())],
41091
41163
  },
41092
41164
  };
41093
41165
  }
@@ -43991,6 +44063,7 @@ function forceUnicityOfFigure(data) {
43991
44063
  return data;
43992
44064
  }
43993
44065
  const figureIds = new Set();
44066
+ const chartIds = new Set();
43994
44067
  const uuidGenerator = new UuidGenerator();
43995
44068
  for (const sheet of data.sheets || []) {
43996
44069
  for (const figure of sheet.figures || []) {
@@ -43998,6 +44071,12 @@ function forceUnicityOfFigure(data) {
43998
44071
  figure.id += uuidGenerator.smallUuid();
43999
44072
  }
44000
44073
  figureIds.add(figure.id);
44074
+ if (figure.tag === "chart") {
44075
+ if (chartIds.has(figure.data?.chartId)) {
44076
+ figure.data.chartId += uuidGenerator.smallUuid();
44077
+ }
44078
+ chartIds.add(figure.data?.chartId);
44079
+ }
44001
44080
  }
44002
44081
  }
44003
44082
  data.uniqueFigureIds = true;
@@ -49751,39 +49830,63 @@ class CellComposerStore extends AbstractComposerStore {
49751
49830
  this.model.dispatch("AUTOFILL_TABLE_COLUMN", { ...this.currentEditedCell });
49752
49831
  this.setContent("");
49753
49832
  }
49754
- getComposerContent(position) {
49833
+ getComposerContent(position, selection) {
49755
49834
  const locale = this.getters.getLocale();
49756
49835
  const cell = this.getters.getCell(position);
49757
49836
  if (cell?.isFormula) {
49758
49837
  const prettifiedContent = this.getPrettifiedFormula(cell);
49759
- return localizeFormula(prettifiedContent, locale);
49838
+ // when a formula is prettified (multi lines, indented), adapt the cursor position
49839
+ // to take into account line breaks and tabs
49840
+ function adjustCursorIndex(targetIndex) {
49841
+ let adjustedIndex = 0;
49842
+ let originalIndex = 0;
49843
+ while (originalIndex < targetIndex) {
49844
+ adjustedIndex++;
49845
+ const char = prettifiedContent[adjustedIndex];
49846
+ if (char !== "\n" && char !== "\t") {
49847
+ originalIndex++;
49848
+ }
49849
+ }
49850
+ return adjustedIndex;
49851
+ }
49852
+ let adjustedSelection = selection;
49853
+ if (selection) {
49854
+ adjustedSelection = {
49855
+ start: adjustCursorIndex(selection.start),
49856
+ end: adjustCursorIndex(selection.end),
49857
+ };
49858
+ }
49859
+ return {
49860
+ text: localizeFormula(prettifiedContent, locale),
49861
+ adjustedSelection,
49862
+ };
49760
49863
  }
49761
49864
  const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
49762
49865
  if (spreader) {
49763
- return "";
49866
+ return { text: "" };
49764
49867
  }
49765
49868
  const { format, value, type, formattedValue } = this.getters.getEvaluatedCell(position);
49766
49869
  switch (type) {
49767
49870
  case CellValueType.empty:
49768
- return "";
49871
+ return { text: "" };
49769
49872
  case CellValueType.text:
49770
49873
  case CellValueType.error:
49771
- return value;
49874
+ return { text: value };
49772
49875
  case CellValueType.boolean:
49773
- return formattedValue;
49876
+ return { text: formattedValue };
49774
49877
  case CellValueType.number:
49775
49878
  if (format && isDateTimeFormat(format)) {
49776
49879
  if (parseDateTime(formattedValue, locale) !== null) {
49777
49880
  // formatted string can be parsed again
49778
- return formattedValue;
49881
+ return { text: formattedValue };
49779
49882
  }
49780
49883
  // display a simplified and parsable string otherwise
49781
49884
  const timeFormat = Number.isInteger(value)
49782
49885
  ? locale.dateFormat
49783
49886
  : getDateTimeFormat(locale);
49784
- return formatValue(value, { locale, format: timeFormat });
49887
+ return { text: formatValue(value, { locale, format: timeFormat }) };
49785
49888
  }
49786
- return this.numberComposerContent(value, format, locale);
49889
+ return { text: this.numberComposerContent(value, format, locale) };
49787
49890
  }
49788
49891
  }
49789
49892
  getPrettifiedFormula(cell) {
@@ -49952,8 +50055,9 @@ class GridComposer extends owl.Component {
49952
50055
  },
49953
50056
  focus: this.focus,
49954
50057
  isDefaultFocus: true,
49955
- onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {
50058
+ onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
49956
50059
  focusMode: "contentFocus",
50060
+ selection,
49957
50061
  }),
49958
50062
  onComposerCellFocused: (content) => this.composerFocusStore.focusComposer(this.composerInterface, {
49959
50063
  focusMode: "cellFocus",
@@ -57359,12 +57463,13 @@ class DataValidationEditor extends owl.Component {
57359
57463
  onCloseSidePanel: { type: Function, optional: true },
57360
57464
  };
57361
57465
  state = owl.useState({ rule: this.defaultDataValidationRule, errors: [] });
57466
+ editingSheetId;
57362
57467
  setup() {
57468
+ this.editingSheetId = this.env.model.getters.getActiveSheetId();
57363
57469
  if (this.props.rule) {
57364
- const sheetId = this.env.model.getters.getActiveSheetId();
57365
57470
  this.state.rule = {
57366
57471
  ...this.props.rule,
57367
- ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, sheetId)),
57472
+ ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, this.editingSheetId)),
57368
57473
  };
57369
57474
  this.state.rule.criterion.type = this.props.rule.criterion.type;
57370
57475
  }
@@ -57398,7 +57503,6 @@ class DataValidationEditor extends owl.Component {
57398
57503
  const locale = this.env.model.getters.getLocale();
57399
57504
  const criterion = rule.criterion;
57400
57505
  const criterionEvaluator = criterionEvaluatorRegistry.get(criterion.type);
57401
- const sheetId = this.env.model.getters.getActiveSheetId();
57402
57506
  const values = criterion.values
57403
57507
  .slice(0, criterionEvaluator.numberOfValues(criterion))
57404
57508
  .map((value) => value?.trim())
@@ -57406,8 +57510,8 @@ class DataValidationEditor extends owl.Component {
57406
57510
  .map((value) => canonicalizeContent(value, locale));
57407
57511
  rule.criterion = { ...criterion, values };
57408
57512
  return {
57409
- sheetId,
57410
- ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
57513
+ sheetId: this.editingSheetId,
57514
+ ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(this.editingSheetId, xc)),
57411
57515
  rule,
57412
57516
  };
57413
57517
  }
@@ -57934,6 +58038,7 @@ css /* scss */ `
57934
58038
  .o-button {
57935
58039
  height: 19px;
57936
58040
  width: 19px;
58041
+ box-sizing: content-box;
57937
58042
  .o-icon {
57938
58043
  height: 14px;
57939
58044
  width: 14px;
@@ -68707,6 +68812,281 @@ class ZoneRBush extends RBush {
68707
68812
  }
68708
68813
  }
68709
68814
 
68815
+ class ZoneSet {
68816
+ profilesStartingPosition = [0];
68817
+ profiles = new Map([[0, []]]);
68818
+ constructor(zones = []) {
68819
+ for (const zone of zones) {
68820
+ this.add(zone);
68821
+ }
68822
+ }
68823
+ isEmpty() {
68824
+ return this.profiles.size === 1 && this.profiles.get(0)?.length === 0;
68825
+ }
68826
+ add(zone) {
68827
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone]);
68828
+ }
68829
+ delete(zone) {
68830
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone], true);
68831
+ }
68832
+ has(zone) {
68833
+ return profilesContainsZone(this.profilesStartingPosition, this.profiles, zone);
68834
+ }
68835
+ difference(other) {
68836
+ const result = this.copy();
68837
+ for (const zone of other) {
68838
+ result.delete(zone);
68839
+ }
68840
+ return result;
68841
+ }
68842
+ copy() {
68843
+ const result = new ZoneSet();
68844
+ result.profilesStartingPosition = [...this.profilesStartingPosition];
68845
+ result.profiles = new Map();
68846
+ for (const [key, value] of this.profiles) {
68847
+ result.profiles.set(key, [...value]);
68848
+ }
68849
+ return result;
68850
+ }
68851
+ size() {
68852
+ let size = 0;
68853
+ for (const profile of this.profiles.values()) {
68854
+ size += profile.length;
68855
+ }
68856
+ return size / 2;
68857
+ }
68858
+ /**
68859
+ * iterator of all the zones in the ZoneSet
68860
+ */
68861
+ [Symbol.iterator]() {
68862
+ return constructZonesFromProfiles(this.profilesStartingPosition, this.profiles)[Symbol.iterator]();
68863
+ }
68864
+ }
68865
+
68866
+ class RangeSet {
68867
+ setsBySheetId = {};
68868
+ constructor(ranges = []) {
68869
+ for (const range of ranges) {
68870
+ this.add(range);
68871
+ }
68872
+ }
68873
+ add(range) {
68874
+ if (!this.setsBySheetId[range.sheetId]) {
68875
+ this.setsBySheetId[range.sheetId] = new ZoneSet();
68876
+ }
68877
+ this.setsBySheetId[range.sheetId].add(range.zone);
68878
+ }
68879
+ addMany(ranges) {
68880
+ for (const range of ranges) {
68881
+ this.add(range);
68882
+ }
68883
+ }
68884
+ addPosition(position) {
68885
+ this.add(positionToBoundedRange(position));
68886
+ }
68887
+ addManyPositions(positions) {
68888
+ for (const position of positions) {
68889
+ this.addPosition(position);
68890
+ }
68891
+ }
68892
+ has(range) {
68893
+ if (!this.setsBySheetId[range.sheetId]) {
68894
+ return false;
68895
+ }
68896
+ return this.setsBySheetId[range.sheetId].has(range.zone);
68897
+ }
68898
+ hasPosition(position) {
68899
+ return this.has(positionToBoundedRange(position));
68900
+ }
68901
+ delete(range) {
68902
+ if (!this.setsBySheetId[range.sheetId]) {
68903
+ return;
68904
+ }
68905
+ this.setsBySheetId[range.sheetId].delete(range.zone);
68906
+ }
68907
+ deleteMany(ranges) {
68908
+ for (const range of ranges) {
68909
+ this.delete(range);
68910
+ }
68911
+ }
68912
+ deleteManyPositions(positions) {
68913
+ for (const position of positions) {
68914
+ this.delete(positionToBoundedRange(position));
68915
+ }
68916
+ }
68917
+ difference(other) {
68918
+ const result = new RangeSet();
68919
+ for (const sheetId in this.setsBySheetId) {
68920
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId];
68921
+ }
68922
+ for (const sheetId in other.setsBySheetId) {
68923
+ if (result.setsBySheetId[sheetId]) {
68924
+ result.setsBySheetId[sheetId] = result.setsBySheetId[sheetId].difference(other.setsBySheetId[sheetId]);
68925
+ }
68926
+ }
68927
+ return result;
68928
+ }
68929
+ copy() {
68930
+ const result = new RangeSet();
68931
+ for (const sheetId in this.setsBySheetId) {
68932
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId].copy();
68933
+ }
68934
+ return result;
68935
+ }
68936
+ clear() {
68937
+ this.setsBySheetId = {};
68938
+ }
68939
+ size() {
68940
+ let size = 0;
68941
+ for (const sheetId in this.setsBySheetId) {
68942
+ size += this.setsBySheetId[sheetId].size();
68943
+ }
68944
+ return size;
68945
+ }
68946
+ isEmpty() {
68947
+ for (const sheetId in this.setsBySheetId) {
68948
+ if (!this.setsBySheetId[sheetId].isEmpty()) {
68949
+ return false;
68950
+ }
68951
+ }
68952
+ return true;
68953
+ }
68954
+ /**
68955
+ * iterator of all the ranges in the RangeSet
68956
+ */
68957
+ [Symbol.iterator]() {
68958
+ const result = [];
68959
+ for (const sheetId in this.setsBySheetId) {
68960
+ for (const zone of this.setsBySheetId[sheetId]) {
68961
+ result.push({ sheetId: sheetId, zone });
68962
+ }
68963
+ }
68964
+ return result[Symbol.iterator]();
68965
+ }
68966
+ }
68967
+
68968
+ /**
68969
+ * R-Tree of ranges, mapping zones (r-tree bounding boxes) to ranges (data of the r-tree item).
68970
+ * Ranges associated to the exact same bounding box are grouped together
68971
+ * to reduce the number of nodes in the R-tree.
68972
+ */
68973
+ class DependenciesRTree {
68974
+ rTree;
68975
+ constructor(items = []) {
68976
+ const compactedBoxes = groupSameBoundingBoxes(items);
68977
+ this.rTree = new SpreadsheetRTree(compactedBoxes);
68978
+ }
68979
+ insert(item) {
68980
+ const data = this.rTree.search(item.boundingBox);
68981
+ const itemBoundingBox = item.boundingBox;
68982
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
68983
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
68984
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
68985
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
68986
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
68987
+ if (exactBoundingBox) {
68988
+ exactBoundingBox.data.add(item.data);
68989
+ }
68990
+ else {
68991
+ this.rTree.insert({ ...item, data: new RangeSet([item.data]) });
68992
+ }
68993
+ }
68994
+ search({ zone, sheetId }) {
68995
+ const results = new RangeSet();
68996
+ for (const { data } of this.rTree.search({ zone, sheetId })) {
68997
+ results.addMany(data);
68998
+ }
68999
+ return results;
69000
+ }
69001
+ remove(item) {
69002
+ const data = this.rTree.search(item.boundingBox);
69003
+ const itemBoundingBox = item.boundingBox;
69004
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
69005
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
69006
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
69007
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
69008
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
69009
+ if (exactBoundingBox) {
69010
+ exactBoundingBox.data.delete(item.data);
69011
+ }
69012
+ else {
69013
+ this.rTree.remove({ ...item, data: new RangeSet([item.data]) });
69014
+ }
69015
+ }
69016
+ }
69017
+ /**
69018
+ * Group together all formulas pointing to the exact same dependency (bounding box).
69019
+ * The goal is to optimize the following case:
69020
+ * - if any cell in B1:B1000 changes, C1 must be recomputed
69021
+ * - if any cell in B1:B1000 changes, C2 must be recomputed
69022
+ * - if any cell in B1:B1000 changes, C3 must be recomputed
69023
+ * ...
69024
+ * - if any cell in B1:B1000 changes, C1000 must be recomputed
69025
+ *
69026
+ * Instead of having 1000 entries in the R-tree, we want to have a single entry
69027
+ * with B1:B1000 (bounding box) pointing to C1:C1000 (formulas).
69028
+ */
69029
+ function groupSameBoundingBoxes(items) {
69030
+ // Important: this function must be as fast as possible. It is on the evaluation hot path.
69031
+ let maxCol = 0;
69032
+ let maxRow = 0;
69033
+ for (let i = 0; i < items.length; i++) {
69034
+ const zone = items[i].boundingBox.zone;
69035
+ if (zone.right > maxCol) {
69036
+ maxCol = zone.right;
69037
+ }
69038
+ if (zone.bottom > maxRow) {
69039
+ maxRow = zone.bottom;
69040
+ }
69041
+ }
69042
+ maxCol += 1;
69043
+ maxRow += 1;
69044
+ // in most real-world cases, we can use a fast numeric key
69045
+ // but if the zones are too far right or bottom, we fallback to a slower string key
69046
+ const maxPossibleKey = (((maxRow + 1) * maxCol + 1) * maxRow + 1) * maxCol;
69047
+ const useFastKey = maxPossibleKey <= Number.MAX_SAFE_INTEGER;
69048
+ if (!useFastKey) {
69049
+ console.warn("Max col/row size exceeded, using slow zone key");
69050
+ }
69051
+ const groupedByBBox = {};
69052
+ for (const item of items) {
69053
+ const sheetId = item.boundingBox.sheetId;
69054
+ if (!groupedByBBox[sheetId]) {
69055
+ groupedByBBox[sheetId] = {};
69056
+ }
69057
+ const bBox = item.boundingBox.zone;
69058
+ let bBoxKey = 0;
69059
+ if (useFastKey) {
69060
+ bBoxKey =
69061
+ bBox.left +
69062
+ bBox.top * maxCol +
69063
+ bBox.right * maxCol * maxRow +
69064
+ bBox.bottom * maxCol * maxRow * maxCol;
69065
+ }
69066
+ else {
69067
+ bBoxKey = `${bBox.left},${bBox.top},${bBox.right},${bBox.bottom}`;
69068
+ }
69069
+ if (groupedByBBox[sheetId][bBoxKey]) {
69070
+ const ranges = groupedByBBox[sheetId][bBoxKey].data;
69071
+ ranges.add(item.data);
69072
+ }
69073
+ else {
69074
+ groupedByBBox[sheetId][bBoxKey] = {
69075
+ boundingBox: item.boundingBox,
69076
+ data: new RangeSet([item.data]),
69077
+ };
69078
+ }
69079
+ }
69080
+ const result = [];
69081
+ for (const sheetId in groupedByBBox) {
69082
+ const map = groupedByBBox[sheetId];
69083
+ for (const key in map) {
69084
+ result.push(map[key]);
69085
+ }
69086
+ }
69087
+ return result;
69088
+ }
69089
+
68710
69090
  /**
68711
69091
  * Implementation of a dependency Graph.
68712
69092
  * The graph is used to evaluate the cells in the correct
@@ -68715,12 +69095,10 @@ class ZoneRBush extends RBush {
68715
69095
  * It uses an R-Tree data structure to efficiently find dependent cells.
68716
69096
  */
68717
69097
  class FormulaDependencyGraph {
68718
- createEmptyPositionSet;
68719
69098
  dependencies = new PositionMap();
68720
69099
  rTree;
68721
- constructor(createEmptyPositionSet, data = []) {
68722
- this.createEmptyPositionSet = createEmptyPositionSet;
68723
- this.rTree = new SpreadsheetRTree(data);
69100
+ constructor(data = []) {
69101
+ this.rTree = new DependenciesRTree(data);
68724
69102
  }
68725
69103
  removeAllDependencies(formulaPosition) {
68726
69104
  const ranges = this.dependencies.get(formulaPosition);
@@ -68734,7 +69112,10 @@ class FormulaDependencyGraph {
68734
69112
  }
68735
69113
  addDependencies(formulaPosition, dependencies) {
68736
69114
  const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
68737
- data: formulaPosition,
69115
+ data: {
69116
+ sheetId: formulaPosition.sheetId,
69117
+ zone: positionToZone(formulaPosition),
69118
+ },
68738
69119
  boundingBox: {
68739
69120
  zone,
68740
69121
  sheetId,
@@ -68752,46 +69133,20 @@ class FormulaDependencyGraph {
68752
69133
  }
68753
69134
  }
68754
69135
  /**
68755
- * Return all the cells that depend on the provided ranges,
68756
- * in the correct order they should be evaluated.
68757
- * This is called a topological ordering (excluding cycles)
69136
+ * Return all the cells that depend on the provided ranges.
68758
69137
  */
68759
- getCellsDependingOn(ranges) {
68760
- const visited = this.createEmptyPositionSet();
69138
+ getCellsDependingOn(ranges, visited = new RangeSet()) {
69139
+ visited = visited.copy();
68761
69140
  const queue = Array.from(ranges).reverse();
68762
69141
  while (queue.length > 0) {
68763
69142
  const range = queue.pop();
68764
- const zone = range.zone;
68765
- const sheetId = range.sheetId;
68766
- for (let col = zone.left; col <= zone.right; col++) {
68767
- for (let row = zone.top; row <= zone.bottom; row++) {
68768
- visited.add({ sheetId, col, row });
68769
- }
68770
- }
68771
- const impactedPositions = this.rTree.search(range).map((dep) => dep.data);
68772
- const nextInQueue = {};
68773
- for (const position of impactedPositions) {
68774
- if (!visited.has(position)) {
68775
- if (!nextInQueue[position.sheetId]) {
68776
- nextInQueue[position.sheetId] = [];
68777
- }
68778
- nextInQueue[position.sheetId].push(positionToZone(position));
68779
- }
68780
- }
68781
- for (const sheetId in nextInQueue) {
68782
- const zones = recomputeZones(nextInQueue[sheetId], []);
68783
- queue.push(...zones.map((zone) => ({ sheetId, zone })));
68784
- }
69143
+ visited.add(range);
69144
+ const impactedRanges = this.rTree.search(range);
69145
+ queue.push(...impactedRanges.difference(visited));
68785
69146
  }
68786
69147
  // remove initial ranges
68787
69148
  for (const range of ranges) {
68788
- const zone = range.zone;
68789
- const sheetId = range.sheetId;
68790
- for (let col = zone.left; col <= zone.right; col++) {
68791
- for (let row = zone.top; row <= zone.bottom; row++) {
68792
- visited.delete({ sheetId, col, row });
68793
- }
68794
- }
69149
+ visited.delete(range);
68795
69150
  }
68796
69151
  return visited;
68797
69152
  }
@@ -69054,7 +69409,7 @@ class Evaluator {
69054
69409
  getters;
69055
69410
  compilationParams;
69056
69411
  evaluatedCells = new PositionMap();
69057
- formulaDependencies = lazy(new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this)));
69412
+ formulaDependencies = lazy(new FormulaDependencyGraph());
69058
69413
  blockedArrayFormulas = new PositionSet({});
69059
69414
  spreadingRelations = new SpreadingRelation();
69060
69415
  constructor(context, getters) {
@@ -69089,7 +69444,7 @@ class Evaluator {
69089
69444
  return undefined;
69090
69445
  }
69091
69446
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
69092
- return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
69447
+ return arrayFormulas.find((position) => !this.blockedArrayFormulas.has(position));
69093
69448
  }
69094
69449
  updateDependencies(position) {
69095
69450
  // removing dependencies is slow because it requires
@@ -69133,57 +69488,72 @@ class Evaluator {
69133
69488
  }
69134
69489
  evaluateCells(positions) {
69135
69490
  const start = performance.now();
69136
- const cellsToCompute = this.createEmptyPositionSet();
69137
- cellsToCompute.addMany(positions);
69491
+ const rangesToCompute = new RangeSet();
69492
+ rangesToCompute.addManyPositions(positions);
69138
69493
  const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);
69139
- cellsToCompute.addMany(this.getCellsDependingOn(positions));
69140
- cellsToCompute.addMany(arrayFormulasPositions);
69141
- cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
69142
- this.evaluate(cellsToCompute);
69494
+ rangesToCompute.addMany(this.getCellsDependingOn(rangesToCompute));
69495
+ rangesToCompute.addMany(arrayFormulasPositions);
69496
+ rangesToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
69497
+ this.evaluate(rangesToCompute);
69143
69498
  console.debug("evaluate Cells", performance.now() - start, "ms");
69144
69499
  }
69145
69500
  getArrayFormulasImpactedByChangesOf(positions) {
69146
- const impactedPositions = this.createEmptyPositionSet();
69501
+ const impactedRanges = new RangeSet();
69147
69502
  for (const position of positions) {
69148
69503
  const content = this.getters.getCell(position)?.content;
69149
69504
  const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
69150
69505
  if (arrayFormulaPosition !== undefined) {
69151
69506
  // take into account new collisions.
69152
- impactedPositions.add(arrayFormulaPosition);
69507
+ impactedRanges.addPosition(arrayFormulaPosition);
69153
69508
  }
69154
69509
  if (!content) {
69155
69510
  // The previous content could have blocked some array formulas
69156
- impactedPositions.add(position);
69511
+ impactedRanges.addPosition(position);
69157
69512
  }
69158
69513
  }
69159
- const zonesBySheetIds = aggregatePositionsToZones(impactedPositions);
69160
- for (const sheetId in zonesBySheetIds) {
69161
- for (const zone of zonesBySheetIds[sheetId]) {
69162
- impactedPositions.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69163
- }
69514
+ for (const range of [...impactedRanges]) {
69515
+ impactedRanges.addMany(this.getArrayFormulasBlockedBy(range.sheetId, range.zone));
69164
69516
  }
69165
- return impactedPositions;
69517
+ return impactedRanges;
69166
69518
  }
69167
69519
  buildDependencyGraph() {
69168
69520
  this.blockedArrayFormulas = this.createEmptyPositionSet();
69169
69521
  this.spreadingRelations = new SpreadingRelation();
69170
69522
  this.formulaDependencies = lazy(() => {
69171
- const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position)
69172
- .filter((range) => !range.invalidSheetName && !range.invalidXc)
69173
- .map((range) => ({
69174
- data: position,
69175
- boundingBox: {
69176
- zone: range.zone,
69177
- sheetId: range.sheetId,
69178
- },
69179
- })));
69180
- return new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this), dependencies);
69523
+ const rTreeItems = [];
69524
+ for (const sheetId of this.getters.getSheetIds()) {
69525
+ const cells = this.getters.getCells(sheetId);
69526
+ for (const cellId in cells) {
69527
+ const cell = cells[cellId];
69528
+ if (cell.isFormula) {
69529
+ const directDependencies = cell.compiledFormula.dependencies;
69530
+ for (const range of directDependencies) {
69531
+ if (range.invalidSheetName || range.invalidXc) {
69532
+ continue;
69533
+ }
69534
+ rTreeItems.push({
69535
+ data: {
69536
+ sheetId,
69537
+ zone: positionToZone(this.getters.getCellPosition(cellId)),
69538
+ },
69539
+ boundingBox: { sheetId: range.sheetId, zone: range.zone },
69540
+ });
69541
+ }
69542
+ }
69543
+ }
69544
+ }
69545
+ return new FormulaDependencyGraph(rTreeItems);
69181
69546
  });
69182
69547
  }
69183
69548
  evaluateAllCells() {
69184
69549
  const start = performance.now();
69185
69550
  this.evaluatedCells = new PositionMap();
69186
- this.evaluate(this.getAllCells());
69551
+ const ranges = [];
69552
+ for (const sheetId of this.getters.getSheetIds()) {
69553
+ const zone = this.getters.getSheetZone(sheetId);
69554
+ ranges.push({ sheetId, zone });
69555
+ }
69556
+ this.evaluate(ranges);
69187
69557
  console.debug("evaluate all cells", performance.now() - start, "ms");
69188
69558
  }
69189
69559
  evaluateFormulaResult(sheetId, formulaString) {
@@ -69207,48 +69577,47 @@ class Evaluator {
69207
69577
  return handleError(error, "");
69208
69578
  }
69209
69579
  }
69210
- getAllCells() {
69211
- const positions = this.createEmptyPositionSet();
69212
- positions.fillAllPositions();
69213
- return positions;
69214
- }
69215
69580
  /**
69216
69581
  * Return the position of formulas blocked by the given positions
69217
69582
  * as well as all their dependencies.
69218
69583
  */
69219
69584
  getArrayFormulasBlockedBy(sheetId, zone) {
69220
- const arrayFormulaPositions = this.createEmptyPositionSet();
69585
+ const arrayFormulaPositions = new RangeSet();
69221
69586
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);
69222
- arrayFormulaPositions.addMany(arrayFormulas);
69587
+ arrayFormulaPositions.addManyPositions(arrayFormulas);
69223
69588
  const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));
69224
69589
  if (spilledPositions.length) {
69225
69590
  // ignore the formula spreading on the position. Keep only the blocked ones
69226
- arrayFormulaPositions.deleteMany(spilledPositions);
69591
+ arrayFormulaPositions.deleteManyPositions(spilledPositions);
69227
69592
  }
69228
69593
  arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));
69229
69594
  return arrayFormulaPositions;
69230
69595
  }
69231
- nextPositionsToUpdate = new PositionSet({});
69596
+ nextRangesToUpdate = new RangeSet();
69232
69597
  cellsBeingComputed = new Set();
69233
69598
  symbolsBeingComputed = new Set();
69234
- evaluate(positions) {
69599
+ evaluate(ranges) {
69235
69600
  this.cellsBeingComputed = new Set();
69236
- this.nextPositionsToUpdate = positions;
69601
+ this.nextRangesToUpdate = new RangeSet(ranges);
69237
69602
  let currentIteration = 0;
69238
- while (!this.nextPositionsToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
69603
+ while (!this.nextRangesToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
69239
69604
  this.updateCompilationParameters();
69240
- const positions = this.nextPositionsToUpdate.clear();
69241
- for (let i = 0; i < positions.length; ++i) {
69242
- this.evaluatedCells.delete(positions[i]);
69243
- }
69244
- for (let i = 0; i < positions.length; ++i) {
69245
- const position = positions[i];
69246
- if (this.nextPositionsToUpdate.has(position)) {
69247
- continue;
69248
- }
69249
- const evaluatedCell = this.computeCell(position);
69250
- if (evaluatedCell !== EMPTY_CELL) {
69251
- this.evaluatedCells.set(position, evaluatedCell);
69605
+ const ranges = [...this.nextRangesToUpdate];
69606
+ this.nextRangesToUpdate.clear();
69607
+ this.clearEvaluatedRanges(ranges);
69608
+ for (const range of ranges) {
69609
+ const { left, bottom, right, top } = range.zone;
69610
+ for (let col = left; col <= right; col++) {
69611
+ for (let row = top; row <= bottom; row++) {
69612
+ const position = { sheetId: range.sheetId, col, row };
69613
+ if (this.nextRangesToUpdate.hasPosition(position)) {
69614
+ continue;
69615
+ }
69616
+ const evaluatedCell = this.computeCell(position);
69617
+ if (evaluatedCell !== EMPTY_CELL) {
69618
+ this.evaluatedCells.set(position, evaluatedCell);
69619
+ }
69620
+ }
69252
69621
  }
69253
69622
  }
69254
69623
  onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
@@ -69257,6 +69626,16 @@ class Evaluator {
69257
69626
  console.warn("Maximum iteration reached while evaluating cells");
69258
69627
  }
69259
69628
  }
69629
+ clearEvaluatedRanges(ranges) {
69630
+ for (const range of ranges) {
69631
+ const { left, bottom, right, top } = range.zone;
69632
+ for (let col = left; col <= right; col++) {
69633
+ for (let row = top; row <= bottom; row++) {
69634
+ this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
69635
+ }
69636
+ }
69637
+ }
69638
+ }
69260
69639
  computeCell(position) {
69261
69640
  const evaluation = this.evaluatedCells.get(position);
69262
69641
  if (evaluation) {
@@ -69329,9 +69708,9 @@ class Evaluator {
69329
69708
  }
69330
69709
  invalidatePositionsDependingOnSpread(sheetId, resultZone) {
69331
69710
  // the result matrix is split in 2 zones to exclude the array formula position
69332
- const invalidatedPositions = this.formulaDependencies().getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
69333
- invalidatedPositions.delete({ sheetId, col: resultZone.left, row: resultZone.top });
69334
- this.nextPositionsToUpdate.addMany(invalidatedPositions);
69711
+ const invalidatedPositions = this.getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
69712
+ invalidatedPositions.delete({ sheetId, zone: resultZone });
69713
+ this.nextRangesToUpdate.addMany(invalidatedPositions);
69335
69714
  }
69336
69715
  assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
69337
69716
  const numberOfCols = this.getters.getNumberCols(sheetId);
@@ -69406,7 +69785,7 @@ class Evaluator {
69406
69785
  }
69407
69786
  const sheetId = position.sheetId;
69408
69787
  this.invalidatePositionsDependingOnSpread(sheetId, zone);
69409
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69788
+ this.nextRangesToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69410
69789
  }
69411
69790
  /**
69412
69791
  * Wraps a GetSymbolValue function to add cycle detection
@@ -69441,13 +69820,8 @@ class Evaluator {
69441
69820
  }
69442
69821
  return cell.compiledFormula.dependencies;
69443
69822
  }
69444
- getCellsDependingOn(positions) {
69445
- const ranges = [];
69446
- const zonesBySheetIds = aggregatePositionsToZones(positions);
69447
- for (const sheetId in zonesBySheetIds) {
69448
- ranges.push(...zonesBySheetIds[sheetId].map((zone) => ({ sheetId, zone })));
69449
- }
69450
- return this.formulaDependencies().getCellsDependingOn(ranges);
69823
+ getCellsDependingOn(ranges) {
69824
+ return this.formulaDependencies().getCellsDependingOn(ranges, this.nextRangesToUpdate);
69451
69825
  }
69452
69826
  }
69453
69827
  function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
@@ -70958,7 +71332,8 @@ class DynamicTablesPlugin extends CoreViewPlugin {
70958
71332
  const topLeft = { col: unionZone.left, row: unionZone.top, sheetId };
70959
71333
  const parentSpreadingCell = this.getters.getArrayFormulaSpreadingOn(topLeft);
70960
71334
  if (!parentSpreadingCell) {
70961
- return false;
71335
+ const evaluatedCell = this.getters.getEvaluatedCell(topLeft);
71336
+ return (evaluatedCell.value === CellErrorType.SpilledBlocked && !evaluatedCell.errorOriginPosition);
70962
71337
  }
70963
71338
  else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
70964
71339
  return true;
@@ -82130,6 +82505,7 @@ class RibbonMenu extends owl.Component {
82130
82505
  static components = { Menu };
82131
82506
  rootItems = topbarMenuRegistry.getMenuItems();
82132
82507
  menuRef = owl.useRef("menu");
82508
+ containerRef = owl.useRef("container");
82133
82509
  state = owl.useState({
82134
82510
  menuItems: this.rootItems,
82135
82511
  title: _t("Menu Bar"),
@@ -82137,6 +82513,7 @@ class RibbonMenu extends owl.Component {
82137
82513
  });
82138
82514
  setup() {
82139
82515
  owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
82516
+ owl.onMounted(this.updateShadows);
82140
82517
  }
82141
82518
  onExternalClick(ev) {
82142
82519
  if (!this.menuRef.el?.contains(ev.target)) {
@@ -82149,6 +82526,7 @@ class RibbonMenu extends owl.Component {
82149
82526
  this.state.parentState = { ...this.state };
82150
82527
  this.state.menuItems = children;
82151
82528
  this.state.title = menu.name(this.env);
82529
+ this.containerRef.el?.scrollTo({ top: 0 });
82152
82530
  }
82153
82531
  else {
82154
82532
  this.state.menuItems = this.rootItems;
@@ -82170,6 +82548,19 @@ class RibbonMenu extends owl.Component {
82170
82548
  height: `${this.props.height}px`,
82171
82549
  });
82172
82550
  }
82551
+ updateShadows() {
82552
+ if (!this.containerRef.el) {
82553
+ return;
82554
+ }
82555
+ this.containerRef.el.classList.remove("scroll-top", "scroll-bottom");
82556
+ const maxScroll = this.containerRef.el.scrollHeight - this.containerRef.el.clientHeight || 0;
82557
+ if (this.containerRef.el.scrollTop < maxScroll - 1) {
82558
+ this.containerRef.el.classList.add("scroll-bottom");
82559
+ }
82560
+ if (this.containerRef.el.scrollTop > 0) {
82561
+ this.containerRef.el.classList.add("scroll-top");
82562
+ }
82563
+ }
82173
82564
  onClickBack() {
82174
82565
  if (!this.state.parentState) {
82175
82566
  this.props.onClose();
@@ -82178,6 +82569,7 @@ class RibbonMenu extends owl.Component {
82178
82569
  this.state.menuItems = this.state.parentState.menuItems;
82179
82570
  this.state.title = this.state.parentState.title;
82180
82571
  this.state.parentState = this.state.parentState.parentState;
82572
+ this.containerRef.el?.scrollTo({ top: 0 });
82181
82573
  }
82182
82574
  get backTitle() {
82183
82575
  return this.state.parentState ? _t("Go to previous menu") : _t("Close menu bar");
@@ -82234,7 +82626,9 @@ class SmallBottomBar extends owl.Component {
82234
82626
  : "inactive";
82235
82627
  }
82236
82628
  get showFxIcon() {
82237
- return this.focus === "inactive" && !this.composerStore.currentContent;
82629
+ return (this.focus === "inactive" &&
82630
+ !this.composerStore.currentContent &&
82631
+ !this.composerStore.placeholder);
82238
82632
  }
82239
82633
  get rect() {
82240
82634
  return this.composerRef.el
@@ -82251,8 +82645,9 @@ class SmallBottomBar extends owl.Component {
82251
82645
  },
82252
82646
  focus: this.focus,
82253
82647
  composerStore: this.composerStore,
82254
- onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {
82648
+ onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
82255
82649
  focusMode: "contentFocus",
82650
+ selection,
82256
82651
  }),
82257
82652
  isDefaultFocus: false,
82258
82653
  inputStyle: cssPropertiesToCss({
@@ -82260,6 +82655,7 @@ class SmallBottomBar extends owl.Component {
82260
82655
  "max-height": `130px`,
82261
82656
  }),
82262
82657
  showAssistant: !isIOS(), // Hide assistant on iOS as it breaks visually
82658
+ placeholder: this.composerStore.placeholder,
82263
82659
  };
82264
82660
  }
82265
82661
  get symbols() {
@@ -82322,7 +82718,9 @@ class TopBarComposer extends owl.Component {
82322
82718
  : "inactive";
82323
82719
  }
82324
82720
  get showFxIcon() {
82325
- return this.focus === "inactive" && !this.composerStore.currentContent;
82721
+ return (this.focus === "inactive" &&
82722
+ !this.composerStore.currentContent &&
82723
+ !this.composerStore.placeholder);
82326
82724
  }
82327
82725
  get composerStyle() {
82328
82726
  const style = {
@@ -88538,6 +88936,6 @@ exports.tokenColors = tokenColors;
88538
88936
  exports.tokenize = tokenize;
88539
88937
 
88540
88938
 
88541
- __info__.version = "19.0.5";
88542
- __info__.date = "2025-10-07T10:04:06.400Z";
88543
- __info__.hash = "86fc442";
88939
+ __info__.version = "19.0.7";
88940
+ __info__.date = "2025-10-23T08:19:01.764Z";
88941
+ __info__.hash = "1c1d1ec";