@odoo/o-spreadsheet 17.3.0-alpha.1 → 17.3.0-alpha.2

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.
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * This file is generated by o-spreadsheet build tools. Do not edit it.
5
5
  * @see https://github.com/odoo/o-spreadsheet
6
- * @version 17.3.0-alpha.1
7
- * @date 2024-03-25T09:43:36.072Z
8
- * @hash 4095c41
6
+ * @version 17.3.0-alpha.2
7
+ * @date 2024-04-05T14:01:07.060Z
8
+ * @hash 8c5a229
9
9
  */
10
10
 
11
11
  'use strict';
@@ -466,7 +466,7 @@ function getItemId(item, itemsDic) {
466
466
  }
467
467
  // Generate new Id if the item didn't exist in the dictionary
468
468
  const ids = Object.keys(itemsDic);
469
- const maxId = ids.length === 0 ? 0 : Math.max(...ids.map((id) => parseInt(id, 10)));
469
+ const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
470
470
  itemsDic[maxId + 1] = item;
471
471
  return maxId + 1;
472
472
  }
@@ -484,7 +484,7 @@ function debounce(func, wait, immediate) {
484
484
  let timeout = undefined;
485
485
  const debounced = function () {
486
486
  const context = this;
487
- const args = arguments;
487
+ const args = Array.from(arguments);
488
488
  function later() {
489
489
  timeout = undefined;
490
490
  if (!immediate) {
@@ -686,6 +686,34 @@ function getSearchRegex(searchStr, searchOptions) {
686
686
  }
687
687
  return RegExp(searchValue, flags);
688
688
  }
689
+ /**
690
+ * Alternative to Math.max that works with large arrays.
691
+ * Typically useful for arrays bigger than 100k elements.
692
+ */
693
+ function largeMax(array) {
694
+ let len = array.length;
695
+ if (len < 100_000)
696
+ return Math.max(...array);
697
+ let max = -Infinity;
698
+ while (len--) {
699
+ max = array[len] > max ? array[len] : max;
700
+ }
701
+ return max;
702
+ }
703
+ /**
704
+ * Alternative to Math.min that works with large arrays.
705
+ * Typically useful for arrays bigger than 100k elements.
706
+ */
707
+ function largeMin(array) {
708
+ let len = array.length;
709
+ if (len < 100_000)
710
+ return Math.min(...array);
711
+ let min = +Infinity;
712
+ while (len--) {
713
+ min = array[len] < min ? array[len] : min;
714
+ }
715
+ return min;
716
+ }
689
717
 
690
718
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
691
719
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
@@ -2088,6 +2116,7 @@ exports.CommandResult = void 0;
2088
2116
  CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
2089
2117
  CommandResult["NoChanges"] = "NoChanges";
2090
2118
  CommandResult["InvalidInputId"] = "InvalidInputId";
2119
+ CommandResult["SheetIsHidden"] = "SheetIsHidden";
2091
2120
  })(exports.CommandResult || (exports.CommandResult = {}));
2092
2121
 
2093
2122
  const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
@@ -4559,8 +4588,9 @@ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
4559
4588
  * Get the default height of the cell given its style.
4560
4589
  */
4561
4590
  function getDefaultCellHeight(ctx, cell, colSize) {
4562
- if (!cell || !cell.content)
4591
+ if (!cell || (!cell.isFormula && !cell.content)) {
4563
4592
  return DEFAULT_CELL_HEIGHT;
4593
+ }
4564
4594
  const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
4565
4595
  const numberOfLines = cell.isFormula
4566
4596
  ? 1
@@ -6994,10 +7024,17 @@ urlRegistry.add("sheet_URL", {
6994
7024
  },
6995
7025
  open(url, env) {
6996
7026
  const sheetId = parseSheetUrl(url);
6997
- env.model.dispatch("ACTIVATE_SHEET", {
7027
+ const result = env.model.dispatch("ACTIVATE_SHEET", {
6998
7028
  sheetIdFrom: env.model.getters.getActiveSheetId(),
6999
7029
  sheetIdTo: sheetId,
7000
7030
  });
7031
+ if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
7032
+ env.notifyUser({
7033
+ type: "warning",
7034
+ sticky: false,
7035
+ text: _t("Cannot open the link because the linked sheet is hidden."),
7036
+ });
7037
+ }
7001
7038
  },
7002
7039
  sequence: 0,
7003
7040
  });
@@ -7113,7 +7150,7 @@ function textCell(value, format, formattedValue) {
7113
7150
  }
7114
7151
  function numberCell(value, format, formattedValue) {
7115
7152
  return {
7116
- value: value || 0,
7153
+ value: value || 0, // necessary to avoid "-0" and NaN values,
7117
7154
  format,
7118
7155
  formattedValue,
7119
7156
  type: CellValueType.number,
@@ -9957,11 +9994,11 @@ autoCompleteProviders.add("dataValidation", {
9957
9994
  }
9958
9995
  else {
9959
9996
  const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
9960
- values = this.getters
9997
+ values = Array.from(new Set(this.getters
9961
9998
  .getRangeValues(range)
9962
9999
  .filter(isNotNull)
9963
10000
  .map((value) => value.toString())
9964
- .filter((val) => val !== "");
10001
+ .filter((val) => val !== "")));
9965
10002
  }
9966
10003
  return values.map((value) => ({ text: value }));
9967
10004
  },
@@ -19408,10 +19445,10 @@ function aggregateDataForLabels(labels, datasets) {
19408
19445
  }
19409
19446
  }
19410
19447
  return {
19411
- labels: Object.keys(labelMap),
19448
+ labels: Array.from(labelSet),
19412
19449
  dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
19413
19450
  ...dataset,
19414
- data: Object.values(labelMap).map((dataOfLabel) => dataOfLabel[indexOfDataset]),
19451
+ data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
19415
19452
  })),
19416
19453
  };
19417
19454
  }
@@ -19430,8 +19467,8 @@ function truncateLabel(label) {
19430
19467
  function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale }) {
19431
19468
  const options = {
19432
19469
  // https://www.chartjs.org/docs/latest/general/responsive.html
19433
- responsive: true,
19434
- maintainAspectRatio: false,
19470
+ responsive: true, // will resize when its container is resized
19471
+ maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
19435
19472
  layout: {
19436
19473
  padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
19437
19474
  },
@@ -19477,7 +19514,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale })
19477
19514
  labels: labels.map(truncateLabel),
19478
19515
  datasets: [],
19479
19516
  },
19480
- platform: undefined,
19517
+ platform: undefined, // This key is optional and will be set by chart.js
19481
19518
  plugins: [],
19482
19519
  };
19483
19520
  }
@@ -19754,7 +19791,7 @@ function getBarConfiguration(chart, labels, localeFormat) {
19754
19791
  },
