@odoo/o-spreadsheet 17.3.0-alpha.0 → 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.0
7
- * @date 2024-03-20T13:42:32.042Z
8
- * @hash 073e154
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';
@@ -31,7 +31,6 @@ const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;
31
31
  const LINK_COLOR = "#017E84";
32
32
  const FILTERS_COLOR = "#188038";
33
33
  const BACKGROUND_HEADER_FILTER_COLOR = "#E6F4EA";
34
- const BACKGROUND_HEADER_SELECTED_FILTER_COLOR = "#CEEAD6";
35
34
  const SEPARATOR_COLOR = "#E0E2E4";
36
35
  const ICONS_COLOR = "#4A4F59";
37
36
  const HEADER_GROUPING_BACKGROUND_COLOR = "#F5F5F5";
@@ -467,7 +466,7 @@ function getItemId(item, itemsDic) {
467
466
  }
468
467
  // Generate new Id if the item didn't exist in the dictionary
469
468
  const ids = Object.keys(itemsDic);
470
- 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)));
471
470
  itemsDic[maxId + 1] = item;
472
471
  return maxId + 1;
473
472
  }
@@ -485,7 +484,7 @@ function debounce(func, wait, immediate) {
485
484
  let timeout = undefined;
486
485
  const debounced = function () {
487
486
  const context = this;
488
- const args = arguments;
487
+ const args = Array.from(arguments);
489
488
  function later() {
490
489
  timeout = undefined;
491
490
  if (!immediate) {
@@ -687,6 +686,34 @@ function getSearchRegex(searchStr, searchOptions) {
687
686
  }
688
687
  return RegExp(searchValue, flags);
689
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
+ }
690
717
 
691
718
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
692
719
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
@@ -2089,6 +2116,7 @@ exports.CommandResult = void 0;
2089
2116
  CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
2090
2117
  CommandResult["NoChanges"] = "NoChanges";
2091
2118
  CommandResult["InvalidInputId"] = "InvalidInputId";
2119
+ CommandResult["SheetIsHidden"] = "SheetIsHidden";
2092
2120
  })(exports.CommandResult || (exports.CommandResult = {}));
2093
2121
 
2094
2122
  const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
@@ -4204,6 +4232,10 @@ function organizeZone(zone) {
4204
4232
  function positionToZone(position) {
4205
4233
  return { left: position.col, right: position.col, top: position.row, bottom: position.row };
4206
4234
  }
4235
+ /** Transform a zone into a zone with only its top-left position */
4236
+ function zoneToTopLeft(zone) {
4237
+ return { ...zone, right: zone.left, bottom: zone.top };
4238
+ }
4207
4239
  function isFullRow(zone) {
4208
4240
  return zone.right === undefined;
4209
4241
  }
@@ -4243,6 +4275,16 @@ function getZonesRows(zones) {
4243
4275
  }
4244
4276
  return set;
4245
4277
  }
4278
+ function unionPositionsToZone(positions) {
4279
+ const zone = { top: Infinity, left: Infinity, bottom: -Infinity, right: -Infinity };
4280
+ for (const { col, row } of positions) {
4281
+ zone.top = Math.min(zone.top, row);
4282
+ zone.left = Math.min(zone.left, col);
4283
+ zone.bottom = Math.max(zone.bottom, row);
4284
+ zone.right = Math.max(zone.right, col);
4285
+ }
4286
+ return zone;
4287
+ }
4246
4288
 
4247
4289
  class RangeImpl {
4248
4290
  getSheetSize;
@@ -4546,8 +4588,9 @@ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
4546
4588
  * Get the default height of the cell given its style.
4547
4589
  */
4548
4590
  function getDefaultCellHeight(ctx, cell, colSize) {
4549
- if (!cell || !cell.content)
4591
+ if (!cell || (!cell.isFormula && !cell.content)) {
4550
4592
  return DEFAULT_CELL_HEIGHT;
4593
+ }
4551
4594
  const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
4552
4595
  const numberOfLines = cell.isFormula
4553
4596
  ? 1
@@ -5572,7 +5615,7 @@ class CellClipboardHandler extends AbstractCellClipboardHandler {
5572
5615
  const spreader = this.getters.getArrayFormulaSpreadingOn(position);
5573
5616
  let cell = this.getters.getCell(position);
5574
5617
  const evaluatedCell = this.getters.getEvaluatedCell(position);
5575
- if (spreader) {
5618
+ if (spreader && !deepEquals(spreader, position)) {
5576
5619
  const isSpreaderCopied = rowsIndexes.includes(spreader.row) && columnsIndexes.includes(spreader.col);
5577
5620
  const content = isSpreaderCopied
5578
5621
  ? ""
@@ -6219,19 +6262,25 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
6219
6262
  tableCellsInRow.push({});
6220
6263
  continue;
6221
6264
  }
6265
+ const coreTable = this.getters.getCoreTable(position);
6266
+ const tableZone = coreTable?.range.zone;
6222
6267
  // Copy whole table
6223
- if (zones.some((z) => isZoneInside(table.range.zone, z))) {
6224
- copiedTablesIds.add(table.id);
6268
+ if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
6269
+ copiedTablesIds.add(coreTable.id);
6225
6270
  const values = [];
6226
- for (const col of range(table.range.zone.left, table.range.zone.right + 1)) {
6227
- values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: table.range.zone.top }));
6271
+ for (const col of range(tableZone.left, tableZone.right + 1)) {
6272
+ values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
6228
6273
  }
6229
6274
  tableCellsInRow.push({
6230
- table: { filtersValues: values, range: table.range, config: table.config },
6275
+ table: {
6276
+ range: coreTable.range,
6277
+ config: coreTable.config,
6278
+ type: coreTable.type,
6279
+ },
6231
6280
  });
6232
6281
  }
6233
6282
  // Copy only style of cell
6234
- else {
6283
+ else if (table) {
6235
6284
  tableCellsInRow.push({ style: this.getTableStyleToCopy(position) });
6236
6285
  }
6237
6286
  }
@@ -6302,7 +6351,7 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
6302
6351
  }
6303
6352
  pasteTableCell(sheetId, tableCell, position, options) {
6304
6353
  if (tableCell.table && !options?.pasteOption) {
6305
- const { range: tableRange, filtersValues } = tableCell.table;
6354
+ const { range: tableRange } = tableCell.table;
6306
6355
  const zoneDims = zoneToDimension(tableRange.zone);
6307
6356
  const newTableZone = {
6308
6357
  left: position.col,
@@ -6314,18 +6363,13 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
6314
6363
  sheetId: position.sheetId,
6315
6364
  ranges: [this.getters.getRangeDataFromZone(sheetId, newTableZone)],
6316
6365
  config: tableCell.table.config,
6366
+ tableType: tableCell.table.type,
6317
6367
  });
6318
- for (const i of range(0, filtersValues.length)) {
6319
- this.dispatch("UPDATE_FILTER", {
6320
- sheetId: position.sheetId,
6321
- col: newTableZone.left + i,
6322
- row: newTableZone.top,
6323
- hiddenValues: filtersValues[i],
6324
- });
6325
- }
6326
6368
  }
6327
6369
  // Do not paste table style if we're inside another table
6328
- if (!this.getters.getTable(position)) {
6370
+ // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
6371
+ // dynamic tables are not yet computed
6372
+ if (!this.getters.getCoreTable(position)) {
6329
6373
  if (tableCell.style?.style && options?.pasteOption !== "asValue") {
6330
6374
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
6331
6375
  }
@@ -6980,10 +7024,17 @@ urlRegistry.add("sheet_URL", {
6980
7024
  },
6981
7025
  open(url, env) {
6982
7026
  const sheetId = parseSheetUrl(url);
6983
- env.model.dispatch("ACTIVATE_SHEET", {
7027
+ const result = env.model.dispatch("ACTIVATE_SHEET", {
6984
7028
  sheetIdFrom: env.model.getters.getActiveSheetId(),
6985
7029
  sheetIdTo: sheetId,
6986
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
+ }
6987
7038
  },
6988
7039
  sequence: 0,
6989
7040
  });
@@ -7099,7 +7150,7 @@ function textCell(value, format, formattedValue) {
7099
7150
  }
7100
7151
  function numberCell(value, format, formattedValue) {
7101
7152
  return {
7102
- value: value || 0,
7153
+ value: value || 0, // necessary to avoid "-0" and NaN values,
7103
7154
  format,
7104
7155
  formattedValue,
7105
7156
  type: CellValueType.number,
@@ -7427,9 +7478,11 @@ const TableTerms = {
7427
7478
  bandedColumns: _t("Banded columns"),
7428
7479
  automaticAutofill: _t("Automatically autofill formulas"),
7429
7480
  totalRow: _t("Total row"),
7481
+ isDynamic: _t("Auto-adjust to formula result"),
7430
7482
  },
7431
7483
  Tooltips: {
7432
7484
  filterWithoutHeader: _t("Cannot have filters without a header row"),
7485
+ isDynamic: _t("For tables based on array formulas only"),
7433
7486
  },
7434
7487
  };
7435
7488
 
@@ -8005,6 +8058,9 @@ class DependencyContainer {
8005
8058
  instantiate(Store, ...args) {
8006
8059
  return this.factory.build(Store, ...args);
8007
8060
  }
8061
+ resetStores() {
8062
+ this.dependencies.clear();
8063
+ }
8008
8064
  }
8009
8065
  class StoreFactory {
8010
8066
  get;
@@ -9938,11 +9994,11 @@ autoCompleteProviders.add("dataValidation", {
9938
9994
  }
9939
9995
  else {
9940
9996
  const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
9941
- values = this.getters
9997
+ values = Array.from(new Set(this.getters
9942
9998
  .getRangeValues(range)
9943
9999
  .filter(isNotNull)
9944
10000
  .map((value) => value.toString())
9945
- .filter((val) => val !== "");
10001
+ .filter((val) => val !== "")));
9946
10002
  }
9947
10003
  return values.map((value) => ({ text: value }));
9948
10004
  },
@@ -19389,10 +19445,10 @@ function aggregateDataForLabels(labels, datasets) {
19389
19445
  }
19390
19446
  }
19391
19447
  return {
19392
- labels: Object.keys(labelMap),
19448
+ labels: Array.from(labelSet),
19393
19449
  dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
19394
19450
  ...dataset,
19395
- data: Object.values(labelMap).map((dataOfLabel) => dataOfLabel[indexOfDataset]),
19451
+ data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
19396
19452
  })),
19397
19453
  };
19398
19454
  }
@@ -19411,8 +19467,8 @@ function truncateLabel(label) {
19411
19467
  function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale }) {
19412
19468
  const options = {
19413
19469
  // https://www.chartjs.org/docs/latest/general/responsive.html
19414
- responsive: true,
19415
- 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
19416
19472
  layout: {
19417
19473
  padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
19418
19474
  },
@@ -19458,7 +19514,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale })
19458
19514
  labels: labels.map(truncateLabel),
19459
19515
  datasets: [],
19460
19516
  },
19461
- platform: undefined,
19517
+ platform: undefined, // This key is optional and will be set by chart.js
19462
19518
  plugins: [],
19463
19519
  };
19464
19520
  }
@@ -19735,7 +19791,7 @@ function getBarConfiguration(chart, labels, localeFormat) {
19735
19791
  },
