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