19755
19792
  y: {
19756
19793
  position: chart.verticalAxisPosition,
19757
- beginAtZero: true,
19794
+ beginAtZero: true, // the origin of the y axis is always zero
19758
19795
  ticks: {
19759
19796
  color: fontColor,
19760
19797
  callback: (value) => {
@@ -19805,6 +19842,204 @@ function createBarChartRuntime(chart, getters) {
19805
19842
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
19806
19843
  }
19807
19844
 
19845
+ class ComboChart extends AbstractChart {
19846
+ useBothYAxis;
19847
+ dataSets;
19848
+ labelRange;
19849
+ background;
19850
+ verticalAxisPosition;
19851
+ legendPosition;
19852
+ aggregated;
19853
+ dataSetsHaveTitle;
19854
+ type = "combo";
19855
+ constructor(definition, sheetId, getters) {
19856
+ super(definition, sheetId, getters);
19857
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
19858
+ this.labelRange = createRange(getters, sheetId, definition.labelRange);
19859
+ this.background = definition.background;
19860
+ this.verticalAxisPosition = definition.verticalAxisPosition;
19861
+ this.legendPosition = definition.legendPosition;
19862
+ this.aggregated = definition.aggregated;
19863
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
19864
+ this.useBothYAxis = definition.useBothYAxis;
19865
+ }
19866
+ static transformDefinition(definition, executed) {
19867
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
19868
+ }
19869
+ static validateChartDefinition(validator, definition) {
19870
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
19871
+ }
19872
+ getContextCreation() {
19873
+ return {
19874
+ background: this.background,
19875
+ title: this.title,
19876
+ range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
19877
+ auxiliaryRange: this.labelRange
19878
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
19879
+ : undefined,
19880
+ aggregated: this.aggregated,
19881
+ };
19882
+ }
19883
+ getDefinition() {
19884
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
19885
+ }
19886
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
19887
+ return {
19888
+ type: "combo",
19889
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
19890
+ background: this.background,
19891
+ dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
19892
+ legendPosition: this.legendPosition,
19893
+ verticalAxisPosition: this.verticalAxisPosition,
19894
+ labelRange: labelRange
19895
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
19896
+ : undefined,
19897
+ title: this.title,
19898
+ aggregated: this.aggregated,
19899
+ useBothYAxis: this.useBothYAxis,
19900
+ };
19901
+ }
19902
+ getDefinitionForExcel() {
19903
+ // Excel does not support aggregating labels
19904
+ if (this.aggregated) {
19905
+ return undefined;
19906
+ }
19907
+ const dataSets = this.dataSets
19908
+ .map((ds) => toExcelDataset(this.getters, ds))
19909
+ .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
19910
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
19911
+ return {
19912
+ ...this.getDefinition(),
19913
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
19914
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
19915
+ dataSets,
19916
+ labelRange,
19917
+ };
19918
+ }
19919
+ updateRanges(applyChange) {
19920
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
19921
+ if (!isStale) {
19922
+ return this;
19923
+ }
19924
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
19925
+ return new ComboChart(definition, this.sheetId, this.getters);
19926
+ }
19927
+ static getDefinitionFromContextCreation(context) {
19928
+ return {
19929
+ background: context.background,
19930
+ dataSets: context.range ? context.range : [],
19931
+ dataSetsHaveTitle: false,
19932
+ aggregated: context.aggregated,
19933
+ legendPosition: "top",
19934
+ title: context.title || "",
19935
+ verticalAxisPosition: "left",
19936
+ labelRange: context.auxiliaryRange || undefined,
19937
+ type: "combo",
19938
+ useBothYAxis: false,
19939
+ };
19940
+ }
19941
+ copyForSheetId(sheetId) {
19942
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
19943
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
19944
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
19945
+ return new ComboChart(definition, sheetId, this.getters);
19946
+ }
19947
+ copyInSheetId(sheetId) {
19948
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
19949
+ return new ComboChart(definition, sheetId, this.getters);
19950
+ }
19951
+ }
19952
+ function createComboChartRuntime(chart, getters) {
19953
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
19954
+ const locale = getters.getLocale();
19955
+ const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
19956
+ let labels = labelValues.formattedValues;
19957
+ let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
19958
+ if (chart.dataSetsHaveTitle &&
19959
+ dataSetsValues[0] &&
19960
+ labels.length > dataSetsValues[0].data.length) {
19961
+ labels.shift();
19962
+ }
19963
+ ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
19964
+ if (chart.aggregated) {
19965
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
19966
+ }
19967
+ const localeFormat = { format: dataSetFormat, locale };
19968
+ const fontColor = chartFontColor(chart.background);
19969
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
19970
+ const legend = {
19971
+ labels: { color: fontColor },
19972
+ };
19973
+ if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
19974
+ legend.display = false;
19975
+ }
19976
+ else {
19977
+ legend.position = chart.legendPosition;
19978
+ }
19979
+ config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
19980
+ config.options.layout = {
19981
+ padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
19982
+ };
19983
+ config.options.scales = {
19984
+ x: {
19985
+ ticks: {
19986
+ padding: 5,
19987
+ color: fontColor,
19988
+ },
19989
+ },
19990
+ };
19991
+ const verticalAxis = {
19992
+ beginAtZero: true, // the origin of the y axis is always zero
19993
+ ticks: {
19994
+ color: fontColor,
19995
+ callback: (value) => {
19996
+ value = Number(value);
19997
+ if (isNaN(value))
19998
+ return value;
19999
+ const { locale, format } = localeFormat;
20000
+ return formatValue(value, {
20001
+ locale,
20002
+ format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
20003
+ });
20004
+ },
20005
+ },
20006
+ };
20007
+ if (chart.useBothYAxis) {
20008
+ config.options.scales.y = {
20009
+ ...verticalAxis,
20010
+ position: "left",
20011
+ };
20012
+ config.options.scales.y1 = {
20013
+ ...verticalAxis,
20014
+ position: "right",
20015
+ grid: {
20016
+ display: false,
20017
+ },
20018
+ };
20019
+ }
20020
+ else {
20021
+ config.options.scales.y = {
20022
+ ...verticalAxis,
20023
+ position: chart.verticalAxisPosition,
20024
+ };
20025
+ }
20026
+ const colors = new ChartColors();
20027
+ for (let [index, { label, data }] of dataSetsValues.entries()) {
20028
+ const color = colors.next();
20029
+ const dataset = {
20030
+ label,
20031
+ data,
20032
+ borderColor: color,
20033
+ backgroundColor: color,
20034
+ yAxisID: index > 0 && chart.useBothYAxis ? "y1" : "y",
20035
+ type: index === 0 ? "bar" : "line",
20036
+ order: -index,
20037
+ };
20038
+ config.data.datasets.push(dataset);
20039
+ }
20040
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
20041
+ }
20042
+
19808
20043
  function isDataRangeValid(definition) {
19809
20044
  return definition.dataRange && !rangeReference.test(definition.dataRange)
19810
20045
  ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
@@ -20143,7 +20378,7 @@ function getBestTimeUnitForScale(labels, format, locale) {
20143
20378
  return undefined;
20144
20379
  }
20145
20380
  const labelsTimestamps = labelDates.map((date) => date.getTime());
20146
- const period = Math.max(...labelsTimestamps) - Math.min(...labelsTimestamps);
20381
+ const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);
20147
20382
  const minUnit = getFormatMinDisplayUnit(format);
20148
20383
  if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
20149
20384
  return "second";
@@ -20275,7 +20510,7 @@ function getLineOrScatterConfiguration(chart, labels, localeFormat) {
20275
20510
  },
20276
20511
  y: {
20277
20512
  position: chart.verticalAxisPosition,
20278
- beginAtZero: true,
20513
+ beginAtZero: true, // the origin of the y axis is always zero
20279
20514
  ticks: {
20280
20515
  color: fontColor,
20281
20516
  callback: (value) => {
@@ -20361,7 +20596,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
20361
20596
  const dataset = {
20362
20597
  label,
20363
20598
  data,
20364
- tension: 0,
20599
+ tension: 0, // 0 -> render straight lines, which is much faster
20365
20600
  borderColor: color,
20366
20601
  backgroundColor,
20367
20602
  pointBackgroundColor: color,
@@ -20583,7 +20818,7 @@ class PieChart extends AbstractChart {
20583
20818
  ...this.getDefinition(),
20584
20819
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
20585
20820
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
20586
- verticalAxisPosition: "left",
20821
+ verticalAxisPosition: "left", //TODO ExcelChartDefinition should be adapted, but can be done later
20587
20822
  dataSets,
20588
20823
  labelRange,
20589
20824
  };
@@ -20631,7 +20866,7 @@ function getPieConfiguration(chart, labels, localeFormat) {
20631
20866
  }
20632
20867
  function getPieColors(colors, dataSetsValues) {
20633
20868
  const pieColors = [];
20634
- const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
20869
+ const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
20635
20870
  for (let i = 0; i <= maxLength; i++) {
20636
20871
  pieColors.push(colors.next());
20637
20872
  }
@@ -20800,7 +21035,7 @@ function createScatterChartRuntime(chart, getters) {
20800
21035
  configOptions.elements = {
20801
21036
  point: {
20802
21037
  radius: 3,
20803
- hoverRadius: 3,
21038
+ hoverRadius: 3, // chartJS seems bugged, the point starts with the "hoverRadius" value
20804
21039
  hitRadius: 8,
20805
21040
  },
20806
21041
  };
@@ -20840,6 +21075,16 @@ chartRegistry.add("bar", {
20840
21075
  name: _t("Bar"),
20841
21076
  sequence: 10,
20842
21077
  });
21078
+ chartRegistry.add("combo", {
21079
+ match: (type) => type === "combo",
21080
+ createChart: (definition, sheetId, getters) => new ComboChart(definition, sheetId, getters),
21081
+ getChartRuntime: createComboChartRuntime,
21082
+ validateChartDefinition: (validator, definition) => ComboChart.validateChartDefinition(validator, definition),
21083
+ transformDefinition: (definition, executed) => ComboChart.transformDefinition(definition, executed),
21084
+ getChartDefinitionFromContextCreation: (context) => ComboChart.getDefinitionFromContextCreation(context),
21085
+ name: _t("Combo"),
21086
+ sequence: 15,
21087
+ });
20843
21088
  chartRegistry.add("line", {
20844
21089
  match: (type) => type === "line",
20845
21090
  createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),
@@ -20893,6 +21138,7 @@ chartRegistry.add("scatter", {
20893
21138
  const chartComponentRegistry = new Registry();
20894
21139
  chartComponentRegistry.add("line", ChartJsComponent);
20895
21140
  chartComponentRegistry.add("bar", ChartJsComponent);
21141
+ chartComponentRegistry.add("combo", ChartJsComponent);
20896
21142
  chartComponentRegistry.add("pie", ChartJsComponent);
20897
21143
  chartComponentRegistry.add("gauge", GaugeChartComponent);
20898
21144
  chartComponentRegistry.add("scatter", ChartJsComponent);
@@ -23151,7 +23397,7 @@ const lightTemplateWithHeader = (colorSet) => ({
23151
23397
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23152
23398
  border: { bottom: { color: colorSet.highlight, style: "thin" } },
23153
23399
  },
23154
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23400
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23155
23401
  firstRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23156
23402
  secondRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23157
23403
  });
@@ -23169,7 +23415,7 @@ const lightTemplateAllBorders = (colorSet) => ({
23169
23415
  },
23170
23416
  },
23171
23417
  headerRow: { border: { bottom: { color: colorSet.highlight, style: "medium" } } },
23172
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23418
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23173
23419
  firstRowStripe: { style: { fillColor: colorSet.light } },
23174
23420
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23175
23421
  });
@@ -23188,7 +23434,7 @@ const mediumTemplateBandedBorders = (colorSet) => ({
23188
23434
  headerRow: {
23189
23435
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23190
23436
  },
23191
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23437
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23192
23438
  firstRowStripe: { style: { fillColor: colorSet.light } },
23193
23439
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23194
23440
  });
@@ -23224,7 +23470,7 @@ const mediumTemplateMinimalBorders = (colorSet) => ({
23224
23470
  bottom: { color: "#000000", style: "medium" },
23225
23471
  },
23226
23472
  },
23227
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23473
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23228
23474
  headerRow: {
23229
23475
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23230
23476
  border: { bottom: { color: "#000000", style: "medium" } },
@@ -23248,7 +23494,7 @@ const mediumTemplateAllBorders = (colorSet) => ({
23248
23494
  },
23249
23495
  style: { fillColor: colorSet.light },
23250
23496
  },
23251
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23497
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23252
23498
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23253
23499
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
23254
23500
  });
@@ -23279,7 +23525,7 @@ const darkTemplateNoBorders = (colorSet) => ({
23279
23525
  category: "dark",
23280
23526
  colorName: colorSet.name,
23281
23527
  wholeTable: { style: { fillColor: colorSet.light } },
23282
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23528
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23283
23529
  headerRow: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
23284
23530
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23285
23531
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
@@ -23444,8 +23690,8 @@ const DELETE_CONTENT_ROWS_NAME = (env) => {
23444
23690
  let last;
23445
23691
  const activesRows = env.model.getters.getActiveRows();
23446
23692
  if (activesRows.size !== 0) {
23447
- first = Math.min(...activesRows);
23448
- last = Math.max(...activesRows);
23693
+ first = largeMin([...activesRows]);
23694
+ last = largeMax([...activesRows]);
23449
23695
  }
23450
23696
  else {
23451
23697
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23473,8 +23719,8 @@ const DELETE_CONTENT_COLUMNS_NAME = (env) => {
23473
23719
  let last;
23474
23720
  const activeCols = env.model.getters.getActiveCols();
23475
23721
  if (activeCols.size !== 0) {
23476
- first = Math.min(...activeCols);
23477
- last = Math.max(...activeCols);
23722
+ first = largeMin([...activeCols]);
23723
+ last = largeMax([...activeCols]);
23478
23724
  }
23479
23725
  else {
23480
23726
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23502,8 +23748,8 @@ const REMOVE_ROWS_NAME = (env) => {
23502
23748
  let last;
23503
23749
  const activesRows = env.model.getters.getActiveRows();
23504
23750
  if (activesRows.size !== 0) {
23505
- first = Math.min(...activesRows);
23506
- last = Math.max(...activesRows);
23751
+ first = largeMin([...activesRows]);
23752
+ last = largeMax([...activesRows]);
23507
23753
  }
23508
23754
  else {
23509
23755
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23544,8 +23790,8 @@ const REMOVE_COLUMNS_NAME = (env) => {
23544
23790
  let last;
23545
23791
  const activeCols = env.model.getters.getActiveCols();
23546
23792
  if (activeCols.size !== 0) {
23547
- first = Math.min(...activeCols);
23548
- last = Math.max(...activeCols);
23793
+ first = largeMin([...activeCols]);
23794
+ last = largeMax([...activeCols]);
23549
23795
  }
23550
23796
  else {
23551
23797
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23586,7 +23832,7 @@ const INSERT_ROWS_BEFORE_ACTION = (env) => {
23586
23832
  let row;
23587
23833
  let quantity;
23588
23834
  if (activeRows.size) {
23589
- row = Math.min(...activeRows);
23835
+ row = largeMin([...activeRows]);
23590
23836
  quantity = activeRows.size;
23591
23837
  }
23592
23838
  else {
@@ -23607,7 +23853,7 @@ const INSERT_ROWS_AFTER_ACTION = (env) => {
23607
23853
  let row;
23608
23854
  let quantity;
23609
23855
  if (activeRows.size) {
23610
- row = Math.max(...activeRows);
23856
+ row = largeMax([...activeRows]);
23611
23857
  quantity = activeRows.size;
23612
23858
  }
23613
23859
  else {
@@ -23628,7 +23874,7 @@ const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
23628
23874
  let column;
23629
23875
  let quantity;
23630
23876
  if (activeCols.size) {
23631
- column = Math.min(...activeCols);
23877
+ column = largeMin([...activeCols]);
23632
23878
  quantity = activeCols.size;
23633
23879
  }
23634
23880
  else {
@@ -23649,7 +23895,7 @@ const INSERT_COLUMNS_AFTER_ACTION = (env) => {
23649
23895
  let column;
23650
23896
  let quantity;
23651
23897
  if (activeCols.size) {
23652
- column = Math.max(...activeCols);
23898
+ column = largeMax([...activeCols]);
23653
23899
  quantity = activeCols.size;
23654
23900
  }
23655
23901
  else {
@@ -27255,6 +27501,22 @@ class BarChartDesignPanel extends LineBarPieDesignPanel {
27255
27501
  static template = "o-spreadsheet-BarChartDesignPanel";
27256
27502
  }
27257
27503
 
27504
+ class ComboChartConfigPanel extends LineBarPieConfigPanel {
27505
+ static template = "o-spreadsheet-ComboChartConfigPanel";
27506
+ get shouldUseRightAxis() {
27507
+ return _t("Use right axis for line series");
27508
+ }
27509
+ onUpdateUseRightAxis(useBothYAxis) {
27510
+ this.props.updateChart(this.props.figureId, {
27511
+ useBothYAxis,
27512
+ });
27513
+ }
27514
+ }
27515
+
27516
+ class ComboChartDesignPanel extends LineBarPieDesignPanel {
27517
+ static template = "o-spreadsheet-ComboChartDesignPanel";
27518
+ }
27519
+
27258
27520
  class GaugeChartConfigPanel extends owl.Component {
27259
27521
  static template = "o-spreadsheet-GaugeChartConfigPanel";
27260
27522
  static components = { ChartErrorSection, ChartDataSeries };
@@ -27612,6 +27874,10 @@ chartSidePanelComponentRegistry
27612
27874
  .add("bar", {
27613
27875
  configuration: BarConfigPanel,
27614
27876
  design: BarChartDesignPanel,
27877
+ })
27878
+ .add("combo", {
27879
+ configuration: ComboChartConfigPanel,
27880
+ design: ComboChartDesignPanel,
27615
27881
  })
27616
27882
  .add("pie", {
27617
27883
  configuration: LineBarPieConfigPanel,
@@ -30252,7 +30518,11 @@ class SplitIntoColumnsPanel extends owl.Component {
30252
30518
  const composerStore = useStore(ComposerStore);
30253
30519
  // The feature makes no sense if we are editing a cell, because then the selection isn't active
30254
30520
  // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
30255
- owl.useEffect(this.props.onCloseSidePanel, () => [composerStore.editionMode]);
30521
+ owl.useEffect((editionMode) => {
30522
+ if (editionMode !== "inactive") {
30523
+ this.props.onCloseSidePanel();
30524
+ }
30525
+ }, () => [composerStore.editionMode]);
30256
30526
  owl.onMounted(() => {
30257
30527
  composerStore.stopEdition();
30258
30528
  });
@@ -31961,6 +32231,9 @@ class FunctionDescriptionProvider extends owl.Component {
31961
32231
  this.assistantState.allowCellSelectionBehind = false;
31962
32232
  }, 2000);
31963
32233
  }
32234
+ get formulaArgSeparator() {
32235
+ return this.env.model.getters.getLocale().formulaArgSeparator + " ";
32236
+ }
31964
32237
  }
31965
32238
 
31966
32239
  const functions$2 = functionRegistry.content;
@@ -32120,6 +32393,12 @@ class Composer extends owl.Component {
32120
32393
  owl.useEffect(() => {
32121
32394
  this.processContent();
32122
32395
  });
32396
+ owl.onPatched(() => {
32397
+ // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
32398
+ if (this.composerStore.editionMode === "inactive") {
32399
+ this.processTokenAtCursor();
32400
+ }
32401
+ });
32123
32402
  }
32124
32403
  // ---------------------------------------------------------------------------
32125
32404
  // Handlers
@@ -34099,11 +34378,6 @@ css /* scss */ `
34099
34378
  height: 10000px;
34100
34379
  background-color: ${SELECTION_BORDER_COLOR};
34101
34380
  }
34102
- .o-unhide-buttons {
34103
- width: fit-content;
34104
- gap: 5px;
34105
- transform: translate(-50%, 0);
34106
- }
34107
34381
  .o-unhide:hover {
34108
34382
  z-index: ${ComponentsImportance.Grid + 1};
34109
34383
  background-color: lightgrey;
@@ -34265,10 +34539,6 @@ css /* scss */ `
34265
34539
  height: 1px;
34266
34540
  background-color: ${SELECTION_BORDER_COLOR};
34267
34541
  }
34268
- .o-unhide-buttons {
34269
- height: fit-content;
34270
- transform: translate(0, -50%);
34271
- }
34272
34542
  .o-unhide:hover {
34273
34543
  z-index: ${ComponentsImportance.Grid + 1};
34274
34544
  background-color: lightgrey;
@@ -35476,7 +35746,7 @@ class VerticalScrollBar extends owl.Component {
35476
35746
  onScroll(offset) {
35477
35747
  const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();
35478
35748
  this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
35479
- offsetX: scrollX,
35749
+ offsetX: scrollX, // offsetX is the same
35480
35750
  offsetY: offset,
35481
35751
  });
35482
35752
  }
@@ -35764,8 +36034,8 @@ class Grid extends owl.Component {
35764
36034
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
35765
36035
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
35766
36036
  "Ctrl+Shift+V": () => PASTE_AS_VALUE_ACTION(this.env),
35767
- "Ctrl+Shift+<": () => this.clearFormatting(),
35768
- "Ctrl+<": () => this.clearFormatting(),
36037
+ "Ctrl+Shift+<": () => this.clearFormatting(), // for qwerty
36038
+ "Ctrl+<": () => this.clearFormatting(), // for azerty
35769
36039
  "Ctrl+Shift+ ": () => {
35770
36040
  this.env.model.selection.selectAll();
35771
36041
  },
@@ -36201,6 +36471,7 @@ const XLSX_CHART_TYPES = [
36201
36471
  "surfaceChart",
36202
36472
  "surface3DChart",
36203
36473
  "bubbleChart",
36474
+ "comboChart",
36204
36475
  ];
36205
36476
 
36206
36477
  /** In XLSX color format (no #) */
@@ -36532,10 +36803,10 @@ function convertCFCellIsOperator(xlsxCfOperator) {
36532
36803
  const CF_TYPE_CONVERSION_MAP = {
36533
36804
  aboveAverage: undefined,
36534
36805
  expression: undefined,
36535
- cellIs: undefined,
36536
- colorScale: undefined,
36806
+ cellIs: undefined, // exist but isn't an operator in o_spreadsheet
36807
+ colorScale: undefined, // exist but isn't an operator in o_spreadsheet
36537
36808
  dataBar: undefined,
36538
- iconSet: undefined,
36809
+ iconSet: undefined, // exist but isn't an operator in o_spreadsheet
36539
36810
  top10: undefined,
36540
36811
  uniqueValues: undefined,
36541
36812
  duplicateValues: undefined,
@@ -36612,6 +36883,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36612
36883
  surfaceChart: undefined,
36613
36884
  surface3DChart: undefined,
36614
36885
  bubbleChart: undefined,
36886
+ comboChart: "combo",
36615
36887
  };
36616
36888
  /** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/
36617
36889
  const SUBTOTAL_FUNCTION_CONVERSION_MAP = {
@@ -36770,7 +37042,7 @@ const XLSX_INDEXED_COLORS = {
36770
37042
  61: "993366",
36771
37043
  62: "333399",
36772
37044
  63: "333333",
36773
- 64: "000000",
37045
+ 64: "000000", // system foreground
36774
37046
  65: "FFFFFF", // system background
36775
37047
  };
36776
37048
  const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
@@ -37962,7 +38234,7 @@ function convertHyperlink(link, cellValue, warningManager) {
37962
38234
  function getSheetDims(sheet) {
37963
38235
  const dims = [0, 0];
37964
38236
  for (let row of sheet.rows) {
37965
- dims[0] = Math.max(dims[0], ...row.cells.map((cell) => toCartesian(cell.xc).col));
38237
+ dims[0] = Math.max(dims[0], largeMax(row.cells.map((cell) => toCartesian(cell.xc).col)));
37966
38238
  dims[1] = Math.max(dims[1], row.index);
37967
38239
  }
37968
38240
  dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
@@ -38828,6 +39100,9 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38828
39100
  if (!CHART_TYPE_CONVERSION_MAP[chartType]) {
38829
39101
  throw new Error(`Unsupported chart type ${chartType}`);
38830
39102
  }
39103
+ if (CHART_TYPE_CONVERSION_MAP[chartType] === "combo") {
39104
+ return this.extractComboChart(rootChartElement);
39105
+ }
38831
39106
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
38832
39107
  const chartTitle = this.mapOnElements({ parent: rootChartElement, query: "c:title a:t" }, (textElement) => {
38833
39108
  return textElement.textContent || "";
@@ -38856,6 +39131,37 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38856
39131
  };
38857
39132
  })[0];
38858
39133
  }
39134
+ extractComboChart(chartElement) {
39135
+ // Title can be separated into multiple xml elements (for styling and such), we only import the text
39136
+ const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
39137
+ return textElement.textContent || "";
39138
+ }).join("");
39139
+ const barChartGrouping = this.extractChildAttr(chartElement, "c:grouping", "val", {
39140
+ default: "clustered",
39141
+ }).asString();
39142
+ return {
39143
+ title: chartTitle,
39144
+ type: "combo",
39145
+ dataSets: [
39146
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:barChart`)),
39147
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:lineChart`)),
39148
+ ],
39149
+ labelRange: this.extractChildTextContent(chartElement, "c:ser c:cat c:f"),
39150
+ backgroundColor: this.extractChildAttr(chartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
39151
+ default: "ffffff",
39152
+ }).asString(),
39153
+ verticalAxisPosition: this.extractChildAttr(chartElement, "c:valAx > c:axPos", "val", {
39154
+ default: "l",
39155
+ }).asString() === "r"
39156
+ ? "right"
39157
+ : "left",
39158
+ legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
39159
+ default: "b",
39160
+ }).asString()],
39161
+ stacked: barChartGrouping === "stacked",
39162
+ fontColor: "000000",
39163
+ };
39164
+ }
38859
39165
  extractChartDatasets(chartElement) {
38860
39166
  return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
38861
39167
  return {
@@ -38873,12 +39179,21 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38873
39179
  if (!plotAreaElement) {
38874
39180
  throw new Error("Missing plot area in the chart definition.");
38875
39181
  }
39182
+ let globalTag = undefined;
38876
39183
  for (let child of plotAreaElement.children) {
38877
39184
  const tag = removeTagEscapedNamespaces(child.tagName);
38878
39185
  if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
38879
- return tag;
39186
+ if (!globalTag) {
39187
+ globalTag = tag;
39188
+ }
39189
+ else if (globalTag !== tag) {
39190
+ globalTag = "comboChart";
39191
+ }
38880
39192
  }
38881
39193
  }
39194
+ if (globalTag) {
39195
+ return globalTag;
39196
+ }
38882
39197
  throw new Error("Unknown chart type");
38883
39198
  }
38884
39199
  }
@@ -42469,6 +42784,9 @@ class DataValidationPlugin extends CorePlugin {
42469
42784
  if (newRule.criterion.type === "isBoolean") {
42470
42785
  this.setCenterStyleToBooleanCells(newRule);
42471
42786
  }
42787
+ else if (newRule.criterion.type === "isValueInList") {
42788
+ newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
42789
+ }
42472
42790
  const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
42473
42791
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
42474
42792
  if (ruleIndex !== -1) {
@@ -42865,7 +43183,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
42865
43183
  if (hiddenElements.size >= elements) {
42866
43184
  return "TooManyHiddenElements" /* CommandResult.TooManyHiddenElements */;
42867
43185
  }
42868
- else if (Math.min(...cmd.elements) < 0 || Math.max(...cmd.elements) > elements) {
43186
+ else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
42869
43187
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
42870
43188
  }
42871
43189
  else {
@@ -43194,7 +43512,6 @@ class MergePlugin extends CorePlugin {
43194
43512
  "isInSameMerge",
43195
43513
  "isMergeHidden",
43196
43514
  "getMainCellPosition",
43197
- "getBottomLeftCell",
43198
43515
  "expandZone",
43199
43516
  "doesIntersectMerge",
43200
43517
  "doesColumnsHaveCommonMerges",
@@ -43376,13 +43693,6 @@ class MergePlugin extends CorePlugin {
43376
43693
  const mergeTopLeftPos = this.getMerge(position).topLeft;
43377
43694
  return { sheetId: position.sheetId, col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };
43378
43695
  }
43379
- getBottomLeftCell(position) {
43380
- if (!this.isInMerge(position)) {
43381
- return position;
43382
- }
43383
- const { bottom, left } = this.getMerge(position);
43384
- return { sheetId: position.sheetId, col: left, row: bottom };
43385
- }
43386
43696
  isMergeHidden(sheetId, merge) {
43387
43697
  const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);
43388
43698
  const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);
@@ -43683,8 +43993,8 @@ class RangeAdapter {
43683
43993
  let newRange = range;
43684
43994
  let changeType = "NONE";
43685
43995
  for (let group of groups) {
43686
- const min = Math.min(...group);
43687
- const max = Math.max(...group);
43996
+ const min = largeMin(group);
43997
+ const max = largeMax(group);
43688
43998
  if (range.zone[start] <= min && min <= range.zone[end]) {
43689
43999
  const toRemove = Math.min(range.zone[end], max) - min + 1;
43690
44000
  changeType = "RESIZE";
@@ -44066,7 +44376,6 @@ class SheetPlugin extends CorePlugin {
44066
44376
  "getSheetIds",
44067
44377
  "getVisibleSheetIds",
44068
44378
  "isSheetVisible",
44069
- "getEvaluationSheets",
44070
44379
  "doesHeaderExist",
44071
44380
  "doesHeadersExist",
44072
44381
  "getCell",
@@ -44133,8 +44442,8 @@ class SheetPlugin extends CorePlugin {
44133
44442
  }
44134
44443
  return "Success" /* CommandResult.Success */;
44135
44444
  case "REMOVE_COLUMNS_ROWS": {
44136
- const min = Math.min(...cmd.elements);
44137
- const max = Math.max(...cmd.elements);
44445
+ const min = largeMin(cmd.elements);
44446
+ const max = largeMax(cmd.elements);
44138
44447
  if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
44139
44448
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
44140
44449
  }
@@ -44325,9 +44634,6 @@ class SheetPlugin extends CorePlugin {
44325
44634
  getVisibleSheetIds() {
44326
44635
  return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));
44327
44636
  }
44328
- getEvaluationSheets() {
44329
- return this.sheets;
44330
- }
44331
44637
  doesHeaderExist(sheetId, dimension, index) {
44332
44638
  return dimension === "COL"
44333
44639
  ? index >= 0 && index < this.getNumberCols(sheetId)
@@ -44336,13 +44642,6 @@ class SheetPlugin extends CorePlugin {
44336
44642
  doesHeadersExist(sheetId, dimension, headerIndexes) {
44337
44643
  return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));
44338
44644
  }
44339
- getRow(sheetId, index) {
44340
- const row = this.getSheet(sheetId).rows[index];
44341
- if (!row) {
44342
- throw new Error(`Row ${row} not found.`);
44343
- }
44344
- return row;
44345
- }
44346
44645
  getCell({ sheetId, col, row }) {
44347
44646
  const sheet = this.tryGetSheet(sheetId);
44348
44647
  const cellId = sheet?.rows[row]?.cells[col];
@@ -45911,12 +46210,6 @@ class CompilationParametersBuilder {
45911
46210
  : _t("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
45912
46211
  }
45913
46212
  const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
45914
- return this.readCell(position);
45915
- }
45916
- readCell(position) {
45917
- if (!this.getters.tryGetSheet(position.sheetId)) {
45918
- throw new EvaluationError(_t("Invalid sheet name"));
45919
- }
45920
46213
  return this.computeCell(position);
45921
46214
  }
45922
46215
  /**
@@ -45952,7 +46245,7 @@ class CompilationParametersBuilder {
45952
46245
  matrix[colIndex] = new Array(height);
45953
46246
  for (let row = _zone.top; row <= _zone.bottom; row++) {
45954
46247
  const rowIndex = row - _zone.top;
45955
- matrix[colIndex][rowIndex] = this.readCell({ sheetId, col, row });
46248
+ matrix[colIndex][rowIndex] = this.computeCell({ sheetId, col, row });
45956
46249
  }
45957
46250
  }
45958
46251
  this.rangeCache[cacheKey] = matrix;
@@ -47060,15 +47353,15 @@ class Evaluator {
47060
47353
  getEvaluatedCell(position) {
47061
47354
  return this.evaluatedCells.get(position) || EMPTY_CELL;
47062
47355
  }
47063
- getSpreadPositionsOf(position) {
47356
+ getSpreadZone(position) {
47064
47357
  if (!this.spreadingRelations.isArrayFormula(position)) {
47065
- return [];
47358
+ return undefined;
47066
47359
  }
47067
47360
  if (this.evaluatedCells.get(position)?.type === CellValueType.error) {
47068
- return [position];
47361
+ return positionToZone(position);
47069
47362
  }
47070
47363
  const spreadPositions = Array.from(this.spreadingRelations.getArrayResultPositions(position));
47071
- return [position, ...spreadPositions];
47364
+ return union(positionToZone(position), unionPositionsToZone(spreadPositions));
47072
47365
  }
47073
47366
  getEvaluatedPositions() {
47074
47367
  return this.evaluatedCells.keys();
@@ -47473,7 +47766,7 @@ class EvaluationPlugin extends UIPlugin {
47473
47766
  "getEvaluatedCell",
47474
47767
  "getEvaluatedCells",
47475
47768
  "getEvaluatedCellsInZone",
47476
- "getSpreadPositionsOf",
47769
+ "getSpreadZone",
47477
47770
  "getArrayFormulaSpreadingOn",
47478
47771
  "isEmpty",
47479
47772
  ];
@@ -47579,8 +47872,11 @@ class EvaluationPlugin extends UIPlugin {
47579
47872
  getEvaluatedCellsInZone(sheetId, zone) {
47580
47873
  return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
47581
47874
  }
47582
- getSpreadPositionsOf(position) {
47583
- return this.evaluator.getSpreadPositionsOf(position);
47875
+ /**
47876
+ * Return the spread zone the position is part of, if any
47877
+ */
47878
+ getSpreadZone(position) {
47879
+ return this.evaluator.getSpreadZone(position);
47584
47880
  }
47585
47881
  getArrayFormulaSpreadingOn(position) {
47586
47882
  return this.evaluator.getArrayFormulaSpreadingOn(position);
@@ -47620,7 +47916,7 @@ class EvaluationPlugin extends UIPlugin {
47620
47916
  ? getItemId(newFormat, data.formats)
47621
47917
  : exportedCellData.format;
47622
47918
  let content;
47623
- if (formulaCell instanceof FormulaCellWithDependencies) {
47919
+ if (isFormula && formulaCell instanceof FormulaCellWithDependencies) {
47624
47920
  content = formulaCell.contentWithFixedReferences;
47625
47921
  }
47626
47922
  else {
@@ -47668,17 +47964,17 @@ function isBadExpression(tokens) {
47668
47964
  */
47669
47965
  function sortWithClusters(colorsToSort) {
47670
47966
  const clusters = [
47671
- { leadColor: rgba(255, 0, 0), colors: [] },
47672
- { leadColor: rgba(255, 128, 0), colors: [] },
47673
- { leadColor: rgba(128, 128, 0), colors: [] },
47674
- { leadColor: rgba(128, 255, 0), colors: [] },
47675
- { leadColor: rgba(0, 255, 0), colors: [] },
47676
- { leadColor: rgba(0, 255, 128), colors: [] },
47677
- { leadColor: rgba(0, 255, 255), colors: [] },
47678
- { leadColor: rgba(0, 127, 255), colors: [] },
47679
- { leadColor: rgba(0, 0, 255), colors: [] },
47680
- { leadColor: rgba(127, 0, 255), colors: [] },
47681
- { leadColor: rgba(128, 0, 128), colors: [] },
47967
+ { leadColor: rgba(255, 0, 0), colors: [] }, // red
47968
+ { leadColor: rgba(255, 128, 0), colors: [] }, // orange
47969
+ { leadColor: rgba(128, 128, 0), colors: [] }, // yellow
47970
+ { leadColor: rgba(128, 255, 0), colors: [] }, // chartreuse
47971
+ { leadColor: rgba(0, 255, 0), colors: [] }, // green
47972
+ { leadColor: rgba(0, 255, 128), colors: [] }, // spring green
47973
+ { leadColor: rgba(0, 255, 255), colors: [] }, // cyan
47974
+ { leadColor: rgba(0, 127, 255), colors: [] }, // azure
47975
+ { leadColor: rgba(0, 0, 255), colors: [] }, // blue
47976
+ { leadColor: rgba(127, 0, 255), colors: [] }, // violet
47977
+ { leadColor: rgba(128, 0, 128), colors: [] }, // magenta
47682
47978
  { leadColor: rgba(255, 0, 128), colors: [] }, // rose
47683
47979
  ];
47684
47980
  for (const color of colorsToSort.map(colorToRGBA)) {
@@ -48069,13 +48365,13 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
48069
48365
  .map((cell) => cell.value);
48070
48366
  switch (threshold.type) {
48071
48367
  case "value":
48072
- const result = functionName === "max" ? Math.max(...rangeValues) : Math.min(...rangeValues);
48368
+ const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
48073
48369
  return result;
48074
48370
  case "number":
48075
48371
  return Number(threshold.value);
48076
48372
  case "percentage":
48077
- const min = Math.min(...rangeValues);
48078
- const max = Math.max(...rangeValues);
48373
+ const min = largeMin(rangeValues);
48374
+ const max = largeMax(rangeValues);
48079
48375
  const delta = max - min;
48080
48376
  return min + (delta * Number(threshold.value)) / 100;
48081
48377
  case "percentile":
@@ -48542,8 +48838,8 @@ class DynamicTablesPlugin extends UIPlugin {
48542
48838
  else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
48543
48839
  return true;
48544
48840
  }
48545
- const spreadPositions = this.getters.getSpreadPositionsOf(parentSpreadingCell);
48546
- return deepEquals(unionZone, unionPositionsToZone(spreadPositions));
48841
+ const zone = this.getters.getSpreadZone(parentSpreadingCell);
48842
+ return deepEquals(unionZone, zone);
48547
48843
  }
48548
48844
  coreTableToTable(sheetId, table) {
48549
48845
  if (table.type !== "dynamic") {
@@ -48551,8 +48847,7 @@ class DynamicTablesPlugin extends UIPlugin {
48551
48847
  }
48552
48848
  const tableZone = table.range.zone;
48553
48849
  const tablePosition = { sheetId, col: tableZone.left, row: tableZone.top };
48554
- const spreadPositions = this.getters.getSpreadPositionsOf(tablePosition);
48555
- const zone = spreadPositions.length ? unionPositionsToZone(spreadPositions) : table.range.zone;
48850
+ const zone = this.getters.getSpreadZone(tablePosition) ?? table.range.zone;
48556
48851
  const range = this.getters.getRangeFromZone(sheetId, zone);
48557
48852
  const filters = this.getDynamicTableFilters(sheetId, table, zone);
48558
48853
  return { id: table.id, range, filters, config: table.config };
@@ -49002,8 +49297,7 @@ class AutofillPlugin extends UIPlugin {
49002
49297
  let row = zone.bottom;
49003
49298
  if (col > 0) {
49004
49299
  let leftPosition = { sheetId, col: col - 1, row };
49005
- while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
49006
- this.getters.getCell(leftPosition)?.content) {
49300
+ while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty) {
49007
49301
  row += 1;
49008
49302
  leftPosition = { sheetId, col: col - 1, row };
49009
49303
  }
@@ -49012,8 +49306,7 @@ class AutofillPlugin extends UIPlugin {
49012
49306
  col = zone.right;
49013
49307
  if (col <= this.getters.getNumberCols(sheetId)) {
49014
49308
  let rightPosition = { sheetId, col: col + 1, row };
49015
- while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
49016
- this.getters.getCell(rightPosition)?.content) {
49309
+ while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty) {
49017
49310
  row += 1;
49018
49311
  rightPosition = { sheetId, col: col + 1, row };
49019
49312
  }
@@ -49323,13 +49616,13 @@ class AutomaticSumPlugin extends UIPlugin {
49323
49616
  const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
49324
49617
  const cellPositions = range(end, -1, -1);
49325
49618
  const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
49326
- const maxValidPosition = Math.max(...invalidCells);
49619
+ const maxValidPosition = largeMax(invalidCells);
49327
49620
  const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
49328
49621
  const firstSequence = numberSequences[0] || [];
49329
- if (Math.max(...firstSequence) < maxValidPosition) {
49622
+ if (largeMax(firstSequence) < maxValidPosition) {
49330
49623
  return Infinity;
49331
49624
  }
49332
- return Math.min(...firstSequence);
49625
+ return largeMin(firstSequence);
49333
49626
  }
49334
49627
  shouldFindData(sheetId, zone) {
49335
49628
  return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
@@ -50904,8 +51197,6 @@ class SheetUIPlugin extends UIPlugin {
50904
51197
  static getters = [
50905
51198
  "doesCellHaveGridIcon",
50906
51199
  "getCellWidth",
50907
- "getCellComputedBorder",
50908
- "getCellComputedStyle",
50909
51200
  "getTextWidth",
50910
51201
  "getCellText",
50911
51202
  "getCellMultiLineText",
@@ -50949,7 +51240,7 @@ class SheetUIPlugin extends UIPlugin {
50949
51240
  // Getters
50950
51241
  // ---------------------------------------------------------------------------
50951
51242
  getCellWidth(position) {
50952
- const style = this.getCellComputedStyle(position);
51243
+ const style = this.getters.getCellComputedStyle(position);
50953
51244
  let contentWidth = 0;
50954
51245
  const content = this.getters.getEvaluatedCell(position).formattedValue;
50955
51246
  if (content) {
@@ -51042,35 +51333,12 @@ class SheetUIPlugin extends UIPlugin {
51042
51333
  */
51043
51334
  isCellEmpty(position) {
51044
51335
  const mainPosition = this.getters.getMainCellPosition(position);
51045
- return !(this.getters.getCorrespondingFormulaCell(mainPosition) ||
51046
- this.getters.getCell(mainPosition)?.content);
51047
- }
51048
- getCellComputedBorder(position) {
51049
- const cellBorder = this.getters.getCellBorder(position) || {};
51050
- const cellTableBorder = this.getters.getCellTableBorder(position) || {};
51051
- // Use removeFalsyAttributes to avoid overwriting borders with undefined values
51052
- const border = { ...cellTableBorder, ...removeFalsyAttributes(cellBorder) };
51053
- return isObjectEmptyRecursive(border) ? null : border;
51054
- }
51055
- getCellComputedStyle(position) {
51056
- const cell = this.getters.getCell(position);
51057
- const cfStyle = this.getters.getCellConditionalFormatStyle(position);
51058
- const tableStyle = this.getters.getCellTableStyle(position);
51059
- const computedStyle = {
51060
- ...removeFalsyAttributes(tableStyle),
51061
- ...removeFalsyAttributes(cell?.style),
51062
- ...removeFalsyAttributes(cfStyle),
51063
- };
51064
- const evaluatedCell = this.getters.getEvaluatedCell(position);
51065
- if (evaluatedCell.link && !computedStyle.textColor) {
51066
- computedStyle.textColor = LINK_COLOR;
51067
- }
51068
- return computedStyle;
51336
+ return this.getters.getEvaluatedCell(mainPosition).type === CellValueType.empty;
51069
51337
  }
51070
51338
  getColMaxWidth(sheetId, index) {
51071
51339
  const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
51072
51340
  const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
51073
- return Math.max(0, ...sizes);
51341
+ return Math.max(0, largeMax(sizes));
51074
51342
  }
51075
51343
  /**
51076
51344
  * Check that any "sheetId" in the command matches an existing
@@ -51099,6 +51367,236 @@ class SheetUIPlugin extends UIPlugin {
51099
51367
  }
51100
51368
  }
51101
51369
 
51370
+ class TableStylePlugin extends UIPlugin {
51371
+ static getters = ["getCellTableStyle", "getCellTableBorder"];
51372
+ tableStyles = {};
51373
+ handle(cmd) {
51374
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51375
+ (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51376
+ cmd.type === "EVALUATE_CELLS") {
51377
+ this.tableStyles = {};
51378
+ return;
51379
+ }
51380
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51381
+ delete this.tableStyles[cmd.sheetId];
51382
+ return;
51383
+ }
51384
+ }
51385
+ finalize() {
51386
+ for (const sheetId of this.getters.getSheetIds()) {
51387
+ if (!this.tableStyles[sheetId]) {
51388
+ this.tableStyles[sheetId] = {};
51389
+ }
51390
+ for (const table of this.getters.getTables(sheetId)) {
51391
+ if (!this.tableStyles[sheetId][table.id]) {
51392
+ this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51393
+ }
51394
+ }
51395
+ }
51396
+ }
51397
+ getCellTableStyle(position) {
51398
+ const table = this.getters.getTable(position);
51399
+ if (!table) {
51400
+ return undefined;
51401
+ }
51402
+ return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51403
+ }
51404
+ getCellTableBorder(position) {
51405
+ const table = this.getters.getTable(position);
51406
+ if (!table) {
51407
+ return undefined;
51408
+ }
51409
+ return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51410
+ }
51411
+ computeTableStyle(sheetId, table) {
51412
+ return lazy(() => {
51413
+ const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51414
+ const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51415
+ // Return the style with sheet coordinates instead of tables coordinates
51416
+ const mapping = this.getTableMapping(sheetId, table);
51417
+ const absoluteTableStyle = { borders: {}, styles: {} };
51418
+ for (let col = 0; col < numberOfCols; col++) {
51419
+ const colInSheet = mapping.colMapping[col];
51420
+ absoluteTableStyle.borders[colInSheet] = {};
51421
+ absoluteTableStyle.styles[colInSheet] = {};
51422
+ for (let row = 0; row < numberOfRows; row++) {
51423
+ const rowInSheet = mapping.rowMapping[row];
51424
+ absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51425
+ absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51426
+ }
51427
+ }
51428
+ return absoluteTableStyle;
51429
+ });
51430
+ }
51431
+ /**
51432
+ * Get the actual table config that will be used to compute the table style. It is different from
51433
+ * the config of the table because of hidden rows and columns in the sheet. For example remove the
51434
+ * hidden rows from config.numberOfHeaders.
51435
+ */
51436
+ getTableRuntimeConfig(sheetId, table) {
51437
+ const tableZone = table.range.zone;
51438
+ const config = { ...table.config };
51439
+ let numberOfCols = tableZone.right - tableZone.left + 1;
51440
+ let numberOfRows = tableZone.bottom - tableZone.top + 1;
51441
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51442
+ if (!this.getters.isRowHidden(sheetId, row)) {
51443
+ continue;
51444
+ }
51445
+ numberOfRows--;
51446
+ if (row - tableZone.top < table.config.numberOfHeaders) {
51447
+ config.numberOfHeaders--;
51448
+ if (config.numberOfHeaders < 0) {
51449
+ config.numberOfHeaders = 0;
51450
+ }
51451
+ }
51452
+ if (row === tableZone.bottom) {
51453
+ config.totalRow = false;
51454
+ }
51455
+ }
51456
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51457
+ if (!this.getters.isColHidden(sheetId, col)) {
51458
+ continue;
51459
+ }
51460
+ numberOfCols--;
51461
+ if (col === tableZone.left) {
51462
+ config.firstColumn = false;
51463
+ }
51464
+ if (col === tableZone.right) {
51465
+ config.lastColumn = false;
51466
+ }
51467
+ }
51468
+ return {
51469
+ config,
51470
+ numberOfCols,
51471
+ numberOfRows,
51472
+ };
51473
+ }
51474
+ /**
51475
+ * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51476
+ */
51477
+ getTableMapping(sheetId, table) {
51478
+ const colMapping = {};
51479
+ const rowMapping = {};
51480
+ let colOffset = 0;
51481
+ let rowOffset = 0;
51482
+ const tableZone = table.range.zone;
51483
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51484
+ if (this.getters.isColHidden(sheetId, col)) {
51485
+ continue;
51486
+ }
51487
+ colMapping[colOffset] = col;
51488
+ colOffset++;
51489
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51490
+ if (this.getters.isRowHidden(sheetId, row)) {
51491
+ continue;
51492
+ }
51493
+ rowMapping[rowOffset] = row;
51494
+ rowOffset++;
51495
+ }
51496
+ }
51497
+ return {
51498
+ colMapping,
51499
+ rowMapping,
51500
+ };
51501
+ }
51502
+ }
51503
+ const invalidateTableStyleCommands = [
51504
+ "HIDE_COLUMNS_ROWS",
51505
+ "UNHIDE_COLUMNS_ROWS",
51506
+ "UNFOLD_HEADER_GROUP",
51507
+ "FOLD_HEADER_GROUP",
51508
+ "FOLD_ALL_HEADER_GROUPS",
51509
+ "UNFOLD_ALL_HEADER_GROUPS",
51510
+ "CREATE_TABLE",
51511
+ "UPDATE_TABLE",
51512
+ "UPDATE_FILTER",
51513
+ ];
51514
+ const invalidateTableStyleCommandsSet = new Set(invalidateTableStyleCommands);
51515
+ function doesCommandInvalidatesTableStyle(cmd) {
51516
+ return invalidateTableStyleCommandsSet.has(cmd.type);
51517
+ }
51518
+
51519
+ class CellComputedStylePlugin extends UIPlugin {
51520
+ static getters = ["getCellComputedBorder", "getCellComputedStyle"];
51521
+ styles = {};
51522
+ borders = {};
51523
+ handle(cmd) {
51524
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51525
+ cmd.type === "UPDATE_CELL" ||
51526
+ cmd.type === "EVALUATE_CELLS") {
51527
+ this.styles = {};
51528
+ this.borders = {};
51529
+ return;
51530
+ }
51531
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51532
+ delete this.styles[cmd.sheetId];
51533
+ delete this.borders[cmd.sheetId];
51534
+ return;
51535
+ }
51536
+ if (invalidateCFEvaluationCommands.has(cmd.type)) {
51537
+ this.styles = {};
51538
+ return;
51539
+ }
51540
+ }
51541
+ getCellComputedBorder(position) {
51542
+ const { sheetId, row, col } = position;
51543
+ if (this.borders[sheetId]?.[row]?.[col] !== undefined) {
51544
+ return this.borders[sheetId][row][col];
51545
+ }
51546
+ if (!this.borders[sheetId]) {
51547
+ this.borders[sheetId] = {};
51548
+ }
51549
+ if (!this.borders[sheetId][row]) {
51550
+ this.borders[sheetId][row] = {};
51551
+ }
51552
+ if (!this.borders[sheetId][row][col]) {
51553
+ this.borders[sheetId][row][col] = this.computeCellBorder(position);
51554
+ }
51555
+ return this.borders[sheetId][row][col];
51556
+ }
51557
+ getCellComputedStyle(position) {
51558
+ const { sheetId, row, col } = position;
51559
+ if (this.styles[sheetId]?.[row]?.[col] !== undefined) {
51560
+ return this.styles[sheetId][row][col];
51561
+ }
51562
+ if (!this.styles[sheetId]) {
51563
+ this.styles[sheetId] = {};
51564
+ }
51565
+ if (!this.styles[sheetId][row]) {
51566
+ this.styles[sheetId][row] = {};
51567
+ }
51568
+ if (!this.styles[sheetId][row][col]) {
51569
+ this.styles[sheetId][row][col] = this.computeCellStyle(position);
51570
+ }
51571
+ return this.styles[sheetId][row][col];
51572
+ }
51573
+ computeCellBorder(position) {
51574
+ const cellBorder = this.getters.getCellBorder(position) || {};
51575
+ const cellTableBorder = this.getters.getCellTableBorder(position) || {};
51576
+ // Use removeFalsyAttributes to avoid overwriting borders with undefined values
51577
+ const border = {
51578
+ ...removeFalsyAttributes(cellTableBorder),
51579
+ ...removeFalsyAttributes(cellBorder),
51580
+ };
51581
+ return isObjectEmptyRecursive(border) ? null : border;
51582
+ }
51583
+ computeCellStyle(position) {
51584
+ const cell = this.getters.getCell(position);
51585
+ const cfStyle = this.getters.getCellConditionalFormatStyle(position);
51586
+ const tableStyle = this.getters.getCellTableStyle(position);
51587
+ const computedStyle = {
51588
+ ...removeFalsyAttributes(tableStyle),
51589
+ ...removeFalsyAttributes(cell?.style),
51590
+ ...removeFalsyAttributes(cfStyle),
51591
+ };
51592
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
51593
+ if (evaluatedCell.link && !computedStyle.textColor) {
51594
+ computedStyle.textColor = LINK_COLOR;
51595
+ }
51596
+ return computedStyle;
51597
+ }
51598
+ }
51599
+
51102
51600
  const genericRepeatsTransforms = [
51103
51601
  repeatSheetDependantCommand,
51104
51602
  repeatTargetDependantCommand,
@@ -51676,148 +52174,6 @@ class TableAutofillPlugin extends UIPlugin {
51676
52174
  }
51677
52175
  }
51678
52176
 
51679
- class TableStylePlugin extends UIPlugin {
51680
- static getters = ["getCellTableStyle", "getCellTableBorder"];
51681
- tableStyles = {};
51682
- handle(cmd) {
51683
- if (invalidateEvaluationCommands.has(cmd.type) ||
51684
- (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51685
- cmd.type === "EVALUATE_CELLS") {
51686
- this.tableStyles = {};
51687
- return;
51688
- }
51689
- switch (cmd.type) {
51690
- case "HIDE_COLUMNS_ROWS":
51691
- case "UNHIDE_COLUMNS_ROWS":
51692
- case "UNFOLD_HEADER_GROUP":
51693
- case "FOLD_HEADER_GROUP":
51694
- case "FOLD_ALL_HEADER_GROUPS":
51695
- case "UNFOLD_ALL_HEADER_GROUPS":
51696
- case "UPDATE_TABLE":
51697
- case "UPDATE_FILTER":
51698
- delete this.tableStyles[cmd.sheetId];
51699
- break;
51700
- }
51701
- }
51702
- finalize() {
51703
- for (const sheetId of this.getters.getSheetIds()) {
51704
- if (!this.tableStyles[sheetId]) {
51705
- this.tableStyles[sheetId] = {};
51706
- }
51707
- for (const table of this.getters.getTables(sheetId)) {
51708
- if (!this.tableStyles[sheetId][table.id]) {
51709
- this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51710
- }
51711
- }
51712
- }
51713
- }
51714
- getCellTableStyle(position) {
51715
- const table = this.getters.getTable(position);
51716
- if (!table) {
51717
- return undefined;
51718
- }
51719
- return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51720
- }
51721
- getCellTableBorder(position) {
51722
- const table = this.getters.getTable(position);
51723
- if (!table) {
51724
- return undefined;
51725
- }
51726
- return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51727
- }
51728
- computeTableStyle(sheetId, table) {
51729
- return lazy(() => {
51730
- const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51731
- const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51732
- // Return the style with sheet coordinates instead of tables coordinates
51733
- const mapping = this.getTableMapping(sheetId, table);
51734
- const absoluteTableStyle = { borders: {}, styles: {} };
51735
- for (let col = 0; col < numberOfCols; col++) {
51736
- const colInSheet = mapping.colMapping[col];
51737
- absoluteTableStyle.borders[colInSheet] = {};
51738
- absoluteTableStyle.styles[colInSheet] = {};
51739
- for (let row = 0; row < numberOfRows; row++) {
51740
- const rowInSheet = mapping.rowMapping[row];
51741
- absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51742
- absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51743
- }
51744
- }
51745
- return absoluteTableStyle;
51746
- });
51747
- }
51748
- /**
51749
- * Get the actual table config that will be used to compute the table style. It is different from
51750
- * the config of the table because of hidden rows and columns in the sheet. For example remove the
51751
- * hidden rows from config.numberOfHeaders.
51752
- */
51753
- getTableRuntimeConfig(sheetId, table) {
51754
- const tableZone = table.range.zone;
51755
- const config = { ...table.config };
51756
- let numberOfCols = tableZone.right - tableZone.left + 1;
51757
- let numberOfRows = tableZone.bottom - tableZone.top + 1;
51758
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51759
- if (!this.getters.isRowHidden(sheetId, row)) {
51760
- continue;
51761
- }
51762
- numberOfRows--;
51763
- if (row - tableZone.top < table.config.numberOfHeaders) {
51764
- config.numberOfHeaders--;
51765
- if (config.numberOfHeaders < 0) {
51766
- config.numberOfHeaders = 0;
51767
- }
51768
- }
51769
- if (row === tableZone.bottom) {
51770
- config.totalRow = false;
51771
- }
51772
- }
51773
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51774
- if (!this.getters.isColHidden(sheetId, col)) {
51775
- continue;
51776
- }
51777
- numberOfCols--;
51778
- if (col === tableZone.left) {
51779
- config.firstColumn = false;
51780
- }
51781
- if (col === tableZone.right) {
51782
- config.lastColumn = false;
51783
- }
51784
- }
51785
- return {
51786
- config,
51787
- numberOfCols,
51788
- numberOfRows,
51789
- };
51790
- }
51791
- /**
51792
- * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51793
- */
51794
- getTableMapping(sheetId, table) {
51795
- const colMapping = {};
51796
- const rowMapping = {};
51797
- let colOffset = 0;
51798
- let rowOffset = 0;
51799
- const tableZone = table.range.zone;
51800
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51801
- if (this.getters.isColHidden(sheetId, col)) {
51802
- continue;
51803
- }
51804
- colMapping[colOffset] = col;
51805
- colOffset++;
51806
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51807
- if (this.getters.isRowHidden(sheetId, row)) {
51808
- continue;
51809
- }
51810
- rowMapping[rowOffset] = row;
51811
- rowOffset++;
51812
- }
51813
- }
51814
- return {
51815
- colMapping,
51816
- rowMapping,
51817
- };
51818
- }
51819
- }
51820
-
51821
52177
  /**
51822
52178
  * Clipboard Plugin
51823
52179
  *
@@ -52527,38 +52883,6 @@ class FilterEvaluationPlugin extends UIPlugin {
52527
52883
  }
52528
52884
  }
52529
52885
 
52530
- const selectionStatisticFunctions = [
52531
- {
52532
- name: _t("Sum"),
52533
- types: [CellValueType.number],
52534
- compute: (values, locale) => sum([[values]], locale),
52535
- },
52536
- {
52537
- name: _t("Avg"),
52538
- types: [CellValueType.number],
52539
- compute: (values, locale) => average([[values]], locale),
52540
- },
52541
- {
52542
- name: _t("Min"),
52543
- types: [CellValueType.number],
52544
- compute: (values, locale) => min([[values]], locale),
52545
- },
52546
- {
52547
- name: _t("Max"),
52548
- types: [CellValueType.number],
52549
- compute: (values, locale) => max([[values]], locale),
52550
- },
52551
- {
52552
- name: _t("Count"),
52553
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52554
- compute: (values) => countAny([[values]]),
52555
- },
52556
- {
52557
- name: _t("Count Numbers"),
52558
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52559
- compute: (values, locale) => countNumbers([[values]], locale),
52560
- },
52561
- ];
52562
52886
  /**
52563
52887
  * SelectionPlugin
52564
52888
  */
@@ -52574,8 +52898,6 @@ class GridSelectionPlugin extends UIPlugin {
52574
52898
  "getSelectedZones",
52575
52899
  "getSelectedZone",
52576
52900
  "getSelectedCells",
52577
- "getStatisticFnResults",
52578
- "getAggregate",
52579
52901
  "getSelectedFigureId",
52580
52902
  "getSelection",
52581
52903
  "getActivePosition",
@@ -52610,7 +52932,10 @@ class GridSelectionPlugin extends UIPlugin {
52610
52932
  switch (cmd.type) {
52611
52933
  case "ACTIVATE_SHEET":
52612
52934
  try {
52613
- this.getters.getSheet(cmd.sheetIdTo);
52935
+ const sheet = this.getters.getSheet(cmd.sheetIdTo);
52936
+ if (!sheet.isVisible) {
52937
+ return "SheetIsHidden" /* CommandResult.SheetIsHidden */;
52938
+ }
52614
52939
  break;
52615
52940
  }
52616
52941
  catch (error) {
@@ -52859,52 +53184,6 @@ class GridSelectionPlugin extends UIPlugin {
52859
53184
  : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });
52860
53185
  }
52861
53186
  }
52862
- getStatisticFnResults() {
52863
- const sheetId = this.getters.getActiveSheetId();
52864
- const cells = new Set();
52865
- for (const zone of this.gridSelection.zones) {
52866
- for (const { col, row } of positions(zone)) {
52867
- if (this.getters.isRowHidden(sheetId, row) || this.getters.isColHidden(sheetId, col)) {
52868
- continue; // Skip hidden cells
52869
- }
52870
- const evaluatedCell = this.getters.getEvaluatedCell({ sheetId, col, row });
52871
- if (evaluatedCell.type !== CellValueType.empty) {
52872
- cells.add(evaluatedCell);
52873
- }
52874
- }
52875
- }
52876
- const locale = this.getters.getLocale();
52877
- let statisticFnResults = {};
52878
- for (let fn of selectionStatisticFunctions) {
52879
- // We don't want to display statistical information when there is no interest:
52880
- // We set the statistical result to undefined if the data handled by the selection
52881
- // does not match the data handled by the function.
52882
- // Ex: if there are only texts in the selection, we prefer that the SUM result
52883
- // be displayed as undefined rather than 0.
52884
- let fnResult = undefined;
52885
- const evaluatedCells = [...cells].filter((c) => fn.types.includes(c.type));
52886
- if (evaluatedCells.length) {
52887
- fnResult = fn.compute(evaluatedCells, locale);
52888
- }
52889
- statisticFnResults[fn.name] = fnResult;
52890
- }
52891
- return statisticFnResults;
52892
- }
52893
- getAggregate() {
52894
- let aggregate = 0;
52895
- let n = 0;
52896
- const sheetId = this.getters.getActiveSheetId();
52897
- const cellPositions = this.gridSelection.zones.map(positions).flat();
52898
- for (const { col, row } of cellPositions) {
52899
- const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
52900
- if (cell.type === CellValueType.number) {
52901
- n++;
52902
- aggregate += cell.value;
52903
- }
52904
- }
52905
- const locale = this.getters.getLocale();
52906
- return n < 2 ? null : formatValue(aggregate, { locale });
52907
- }
52908
53187
  isSelected(zone) {
52909
53188
  return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));
52910
53189
  }
@@ -52946,9 +53225,6 @@ class GridSelectionPlugin extends UIPlugin {
52946
53225
  // Other
52947
53226
  // ---------------------------------------------------------------------------
52948
53227
  activateSheet(sheetIdFrom, sheetIdTo) {
52949
- if (!this.getters.isSheetVisible(sheetIdTo)) {
52950
- this.dispatch("SHOW_SHEET", { sheetId: sheetIdTo });
52951
- }
52952
53228
  this.setActiveSheet(sheetIdTo);
52953
53229
  this.sheetsData[sheetIdFrom] = {
52954
53230
  gridSelection: deepCopy(this.gridSelection),
@@ -53955,7 +54231,7 @@ class SheetViewPlugin extends UIPlugin {
53955
54231
  * column of the current viewport
53956
54232
  */
53957
54233
  getColDimensionsInViewport(sheetId, col) {
53958
- const left = Math.min(...this.getters.getSheetViewVisibleCols());
54234
+ const left = largeMin(this.getters.getSheetViewVisibleCols());
53959
54235
  const start = this.getters.getColRowOffsetInViewport("COL", left, col);
53960
54236
  const size = this.getters.getColSize(sheetId, col);
53961
54237
  const isColHidden = this.getters.isColHidden(sheetId, col);
@@ -53970,7 +54246,7 @@ class SheetViewPlugin extends UIPlugin {
53970
54246
  * of the current viewport
53971
54247
  */
53972
54248
  getRowDimensionsInViewport(sheetId, row) {
53973
- const top = Math.min(...this.getters.getSheetViewVisibleRows());
54249
+ const top = largeMin(this.getters.getSheetViewVisibleRows());
53974
54250
  const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
53975
54251
  const size = this.getters.getRowSize(sheetId, row);
53976
54252
  const isRowHidden = this.getters.isRowHidden(sheetId, row);
@@ -54322,6 +54598,7 @@ const statefulUIPluginRegistry = new Registry()
54322
54598
  .add("evaluation_filter", FilterEvaluationPlugin)
54323
54599
  .add("header_visibility_ui", HeaderVisibilityUIPlugin)
54324
54600
  .add("table_style", TableStylePlugin)
54601
+ .add("cell_computed_style", CellComputedStylePlugin)
54325
54602
  .add("header_positions", HeaderPositionsUIPlugin)
54326
54603
  .add("viewport", SheetViewPlugin)
54327
54604
  .add("clipboard", ClipboardPlugin);
@@ -54382,6 +54659,38 @@ class ImageProvider {
54382
54659
  }
54383
54660
  }
54384
54661
 
54662
+ class ArrayFormulaHighlight extends SpreadsheetStore {
54663
+ highlightStore = this.get(HighlightStore);
54664
+ constructor(get) {
54665
+ super(get);
54666
+ this.highlightStore.register(this);
54667
+ }
54668
+ get highlights() {
54669
+ const zone = this.getHighlightZone();
54670
+ if (!zone) {
54671
+ return [];
54672
+ }
54673
+ const sheetId = this.model.getters.getActiveSheetId();
54674
+ return [
54675
+ {
54676
+ sheetId,
54677
+ zone,
54678
+ color: "#17A2B8",
54679
+ noFill: true,
54680
+ thinLine: true,
54681
+ },
54682
+ ];
54683
+ }
54684
+ getHighlightZone() {
54685
+ const position = this.model.getters.getActivePosition();
54686
+ const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
54687
+ const spreadZone = spreader
54688
+ ? this.model.getters.getSpreadZone(spreader)
54689
+ : this.model.getters.getSpreadZone(position);
54690
+ return spreadZone;
54691
+ }
54692
+ }
54693
+
54385
54694
  const RIPPLE_KEY_FRAMES = [
54386
54695
  { transform: "scale(0)" },
54387
54696
  { transform: "scale(0.8)", offset: 0.33 },
@@ -54684,12 +54993,14 @@ class BottomBarSheet extends owl.Component {
54684
54993
  this.editionState = "initializing";
54685
54994
  }
54686
54995
  stopEdition() {
54687
- if (!this.state.isEditing)
54996
+ const input = this.sheetNameRef.el;
54997
+ if (!this.state.isEditing || !input)
54688
54998
  return;
54689
54999
  this.state.isEditing = false;
54690
55000
  this.editionState = "initializing";
54691
- this.sheetNameRef.el?.blur();
55001
+ input.blur();
54692
55002
  const inputValue = this.getInputContent() || "";
55003
+ input.innerText = inputValue;
54693
55004
  interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
54694
55005
  }
54695
55006
  cancelEdition() {
@@ -54733,6 +55044,115 @@ class BottomBarSheet extends owl.Component {
54733
55044
  }
54734
55045
  }
54735
55046
 
55047
+ const selectionStatisticFunctions = [
55048
+ {
55049
+ name: _t("Sum"),
55050
+ types: [CellValueType.number],
55051
+ compute: (values, locale) => sum([[values]], locale),
55052
+ },
55053
+ {
55054
+ name: _t("Avg"),
55055
+ types: [CellValueType.number],
55056
+ compute: (values, locale) => average([[values]], locale),
55057
+ },
55058
+ {
55059
+ name: _t("Min"),
55060
+ types: [CellValueType.number],
55061
+ compute: (values, locale) => min([[values]], locale),
55062
+ },
55063
+ {
55064
+ name: _t("Max"),
55065
+ types: [CellValueType.number],
55066
+ compute: (values, locale) => max([[values]], locale),
55067
+ },
55068
+ {
55069
+ name: _t("Count"),
55070
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55071
+ compute: (values) => countAny([[values]]),
55072
+ },
55073
+ {
55074
+ name: _t("Count Numbers"),
55075
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55076
+ compute: (values, locale) => countNumbers([[values]], locale),
55077
+ },
55078
+ ];
55079
+ class AggregateStatisticsStore extends SpreadsheetStore {
55080
+ statisticFnResults = this._computeStatisticFnResults();
55081
+ isDirty = false;
55082
+ constructor(get) {
55083
+ super(get);
55084
+ this.model.selection.observe(this, {
55085
+ handleEvent: this.handleEvent.bind(this),
55086
+ });
55087
+ }
55088
+ handle(cmd) {
55089
+ if (invalidateEvaluationCommands.has(cmd.type) ||
55090
+ (cmd.type === "UPDATE_CELL" && "content" in cmd)) {
55091
+ this.isDirty = true;
55092
+ }
55093
+ switch (cmd.type) {
55094
+ case "HIDE_COLUMNS_ROWS":
55095
+ case "UNHIDE_COLUMNS_ROWS":
55096
+ case "GROUP_HEADERS":
55097
+ case "UNGROUP_HEADERS":
55098
+ case "ACTIVATE_SHEET":
55099
+ case "ACTIVATE_NEXT_SHEET":
55100
+ case "ACTIVATE_PREVIOUS_SHEET":
55101
+ case "EVALUATE_CELLS":
55102
+ case "UNDO":
55103
+ case "REDO":
55104
+ this.isDirty = true;
55105
+ }
55106
+ }
55107
+ finalize() {
55108
+ if (this.isDirty) {
55109
+ this.isDirty = false;
55110
+ this.statisticFnResults = this._computeStatisticFnResults();
55111
+ }
55112
+ }
55113
+ handleEvent() {
55114
+ if (this.getters.isGridSelectionActive()) {
55115
+ this.statisticFnResults = this._computeStatisticFnResults();
55116
+ }
55117
+ }
55118
+ _computeStatisticFnResults() {
55119
+ const getters = this.getters;
55120
+ const sheetId = getters.getActiveSheetId();
55121
+ const cells = new Set();
55122
+ const zones = getters.getSelectedZones();
55123
+ for (const zone of zones) {
55124
+ for (let col = zone.left; col <= zone.right; col++) {
55125
+ for (let row = zone.top; row <= zone.bottom; row++) {
55126
+ if (getters.isRowHidden(sheetId, row) || getters.isColHidden(sheetId, col)) {
55127
+ continue; // Skip hidden cells
55128
+ }
55129
+ const evaluatedCell = getters.getEvaluatedCell({ sheetId, col, row });
55130
+ if (evaluatedCell.type !== CellValueType.empty) {
55131
+ cells.add(evaluatedCell);
55132
+ }
55133
+ }
55134
+ }
55135
+ }
55136
+ const locale = getters.getLocale();
55137
+ let statisticFnResults = {};
55138
+ const cellsArray = [...cells];
55139
+ for (let fn of selectionStatisticFunctions) {
55140
+ // We don't want to display statistical information when there is no interest:
55141
+ // We set the statistical result to undefined if the data handled by the selection
55142
+ // does not match the data handled by the function.
55143
+ // Ex: if there are only texts in the selection, we prefer that the SUM result
55144
+ // be displayed as undefined rather than 0.
55145
+ let fnResult = undefined;
55146
+ const evaluatedCells = cellsArray.filter((c) => fn.types.includes(c.type));
55147
+ if (evaluatedCells.length) {
55148
+ fnResult = fn.compute(evaluatedCells, locale);
55149
+ }
55150
+ statisticFnResults[fn.name] = fnResult;
55151
+ }
55152
+ return statisticFnResults;
55153
+ }
55154
+ }
55155
+
54736
55156
  // -----------------------------------------------------------------------------
54737
55157
  // SpreadSheet
54738
55158
  // -----------------------------------------------------------------------------
@@ -54748,40 +55168,38 @@ css /* scss */ `
54748
55168
  }
54749
55169
  `;
54750
55170
  class BottomBarStatistic extends owl.Component {
54751
- static template = "o-spreadsheet-BottomBarStatisic";
55171
+ static template = "o-spreadsheet-BottomBarStatistic";
54752
55172
  static props = {
54753
55173
  openContextMenu: Function,
54754
55174
  closeContextMenu: Function,
54755
55175
  };
54756
55176
  static components = { Ripple };
54757
55177
  selectedStatisticFn = "";
54758
- statisticFnResults = {};
55178
+ store;
54759
55179
  setup() {
54760
- this.statisticFnResults = this.env.model.getters.getStatisticFnResults();
55180
+ this.store = useStore(AggregateStatisticsStore);
54761
55181
  owl.onWillUpdateProps(() => {
54762
- const newStatisticFnResults = this.env.model.getters.getStatisticFnResults();
54763
- if (!deepEquals(newStatisticFnResults, this.statisticFnResults)) {
55182
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54764
55183
  this.props.closeContextMenu();
54765
55184
  }
54766
- this.statisticFnResults = newStatisticFnResults;
54767
55185
  });
54768
55186
  }
54769
55187
  getSelectedStatistic() {
54770
55188
  // don't display button if no function has a result
54771
- if (Object.values(this.statisticFnResults).every((result) => result === undefined)) {
55189
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54772
55190
  return undefined;
54773
55191
  }
54774
55192
  if (this.selectedStatisticFn === "") {
54775
- this.selectedStatisticFn = Object.keys(this.statisticFnResults)[0];
55193
+ this.selectedStatisticFn = Object.keys(this.store.statisticFnResults)[0];
54776
55194
  }
54777
- return this.getComposedFnName(this.selectedStatisticFn, this.statisticFnResults[this.selectedStatisticFn]);
55195
+ return this.getComposedFnName(this.selectedStatisticFn);
54778
55196
  }
54779
55197
  listSelectionStatistics(ev) {
54780
55198
  const registry = new MenuItemRegistry();
54781
55199
  let i = 0;
54782
- for (let [fnName, fnValue] of Object.entries(this.statisticFnResults)) {
55200
+ for (let [fnName] of Object.entries(this.store.statisticFnResults)) {
54783
55201
  registry.add(fnName, {
54784
- name: this.getComposedFnName(fnName, fnValue),
55202
+ name: () => this.getComposedFnName(fnName),
54785
55203
  sequence: i,
54786
55204
  isReadonlyAllowed: true,
54787
55205
  execute: () => {
@@ -54794,8 +55212,9 @@ class BottomBarStatistic extends owl.Component {
54794
55212
  const { top, left, width } = target.getBoundingClientRect();
54795
55213
  this.props.openContextMenu(left + width, top, registry);
54796
55214
  }
54797
- getComposedFnName(fnName, fnValue) {
55215
+ getComposedFnName(fnName) {
54798
55216
  const locale = this.env.model.getters.getLocale();
55217
+ const fnValue = this.store.statisticFnResults[fnName];
54799
55218
  return fnName + ": " + (fnValue !== undefined ? formatValue(fnValue, { locale }) : "__");
54800
55219
  }
54801
55220
  }
@@ -54904,10 +55323,14 @@ class BottomBar extends owl.Component {
54904
55323
  name: sheet.name,
54905
55324
  sequence: i,
54906
55325
  isReadonlyAllowed: true,
54907
- textColor: sheet.isVisible ? undefined : "grey",
55326
+ textColor: sheet.isVisible ? undefined : "#808080",
54908
55327
  execute: (env) => {
55328
+ if (!this.env.model.getters.isSheetVisible(sheetId)) {
55329
+ this.env.model.dispatch("SHOW_SHEET", { sheetId });
55330
+ }
54909
55331
  env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: from, sheetIdTo: sheetId });
54910
55332
  },
55333
+ isEnabled: (env) => (env.model.getters.isReadonly() ? sheet.isVisible : true),
54911
55334
  });
54912
55335
  i++;
54913
55336
  }
@@ -54980,7 +55403,7 @@ class BottomBar extends owl.Component {
54980
55403
  this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
54981
55404
  }
54982
55405
  onSheetMouseDown(sheetId, event) {
54983
- if (event.button !== 0)
55406
+ if (event.button !== 0 || this.env.model.getters.isReadonly())
54984
55407
  return;
54985
55408
  this.closeMenu();
54986
55409
  const visibleSheets = this.getVisibleSheets();
@@ -55016,7 +55439,7 @@ class BottomBar extends owl.Component {
55016
55439
  .map((sheetEl) => sheetEl.getBoundingClientRect())
55017
55440
  .map((rect) => ({
55018
55441
  x: rect.x,
55019
- width: rect.width - 1,
55442
+ width: rect.width - 1, // -1 to compensate negative margin
55020
55443
  y: rect.y,
55021
55444
  height: rect.height,
55022
55445
  }));
@@ -55243,7 +55666,7 @@ class RowGroup extends AbstractHeaderGroup {
55243
55666
  }
55244
55667
  return cssPropertiesToCss({
55245
55668
  top: `${groupBox.headerRect.height / 2}px`,
55246
- left: `calc(50% - 1px)`,
55669
+ left: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55247
55670
  width: `30%`,
55248
55671
  height: `calc(100% - ${groupBox.headerRect.height / 2}px)`,
55249
55672
  "border-left": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
@@ -55295,7 +55718,7 @@ class ColGroup extends AbstractHeaderGroup {
55295
55718
  return "";
55296
55719
  }
55297
55720
  return cssPropertiesToCss({
55298
- top: `calc(50% - 1px)`,
55721
+ top: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55299
55722
  left: `${groupBox.headerRect.width / 2}px`,
55300
55723
  width: `calc(100% - ${groupBox.headerRect.width / 2}px)`,
55301
55724
  height: `30%`,
@@ -56386,7 +56809,7 @@ css /* scss */ `
56386
56809
  }
56387
56810
  .o-disabled {
56388
56811
  opacity: 0.4;
56389
- pointer: default;
56812
+ cursor: default;
56390
56813
  pointer-events: none;
56391
56814
  }
56392
56815
 
@@ -56552,7 +56975,7 @@ css /* scss */ `
56552
56975
 
56553
56976
  .o-number-input {
56554
56977
  /* Remove number input arrows */
56555
- -moz-appearance: textfield;
56978
+ appearance: textfield;
56556
56979
  &::-webkit-outer-spin-button,
56557
56980
  &::-webkit-inner-spin-button {
56558
56981
  -webkit-appearance: none;
@@ -56595,6 +57018,7 @@ class Spreadsheet extends owl.Component {
56595
57018
  this.notificationStore = useStore(NotificationStore);
56596
57019
  this.composerFocusStore = useStore(ComposerFocusStore);
56597
57020
  this.sidePanel = useStore(SidePanelStore);
57021
+ useStore(ArrayFormulaHighlight);
56598
57022
  this.keyDownMapping = {
56599
57023
  "CTRL+H": () => this.sidePanel.toggle("FindAndReplace", {}),
56600
57024
  "CTRL+F": () => this.sidePanel.toggle("FindAndReplace", {}),
@@ -56697,7 +57121,7 @@ class Spreadsheet extends owl.Component {
56697
57121
  const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
56698
57122
  const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;
56699
57123
  return cssPropertiesToCss({
56700
- "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`,
57124
+ "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`, // +2: margins
56701
57125
  "grid-template-rows": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,
56702
57126
  });
56703
57127
  }
@@ -57429,14 +57853,6 @@ class SelectiveHistory {
57429
57853
  this.revertBefore(operationId);
57430
57854
  this.tree.drop(operationId);
57431
57855
  }
57432
- getRevertedExecution() {
57433
- const data = [];
57434
- const operations = this.tree.revertedExecution(this.HEAD_BRANCH);
57435
- for (const { operation } of operations) {
57436
- data.push(operation.data);
57437
- }
57438
- return data;
57439
- }
57440
57856
  /**
57441
57857
  * Revert the state as it was *before* the given operation was executed.
57442
57858
  */
@@ -58225,6 +58641,9 @@ function createChart(chart, chartSheetIndex, data) {
58225
58641
  case "bar":
58226
58642
  plot = addBarChart(chart.data);
58227
58643
  break;
58644
+ case "combo":
58645
+ plot = addComboChart(chart.data);
58646
+ break;
58228
58647
  case "line":
58229
58648
  plot = addLineChart(chart.data);
58230
58649
  break;
@@ -58387,6 +58806,79 @@ function addBarChart(chart) {
58387
58806
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58388
58807
  `;
58389
58808
  }
58809
+ function addComboChart(chart) {
58810
+ // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)
58811
+ // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB
58812
+ // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB
58813
+ //
58814
+ // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.
58815
+ // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage
58816
+ const colors = new ChartColors();
58817
+ const dataSetsNodes = [];
58818
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
58819
+ const color = toXlsxHexColor(colors.next());
58820
+ const dataShapeProperty = shapeProperty({
58821
+ backgroundColor: color,
58822
+ line: { color },
58823
+ });
58824
+ dataSetsNodes.push(dsIndex === "0"
58825
+ ? escapeXml /*xml*/ `
58826
+ <c:ser>
58827
+ <c:idx val="${dsIndex}"/>
58828
+ <c:order val="${dsIndex}"/>
58829
+ ${dataset.label ? escapeXml /*xml*/ `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
58830
+ ${dataShapeProperty}
58831
+ ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
58832
+ <c:val> <!-- x-coordinate values -->
58833
+ ${numberRef(dataset.range)}
58834
+ </c:val>
58835
+ </c:ser>
58836
+ `
58837
+ : escapeXml /*xml*/ `
58838
+ <c:ser>
58839
+ <c:idx val="${dsIndex}"/>
58840
+ <c:order val="${dsIndex}"/>
58841
+ <c:smooth val="0"/>
58842
+ <c:marker>
58843
+ <c:symbol val="circle" />
58844
+ <c:size val="5"/>
58845
+ </c:marker>
58846
+ ${dataset.label ? escapeXml `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
58847
+ ${dataShapeProperty}
58848
+ ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
58849
+ <c:val> <!-- x-coordinate values -->
58850
+ ${numberRef(dataset.range)}
58851
+ </c:val>
58852
+ </c:ser>
58853
+ `);
58854
+ }
58855
+ // Excel does not support this feature
58856
+ const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
58857
+ const overlap = chart.stacked ? 100 : -20;
58858
+ return escapeXml /*xml*/ `
58859
+ <c:barChart>
58860
+ <c:barDir val="col"/>
58861
+ <c:grouping val="clustered"/>
58862
+ <c:overlap val="${overlap}"/>
58863
+ <c:gapWidth val="70"/>
58864
+ <!-- each data marker in the series does not have a different color -->
58865
+ <c:varyColors val="0"/>
58866
+ ${dataSetsNodes[0]}
58867
+ <c:axId val="${catAxId}" />
58868
+ <c:axId val="${valAxId}" />
58869
+ </c:barChart>
58870
+ <c:lineChart>
58871
+ <c:grouping val="standard"/>
58872
+ <!-- each data marker in the series does not have a different color -->
58873
+ <c:varyColors val="0"/>
58874
+ ${joinXmlNodes(dataSetsNodes.slice(1))}
58875
+ <c:axId val="${catAxId}" />
58876
+ <c:axId val="${valAxId}" />
58877
+ </c:lineChart>
58878
+ ${addAx("b", "c:catAx", catAxId, valAxId, { fontColor: chart.fontColor })}
58879
+ ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58880
+ `;
58881
+ }
58390
58882
  function addLineChart(chart) {
58391
58883
  const colors = new ChartColors();
58392
58884
  const dataSetsNodes = [];
@@ -58434,7 +58926,7 @@ function addLineChart(chart) {
58434
58926
  }
58435
58927
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
58436
58928
  const colors = new ChartColors();
58437
- const maxLength = Math.max(...chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
58929
+ const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
58438
58930
  const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
58439
58931
  const dataSetsNodes = [];
58440
58932
  for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
@@ -59312,7 +59804,7 @@ function addTableColumns(table, sheetData) {
59312
59804
  const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
59313
59805
  const colName = sheetData.cells[colHeaderXc]?.content || `col${i}`;
59314
59806
  const colAttributes = [
59315
- ["id", i + 1],
59807
+ ["id", i + 1], // id cannot be 0
59316
59808
  ["name", colName],
59317
59809
  ];
59318
59810
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
@@ -59528,6 +60020,7 @@ function addSheetViews(sheet) {
59528
60020
  * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
59529
60021
  */
59530
60022
  function getXLSX(data) {
60023
+ data = fixLengthySheetNames(data);
59531
60024
  const files = [];
59532
60025
  const construct = getDefaultXLSXStructure();
59533
60026
  files.push(createWorkbook(data, construct));
@@ -59775,6 +60268,40 @@ function createRelRoot() {
59775
60268
  `;
59776
60269
  return createXMLFile(parseXML(xml), "_rels/.rels");
59777
60270
  }
60271
+ /**
60272
+ * Excel sheet names are maximum 31 characters while o-spreadsheet do not have this limit.
60273
+ * This method converts the sheet names to be within the 31 characters limit.
60274
+ * The cells/charts referencing this sheet will be updated accordingly.
60275
+ */
60276
+ function fixLengthySheetNames(data) {
60277
+ const nameMapping = {};
60278
+ const newNames = new Set();
60279
+ for (const sheet of data.sheets) {
60280
+ let newName = sheet.name.slice(0, 31);
60281
+ let i = 1;
60282
+ while (newNames.has(newName)) {
60283
+ newName = newName.slice(0, 31 - String(i).length) + i++;
60284
+ }
60285
+ newNames.add(newName);
60286
+ if (newName !== sheet.name) {
60287
+ nameMapping[sheet.name] = newName;
60288
+ sheet.name = newName;
60289
+ }
60290
+ }
60291
+ if (!Object.keys(nameMapping).length) {
60292
+ return data;
60293
+ }
60294
+ const sheetWithNewNames = Object.keys(nameMapping).sort((a, b) => b.length - a.length);
60295
+ let stringifiedData = JSON.stringify(data);
60296
+ for (const sheetName of sheetWithNewNames) {
60297
+ const regex = new RegExp(`'?${escapeRegExp(sheetName)}'?!`, "g");
60298
+ stringifiedData = stringifiedData.replaceAll(regex, (match) => {
60299
+ const newName = nameMapping[sheetName];
60300
+ return match.replace(sheetName, newName);
60301
+ });
60302
+ }
60303
+ return JSON.parse(stringifiedData);
60304
+ }
59778
60305
 
59779
60306
  var Status;
59780
60307
  (function (Status) {
@@ -60464,6 +60991,6 @@ exports.tokenColors = tokenColors;
60464
60991
  exports.tokenize = tokenize;
60465
60992
 
60466
60993
 
60467
- __info__.version = "17.3.0-alpha.1";
60468
- __info__.date = "2024-03-25T09:43:36.072Z";
60469
- __info__.hash = "4095c41";
60994
+ __info__.version = "17.3.0-alpha.2";
60995
+ __info__.date = "2024-04-05T14:01:07.060Z";
60996
+ __info__.hash = "8c5a229";