19736
19792
  y: {
19737
19793
  position: chart.verticalAxisPosition,
19738
- beginAtZero: true,
19794
+ beginAtZero: true, // the origin of the y axis is always zero
19739
19795
  ticks: {
19740
19796
  color: fontColor,
19741
19797
  callback: (value) => {
@@ -19786,6 +19842,204 @@ function createBarChartRuntime(chart, getters) {
19786
19842
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
19787
19843
  }
19788
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
+
19789
20043
  function isDataRangeValid(definition) {
19790
20044
  return definition.dataRange && !rangeReference.test(definition.dataRange)
19791
20045
  ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
@@ -20124,7 +20378,7 @@ function getBestTimeUnitForScale(labels, format, locale) {
20124
20378
  return undefined;
20125
20379
  }
20126
20380
  const labelsTimestamps = labelDates.map((date) => date.getTime());
20127
- const period = Math.max(...labelsTimestamps) - Math.min(...labelsTimestamps);
20381
+ const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);
20128
20382
  const minUnit = getFormatMinDisplayUnit(format);
20129
20383
  if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
20130
20384
  return "second";
@@ -20256,7 +20510,7 @@ function getLineOrScatterConfiguration(chart, labels, localeFormat) {
20256
20510
  },
20257
20511
  y: {
20258
20512
  position: chart.verticalAxisPosition,
20259
- beginAtZero: true,
20513
+ beginAtZero: true, // the origin of the y axis is always zero
20260
20514
  ticks: {
20261
20515
  color: fontColor,
20262
20516
  callback: (value) => {
@@ -20342,7 +20596,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
20342
20596
  const dataset = {
20343
20597
  label,
20344
20598
  data,
20345
- tension: 0,
20599
+ tension: 0, // 0 -> render straight lines, which is much faster
20346
20600
  borderColor: color,
20347
20601
  backgroundColor,
20348
20602
  pointBackgroundColor: color,
@@ -20564,7 +20818,7 @@ class PieChart extends AbstractChart {
20564
20818
  ...this.getDefinition(),
20565
20819
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
20566
20820
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
20567
- verticalAxisPosition: "left",
20821
+ verticalAxisPosition: "left", //TODO ExcelChartDefinition should be adapted, but can be done later
20568
20822
  dataSets,
20569
20823
  labelRange,
20570
20824
  };
@@ -20612,7 +20866,7 @@ function getPieConfiguration(chart, labels, localeFormat) {
20612
20866
  }
20613
20867
  function getPieColors(colors, dataSetsValues) {
20614
20868
  const pieColors = [];
20615
- const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
20869
+ const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
20616
20870
  for (let i = 0; i <= maxLength; i++) {
20617
20871
  pieColors.push(colors.next());
20618
20872
  }
@@ -20781,7 +21035,7 @@ function createScatterChartRuntime(chart, getters) {
20781
21035
  configOptions.elements = {
20782
21036
  point: {
20783
21037
  radius: 3,
20784
- hoverRadius: 3,
21038
+ hoverRadius: 3, // chartJS seems bugged, the point starts with the "hoverRadius" value
20785
21039
  hitRadius: 8,
20786
21040
  },
20787
21041
  };
@@ -20821,6 +21075,16 @@ chartRegistry.add("bar", {
20821
21075
  name: _t("Bar"),
20822
21076
  sequence: 10,
20823
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
+ });
20824
21088
  chartRegistry.add("line", {
20825
21089
  match: (type) => type === "line",
20826
21090
  createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),
@@ -20874,6 +21138,7 @@ chartRegistry.add("scatter", {
20874
21138
  const chartComponentRegistry = new Registry();
20875
21139
  chartComponentRegistry.add("line", ChartJsComponent);
20876
21140
  chartComponentRegistry.add("bar", ChartJsComponent);
21141
+ chartComponentRegistry.add("combo", ChartJsComponent);
20877
21142
  chartComponentRegistry.add("pie", ChartJsComponent);
20878
21143
  chartComponentRegistry.add("gauge", GaugeChartComponent);
20879
21144
  chartComponentRegistry.add("scatter", ChartJsComponent);
@@ -22227,6 +22492,7 @@ class LinkDisplay extends owl.Component {
22227
22492
  }
22228
22493
  edit() {
22229
22494
  const { col, row } = this.props.cellPosition;
22495
+ this.env.model.selection.selectCell(col, row);
22230
22496
  this.cellPopovers.open({ col, row }, "LinkEditor");
22231
22497
  }
22232
22498
  unlink() {
@@ -23131,7 +23397,7 @@ const lightTemplateWithHeader = (colorSet) => ({
23131
23397
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23132
23398
  border: { bottom: { color: colorSet.highlight, style: "thin" } },
23133
23399
  },
23134
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23400
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23135
23401
  firstRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23136
23402
  secondRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23137
23403
  });
@@ -23149,7 +23415,7 @@ const lightTemplateAllBorders = (colorSet) => ({
23149
23415
  },
23150
23416
  },
23151
23417
  headerRow: { border: { bottom: { color: colorSet.highlight, style: "medium" } } },
23152
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23418
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23153
23419
  firstRowStripe: { style: { fillColor: colorSet.light } },
23154
23420
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23155
23421
  });
@@ -23168,7 +23434,7 @@ const mediumTemplateBandedBorders = (colorSet) => ({
23168
23434
  headerRow: {
23169
23435
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23170
23436
  },
23171
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23437
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23172
23438
  firstRowStripe: { style: { fillColor: colorSet.light } },
23173
23439
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23174
23440
  });
@@ -23204,7 +23470,7 @@ const mediumTemplateMinimalBorders = (colorSet) => ({
23204
23470
  bottom: { color: "#000000", style: "medium" },
23205
23471
  },
23206
23472
  },
23207
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23473
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23208
23474
  headerRow: {
23209
23475
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23210
23476
  border: { bottom: { color: "#000000", style: "medium" } },
@@ -23228,7 +23494,7 @@ const mediumTemplateAllBorders = (colorSet) => ({
23228
23494
  },
23229
23495
  style: { fillColor: colorSet.light },
23230
23496
  },
23231
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23497
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23232
23498
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23233
23499
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
23234
23500
  });
@@ -23259,7 +23525,7 @@ const darkTemplateNoBorders = (colorSet) => ({
23259
23525
  category: "dark",
23260
23526
  colorName: colorSet.name,
23261
23527
  wholeTable: { style: { fillColor: colorSet.light } },
23262
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23528
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23263
23529
  headerRow: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
23264
23530
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23265
23531
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
@@ -23343,13 +23609,20 @@ const TABLE_PRESETS = {
23343
23609
  * If a single cell is selected, expand the selection to non-empty adjacent cells to create a table.
23344
23610
  */
23345
23611
  function interactiveCreateTable(env, sheetId, tableConfig) {
23346
- const selection = env.model.getters.getSelectedZones();
23347
- if (selection.length === 1 && getZoneArea(selection[0]) === 1) {
23612
+ let target = env.model.getters.getSelectedZones();
23613
+ let isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
23614
+ if (target.length === 1 && !isDynamic && getZoneArea(target[0]) === 1) {
23348
23615
  env.model.selection.selectTableAroundSelection();
23616
+ target = env.model.getters.getSelectedZones();
23617
+ isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
23349
23618
  }
23350
- const target = env.model.getters.getSelectedZones();
23351
23619
  const ranges = target.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
23352
- const result = env.model.dispatch("CREATE_TABLE", { ranges, sheetId, config: tableConfig });
23620
+ const result = env.model.dispatch("CREATE_TABLE", {
23621
+ ranges,
23622
+ sheetId,
23623
+ config: tableConfig,
23624
+ tableType: isDynamic ? "dynamic" : "static",
23625
+ });
23353
23626
  if (result.isCancelledBecause("TableOverlap" /* CommandResult.TableOverlap */)) {
23354
23627
  env.raiseError(TableTerms.Errors.TableOverlap);
23355
23628
  }
@@ -23417,8 +23690,8 @@ const DELETE_CONTENT_ROWS_NAME = (env) => {
23417
23690
  let last;
23418
23691
  const activesRows = env.model.getters.getActiveRows();
23419
23692
  if (activesRows.size !== 0) {
23420
- first = Math.min(...activesRows);
23421
- last = Math.max(...activesRows);
23693
+ first = largeMin([...activesRows]);
23694
+ last = largeMax([...activesRows]);
23422
23695
  }
23423
23696
  else {
23424
23697
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23446,8 +23719,8 @@ const DELETE_CONTENT_COLUMNS_NAME = (env) => {
23446
23719
  let last;
23447
23720
  const activeCols = env.model.getters.getActiveCols();
23448
23721
  if (activeCols.size !== 0) {
23449
- first = Math.min(...activeCols);
23450
- last = Math.max(...activeCols);
23722
+ first = largeMin([...activeCols]);
23723
+ last = largeMax([...activeCols]);
23451
23724
  }
23452
23725
  else {
23453
23726
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23475,8 +23748,8 @@ const REMOVE_ROWS_NAME = (env) => {
23475
23748
  let last;
23476
23749
  const activesRows = env.model.getters.getActiveRows();
23477
23750
  if (activesRows.size !== 0) {
23478
- first = Math.min(...activesRows);
23479
- last = Math.max(...activesRows);
23751
+ first = largeMin([...activesRows]);
23752
+ last = largeMax([...activesRows]);
23480
23753
  }
23481
23754
  else {
23482
23755
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23517,8 +23790,8 @@ const REMOVE_COLUMNS_NAME = (env) => {
23517
23790
  let last;
23518
23791
  const activeCols = env.model.getters.getActiveCols();
23519
23792
  if (activeCols.size !== 0) {
23520
- first = Math.min(...activeCols);
23521
- last = Math.max(...activeCols);
23793
+ first = largeMin([...activeCols]);
23794
+ last = largeMax([...activeCols]);
23522
23795
  }
23523
23796
  else {
23524
23797
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23559,7 +23832,7 @@ const INSERT_ROWS_BEFORE_ACTION = (env) => {
23559
23832
  let row;
23560
23833
  let quantity;
23561
23834
  if (activeRows.size) {
23562
- row = Math.min(...activeRows);
23835
+ row = largeMin([...activeRows]);
23563
23836
  quantity = activeRows.size;
23564
23837
  }
23565
23838
  else {
@@ -23580,7 +23853,7 @@ const INSERT_ROWS_AFTER_ACTION = (env) => {
23580
23853
  let row;
23581
23854
  let quantity;
23582
23855
  if (activeRows.size) {
23583
- row = Math.max(...activeRows);
23856
+ row = largeMax([...activeRows]);
23584
23857
  quantity = activeRows.size;
23585
23858
  }
23586
23859
  else {
@@ -23601,7 +23874,7 @@ const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
23601
23874
  let column;
23602
23875
  let quantity;
23603
23876
  if (activeCols.size) {
23604
- column = Math.min(...activeCols);
23877
+ column = largeMin([...activeCols]);
23605
23878
  quantity = activeCols.size;
23606
23879
  }
23607
23880
  else {
@@ -23622,7 +23895,7 @@ const INSERT_COLUMNS_AFTER_ACTION = (env) => {
23622
23895
  let column;
23623
23896
  let quantity;
23624
23897
  if (activeCols.size) {
23625
- column = Math.max(...activeCols);
23898
+ column = largeMax([...activeCols]);
23626
23899
  quantity = activeCols.size;
23627
23900
  }
23628
23901
  else {
@@ -23797,9 +24070,13 @@ const INSERT_TABLE = (env) => {
23797
24070
  };
23798
24071
  const DELETE_SELECTED_TABLE = (env) => {
23799
24072
  const position = env.model.getters.getActivePosition();
24073
+ const table = env.model.getters.getTable(position);
24074
+ if (!table) {
24075
+ return;
24076
+ }
23800
24077
  env.model.dispatch("REMOVE_TABLE", {
23801
24078
  sheetId: position.sheetId,
23802
- target: [positionToZone(position)],
24079
+ target: [table.range.zone],
23803
24080
  });
23804
24081
  };
23805
24082
  //------------------------------------------------------------------------------
@@ -24215,14 +24492,19 @@ const categorieFunctionAll = {
24215
24492
  children: [allFunctionListMenuBuilder],
24216
24493
  };
24217
24494
  function allFunctionListMenuBuilder() {
24218
- const fnNames = functionRegistry.getKeys();
24495
+ const fnNames = functionRegistry.getKeys().filter((key) => !functionRegistry.get(key).hidden);
24219
24496
  return createFormulaFunctions(fnNames);
24220
24497
  }
24221
24498
  const categoriesFunctionListMenuBuilder = () => {
24222
24499
  const functions = functionRegistry.content;
24223
- const categories = [...new Set(functionRegistry.getAll().map((fn) => fn.category))].filter(isDefined$1);
24500
+ const categories = [
24501
+ ...new Set(functionRegistry
24502
+ .getAll()
24503
+ .filter((fn) => !fn.hidden)
24504
+ .map((fn) => fn.category)),
24505
+ ].filter(isDefined$1);
24224
24506
  return categories.sort().map((category, i) => {
24225
- const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category);
24507
+ const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category && !functions[key].hidden);
24226
24508
  return {
24227
24509
  name: category,
24228
24510
  children: createFormulaFunctions(functionsInCategory),
@@ -27219,6 +27501,22 @@ class BarChartDesignPanel extends LineBarPieDesignPanel {
27219
27501
  static template = "o-spreadsheet-BarChartDesignPanel";
27220
27502
  }
27221
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
+
27222
27520
  class GaugeChartConfigPanel extends owl.Component {
27223
27521
  static template = "o-spreadsheet-GaugeChartConfigPanel";
27224
27522
  static components = { ChartErrorSection, ChartDataSeries };
@@ -27576,6 +27874,10 @@ chartSidePanelComponentRegistry
27576
27874
  .add("bar", {
27577
27875
  configuration: BarConfigPanel,
27578
27876
  design: BarChartDesignPanel,
27877
+ })
27878
+ .add("combo", {
27879
+ configuration: ComboChartConfigPanel,
27880
+ design: ComboChartDesignPanel,
27579
27881
  })
27580
27882
  .add("pie", {
27581
27883
  configuration: LineBarPieConfigPanel,
@@ -28380,11 +28682,9 @@ css /* scss */ `
28380
28682
  }
28381
28683
  .o-cell-is-operator {
28382
28684
  margin-bottom: 5px;
28383
- width: 96%;
28384
28685
  }
28385
28686
  .o-cell-is-value {
28386
28687
  margin-bottom: 5px;
28387
- width: 96%;
28388
28688
  }
28389
28689
  .o-color-picker-widget .o-color-picker-button {
28390
28690
  pointer-events: all;
@@ -30218,7 +30518,11 @@ class SplitIntoColumnsPanel extends owl.Component {
30218
30518
  const composerStore = useStore(ComposerStore);
30219
30519
  // The feature makes no sense if we are editing a cell, because then the selection isn't active
30220
30520
  // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
30221
- owl.useEffect(this.props.onCloseSidePanel, () => [composerStore.editionMode]);
30521
+ owl.useEffect((editionMode) => {
30522
+ if (editionMode !== "inactive") {
30523
+ this.props.onCloseSidePanel();
30524
+ }
30525
+ }, () => [composerStore.editionMode]);
30222
30526
  owl.onMounted(() => {
30223
30527
  composerStore.stopEdition();
30224
30528
  });
@@ -30304,6 +30608,24 @@ function getTableContentZone(tableZone, tableConfig) {
30304
30608
  const contentZone = { ...tableZone, top: tableZone.top + numberOfHeaders };
30305
30609
  return contentZone.top <= contentZone.bottom ? contentZone : undefined;
30306
30610
  }
30611
+ function getTableTopLeft(table) {
30612
+ const range = table.range;
30613
+ return { row: range.zone.top, col: range.zone.left, sheetId: range.sheetId };
30614
+ }
30615
+ function createFilter(id, range, config, createRange) {
30616
+ const zone = range.zone;
30617
+ if (zone.left !== zone.right) {
30618
+ throw new Error("Can only define a filter on a single column");
30619
+ }
30620
+ const filteredZone = { ...zone, top: zone.top + config.numberOfHeaders };
30621
+ const filteredRange = createRange(range.sheetId, filteredZone);
30622
+ return {
30623
+ id,
30624
+ rangeWithHeaders: range,
30625
+ col: zone.left,
30626
+ filteredRange: filteredZone.top > filteredZone.bottom ? undefined : filteredRange,
30627
+ };
30628
+ }
30307
30629
  function getComputedTableStyle(tableConfig, numberOfCols, numberOfRows) {
30308
30630
  return {
30309
30631
  borders: getAllTableBorders(tableConfig, numberOfCols, numberOfRows),
@@ -30741,6 +31063,11 @@ css /* scss */ `
30741
31063
  color: #ffffff;
30742
31064
  background: #d94b4b;
30743
31065
  }
31066
+
31067
+ .o-info-icon {
31068
+ width: 14px;
31069
+ height: 14px;
31070
+ }
30744
31071
  }
30745
31072
  `;
30746
31073
  class TablePanel extends owl.Component {
@@ -30772,6 +31099,29 @@ class TablePanel extends owl.Component {
30772
31099
  const numberOfHeaders = hasHeaders ? 1 : 0;
30773
31100
  this.updateNumberOfHeaders(numberOfHeaders);
30774
31101
  }
31102
+ updateTableIsDynamic(isDynamic) {
31103
+ const newTableType = isDynamic ? "dynamic" : "forceStatic";
31104
+ if (newTableType === this.props.table.type) {
31105
+ return;
31106
+ }
31107
+ const uiTable = this.env.model.getters.getTable(getTableTopLeft(this.props.table));
31108
+ if (!uiTable) {
31109
+ return;
31110
+ }
31111
+ const sheetId = this.env.model.getters.getActiveSheetId();
31112
+ const result = this.env.model.dispatch("UPDATE_TABLE", {
31113
+ sheetId,
31114
+ zone: this.props.table.range.zone,
31115
+ newTableRange: uiTable.range.rangeData,
31116
+ tableType: newTableType,
31117
+ });
31118
+ const updatedTable = this.env.model.getters.getCoreTable(getTableTopLeft(this.props.table));
31119
+ if (result.isSuccessful && updatedTable) {
31120
+ const newTableRange = updatedTable.range;
31121
+ this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);
31122
+ this.state.tableZoneErrors = [];
31123
+ }
31124
+ }
30775
31125
  onChangeNumberOfHeaders(ev) {
30776
31126
  const input = ev.target;
30777
31127
  const numberOfHeaders = parseInt(input.value);
@@ -30795,35 +31145,59 @@ class TablePanel extends owl.Component {
30795
31145
  }
30796
31146
  const sheetId = this.env.model.getters.getActiveSheetId();
30797
31147
  this.state.tableXc = ranges[0];
31148
+ const newTableRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
30798
31149
  this.state.tableZoneErrors = this.env.model.canDispatch("UPDATE_TABLE", {
30799
31150
  sheetId,
30800
31151
  zone: this.props.table.range.zone,
30801
31152
  newTableRange: this.env.model.getters.getRangeDataFromXc(sheetId, this.state.tableXc),
31153
+ tableType: this.getNewTableType(newTableRange.zone),
30802
31154
  }).reasons;
30803
31155
  }
30804
31156
  onRangeConfirmed() {
30805
31157
  const sheetId = this.env.model.getters.getActiveSheetId();
30806
- const newRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
31158
+ let newRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
31159
+ if (getZoneArea(newRange.zone) === 1) {
31160
+ const extendedZone = this.env.model.getters.getContiguousZone(sheetId, newRange.zone);
31161
+ newRange = this.env.model.getters.getRangeFromZone(sheetId, extendedZone);
31162
+ }
30807
31163
  const result = this.env.model.dispatch("UPDATE_TABLE", {
30808
31164
  sheetId,
30809
31165
  zone: this.props.table.range.zone,
30810
31166
  newTableRange: newRange.rangeData,
31167
+ tableType: this.getNewTableType(newRange.zone),
30811
31168
  });
30812
- if (result.isSuccessful) {
30813
- const position = { col: newRange.zone.left, row: newRange.zone.top };
31169
+ const position = { sheetId, col: newRange.zone.left, row: newRange.zone.top };
31170
+ const updatedTable = this.env.model.getters.getCoreTable(position);
31171
+ if (result.isSuccessful && updatedTable) {
31172
+ const newTopLeft = getTableTopLeft(updatedTable);
30814
31173
  this.env.model.selection.selectZone({
30815
- zone: positionToZone(position),
30816
- cell: position,
31174
+ zone: positionToZone(newTopLeft),
31175
+ cell: newTopLeft,
30817
31176
  });
31177
+ const newTableRange = updatedTable.range;
31178
+ this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);
31179
+ }
31180
+ else {
31181
+ const oldTableRange = this.props.table.range;
31182
+ this.state.tableXc = this.env.model.getters.getRangeString(oldTableRange, sheetId);
30818
31183
  }
30819
31184
  this.state.tableZoneErrors = [];
30820
- this.state.tableXc = result.isSuccessful
30821
- ? this.state.tableXc
30822
- : this.env.model.getters.getRangeString(this.props.table.range, sheetId);
30823
31185
  }
30824
31186
  deleteTable() {
30825
31187
  const sheetId = this.env.model.getters.getActiveSheetId();
30826
- this.env.model.dispatch("REMOVE_TABLE", { sheetId, target: [this.props.table.range.zone] });
31188
+ this.env.model.dispatch("REMOVE_TABLE", {
31189
+ sheetId,
31190
+ target: [this.props.table.range.zone],
31191
+ });
31192
+ }
31193
+ getNewTableType(newTableZone) {
31194
+ if (this.props.table.type === "forceStatic") {
31195
+ return "forceStatic";
31196
+ }
31197
+ const sheetId = this.env.model.getters.getActiveSheetId();
31198
+ return this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [newTableZone])
31199
+ ? "dynamic"
31200
+ : "static";
30827
31201
  }
30828
31202
  get tableConfig() {
30829
31203
  return this.props.table.config;
@@ -30841,6 +31215,14 @@ class TablePanel extends owl.Component {
30841
31215
  get hasFilterCheckboxTooltip() {
30842
31216
  return this.canHaveFilters ? undefined : TableTerms.Tooltips.filterWithoutHeader;
30843
31217
  }
31218
+ get canBeDynamic() {
31219
+ const sheetId = this.env.model.getters.getActiveSheetId();
31220
+ return (this.props.table.type === "dynamic" ||
31221
+ this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [this.props.table.range.zone]));
31222
+ }
31223
+ get dynamicTableTooltip() {
31224
+ return TableTerms.Tooltips.isDynamic;
31225
+ }
30844
31226
  }
30845
31227
 
30846
31228
  const sidePanelRegistry = new Registry();
@@ -30903,11 +31285,8 @@ sidePanelRegistry.add("TableSidePanel", {
30903
31285
  if (!table) {
30904
31286
  return { isOpen: false };
30905
31287
  }
30906
- return {
30907
- isOpen: true,
30908
- props: { table },
30909
- key: table.id,
30910
- };
31288
+ const coreTable = getters.getCoreTable(getTableTopLeft(table));
31289
+ return { isOpen: true, props: { table: coreTable }, key: table.id };
30911
31290
  },
30912
31291
  });
30913
31292
 
@@ -31086,6 +31465,9 @@ class FigureComponent extends owl.Component {
31086
31465
  el?.focus({ preventScroll: true });
31087
31466
  }
31088
31467
  }, () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el]);
31468
+ owl.onWillUnmount(() => {
31469
+ this.props.onFigureDeleted();
31470
+ });
31089
31471
  }
31090
31472
  clickAnchor(dirX, dirY, ev) {
31091
31473
  this.props.onClickAnchor(dirX, dirY, ev);
@@ -31104,6 +31486,7 @@ class FigureComponent extends owl.Component {
31104
31486
  this.props.onFigureDeleted();
31105
31487
  ev.stopPropagation();
31106
31488
  ev.preventDefault();
31489
+ ev.stopPropagation();
31107
31490
  break;
31108
31491
  case "ArrowDown":
31109
31492
  case "ArrowLeft":
@@ -31124,6 +31507,7 @@ class FigureComponent extends owl.Component {
31124
31507
  });
31125
31508
  ev.stopPropagation();
31126
31509
  ev.preventDefault();
31510
+ ev.stopPropagation();
31127
31511
  break;
31128
31512
  }
31129
31513
  }
@@ -31791,6 +32175,7 @@ function compareContentToSpanElement(content, node) {
31791
32175
  // -----------------------------------------------------------------------------
31792
32176
  css /* scss */ `
31793
32177
  .o-formula-assistant {
32178
+ background: #ffffff;
31794
32179
  .o-formula-assistant-head {
31795
32180
  background-color: #f2f2f2;
31796
32181
  padding: 10px;
@@ -31846,6 +32231,9 @@ class FunctionDescriptionProvider extends owl.Component {
31846
32231
  this.assistantState.allowCellSelectionBehind = false;
31847
32232
  }, 2000);
31848
32233
  }
32234
+ get formulaArgSeparator() {
32235
+ return this.env.model.getters.getLocale().formulaArgSeparator + " ";
32236
+ }
31849
32237
  }
31850
32238
 
31851
32239
  const functions$2 = functionRegistry.content;
@@ -32005,6 +32393,12 @@ class Composer extends owl.Component {
32005
32393
  owl.useEffect(() => {
32006
32394
  this.processContent();
32007
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
+ });
32008
32402
  }
32009
32403
  // ---------------------------------------------------------------------------
32010
32404
  // Handlers
@@ -32743,8 +33137,7 @@ class FilterIconsOverlay extends owl.Component {
32743
33137
  };
32744
33138
  getFilterHeadersPositions() {
32745
33139
  const sheetId = this.env.model.getters.getActiveSheetId();
32746
- const headerPositions = this.env.model.getters.getFilterHeaders(sheetId);
32747
- return headerPositions.map((position) => ({ sheetId, ...position }));
33140
+ return this.env.model.getters.getFilterHeaders(sheetId);
32748
33141
  }
32749
33142
  }
32750
33143
 
@@ -33985,11 +34378,6 @@ css /* scss */ `
33985
34378
  height: 10000px;
33986
34379
  background-color: ${SELECTION_BORDER_COLOR};
33987
34380
  }
33988
- .o-unhide-buttons {
33989
- width: fit-content;
33990
- gap: 5px;
33991
- transform: translate(-50%, 0);
33992
- }
33993
34381
  .o-unhide:hover {
33994
34382
  z-index: ${ComponentsImportance.Grid + 1};
33995
34383
  background-color: lightgrey;
@@ -34151,10 +34539,6 @@ css /* scss */ `
34151
34539
  height: 1px;
34152
34540
  background-color: ${SELECTION_BORDER_COLOR};
34153
34541
  }
34154
- .o-unhide-buttons {
34155
- height: fit-content;
34156
- transform: translate(0, -50%);
34157
- }
34158
34542
  .o-unhide:hover {
34159
34543
  z-index: ${ComponentsImportance.Grid + 1};
34160
34544
  background-color: lightgrey;
@@ -34615,19 +34999,16 @@ class GridRenderer {
34615
34999
  for (let col = left; col <= right; col++) {
34616
35000
  const colZone = { left: col, right: col, top: 0, bottom: numberOfRows - 1 };
34617
35001
  const { x, width } = this.getters.getVisibleRect(colZone);
34618
- const colHasFilter = this.getters.doesZonesContainFilter(sheetId, [colZone]);
34619
35002
  const isColActive = activeCols.has(col);
34620
35003
  const isColSelected = selectedCols.has(col);
34621
35004
  if (isColActive) {
34622
- ctx.fillStyle = colHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;
35005
+ ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;
34623
35006
  }
34624
35007
  else if (isColSelected) {
34625
- ctx.fillStyle = colHasFilter
34626
- ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR
34627
- : BACKGROUND_HEADER_SELECTED_COLOR;
35008
+ ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;
34628
35009
  }
34629
35010
  else {
34630
- ctx.fillStyle = colHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;
35011
+ ctx.fillStyle = BACKGROUND_HEADER_COLOR;
34631
35012
  }
34632
35013
  ctx.fillRect(x, 0, width, HEADER_HEIGHT);
34633
35014
  }
@@ -34635,19 +35016,16 @@ class GridRenderer {
34635
35016
  for (let row = top; row <= bottom; row++) {
34636
35017
  const rowZone = { top: row, bottom: row, left: 0, right: numberOfCols - 1 };
34637
35018
  const { y, height } = this.getters.getVisibleRect(rowZone);
34638
- const rowHasFilter = this.getters.doesZonesContainFilter(sheetId, [rowZone]);
34639
35019
  const isRowActive = activeRows.has(row);
34640
35020
  const isRowSelected = selectedRows.has(row);
34641
35021
  if (isRowActive) {
34642
- ctx.fillStyle = rowHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;
35022
+ ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;
34643
35023
  }
34644
35024
  else if (isRowSelected) {
34645
- ctx.fillStyle = rowHasFilter
34646
- ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR
34647
- : BACKGROUND_HEADER_SELECTED_COLOR;
35025
+ ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;
34648
35026
  }
34649
35027
  else {
34650
- ctx.fillStyle = rowHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;
35028
+ ctx.fillStyle = BACKGROUND_HEADER_COLOR;
34651
35029
  }
34652
35030
  ctx.fillRect(0, y, HEADER_WIDTH, height);
34653
35031
  }
@@ -35368,7 +35746,7 @@ class VerticalScrollBar extends owl.Component {
35368
35746
  onScroll(offset) {
35369
35747
  const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();
35370
35748
  this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
35371
- offsetX: scrollX,
35749
+ offsetX: scrollX, // offsetX is the same
35372
35750
  offsetY: offset,
35373
35751
  });
35374
35752
  }
@@ -35656,8 +36034,8 @@ class Grid extends owl.Component {
35656
36034
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
35657
36035
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
35658
36036
  "Ctrl+Shift+V": () => PASTE_AS_VALUE_ACTION(this.env),
35659
- "Ctrl+Shift+<": () => this.clearFormatting(),
35660
- "Ctrl+<": () => this.clearFormatting(),
36037
+ "Ctrl+Shift+<": () => this.clearFormatting(), // for qwerty
36038
+ "Ctrl+<": () => this.clearFormatting(), // for azerty
35661
36039
  "Ctrl+Shift+ ": () => {
35662
36040
  this.env.model.selection.selectAll();
35663
36041
  },
@@ -36093,6 +36471,7 @@ const XLSX_CHART_TYPES = [
36093
36471
  "surfaceChart",
36094
36472
  "surface3DChart",
36095
36473
  "bubbleChart",
36474
+ "comboChart",
36096
36475
  ];
36097
36476
 
36098
36477
  /** In XLSX color format (no #) */
@@ -36424,10 +36803,10 @@ function convertCFCellIsOperator(xlsxCfOperator) {
36424
36803
  const CF_TYPE_CONVERSION_MAP = {
36425
36804
  aboveAverage: undefined,
36426
36805
  expression: undefined,
36427
- cellIs: undefined,
36428
- 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
36429
36808
  dataBar: undefined,
36430
- iconSet: undefined,
36809
+ iconSet: undefined, // exist but isn't an operator in o_spreadsheet
36431
36810
  top10: undefined,
36432
36811
  uniqueValues: undefined,
36433
36812
  duplicateValues: undefined,
@@ -36504,6 +36883,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36504
36883
  surfaceChart: undefined,
36505
36884
  surface3DChart: undefined,
36506
36885
  bubbleChart: undefined,
36886
+ comboChart: "combo",
36507
36887
  };
36508
36888
  /** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/
36509
36889
  const SUBTOTAL_FUNCTION_CONVERSION_MAP = {
@@ -36662,7 +37042,7 @@ const XLSX_INDEXED_COLORS = {
36662
37042
  61: "993366",
36663
37043
  62: "333399",
36664
37044
  63: "333333",
36665
- 64: "000000",
37045
+ 64: "000000", // system foreground
36666
37046
  65: "FFFFFF", // system background
36667
37047
  };
36668
37048
  const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
@@ -37854,7 +38234,7 @@ function convertHyperlink(link, cellValue, warningManager) {
37854
38234
  function getSheetDims(sheet) {
37855
38235
  const dims = [0, 0];
37856
38236
  for (let row of sheet.rows) {
37857
- 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)));
37858
38238
  dims[1] = Math.max(dims[1], row.index);
37859
38239
  }
37860
38240
  dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
@@ -38720,6 +39100,9 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38720
39100
  if (!CHART_TYPE_CONVERSION_MAP[chartType]) {
38721
39101
  throw new Error(`Unsupported chart type ${chartType}`);
38722
39102
  }
39103
+ if (CHART_TYPE_CONVERSION_MAP[chartType] === "combo") {
39104
+ return this.extractComboChart(rootChartElement);
39105
+ }
38723
39106
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
38724
39107
  const chartTitle = this.mapOnElements({ parent: rootChartElement, query: "c:title a:t" }, (textElement) => {
38725
39108
  return textElement.textContent || "";
@@ -38748,6 +39131,37 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38748
39131
  };
38749
39132
  })[0];
38750
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
+ }
38751
39165
  extractChartDatasets(chartElement) {
38752
39166
  return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
38753
39167
  return {
@@ -38765,12 +39179,21 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38765
39179
  if (!plotAreaElement) {
38766
39180
  throw new Error("Missing plot area in the chart definition.");
38767
39181
  }
39182
+ let globalTag = undefined;
38768
39183
  for (let child of plotAreaElement.children) {
38769
39184
  const tag = removeTagEscapedNamespaces(child.tagName);
38770
39185
  if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
38771
- return tag;
39186
+ if (!globalTag) {
39187
+ globalTag = tag;
39188
+ }
39189
+ else if (globalTag !== tag) {
39190
+ globalTag = "comboChart";
39191
+ }
38772
39192
  }
38773
39193
  }
39194
+ if (globalTag) {
39195
+ return globalTag;
39196
+ }
38774
39197
  throw new Error("Unknown chart type");
38775
39198
  }
38776
39199
  }
@@ -42361,6 +42784,9 @@ class DataValidationPlugin extends CorePlugin {
42361
42784
  if (newRule.criterion.type === "isBoolean") {
42362
42785
  this.setCenterStyleToBooleanCells(newRule);
42363
42786
  }
42787
+ else if (newRule.criterion.type === "isValueInList") {
42788
+ newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
42789
+ }
42364
42790
  const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
42365
42791
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
42366
42792
  if (ruleIndex !== -1) {
@@ -42757,7 +43183,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
42757
43183
  if (hiddenElements.size >= elements) {
42758
43184
  return "TooManyHiddenElements" /* CommandResult.TooManyHiddenElements */;
42759
43185
  }
42760
- else if (Math.min(...cmd.elements) < 0 || Math.max(...cmd.elements) > elements) {
43186
+ else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
42761
43187
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
42762
43188
  }
42763
43189
  else {
@@ -43086,7 +43512,6 @@ class MergePlugin extends CorePlugin {
43086
43512
  "isInSameMerge",
43087
43513
  "isMergeHidden",
43088
43514
  "getMainCellPosition",
43089
- "getBottomLeftCell",
43090
43515
  "expandZone",
43091
43516
  "doesIntersectMerge",
43092
43517
  "doesColumnsHaveCommonMerges",
@@ -43268,13 +43693,6 @@ class MergePlugin extends CorePlugin {
43268
43693
  const mergeTopLeftPos = this.getMerge(position).topLeft;
43269
43694
  return { sheetId: position.sheetId, col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };
43270
43695
  }
43271
- getBottomLeftCell(position) {
43272
- if (!this.isInMerge(position)) {
43273
- return position;
43274
- }
43275
- const { bottom, left } = this.getMerge(position);
43276
- return { sheetId: position.sheetId, col: left, row: bottom };
43277
- }
43278
43696
  isMergeHidden(sheetId, merge) {
43279
43697
  const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);
43280
43698
  const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);
@@ -43575,8 +43993,8 @@ class RangeAdapter {
43575
43993
  let newRange = range;
43576
43994
  let changeType = "NONE";
43577
43995
  for (let group of groups) {
43578
- const min = Math.min(...group);
43579
- const max = Math.max(...group);
43996
+ const min = largeMin(group);
43997
+ const max = largeMax(group);
43580
43998
  if (range.zone[start] <= min && min <= range.zone[end]) {
43581
43999
  const toRemove = Math.min(range.zone[end], max) - min + 1;
43582
44000
  changeType = "RESIZE";
@@ -43958,7 +44376,6 @@ class SheetPlugin extends CorePlugin {
43958
44376
  "getSheetIds",
43959
44377
  "getVisibleSheetIds",
43960
44378
  "isSheetVisible",
43961
- "getEvaluationSheets",
43962
44379
  "doesHeaderExist",
43963
44380
  "doesHeadersExist",
43964
44381
  "getCell",
@@ -44025,8 +44442,8 @@ class SheetPlugin extends CorePlugin {
44025
44442
  }
44026
44443
  return "Success" /* CommandResult.Success */;
44027
44444
  case "REMOVE_COLUMNS_ROWS": {
44028
- const min = Math.min(...cmd.elements);
44029
- const max = Math.max(...cmd.elements);
44445
+ const min = largeMin(cmd.elements);
44446
+ const max = largeMax(cmd.elements);
44030
44447
  if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
44031
44448
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
44032
44449
  }
@@ -44217,9 +44634,6 @@ class SheetPlugin extends CorePlugin {
44217
44634
  getVisibleSheetIds() {
44218
44635
  return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));
44219
44636
  }
44220
- getEvaluationSheets() {
44221
- return this.sheets;
44222
- }
44223
44637
  doesHeaderExist(sheetId, dimension, index) {
44224
44638
  return dimension === "COL"
44225
44639
  ? index >= 0 && index < this.getNumberCols(sheetId)
@@ -44228,13 +44642,6 @@ class SheetPlugin extends CorePlugin {
44228
44642
  doesHeadersExist(sheetId, dimension, headerIndexes) {
44229
44643
  return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));
44230
44644
  }
44231
- getRow(sheetId, index) {
44232
- const row = this.getSheet(sheetId).rows[index];
44233
- if (!row) {
44234
- throw new Error(`Row ${row} not found.`);
44235
- }
44236
- return row;
44237
- }
44238
44645
  getCell({ sheetId, col, row }) {
44239
44646
  const sheet = this.tryGetSheet(sheetId);
44240
44647
  const cellId = sheet?.rows[row]?.cells[col];
@@ -44819,23 +45226,12 @@ class SheetPlugin extends CorePlugin {
44819
45226
  }
44820
45227
 
44821
45228
  class TablePlugin extends CorePlugin {
44822
- static getters = [
44823
- "doesZonesContainFilter",
44824
- "getFilter",
44825
- "getFilters",
44826
- "getTable",
44827
- "getTables",
44828
- "getTablesInZone",
44829
- "getTablesOverlappingZones",
44830
- "getFilterId",
44831
- "getFilterHeaders",
44832
- "isFilterHeader",
44833
- ];
45229
+ static getters = ["getCoreTable", "getCoreTables"];
44834
45230
  tables = {};
44835
45231
  adaptRanges(applyChange, sheetId) {
44836
45232
  const sheetIds = sheetId ? [sheetId] : this.getters.getSheetIds();
44837
45233
  for (const sheetId of sheetIds) {
44838
- for (const table of this.getTables(sheetId)) {
45234
+ for (const table of this.getCoreTables(sheetId)) {
44839
45235
  this.applyRangeChangeOnTable(sheetId, table, applyChange);
44840
45236
  }
44841
45237
  }
@@ -44851,15 +45247,16 @@ class TablePlugin extends CorePlugin {
44851
45247
  ? "TableOverlap" /* CommandResult.TableOverlap */
44852
45248
  : "Success" /* CommandResult.Success */, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));
44853
45249
  case "UPDATE_TABLE":
44854
- const updatedTable = this.getTables(cmd.sheetId).find((table) => deepEquals(table.range.zone, cmd.zone));
45250
+ const updatedTable = this.getTableFromZone(cmd.sheetId, cmd.zone);
44855
45251
  if (!updatedTable) {
44856
45252
  return "TableNotFound" /* CommandResult.TableNotFound */;
44857
45253
  }
44858
45254
  return this.checkValidations(cmd, this.checkUpdatedTableZoneIsValid, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));
44859
45255
  case "ADD_MERGE":
44860
- for (const merge of cmd.target) {
44861
- for (const table of this.getTables(cmd.sheetId)) {
44862
- if (overlap(table.range.zone, merge)) {
45256
+ for (const table of this.getCoreTables(cmd.sheetId)) {
45257
+ const tableZone = table.range.zone;
45258
+ for (const merge of cmd.target) {
45259
+ if (overlap(tableZone, merge)) {
44863
45260
  return "MergeInTable" /* CommandResult.MergeInTable */;
44864
45261
  }
44865
45262
  }
@@ -44881,8 +45278,11 @@ class TablePlugin extends CorePlugin {
44881
45278
  }
44882
45279
  case "DUPLICATE_SHEET": {
44883
45280
  const newTables = {};
44884
- for (const table of this.getTables(cmd.sheetId)) {
44885
- newTables[table.id] = this.copyTableForSheet(cmd.sheetIdTo, table);
45281
+ for (const table of this.getCoreTables(cmd.sheetId)) {
45282
+ newTables[table.id] =
45283
+ table.type === "dynamic"
45284
+ ? this.copyDynamicTableForSheet(cmd.sheetIdTo, table)
45285
+ : this.copyStaticTableForSheet(cmd.sheetIdTo, table);
44886
45286
  }
44887
45287
  this.history.update("tables", cmd.sheetIdTo, newTables);
44888
45288
  break;
@@ -44893,14 +45293,17 @@ class TablePlugin extends CorePlugin {
44893
45293
  const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
44894
45294
  this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
44895
45295
  const id = this.uuidGenerator.uuidv4();
44896
- const newTable = this.createTable(id, union, cmd.config || DEFAULT_TABLE_CONFIG);
45296
+ const config = cmd.config || DEFAULT_TABLE_CONFIG;
45297
+ const newTable = cmd.tableType === "dynamic"
45298
+ ? this.createDynamicTable(id, union, config)
45299
+ : this.createStaticTable(id, cmd.tableType, union, config);
44897
45300
  this.history.update("tables", cmd.sheetId, newTable.id, newTable);
44898
45301
  break;
44899
45302
  }
44900
45303
  case "REMOVE_TABLE": {
44901
45304
  const tables = {};
44902
- for (const table of this.getTables(cmd.sheetId)) {
44903
- if (cmd.target.every((zone) => !intersection(zone, table.range.zone))) {
45305
+ for (const table of this.getCoreTables(cmd.sheetId)) {
45306
+ if (cmd.target.every((zone) => !intersection(table.range.zone, zone))) {
44904
45307
  tables[table.id] = table;
44905
45308
  }
44906
45309
  }
@@ -44908,23 +45311,15 @@ class TablePlugin extends CorePlugin {
44908
45311
  break;
44909
45312
  }
44910
45313
  case "UPDATE_TABLE": {
44911
- const table = this.getTables(cmd.sheetId).find((table) => deepEquals(table.range.zone, cmd.zone));
44912
- if (table) {
44913
- const newTableRange = cmd.newTableRange
44914
- ? this.getters.getRangeFromRangeData(cmd.newTableRange)
44915
- : undefined;
44916
- if (newTableRange) {
44917
- const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, newTableRange.zone);
44918
- this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
44919
- }
44920
- const newTable = this.updateTable(table, newTableRange, cmd.config);
44921
- this.history.update("tables", cmd.sheetId, table.id, newTable);
44922
- }
45314
+ this.updateTable(cmd);
44923
45315
  break;
44924
45316
  }
44925
45317
  case "UPDATE_CELL": {
44926
45318
  const sheetId = cmd.sheetId;
44927
- for (const table of this.getTables(sheetId)) {
45319
+ for (const table of this.getCoreTables(sheetId)) {
45320
+ if (table.type === "dynamic") {
45321
+ continue;
45322
+ }
44928
45323
  const direction = this.canUpdateCellCmdExtendTable(cmd, table);
44929
45324
  if (direction === "down") {
44930
45325
  this.extendTableDown(sheetId, table);
@@ -44948,66 +45343,24 @@ class TablePlugin extends CorePlugin {
44948
45343
  }
44949
45344
  }
44950
45345
  }
44951
- getFilters(sheetId) {
44952
- return this.getTables(sheetId)
44953
- .filter((table) => table.config.hasFilters)
44954
- .map((table) => table.filters)
44955
- .flat();
44956
- }
44957
- getTables(sheetId) {
45346
+ getCoreTables(sheetId) {
44958
45347
  return this.tables[sheetId] ? Object.values(this.tables[sheetId]).filter(isDefined$1) : [];
44959
45348
  }
44960
- getFilter(position) {
44961
- const table = this.getTable(position);
44962
- if (!table || !table.config.hasFilters) {
44963
- return undefined;
44964
- }
44965
- return table.filters.find((filter) => filter.col === position.col);
44966
- }
44967
- getFilterId(position) {
44968
- return this.getFilter(position)?.id;
44969
- }
44970
- getTable({ sheetId, col, row }) {
44971
- return this.getTables(sheetId).find((table) => isInside(col, row, table.range.zone));
44972
- }
44973
- /** Get the filter tables that are fully inside the given zone */
44974
- getTablesInZone(sheetId, zone) {
44975
- return this.getTables(sheetId).filter((table) => isZoneInside(table.range.zone, zone));
45349
+ getCoreTable({ sheetId, col, row }) {
45350
+ return this.getCoreTables(sheetId).find((table) => isInside(col, row, table.range.zone));
44976
45351
  }
44977
45352
  getTablesOverlappingZones(sheetId, zones) {
44978
- return this.getTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
44979
- }
44980
- doesZonesContainFilter(sheetId, zones) {
44981
- return (this.getTablesOverlappingZones(sheetId, zones).filter((table) => table.config.hasFilters)
44982
- .length > 0);
44983
- }
44984
- getFilterHeaders(sheetId) {
44985
- const headers = [];
44986
- for (const table of this.getTables(sheetId)) {
44987
- if (!table.config.hasFilters) {
44988
- continue;
44989
- }
44990
- const zone = table.range.zone;
44991
- const row = zone.top;
44992
- for (let col = zone.left; col <= zone.right; col++) {
44993
- headers.push({ col, row });
44994
- }
44995
- }
44996
- return headers;
44997
- }
44998
- isFilterHeader({ sheetId, col, row }) {
44999
- const headers = this.getFilterHeaders(sheetId);
45000
- return headers.some((header) => header.col === col && header.row === row);
45353
+ return this.getCoreTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
45001
45354
  }
45002
45355
  /** Extend a table down one row */
45003
45356
  extendTableDown(sheetId, table) {
45004
45357
  const newRange = this.getters.extendRange(table.range, "ROW", 1);
45005
- this.history.update("tables", sheetId, table.id, this.updateTable(table, newRange));
45358
+ this.history.update("tables", sheetId, table.id, this.updateStaticTable(table, newRange));
45006
45359
  }
45007
45360
  /** Extend a table right one col */
45008
45361
  extendTableRight(sheetId, table) {
45009
45362
  const newRange = this.getters.extendRange(table.range, "COL", 1);
45010
- this.history.update("tables", sheetId, table.id, this.updateTable(table, newRange));
45363
+ this.history.update("tables", sheetId, table.id, this.updateStaticTable(table, newRange));
45011
45364
  }
45012
45365
  /**
45013
45366
  * Check if an UpdateCell command should cause the given table to be extended by one row or col.
@@ -45044,12 +45397,22 @@ class TablePlugin extends CorePlugin {
45044
45397
  const cellContent = this.getters.getCell(cellPosition)?.content;
45045
45398
  if (cellContent ||
45046
45399
  this.getters.isInMerge(cellPosition) ||
45047
- this.getters.getTable(cellPosition)) {
45400
+ this.getTablesOverlappingZones(sheetId, [positionToZone(position)]).length) {
45048
45401
  return "none";
45049
45402
  }
45050
45403
  }
45051
45404
  return direction;
45052
45405
  }
45406
+ getTableFromZone(sheetId, zone) {
45407
+ for (const table of this.getCoreTables(sheetId)) {
45408
+ const tableZone = table.range.zone;
45409
+ // Only check top left to match dynamic tables
45410
+ if (tableZone.left === zone.left && tableZone.top === zone.top) {
45411
+ return table;
45412
+ }
45413
+ }
45414
+ return undefined;
45415
+ }
45053
45416
  checkUpdatedTableZoneIsValid(cmd) {
45054
45417
  if (!cmd.newTableRange) {
45055
45418
  return "Success" /* CommandResult.Success */;
@@ -45059,7 +45422,11 @@ class TablePlugin extends CorePlugin {
45059
45422
  if (zoneIsInSheet !== "Success" /* CommandResult.Success */) {
45060
45423
  return zoneIsInSheet;
45061
45424
  }
45062
- const overlappingTables = this.getTablesOverlappingZones(cmd.sheetId, [newTableZone]).filter((table) => !deepEquals(table.range.zone, cmd.zone));
45425
+ const updatedTable = this.getTableFromZone(cmd.sheetId, cmd.zone);
45426
+ if (!updatedTable) {
45427
+ return "TableNotFound" /* CommandResult.TableNotFound */;
45428
+ }
45429
+ const overlappingTables = this.getTablesOverlappingZones(cmd.sheetId, [newTableZone]).filter((table) => table.id !== updatedTable.id);
45063
45430
  return overlappingTables.length ? "TableOverlap" /* CommandResult.TableOverlap */ : "Success" /* CommandResult.Success */;
45064
45431
  }
45065
45432
  checkTableConfigUpdateIsValid(config) {
@@ -45077,7 +45444,7 @@ class TablePlugin extends CorePlugin {
45077
45444
  }
45078
45445
  return "Success" /* CommandResult.Success */;
45079
45446
  }
45080
- createTable(id, tableRange, config, filters) {
45447
+ createStaticTable(id, type, tableRange, config, filters) {
45081
45448
  const zone = tableRange.zone;
45082
45449
  if (!filters) {
45083
45450
  filters = [];
@@ -45092,9 +45459,51 @@ class TablePlugin extends CorePlugin {
45092
45459
  range: tableRange,
45093
45460
  filters,
45094
45461
  config,
45462
+ type,
45095
45463
  };
45096
45464
  }
45097
- updateTable(table, newRange, configUpdate) {
45465
+ createDynamicTable(id, tableRange, config) {
45466
+ const zone = zoneToTopLeft(tableRange.zone);
45467
+ return {
45468
+ id,
45469
+ range: this.getters.getRangeFromZone(tableRange.sheetId, zone),
45470
+ config,
45471
+ type: "dynamic",
45472
+ };
45473
+ }
45474
+ updateTable(cmd) {
45475
+ const table = this.getTableFromZone(cmd.sheetId, cmd.zone);
45476
+ if (!table) {
45477
+ return;
45478
+ }
45479
+ const newTableRange = cmd.newTableRange
45480
+ ? this.getters.getRangeFromRangeData(cmd.newTableRange)
45481
+ : undefined;
45482
+ if (newTableRange) {
45483
+ const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, newTableRange.zone);
45484
+ this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
45485
+ }
45486
+ const range = newTableRange || table.range;
45487
+ const newConfig = this.updateTableConfig(cmd.config, table.config);
45488
+ const newTableType = cmd.tableType ?? table.type;
45489
+ if ((newTableType === "dynamic" && table.type !== "dynamic") ||
45490
+ (newTableType !== "dynamic" && table.type === "dynamic")) {
45491
+ const newTable = newTableType === "dynamic"
45492
+ ? this.createDynamicTable(table.id, range, newConfig)
45493
+ : this.createStaticTable(table.id, newTableType, range, newConfig);
45494
+ this.history.update("tables", cmd.sheetId, table.id, newTable);
45495
+ }
45496
+ else {
45497
+ const updatedTable = table.type === "dynamic"
45498
+ ? this.updateDynamicTable(table, range, newConfig)
45499
+ : this.updateStaticTable(table, range, newConfig, newTableType);
45500
+ this.history.update("tables", cmd.sheetId, table.id, updatedTable);
45501
+ }
45502
+ }
45503
+ updateStaticTable(table, newRange, configUpdate, newTableType = table.type) {
45504
+ if (newTableType === "dynamic") {
45505
+ throw new Error("Cannot use updateStaticTable to update a dynamic table");
45506
+ }
45098
45507
  const tableRange = newRange ? newRange : table.range;
45099
45508
  const tableZone = tableRange.zone;
45100
45509
  const newConfig = this.updateTableConfig(configUpdate, table.config);
@@ -45115,8 +45524,16 @@ class TablePlugin extends CorePlugin {
45115
45524
  range: tableRange,
45116
45525
  config,
45117
45526
  filters: filters.length ? filters : table.filters,
45527
+ type: newTableType,
45118
45528
  };
45119
45529
  }
45530
+ updateDynamicTable(table, newRange, newConfig) {
45531
+ const range = newRange
45532
+ ? this.getters.getRangeFromZone(newRange.sheetId, zoneToTopLeft(newRange.zone))
45533
+ : table.range;
45534
+ const config = newConfig ? newConfig : table.config;
45535
+ return { ...table, range, config };
45536
+ }
45120
45537
  /**
45121
45538
  * Update the old config of a table with the new partial config from an UpdateTable command.
45122
45539
  *
@@ -45138,35 +45555,29 @@ class TablePlugin extends CorePlugin {
45138
45555
  }
45139
45556
  createFilterFromZone(id, sheetId, zone, config) {
45140
45557
  const range = this.getters.getRangeFromZone(sheetId, zone);
45141
- return this.createFilter(id, range, config);
45142
- }
45143
- createFilter(id, range, config) {
45144
- const zone = range.zone;
45145
- if (zone.left !== zone.right) {
45146
- throw new Error("Can only define a filter on a single column");
45147
- }
45148
- const contentZone = getTableContentZone(zone, config);
45149
- const filteredRange = contentZone
45150
- ? this.getters.getRangeFromZone(range.sheetId, contentZone)
45151
- : undefined;
45152
- return {
45153
- id,
45154
- rangeWithHeaders: range,
45155
- col: zone.left,
45156
- filteredRange,
45157
- };
45558
+ return createFilter(id, range, config, this.getters.getRangeFromZone);
45158
45559
  }
45159
- copyTableForSheet(sheetId, table) {
45560
+ copyStaticTableForSheet(sheetId, table) {
45160
45561
  const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);
45161
45562
  const newFilters = table.filters.map((filter) => {
45162
45563
  const newFilterRange = this.getters.getRangeFromZone(sheetId, filter.rangeWithHeaders.zone);
45163
- return this.createFilter(filter.id, newFilterRange, table.config);
45564
+ return createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);
45164
45565
  });
45165
45566
  return {
45166
45567
  id: table.id,
45167
45568
  range: newRange,
45168
45569
  filters: newFilters,
45169
45570
  config: deepCopy(table.config),
45571
+ type: table.type,
45572
+ };
45573
+ }
45574
+ copyDynamicTableForSheet(sheetId, table) {
45575
+ const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);
45576
+ return {
45577
+ id: table.id,
45578
+ range: newRange,
45579
+ config: deepCopy(table.config),
45580
+ type: "dynamic",
45170
45581
  };
45171
45582
  }
45172
45583
  applyRangeChangeOnTable(sheetId, table, applyChange) {
@@ -45181,6 +45592,11 @@ class TablePlugin extends CorePlugin {
45181
45592
  default:
45182
45593
  newTableRange = tableRangeChange.range;
45183
45594
  }
45595
+ if (table.type === "dynamic") {
45596
+ const newTable = this.updateDynamicTable(table, newTableRange);
45597
+ this.history.update("tables", sheetId, table.id, newTable);
45598
+ return;
45599
+ }
45184
45600
  const filters = [];
45185
45601
  for (const filter of table.filters) {
45186
45602
  const filterRangeChange = applyChange(filter.rangeWithHeaders);
@@ -45192,7 +45608,7 @@ class TablePlugin extends CorePlugin {
45192
45608
  break;
45193
45609
  default:
45194
45610
  const newFilterRange = filterRangeChange.range;
45195
- const newFilter = this.createFilter(filter.id, newFilterRange, table.config);
45611
+ const newFilter = createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);
45196
45612
  filters.push(newFilter);
45197
45613
  }
45198
45614
  }
@@ -45207,7 +45623,7 @@ class TablePlugin extends CorePlugin {
45207
45623
  }
45208
45624
  filters.sort((f1, f2) => f1.col - f2.col);
45209
45625
  }
45210
- const newTable = this.createTable(table.id, newTableRange, table.config, filters);
45626
+ const newTable = this.createStaticTable(table.id, table.type, newTableRange, table.config, filters);
45211
45627
  this.history.update("tables", sheetId, table.id, newTable);
45212
45628
  }
45213
45629
  // ---------------------------------------------------------------------------
@@ -45218,16 +45634,20 @@ class TablePlugin extends CorePlugin {
45218
45634
  for (const tableData of sheet.tables || []) {
45219
45635
  const uuid = this.uuidGenerator.uuidv4();
45220
45636
  const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;
45221
- const tableRange = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
45222
- const table = this.createTable(uuid, tableRange, tableConfig);
45637
+ const range = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
45638
+ const tableType = tableData.type || "static";
45639
+ const table = tableType === "dynamic"
45640
+ ? this.createDynamicTable(uuid, range, tableConfig)
45641
+ : this.createStaticTable(uuid, tableType, range, tableConfig);
45223
45642
  this.history.update("tables", sheet.id, table.id, table);
45224
45643
  }
45225
45644
  }
45226
45645
  }
45227
45646
  export(data) {
45228
45647
  for (const sheet of data.sheets) {
45229
- for (const table of this.getTables(sheet.id)) {
45230
- const tableData = { range: zoneToXc(table.range.zone) };
45648
+ for (const table of this.getCoreTables(sheet.id)) {
45649
+ const range = zoneToXc(table.range.zone);
45650
+ const tableData = { range, type: table.type };
45231
45651
  if (!deepEquals(table.config, DEFAULT_TABLE_CONFIG)) {
45232
45652
  tableData.config = table.config;
45233
45653
  }
@@ -45790,12 +46210,6 @@ class CompilationParametersBuilder {
45790
46210
  : _t("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
45791
46211
  }
45792
46212
  const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
45793
- return this.readCell(position);
45794
- }
45795
- readCell(position) {
45796
- if (!this.getters.tryGetSheet(position.sheetId)) {
45797
- throw new EvaluationError(_t("Invalid sheet name"));
45798
- }
45799
46213
  return this.computeCell(position);
45800
46214
  }
45801
46215
  /**
@@ -45831,7 +46245,7 @@ class CompilationParametersBuilder {
45831
46245
  matrix[colIndex] = new Array(height);
45832
46246
  for (let row = _zone.top; row <= _zone.bottom; row++) {
45833
46247
  const rowIndex = row - _zone.top;
45834
- matrix[colIndex][rowIndex] = this.readCell({ sheetId, col, row });
46248
+ matrix[colIndex][rowIndex] = this.computeCell({ sheetId, col, row });
45835
46249
  }
45836
46250
  }
45837
46251
  this.rangeCache[cacheKey] = matrix;
@@ -46939,18 +47353,22 @@ class Evaluator {
46939
47353
  getEvaluatedCell(position) {
46940
47354
  return this.evaluatedCells.get(position) || EMPTY_CELL;
46941
47355
  }
46942
- getSpreadPositionsOf(position) {
47356
+ getSpreadZone(position) {
46943
47357
  if (!this.spreadingRelations.isArrayFormula(position)) {
46944
- return [];
47358
+ return undefined;
46945
47359
  }
46946
- return Array.from(this.spreadingRelations.getArrayResultPositions(position));
47360
+ if (this.evaluatedCells.get(position)?.type === CellValueType.error) {
47361
+ return positionToZone(position);
47362
+ }
47363
+ const spreadPositions = Array.from(this.spreadingRelations.getArrayResultPositions(position));
47364
+ return union(positionToZone(position), unionPositionsToZone(spreadPositions));
46947
47365
  }
46948
47366
  getEvaluatedPositions() {
46949
47367
  return this.evaluatedCells.keys();
46950
47368
  }
46951
47369
  getArrayFormulaSpreadingOn(position) {
46952
47370
  if (!this.spreadingRelations.hasArrayFormulaResult(position)) {
46953
- return undefined;
47371
+ return this.spreadingRelations.isArrayFormula(position) ? position : undefined;
46954
47372
  }
46955
47373
  const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(position);
46956
47374
  return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
@@ -47079,6 +47497,7 @@ class Evaluator {
47079
47497
  if (!this.blockedArrayFormulas.has(position)) {
47080
47498
  this.invalidateSpreading(position);
47081
47499
  }
47500
+ this.spreadingRelations.removeNode(position);
47082
47501
  const cell = this.getters.getCell(position);
47083
47502
  if (cell === undefined) {
47084
47503
  return EMPTY_CELL;
@@ -47189,7 +47608,6 @@ class Evaluator {
47189
47608
  this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([child]));
47190
47609
  this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(child));
47191
47610
  }
47192
- this.spreadingRelations.removeNode(position);
47193
47611
  }
47194
47612
  // ----------------------------------------------------------
47195
47613
  // COMMON FUNCTIONALITY
@@ -47348,7 +47766,7 @@ class EvaluationPlugin extends UIPlugin {
47348
47766
  "getEvaluatedCell",
47349
47767
  "getEvaluatedCells",
47350
47768
  "getEvaluatedCellsInZone",
47351
- "getSpreadPositionsOf",
47769
+ "getSpreadZone",
47352
47770
  "getArrayFormulaSpreadingOn",
47353
47771
  "isEmpty",
47354
47772
  ];
@@ -47454,8 +47872,11 @@ class EvaluationPlugin extends UIPlugin {
47454
47872
  getEvaluatedCellsInZone(sheetId, zone) {
47455
47873
  return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
47456
47874
  }
47457
- getSpreadPositionsOf(position) {
47458
- 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);
47459
47880
  }
47460
47881
  getArrayFormulaSpreadingOn(position) {
47461
47882
  return this.evaluator.getArrayFormulaSpreadingOn(position);
@@ -47495,7 +47916,7 @@ class EvaluationPlugin extends UIPlugin {
47495
47916
  ? getItemId(newFormat, data.formats)
47496
47917
  : exportedCellData.format;
47497
47918
  let content;
47498
- if (formulaCell instanceof FormulaCellWithDependencies) {
47919
+ if (isFormula && formulaCell instanceof FormulaCellWithDependencies) {
47499
47920
  content = formulaCell.contentWithFixedReferences;
47500
47921
  }
47501
47922
  else {
@@ -47543,17 +47964,17 @@ function isBadExpression(tokens) {
47543
47964
  */
47544
47965
  function sortWithClusters(colorsToSort) {
47545
47966
  const clusters = [
47546
- { leadColor: rgba(255, 0, 0), colors: [] },
47547
- { leadColor: rgba(255, 128, 0), colors: [] },
47548
- { leadColor: rgba(128, 128, 0), colors: [] },
47549
- { leadColor: rgba(128, 255, 0), colors: [] },
47550
- { leadColor: rgba(0, 255, 0), colors: [] },
47551
- { leadColor: rgba(0, 255, 128), colors: [] },
47552
- { leadColor: rgba(0, 255, 255), colors: [] },
47553
- { leadColor: rgba(0, 127, 255), colors: [] },
47554
- { leadColor: rgba(0, 0, 255), colors: [] },
47555
- { leadColor: rgba(127, 0, 255), colors: [] },
47556
- { 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
47557
47978
  { leadColor: rgba(255, 0, 128), colors: [] }, // rose
47558
47979
  ];
47559
47980
  for (const color of colorsToSort.map(colorToRGBA)) {
@@ -47944,13 +48365,13 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
47944
48365
  .map((cell) => cell.value);
47945
48366
  switch (threshold.type) {
47946
48367
  case "value":
47947
- const result = functionName === "max" ? Math.max(...rangeValues) : Math.min(...rangeValues);
48368
+ const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
47948
48369
  return result;
47949
48370
  case "number":
47950
48371
  return Number(threshold.value);
47951
48372
  case "percentage":
47952
- const min = Math.min(...rangeValues);
47953
- const max = Math.max(...rangeValues);
48373
+ const min = largeMin(rangeValues);
48374
+ const max = largeMax(rangeValues);
47954
48375
  const delta = max - min;
47955
48376
  return min + (delta * Number(threshold.value)) / 100;
47956
48377
  case "percentile":
@@ -48287,6 +48708,180 @@ class EvaluationDataValidationPlugin extends UIPlugin {
48287
48708
  }
48288
48709
  }
48289
48710
 
48711
+ class DynamicTablesPlugin extends UIPlugin {
48712
+ static getters = [
48713
+ "canCreateDynamicTableOnZones",
48714
+ "doesZonesContainFilter",
48715
+ "getFilter",
48716
+ "getFilters",
48717
+ "getTable",
48718
+ "getTables",
48719
+ "getTablesOverlappingZones",
48720
+ "getFilterId",
48721
+ "getFilterHeaders",
48722
+ "isFilterHeader",
48723
+ ];
48724
+ tables = {};
48725
+ handle(cmd) {
48726
+ if (invalidateEvaluationCommands.has(cmd.type) ||
48727
+ (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
48728
+ cmd.type === "EVALUATE_CELLS") {
48729
+ this.tables = {};
48730
+ return;
48731
+ }
48732
+ switch (cmd.type) {
48733
+ case "CREATE_TABLE":
48734
+ case "REMOVE_TABLE":
48735
+ case "UPDATE_TABLE":
48736
+ case "DELETE_CONTENT":
48737
+ this.tables = {};
48738
+ break;
48739
+ }
48740
+ }
48741
+ finalize() {
48742
+ for (const sheetId of this.getters.getSheetIds()) {
48743
+ if (!this.tables[sheetId]) {
48744
+ this.tables[sheetId] = this.computeTables(sheetId);
48745
+ }
48746
+ }
48747
+ }
48748
+ computeTables(sheetId) {
48749
+ const tables = [];
48750
+ const coreTables = this.getters.getCoreTables(sheetId);
48751
+ // First we create the static tables, so we can use them to compute collision with dynamic tables
48752
+ for (const table of coreTables) {
48753
+ if (table.type === "dynamic")
48754
+ continue;
48755
+ tables.push(table);
48756
+ }
48757
+ const staticTables = [...tables];
48758
+ // Then we create the dynamic tables
48759
+ for (const coreTable of coreTables) {
48760
+ if (coreTable.type !== "dynamic")
48761
+ continue;
48762
+ const table = this.coreTableToTable(sheetId, coreTable);
48763
+ let tableZone = table.range.zone;
48764
+ // Reduce the zone to avoid collision with static tables. Per design, dynamic tables can't overlap with other
48765
+ // dynamic tables, because formulas cannot spread on the same area, so we don't need to check for that.
48766
+ for (const staticTable of staticTables) {
48767
+ if (overlap(tableZone, staticTable.range.zone)) {
48768
+ tableZone = { ...tableZone, right: staticTable.range.zone.left - 1 };
48769
+ }
48770
+ }
48771
+ tables.push({ ...table, range: this.getters.getRangeFromZone(sheetId, tableZone) });
48772
+ }
48773
+ return tables;
48774
+ }
48775
+ getFilters(sheetId) {
48776
+ return this.getTables(sheetId)
48777
+ .filter((table) => table.config.hasFilters)
48778
+ .map((table) => table.filters)
48779
+ .flat();
48780
+ }
48781
+ getTables(sheetId) {
48782
+ return this.tables[sheetId] || [];
48783
+ }
48784
+ getFilter(position) {
48785
+ const table = this.getTable(position);
48786
+ if (!table || !table.config.hasFilters) {
48787
+ return undefined;
48788
+ }
48789
+ return table.filters.find((filter) => filter.col === position.col);
48790
+ }
48791
+ getFilterId(position) {
48792
+ return this.getFilter(position)?.id;
48793
+ }
48794
+ getTable({ sheetId, col, row }) {
48795
+ return this.getTables(sheetId).find((table) => isInside(col, row, table.range.zone));
48796
+ }
48797
+ getTablesOverlappingZones(sheetId, zones) {
48798
+ return this.getTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
48799
+ }
48800
+ doesZonesContainFilter(sheetId, zones) {
48801
+ return this.getTablesOverlappingZones(sheetId, zones).some((table) => table.config.hasFilters);
48802
+ }
48803
+ getFilterHeaders(sheetId) {
48804
+ const headers = [];
48805
+ for (const table of this.getTables(sheetId)) {
48806
+ if (!table.config.hasFilters) {
48807
+ continue;
48808
+ }
48809
+ const zone = table.range.zone;
48810
+ const row = zone.top;
48811
+ for (let col = zone.left; col <= zone.right; col++) {
48812
+ headers.push({ sheetId, col, row });
48813
+ }
48814
+ }
48815
+ return headers;
48816
+ }
48817
+ isFilterHeader({ sheetId, col, row }) {
48818
+ const headers = this.getFilterHeaders(sheetId);
48819
+ return headers.some((header) => header.col === col && header.row === row);
48820
+ }
48821
+ /**
48822
+ * Check if we can create a dynamic table on the given zones.
48823
+ * - The zones must be continuous
48824
+ * - The union of the zones must be either:
48825
+ * - A single cell that contains an array formula
48826
+ * - All the spread cells of a single array formula
48827
+ */
48828
+ canCreateDynamicTableOnZones(sheetId, zones) {
48829
+ if (!areZonesContinuous(zones)) {
48830
+ return false;
48831
+ }
48832
+ const unionZone = union(...zones);
48833
+ const topLeft = { col: unionZone.left, row: unionZone.top, sheetId };
48834
+ const parentSpreadingCell = this.getters.getArrayFormulaSpreadingOn(topLeft);
48835
+ if (!parentSpreadingCell) {
48836
+ return false;
48837
+ }
48838
+ else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
48839
+ return true;
48840
+ }
48841
+ const zone = this.getters.getSpreadZone(parentSpreadingCell);
48842
+ return deepEquals(unionZone, zone);
48843
+ }
48844
+ coreTableToTable(sheetId, table) {
48845
+ if (table.type !== "dynamic") {
48846
+ return table;
48847
+ }
48848
+ const tableZone = table.range.zone;
48849
+ const tablePosition = { sheetId, col: tableZone.left, row: tableZone.top };
48850
+ const zone = this.getters.getSpreadZone(tablePosition) ?? table.range.zone;
48851
+ const range = this.getters.getRangeFromZone(sheetId, zone);
48852
+ const filters = this.getDynamicTableFilters(sheetId, table, zone);
48853
+ return { id: table.id, range, filters, config: table.config };
48854
+ }
48855
+ getDynamicTableFilters(sheetId, table, tableZone) {
48856
+ const filters = [];
48857
+ const { top, bottom, left, right } = tableZone;
48858
+ for (let col = left; col <= right; col++) {
48859
+ const tableColIndex = col - left;
48860
+ const zone = { left: col, right: col, top, bottom };
48861
+ const filter = createFilter(this.getDynamicTableFilterId(table.id, tableColIndex), this.getters.getRangeFromZone(sheetId, zone), table.config, this.getters.getRangeFromZone);
48862
+ filters.push(filter);
48863
+ }
48864
+ return filters;
48865
+ }
48866
+ getDynamicTableFilterId(tableId, tableCol) {
48867
+ return tableId + "_" + tableCol;
48868
+ }
48869
+ exportForExcel(data) {
48870
+ for (const sheet of data.sheets) {
48871
+ for (const tableData of sheet.tables) {
48872
+ const zone = toZone(tableData.range);
48873
+ const topLeft = { sheetId: sheet.id, col: zone.left, row: zone.top };
48874
+ const coreTable = this.getters.getCoreTable(topLeft);
48875
+ const table = this.getTable(topLeft);
48876
+ if (coreTable?.type !== "dynamic" || !table) {
48877
+ continue;
48878
+ }
48879
+ tableData.range = zoneToXc(table.range.zone);
48880
+ }
48881
+ }
48882
+ }
48883
+ }
48884
+
48290
48885
  class HeaderSizeUIPlugin extends UIPlugin {
48291
48886
  static getters = ["getRowSize", "getHeaderSize"];
48292
48887
  tallestCellInRow = {};
@@ -48702,8 +49297,7 @@ class AutofillPlugin extends UIPlugin {
48702
49297
  let row = zone.bottom;
48703
49298
  if (col > 0) {
48704
49299
  let leftPosition = { sheetId, col: col - 1, row };
48705
- while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
48706
- this.getters.getCell(leftPosition)?.content) {
49300
+ while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty) {
48707
49301
  row += 1;
48708
49302
  leftPosition = { sheetId, col: col - 1, row };
48709
49303
  }
@@ -48712,8 +49306,7 @@ class AutofillPlugin extends UIPlugin {
48712
49306
  col = zone.right;
48713
49307
  if (col <= this.getters.getNumberCols(sheetId)) {
48714
49308
  let rightPosition = { sheetId, col: col + 1, row };
48715
- while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
48716
- this.getters.getCell(rightPosition)?.content) {
49309
+ while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty) {
48717
49310
  row += 1;
48718
49311
  rightPosition = { sheetId, col: col + 1, row };
48719
49312
  }
@@ -49023,13 +49616,13 @@ class AutomaticSumPlugin extends UIPlugin {
49023
49616
  const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
49024
49617
  const cellPositions = range(end, -1, -1);
49025
49618
  const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
49026
- const maxValidPosition = Math.max(...invalidCells);
49619
+ const maxValidPosition = largeMax(invalidCells);
49027
49620
  const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
49028
49621
  const firstSequence = numberSequences[0] || [];
49029
- if (Math.max(...firstSequence) < maxValidPosition) {
49622
+ if (largeMax(firstSequence) < maxValidPosition) {
49030
49623
  return Infinity;
49031
49624
  }
49032
- return Math.min(...firstSequence);
49625
+ return largeMin(firstSequence);
49033
49626
  }
49034
49627
  shouldFindData(sheetId, zone) {
49035
49628
  return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
@@ -50604,8 +51197,6 @@ class SheetUIPlugin extends UIPlugin {
50604
51197
  static getters = [
50605
51198
  "doesCellHaveGridIcon",
50606
51199
  "getCellWidth",
50607
- "getCellComputedBorder",
50608
- "getCellComputedStyle",
50609
51200
  "getTextWidth",
50610
51201
  "getCellText",
50611
51202
  "getCellMultiLineText",
@@ -50649,7 +51240,7 @@ class SheetUIPlugin extends UIPlugin {
50649
51240
  // Getters
50650
51241
  // ---------------------------------------------------------------------------
50651
51242
  getCellWidth(position) {
50652
- const style = this.getCellComputedStyle(position);
51243
+ const style = this.getters.getCellComputedStyle(position);
50653
51244
  let contentWidth = 0;
50654
51245
  const content = this.getters.getEvaluatedCell(position).formattedValue;
50655
51246
  if (content) {
@@ -50742,35 +51333,12 @@ class SheetUIPlugin extends UIPlugin {
50742
51333
  */
50743
51334
  isCellEmpty(position) {
50744
51335
  const mainPosition = this.getters.getMainCellPosition(position);
50745
- return !(this.getters.getCorrespondingFormulaCell(mainPosition) ||
50746
- this.getters.getCell(mainPosition)?.content);
50747
- }
50748
- getCellComputedBorder(position) {
50749
- const cellBorder = this.getters.getCellBorder(position) || {};
50750
- const cellTableBorder = this.getters.getCellTableBorder(position) || {};
50751
- // Use removeFalsyAttributes to avoid overwriting borders with undefined values
50752
- const border = { ...cellTableBorder, ...removeFalsyAttributes(cellBorder) };
50753
- return isObjectEmptyRecursive(border) ? null : border;
50754
- }
50755
- getCellComputedStyle(position) {
50756
- const cell = this.getters.getCell(position);
50757
- const cfStyle = this.getters.getCellConditionalFormatStyle(position);
50758
- const tableStyle = this.getters.getCellTableStyle(position);
50759
- const computedStyle = {
50760
- ...removeFalsyAttributes(tableStyle),
50761
- ...removeFalsyAttributes(cell?.style),
50762
- ...removeFalsyAttributes(cfStyle),
50763
- };
50764
- const evaluatedCell = this.getters.getEvaluatedCell(position);
50765
- if (evaluatedCell.link && !computedStyle.textColor) {
50766
- computedStyle.textColor = LINK_COLOR;
50767
- }
50768
- return computedStyle;
51336
+ return this.getters.getEvaluatedCell(mainPosition).type === CellValueType.empty;
50769
51337
  }
50770
51338
  getColMaxWidth(sheetId, index) {
50771
51339
  const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
50772
51340
  const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
50773
- return Math.max(0, ...sizes);
51341
+ return Math.max(0, largeMax(sizes));
50774
51342
  }
50775
51343
  /**
50776
51344
  * Check that any "sheetId" in the command matches an existing
@@ -50799,6 +51367,236 @@ class SheetUIPlugin extends UIPlugin {
50799
51367
  }
50800
51368
  }
50801
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
+
50802
51600
  const genericRepeatsTransforms = [
50803
51601
  repeatSheetDependantCommand,
50804
51602
  repeatTargetDependantCommand,
@@ -51334,10 +52132,11 @@ class TableAutofillPlugin extends UIPlugin {
51334
52132
  handle(cmd) {
51335
52133
  switch (cmd.type) {
51336
52134
  case "AUTOFILL_TABLE_COLUMN":
51337
- const table = this.getters.getTable(cmd);
52135
+ const table = this.getters.getCoreTable(cmd);
51338
52136
  const cell = this.getters.getCell(cmd);
51339
- if (!table || !table.config.automaticAutofill || !cell?.isFormula)
52137
+ if (!table?.config.automaticAutofill || table.type === "dynamic" || !cell?.isFormula) {
51340
52138
  return;
52139
+ }
51341
52140
  const { col, row } = cmd;
51342
52141
  const tableContentZone = getTableContentZone(table.range.zone, table.config);
51343
52142
  if (tableContentZone && isInside(col, row, tableContentZone)) {
@@ -51375,148 +52174,6 @@ class TableAutofillPlugin extends UIPlugin {
51375
52174
  }
51376
52175
  }
51377
52176
 
51378
- class TableStylePlugin extends UIPlugin {
51379
- static getters = ["getCellTableStyle", "getCellTableBorder"];
51380
- tableStyles = {};
51381
- handle(cmd) {
51382
- if (invalidateEvaluationCommands.has(cmd.type) ||
51383
- (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51384
- cmd.type === "EVALUATE_CELLS") {
51385
- this.tableStyles = {};
51386
- return;
51387
- }
51388
- switch (cmd.type) {
51389
- case "HIDE_COLUMNS_ROWS":
51390
- case "UNHIDE_COLUMNS_ROWS":
51391
- case "UNFOLD_HEADER_GROUP":
51392
- case "FOLD_HEADER_GROUP":
51393
- case "FOLD_ALL_HEADER_GROUPS":
51394
- case "UNFOLD_ALL_HEADER_GROUPS":
51395
- case "UPDATE_TABLE":
51396
- case "UPDATE_FILTER":
51397
- delete this.tableStyles[cmd.sheetId];
51398
- break;
51399
- }
51400
- }
51401
- finalize() {
51402
- for (const sheetId of this.getters.getSheetIds()) {
51403
- if (!this.tableStyles[sheetId]) {
51404
- this.tableStyles[sheetId] = {};
51405
- }
51406
- for (const table of this.getters.getTables(sheetId)) {
51407
- if (!this.tableStyles[sheetId][table.id]) {
51408
- this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51409
- }
51410
- }
51411
- }
51412
- }
51413
- getCellTableStyle(position) {
51414
- const table = this.getters.getTable(position);
51415
- if (!table) {
51416
- return undefined;
51417
- }
51418
- return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51419
- }
51420
- getCellTableBorder(position) {
51421
- const table = this.getters.getTable(position);
51422
- if (!table) {
51423
- return undefined;
51424
- }
51425
- return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51426
- }
51427
- computeTableStyle(sheetId, table) {
51428
- return lazy(() => {
51429
- const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51430
- const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51431
- // Return the style with sheet coordinates instead of tables coordinates
51432
- const mapping = this.getTableMapping(sheetId, table);
51433
- const absoluteTableStyle = { borders: {}, styles: {} };
51434
- for (let col = 0; col < numberOfCols; col++) {
51435
- const colInSheet = mapping.colMapping[col];
51436
- absoluteTableStyle.borders[colInSheet] = {};
51437
- absoluteTableStyle.styles[colInSheet] = {};
51438
- for (let row = 0; row < numberOfRows; row++) {
51439
- const rowInSheet = mapping.rowMapping[row];
51440
- absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51441
- absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51442
- }
51443
- }
51444
- return absoluteTableStyle;
51445
- });
51446
- }
51447
- /**
51448
- * Get the actual table config that will be used to compute the table style. It is different from
51449
- * the config of the table because of hidden rows and columns in the sheet. For example remove the
51450
- * hidden rows from config.numberOfHeaders.
51451
- */
51452
- getTableRuntimeConfig(sheetId, table) {
51453
- const tableZone = table.range.zone;
51454
- const config = { ...table.config };
51455
- let numberOfCols = tableZone.right - tableZone.left + 1;
51456
- let numberOfRows = tableZone.bottom - tableZone.top + 1;
51457
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51458
- if (!this.getters.isRowHidden(sheetId, row)) {
51459
- continue;
51460
- }
51461
- numberOfRows--;
51462
- if (row - tableZone.top < table.config.numberOfHeaders) {
51463
- config.numberOfHeaders--;
51464
- if (config.numberOfHeaders < 0) {
51465
- config.numberOfHeaders = 0;
51466
- }
51467
- }
51468
- if (row === tableZone.bottom) {
51469
- config.totalRow = false;
51470
- }
51471
- }
51472
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51473
- if (!this.getters.isColHidden(sheetId, col)) {
51474
- continue;
51475
- }
51476
- numberOfCols--;
51477
- if (col === tableZone.left) {
51478
- config.firstColumn = false;
51479
- }
51480
- if (col === tableZone.right) {
51481
- config.lastColumn = false;
51482
- }
51483
- }
51484
- return {
51485
- config,
51486
- numberOfCols,
51487
- numberOfRows,
51488
- };
51489
- }
51490
- /**
51491
- * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51492
- */
51493
- getTableMapping(sheetId, table) {
51494
- const colMapping = {};
51495
- const rowMapping = {};
51496
- let colOffset = 0;
51497
- let rowOffset = 0;
51498
- const tableZone = table.range.zone;
51499
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51500
- if (this.getters.isColHidden(sheetId, col)) {
51501
- continue;
51502
- }
51503
- colMapping[colOffset] = col;
51504
- colOffset++;
51505
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51506
- if (this.getters.isRowHidden(sheetId, row)) {
51507
- continue;
51508
- }
51509
- rowMapping[rowOffset] = row;
51510
- rowOffset++;
51511
- }
51512
- }
51513
- return {
51514
- colMapping,
51515
- rowMapping,
51516
- };
51517
- }
51518
- }
51519
-
51520
52177
  /**
51521
52178
  * Clipboard Plugin
51522
52179
  *
@@ -52066,9 +52723,6 @@ class FilterEvaluationPlugin extends UIPlugin {
52066
52723
  case "START":
52067
52724
  for (const sheetId of this.getters.getSheetIds()) {
52068
52725
  this.filterValues[sheetId] = {};
52069
- for (const filter of this.getters.getFilters(sheetId)) {
52070
- this.filterValues[sheetId][filter.id] = [];
52071
- }
52072
52726
  }
52073
52727
  break;
52074
52728
  case "CREATE_SHEET":
@@ -52089,16 +52743,7 @@ class FilterEvaluationPlugin extends UIPlugin {
52089
52743
  this.updateHiddenRows();
52090
52744
  break;
52091
52745
  case "DUPLICATE_SHEET":
52092
- const filterValues = {};
52093
- for (const newFilter of this.getters.getFilters(cmd.sheetIdTo)) {
52094
- const zone = newFilter.rangeWithHeaders.zone;
52095
- filterValues[newFilter.id] = this.getFilterHiddenValues({
52096
- sheetId: cmd.sheetId,
52097
- col: zone.left,
52098
- row: zone.top,
52099
- });
52100
- }
52101
- this.filterValues[cmd.sheetIdTo] = filterValues;
52746
+ this.filterValues[cmd.sheetIdTo] = deepCopy(this.filterValues[cmd.sheetId]);
52102
52747
  break;
52103
52748
  // If we don't handle DELETE_SHEET, on one hand we will have some residual data, on the other hand we keep the data
52104
52749
  // on DELETE_SHEET followed by undo
@@ -52238,38 +52883,6 @@ class FilterEvaluationPlugin extends UIPlugin {
52238
52883
  }
52239
52884
  }
52240
52885
 
52241
- const selectionStatisticFunctions = [
52242
- {
52243
- name: _t("Sum"),
52244
- types: [CellValueType.number],
52245
- compute: (values, locale) => sum([[values]], locale),
52246
- },
52247
- {
52248
- name: _t("Avg"),
52249
- types: [CellValueType.number],
52250
- compute: (values, locale) => average([[values]], locale),
52251
- },
52252
- {
52253
- name: _t("Min"),
52254
- types: [CellValueType.number],
52255
- compute: (values, locale) => min([[values]], locale),
52256
- },
52257
- {
52258
- name: _t("Max"),
52259
- types: [CellValueType.number],
52260
- compute: (values, locale) => max([[values]], locale),
52261
- },
52262
- {
52263
- name: _t("Count"),
52264
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52265
- compute: (values) => countAny([[values]]),
52266
- },
52267
- {
52268
- name: _t("Count Numbers"),
52269
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52270
- compute: (values, locale) => countNumbers([[values]], locale),
52271
- },
52272
- ];
52273
52886
  /**
52274
52887
  * SelectionPlugin
52275
52888
  */
@@ -52285,8 +52898,6 @@ class GridSelectionPlugin extends UIPlugin {
52285
52898
  "getSelectedZones",
52286
52899
  "getSelectedZone",
52287
52900
  "getSelectedCells",
52288
- "getStatisticFnResults",
52289
- "getAggregate",
52290
52901
  "getSelectedFigureId",
52291
52902
  "getSelection",
52292
52903
  "getActivePosition",
@@ -52321,7 +52932,10 @@ class GridSelectionPlugin extends UIPlugin {
52321
52932
  switch (cmd.type) {
52322
52933
  case "ACTIVATE_SHEET":
52323
52934
  try {
52324
- this.getters.getSheet(cmd.sheetIdTo);
52935
+ const sheet = this.getters.getSheet(cmd.sheetIdTo);
52936
+ if (!sheet.isVisible) {
52937
+ return "SheetIsHidden" /* CommandResult.SheetIsHidden */;
52938
+ }
52325
52939
  break;
52326
52940
  }
52327
52941
  catch (error) {
@@ -52475,6 +53089,7 @@ class GridSelectionPlugin extends UIPlugin {
52475
53089
  this.gridSelection.zones = this.gridSelection.zones.map((z) => this.getters.expandZone(sheetId, z));
52476
53090
  this.gridSelection.anchor.zone = this.getters.expandZone(sheetId, this.gridSelection.anchor.zone);
52477
53091
  this.setSelectionMixin(this.gridSelection.anchor, this.gridSelection.zones);
53092
+ this.selectedFigureId = null;
52478
53093
  break;
52479
53094
  }
52480
53095
  /** Any change to the selection has to be reflected in the selection processor. */
@@ -52569,52 +53184,6 @@ class GridSelectionPlugin extends UIPlugin {
52569
53184
  : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });
52570
53185
  }
52571
53186
  }
52572
- getStatisticFnResults() {
52573
- const sheetId = this.getters.getActiveSheetId();
52574
- const cells = new Set();
52575
- for (const zone of this.gridSelection.zones) {
52576
- for (const { col, row } of positions(zone)) {
52577
- if (this.getters.isRowHidden(sheetId, row) || this.getters.isColHidden(sheetId, col)) {
52578
- continue; // Skip hidden cells
52579
- }
52580
- const evaluatedCell = this.getters.getEvaluatedCell({ sheetId, col, row });
52581
- if (evaluatedCell.type !== CellValueType.empty) {
52582
- cells.add(evaluatedCell);
52583
- }
52584
- }
52585
- }
52586
- const locale = this.getters.getLocale();
52587
- let statisticFnResults = {};
52588
- for (let fn of selectionStatisticFunctions) {
52589
- // We don't want to display statistical information when there is no interest:
52590
- // We set the statistical result to undefined if the data handled by the selection
52591
- // does not match the data handled by the function.
52592
- // Ex: if there are only texts in the selection, we prefer that the SUM result
52593
- // be displayed as undefined rather than 0.
52594
- let fnResult = undefined;
52595
- const evaluatedCells = [...cells].filter((c) => fn.types.includes(c.type));
52596
- if (evaluatedCells.length) {
52597
- fnResult = fn.compute(evaluatedCells, locale);
52598
- }
52599
- statisticFnResults[fn.name] = fnResult;
52600
- }
52601
- return statisticFnResults;
52602
- }
52603
- getAggregate() {
52604
- let aggregate = 0;
52605
- let n = 0;
52606
- const sheetId = this.getters.getActiveSheetId();
52607
- const cellPositions = this.gridSelection.zones.map(positions).flat();
52608
- for (const { col, row } of cellPositions) {
52609
- const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
52610
- if (cell.type === CellValueType.number) {
52611
- n++;
52612
- aggregate += cell.value;
52613
- }
52614
- }
52615
- const locale = this.getters.getLocale();
52616
- return n < 2 ? null : formatValue(aggregate, { locale });
52617
- }
52618
53187
  isSelected(zone) {
52619
53188
  return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));
52620
53189
  }
@@ -52656,9 +53225,6 @@ class GridSelectionPlugin extends UIPlugin {
52656
53225
  // Other
52657
53226
  // ---------------------------------------------------------------------------
52658
53227
  activateSheet(sheetIdFrom, sheetIdTo) {
52659
- if (!this.getters.isSheetVisible(sheetIdTo)) {
52660
- this.dispatch("SHOW_SHEET", { sheetId: sheetIdTo });
52661
- }
52662
53228
  this.setActiveSheet(sheetIdTo);
52663
53229
  this.sheetsData[sheetIdFrom] = {
52664
53230
  gridSelection: deepCopy(this.gridSelection),
@@ -53665,7 +54231,7 @@ class SheetViewPlugin extends UIPlugin {
53665
54231
  * column of the current viewport
53666
54232
  */
53667
54233
  getColDimensionsInViewport(sheetId, col) {
53668
- const left = Math.min(...this.getters.getSheetViewVisibleCols());
54234
+ const left = largeMin(this.getters.getSheetViewVisibleCols());
53669
54235
  const start = this.getters.getColRowOffsetInViewport("COL", left, col);
53670
54236
  const size = this.getters.getColSize(sheetId, col);
53671
54237
  const isColHidden = this.getters.isColHidden(sheetId, col);
@@ -53680,7 +54246,7 @@ class SheetViewPlugin extends UIPlugin {
53680
54246
  * of the current viewport
53681
54247
  */
53682
54248
  getRowDimensionsInViewport(sheetId, row) {
53683
- const top = Math.min(...this.getters.getSheetViewVisibleRows());
54249
+ const top = largeMin(this.getters.getSheetViewVisibleRows());
53684
54250
  const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
53685
54251
  const size = this.getters.getRowSize(sheetId, row);
53686
54252
  const isRowHidden = this.getters.isRowHidden(sheetId, row);
@@ -54032,6 +54598,7 @@ const statefulUIPluginRegistry = new Registry()
54032
54598
  .add("evaluation_filter", FilterEvaluationPlugin)
54033
54599
  .add("header_visibility_ui", HeaderVisibilityUIPlugin)
54034
54600
  .add("table_style", TableStylePlugin)
54601
+ .add("cell_computed_style", CellComputedStylePlugin)
54035
54602
  .add("header_positions", HeaderPositionsUIPlugin)
54036
54603
  .add("viewport", SheetViewPlugin)
54037
54604
  .add("clipboard", ClipboardPlugin);
@@ -54041,8 +54608,9 @@ const coreViewsPluginRegistry = new Registry()
54041
54608
  .add("evaluation_chart", EvaluationChartPlugin)
54042
54609
  .add("evaluation_cf", EvaluationConditionalFormatPlugin)
54043
54610
  .add("row_size", HeaderSizeUIPlugin)
54044
- .add("custom_colors", CustomColorsPlugin)
54045
- .add("data_validation_ui", EvaluationDataValidationPlugin);
54611
+ .add("data_validation_ui", EvaluationDataValidationPlugin)
54612
+ .add("dynamic_tables", DynamicTablesPlugin)
54613
+ .add("custom_colors", CustomColorsPlugin);
54046
54614
 
54047
54615
  const clickableCellRegistry = new Registry();
54048
54616
  clickableCellRegistry.add("link", {
@@ -54091,6 +54659,38 @@ class ImageProvider {
54091
54659
  }
54092
54660
  }
54093
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
+
54094
54694
  const RIPPLE_KEY_FRAMES = [
54095
54695
  { transform: "scale(0)" },
54096
54696
  { transform: "scale(0.8)", offset: 0.33 },
@@ -54393,12 +54993,14 @@ class BottomBarSheet extends owl.Component {
54393
54993
  this.editionState = "initializing";
54394
54994
  }
54395
54995
  stopEdition() {
54396
- if (!this.state.isEditing)
54996
+ const input = this.sheetNameRef.el;
54997
+ if (!this.state.isEditing || !input)
54397
54998
  return;
54398
54999
  this.state.isEditing = false;
54399
55000
  this.editionState = "initializing";
54400
- this.sheetNameRef.el?.blur();
55001
+ input.blur();
54401
55002
  const inputValue = this.getInputContent() || "";
55003
+ input.innerText = inputValue;
54402
55004
  interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
54403
55005
  }
54404
55006
  cancelEdition() {
@@ -54442,6 +55044,115 @@ class BottomBarSheet extends owl.Component {
54442
55044
  }
54443
55045
  }
54444
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
+
54445
55156
  // -----------------------------------------------------------------------------
54446
55157
  // SpreadSheet
54447
55158
  // -----------------------------------------------------------------------------
@@ -54457,40 +55168,38 @@ css /* scss */ `
54457
55168
  }
54458
55169
  `;
54459
55170
  class BottomBarStatistic extends owl.Component {
54460
- static template = "o-spreadsheet-BottomBarStatisic";
55171
+ static template = "o-spreadsheet-BottomBarStatistic";
54461
55172
  static props = {
54462
55173
  openContextMenu: Function,
54463
55174
  closeContextMenu: Function,
54464
55175
  };
54465
55176
  static components = { Ripple };
54466
55177
  selectedStatisticFn = "";
54467
- statisticFnResults = {};
55178
+ store;
54468
55179
  setup() {
54469
- this.statisticFnResults = this.env.model.getters.getStatisticFnResults();
55180
+ this.store = useStore(AggregateStatisticsStore);
54470
55181
  owl.onWillUpdateProps(() => {
54471
- const newStatisticFnResults = this.env.model.getters.getStatisticFnResults();
54472
- if (!deepEquals(newStatisticFnResults, this.statisticFnResults)) {
55182
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54473
55183
  this.props.closeContextMenu();
54474
55184
  }
54475
- this.statisticFnResults = newStatisticFnResults;
54476
55185
  });
54477
55186
  }
54478
55187
  getSelectedStatistic() {
54479
55188
  // don't display button if no function has a result
54480
- if (Object.values(this.statisticFnResults).every((result) => result === undefined)) {
55189
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54481
55190
  return undefined;
54482
55191
  }
54483
55192
  if (this.selectedStatisticFn === "") {
54484
- this.selectedStatisticFn = Object.keys(this.statisticFnResults)[0];
55193
+ this.selectedStatisticFn = Object.keys(this.store.statisticFnResults)[0];
54485
55194
  }
54486
- return this.getComposedFnName(this.selectedStatisticFn, this.statisticFnResults[this.selectedStatisticFn]);
55195
+ return this.getComposedFnName(this.selectedStatisticFn);
54487
55196
  }
54488
55197
  listSelectionStatistics(ev) {
54489
55198
  const registry = new MenuItemRegistry();
54490
55199
  let i = 0;
54491
- for (let [fnName, fnValue] of Object.entries(this.statisticFnResults)) {
55200
+ for (let [fnName] of Object.entries(this.store.statisticFnResults)) {
54492
55201
  registry.add(fnName, {
54493
- name: this.getComposedFnName(fnName, fnValue),
55202
+ name: () => this.getComposedFnName(fnName),
54494
55203
  sequence: i,
54495
55204
  isReadonlyAllowed: true,
54496
55205
  execute: () => {
@@ -54503,8 +55212,9 @@ class BottomBarStatistic extends owl.Component {
54503
55212
  const { top, left, width } = target.getBoundingClientRect();
54504
55213
  this.props.openContextMenu(left + width, top, registry);
54505
55214
  }
54506
- getComposedFnName(fnName, fnValue) {
55215
+ getComposedFnName(fnName) {
54507
55216
  const locale = this.env.model.getters.getLocale();
55217
+ const fnValue = this.store.statisticFnResults[fnName];
54508
55218
  return fnName + ": " + (fnValue !== undefined ? formatValue(fnValue, { locale }) : "__");
54509
55219
  }
54510
55220
  }
@@ -54613,10 +55323,14 @@ class BottomBar extends owl.Component {
54613
55323
  name: sheet.name,
54614
55324
  sequence: i,
54615
55325
  isReadonlyAllowed: true,
54616
- textColor: sheet.isVisible ? undefined : "grey",
55326
+ textColor: sheet.isVisible ? undefined : "#808080",
54617
55327
  execute: (env) => {
55328
+ if (!this.env.model.getters.isSheetVisible(sheetId)) {
55329
+ this.env.model.dispatch("SHOW_SHEET", { sheetId });
55330
+ }
54618
55331
  env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: from, sheetIdTo: sheetId });
54619
55332
  },
55333
+ isEnabled: (env) => (env.model.getters.isReadonly() ? sheet.isVisible : true),
54620
55334
  });
54621
55335
  i++;
54622
55336
  }
@@ -54689,7 +55403,7 @@ class BottomBar extends owl.Component {
54689
55403
  this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
54690
55404
  }
54691
55405
  onSheetMouseDown(sheetId, event) {
54692
- if (event.button !== 0)
55406
+ if (event.button !== 0 || this.env.model.getters.isReadonly())
54693
55407
  return;
54694
55408
  this.closeMenu();
54695
55409
  const visibleSheets = this.getVisibleSheets();
@@ -54725,7 +55439,7 @@ class BottomBar extends owl.Component {
54725
55439
  .map((sheetEl) => sheetEl.getBoundingClientRect())
54726
55440
  .map((rect) => ({
54727
55441
  x: rect.x,
54728
- width: rect.width - 1,
55442
+ width: rect.width - 1, // -1 to compensate negative margin
54729
55443
  y: rect.y,
54730
55444
  height: rect.height,
54731
55445
  }));
@@ -54952,7 +55666,7 @@ class RowGroup extends AbstractHeaderGroup {
54952
55666
  }
54953
55667
  return cssPropertiesToCss({
54954
55668
  top: `${groupBox.headerRect.height / 2}px`,
54955
- left: `calc(50% - 1px)`,
55669
+ left: `calc(50% - 1px)`, // -1px: we want the border to be on the center
54956
55670
  width: `30%`,
54957
55671
  height: `calc(100% - ${groupBox.headerRect.height / 2}px)`,
54958
55672
  "border-left": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
@@ -55004,7 +55718,7 @@ class ColGroup extends AbstractHeaderGroup {
55004
55718
  return "";
55005
55719
  }
55006
55720
  return cssPropertiesToCss({
55007
- top: `calc(50% - 1px)`,
55721
+ top: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55008
55722
  left: `${groupBox.headerRect.width / 2}px`,
55009
55723
  width: `calc(100% - ${groupBox.headerRect.width / 2}px)`,
55010
55724
  height: `30%`,
@@ -56093,12 +56807,9 @@ css /* scss */ `
56093
56807
  .text-muted {
56094
56808
  color: grey !important;
56095
56809
  }
56096
- button {
56097
- color: #333;
56098
- }
56099
56810
  .o-disabled {
56100
56811
  opacity: 0.4;
56101
- pointer: default;
56812
+ cursor: default;
56102
56813
  pointer-events: none;
56103
56814
  }
56104
56815
 
@@ -56216,17 +56927,17 @@ css /* scss */ `
56216
56927
  }
56217
56928
 
56218
56929
  .o-button {
56219
- border: 1px solid lightgrey;
56930
+ border: 1px solid;
56220
56931
  padding: 0px 20px 0px 20px;
56221
56932
  border-radius: 4px;
56222
56933
  font-weight: 500;
56223
56934
  font-size: 14px;
56224
56935
  height: 30px;
56225
56936
  line-height: 16px;
56226
- background: white;
56227
56937
  margin-right: 8px;
56228
- &:hover:enabled {
56229
- background-color: rgba(0, 0, 0, 0.08);
56938
+
56939
+ &:not(:hover) {
56940
+ background-color: transparent;
56230
56941
  }
56231
56942
 
56232
56943
  &:enabled {
@@ -56240,6 +56951,15 @@ css /* scss */ `
56240
56951
  &:last-child {
56241
56952
  margin-right: 0px;
56242
56953
  }
56954
+
56955
+ &.o-button-grey {
56956
+ border-color: lightgrey;
56957
+ background: #ffffff;
56958
+ color: #333;
56959
+ &:hover:enabled {
56960
+ background-color: rgba(0, 0, 0, 0.08);
56961
+ }
56962
+ }
56243
56963
  }
56244
56964
 
56245
56965
  .o-input {
@@ -56255,7 +56975,7 @@ css /* scss */ `
56255
56975
 
56256
56976
  .o-number-input {
56257
56977
  /* Remove number input arrows */
56258
- -moz-appearance: textfield;
56978
+ appearance: textfield;
56259
56979
  &::-webkit-outer-spin-button,
56260
56980
  &::-webkit-inner-spin-button {
56261
56981
  -webkit-appearance: none;
@@ -56298,6 +57018,7 @@ class Spreadsheet extends owl.Component {
56298
57018
  this.notificationStore = useStore(NotificationStore);
56299
57019
  this.composerFocusStore = useStore(ComposerFocusStore);
56300
57020
  this.sidePanel = useStore(SidePanelStore);
57021
+ useStore(ArrayFormulaHighlight);
56301
57022
  this.keyDownMapping = {
56302
57023
  "CTRL+H": () => this.sidePanel.toggle("FindAndReplace", {}),
56303
57024
  "CTRL+F": () => this.sidePanel.toggle("FindAndReplace", {}),
@@ -56400,7 +57121,7 @@ class Spreadsheet extends owl.Component {
56400
57121
  const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
56401
57122
  const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;
56402
57123
  return cssPropertiesToCss({
56403
- "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`,
57124
+ "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`, // +2: margins
56404
57125
  "grid-template-rows": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,
56405
57126
  });
56406
57127
  }
@@ -57132,14 +57853,6 @@ class SelectiveHistory {
57132
57853
  this.revertBefore(operationId);
57133
57854
  this.tree.drop(operationId);
57134
57855
  }
57135
- getRevertedExecution() {
57136
- const data = [];
57137
- const operations = this.tree.revertedExecution(this.HEAD_BRANCH);
57138
- for (const { operation } of operations) {
57139
- data.push(operation.data);
57140
- }
57141
- return data;
57142
- }
57143
57856
  /**
57144
57857
  * Revert the state as it was *before* the given operation was executed.
57145
57858
  */
@@ -57928,6 +58641,9 @@ function createChart(chart, chartSheetIndex, data) {
57928
58641
  case "bar":
57929
58642
  plot = addBarChart(chart.data);
57930
58643
  break;
58644
+ case "combo":
58645
+ plot = addComboChart(chart.data);
58646
+ break;
57931
58647
  case "line":
57932
58648
  plot = addLineChart(chart.data);
57933
58649
  break;
@@ -58090,6 +58806,79 @@ function addBarChart(chart) {
58090
58806
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58091
58807
  `;
58092
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
+ }
58093
58882
  function addLineChart(chart) {
58094
58883
  const colors = new ChartColors();
58095
58884
  const dataSetsNodes = [];
@@ -58137,7 +58926,7 @@ function addLineChart(chart) {
58137
58926
  }
58138
58927
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
58139
58928
  const colors = new ChartColors();
58140
- 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)));
58141
58930
  const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
58142
58931
  const dataSetsNodes = [];
58143
58932
  for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
@@ -59015,7 +59804,7 @@ function addTableColumns(table, sheetData) {
59015
59804
  const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
59016
59805
  const colName = sheetData.cells[colHeaderXc]?.content || `col${i}`;
59017
59806
  const colAttributes = [
59018
- ["id", i + 1],
59807
+ ["id", i + 1], // id cannot be 0
59019
59808
  ["name", colName],
59020
59809
  ];
59021
59810
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
@@ -59231,6 +60020,7 @@ function addSheetViews(sheet) {
59231
60020
  * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
59232
60021
  */
59233
60022
  function getXLSX(data) {
60023
+ data = fixLengthySheetNames(data);
59234
60024
  const files = [];
59235
60025
  const construct = getDefaultXLSXStructure();
59236
60026
  files.push(createWorkbook(data, construct));
@@ -59478,6 +60268,40 @@ function createRelRoot() {
59478
60268
  `;
59479
60269
  return createXMLFile(parseXML(xml), "_rels/.rels");
59480
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
+ }
59481
60305
 
59482
60306
  var Status;
59483
60307
  (function (Status) {
@@ -60167,6 +60991,6 @@ exports.tokenColors = tokenColors;
60167
60991
  exports.tokenize = tokenize;
60168
60992
 
60169
60993
 
60170
- __info__.version = "17.3.0-alpha.0";
60171
- __info__.date = "2024-03-20T13:42:32.042Z";
60172
- __info__.hash = "073e154";
60994
+ __info__.version = "17.3.0-alpha.2";
60995
+ __info__.date = "2024-04-05T14:01:07.060Z";
60996
+ __info__.hash = "8c5a229";