@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
  import { reactive, useEnv, useSubEnv, useState, onWillUnmount, markRaw, toRaw, Component, useRef, onMounted, useEffect, onPatched, useComponent, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv } from '@odoo/owl';
@@ -29,7 +29,6 @@ const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;
29
29
  const LINK_COLOR = "#017E84";
30
30
  const FILTERS_COLOR = "#188038";
31
31
  const BACKGROUND_HEADER_FILTER_COLOR = "#E6F4EA";
32
- const BACKGROUND_HEADER_SELECTED_FILTER_COLOR = "#CEEAD6";
33
32
  const SEPARATOR_COLOR = "#E0E2E4";
34
33
  const ICONS_COLOR = "#4A4F59";
35
34
  const HEADER_GROUPING_BACKGROUND_COLOR = "#F5F5F5";
@@ -465,7 +464,7 @@ function getItemId(item, itemsDic) {
465
464
  }
466
465
  // Generate new Id if the item didn't exist in the dictionary
467
466
  const ids = Object.keys(itemsDic);
468
- const maxId = ids.length === 0 ? 0 : Math.max(...ids.map((id) => parseInt(id, 10)));
467
+ const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
469
468
  itemsDic[maxId + 1] = item;
470
469
  return maxId + 1;
471
470
  }
@@ -483,7 +482,7 @@ function debounce(func, wait, immediate) {
483
482
  let timeout = undefined;
484
483
  const debounced = function () {
485
484
  const context = this;
486
- const args = arguments;
485
+ const args = Array.from(arguments);
487
486
  function later() {
488
487
  timeout = undefined;
489
488
  if (!immediate) {
@@ -685,6 +684,34 @@ function getSearchRegex(searchStr, searchOptions) {
685
684
  }
686
685
  return RegExp(searchValue, flags);
687
686
  }
687
+ /**
688
+ * Alternative to Math.max that works with large arrays.
689
+ * Typically useful for arrays bigger than 100k elements.
690
+ */
691
+ function largeMax(array) {
692
+ let len = array.length;
693
+ if (len < 100_000)
694
+ return Math.max(...array);
695
+ let max = -Infinity;
696
+ while (len--) {
697
+ max = array[len] > max ? array[len] : max;
698
+ }
699
+ return max;
700
+ }
701
+ /**
702
+ * Alternative to Math.min that works with large arrays.
703
+ * Typically useful for arrays bigger than 100k elements.
704
+ */
705
+ function largeMin(array) {
706
+ let len = array.length;
707
+ if (len < 100_000)
708
+ return Math.min(...array);
709
+ let min = +Infinity;
710
+ while (len--) {
711
+ min = array[len] < min ? array[len] : min;
712
+ }
713
+ return min;
714
+ }
688
715
 
689
716
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
690
717
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
@@ -2087,6 +2114,7 @@ var CommandResult;
2087
2114
  CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
2088
2115
  CommandResult["NoChanges"] = "NoChanges";
2089
2116
  CommandResult["InvalidInputId"] = "InvalidInputId";
2117
+ CommandResult["SheetIsHidden"] = "SheetIsHidden";
2090
2118
  })(CommandResult || (CommandResult = {}));
2091
2119
 
2092
2120
  const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
@@ -4202,6 +4230,10 @@ function organizeZone(zone) {
4202
4230
  function positionToZone(position) {
4203
4231
  return { left: position.col, right: position.col, top: position.row, bottom: position.row };
4204
4232
  }
4233
+ /** Transform a zone into a zone with only its top-left position */
4234
+ function zoneToTopLeft(zone) {
4235
+ return { ...zone, right: zone.left, bottom: zone.top };
4236
+ }
4205
4237
  function isFullRow(zone) {
4206
4238
  return zone.right === undefined;
4207
4239
  }
@@ -4241,6 +4273,16 @@ function getZonesRows(zones) {
4241
4273
  }
4242
4274
  return set;
4243
4275
  }
4276
+ function unionPositionsToZone(positions) {
4277
+ const zone = { top: Infinity, left: Infinity, bottom: -Infinity, right: -Infinity };
4278
+ for (const { col, row } of positions) {
4279
+ zone.top = Math.min(zone.top, row);
4280
+ zone.left = Math.min(zone.left, col);
4281
+ zone.bottom = Math.max(zone.bottom, row);
4282
+ zone.right = Math.max(zone.right, col);
4283
+ }
4284
+ return zone;
4285
+ }
4244
4286
 
4245
4287
  class RangeImpl {
4246
4288
  getSheetSize;
@@ -4544,8 +4586,9 @@ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
4544
4586
  * Get the default height of the cell given its style.
4545
4587
  */
4546
4588
  function getDefaultCellHeight(ctx, cell, colSize) {
4547
- if (!cell || !cell.content)
4589
+ if (!cell || (!cell.isFormula && !cell.content)) {
4548
4590
  return DEFAULT_CELL_HEIGHT;
4591
+ }
4549
4592
  const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
4550
4593
  const numberOfLines = cell.isFormula
4551
4594
  ? 1
@@ -5570,7 +5613,7 @@ class CellClipboardHandler extends AbstractCellClipboardHandler {
5570
5613
  const spreader = this.getters.getArrayFormulaSpreadingOn(position);
5571
5614
  let cell = this.getters.getCell(position);
5572
5615
  const evaluatedCell = this.getters.getEvaluatedCell(position);
5573
- if (spreader) {
5616
+ if (spreader && !deepEquals(spreader, position)) {
5574
5617
  const isSpreaderCopied = rowsIndexes.includes(spreader.row) && columnsIndexes.includes(spreader.col);
5575
5618
  const content = isSpreaderCopied
5576
5619
  ? ""
@@ -6217,19 +6260,25 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
6217
6260
  tableCellsInRow.push({});
6218
6261
  continue;
6219
6262
  }
6263
+ const coreTable = this.getters.getCoreTable(position);
6264
+ const tableZone = coreTable?.range.zone;
6220
6265
  // Copy whole table
6221
- if (zones.some((z) => isZoneInside(table.range.zone, z))) {
6222
- copiedTablesIds.add(table.id);
6266
+ if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
6267
+ copiedTablesIds.add(coreTable.id);
6223
6268
  const values = [];
6224
- for (const col of range(table.range.zone.left, table.range.zone.right + 1)) {
6225
- values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: table.range.zone.top }));
6269
+ for (const col of range(tableZone.left, tableZone.right + 1)) {
6270
+ values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
6226
6271
  }
6227
6272
  tableCellsInRow.push({
6228
- table: { filtersValues: values, range: table.range, config: table.config },
6273
+ table: {
6274
+ range: coreTable.range,
6275
+ config: coreTable.config,
6276
+ type: coreTable.type,
6277
+ },
6229
6278
  });
6230
6279
  }
6231
6280
  // Copy only style of cell
6232
- else {
6281
+ else if (table) {
6233
6282
  tableCellsInRow.push({ style: this.getTableStyleToCopy(position) });
6234
6283
  }
6235
6284
  }
@@ -6300,7 +6349,7 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
6300
6349
  }
6301
6350
  pasteTableCell(sheetId, tableCell, position, options) {
6302
6351
  if (tableCell.table && !options?.pasteOption) {
6303
- const { range: tableRange, filtersValues } = tableCell.table;
6352
+ const { range: tableRange } = tableCell.table;
6304
6353
  const zoneDims = zoneToDimension(tableRange.zone);
6305
6354
  const newTableZone = {
6306
6355
  left: position.col,
@@ -6312,18 +6361,13 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
6312
6361
  sheetId: position.sheetId,
6313
6362
  ranges: [this.getters.getRangeDataFromZone(sheetId, newTableZone)],
6314
6363
  config: tableCell.table.config,
6364
+ tableType: tableCell.table.type,
6315
6365
  });
6316
- for (const i of range(0, filtersValues.length)) {
6317
- this.dispatch("UPDATE_FILTER", {
6318
- sheetId: position.sheetId,
6319
- col: newTableZone.left + i,
6320
- row: newTableZone.top,
6321
- hiddenValues: filtersValues[i],
6322
- });
6323
- }
6324
6366
  }
6325
6367
  // Do not paste table style if we're inside another table
6326
- if (!this.getters.getTable(position)) {
6368
+ // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
6369
+ // dynamic tables are not yet computed
6370
+ if (!this.getters.getCoreTable(position)) {
6327
6371
  if (tableCell.style?.style && options?.pasteOption !== "asValue") {
6328
6372
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
6329
6373
  }
@@ -6978,10 +7022,17 @@ urlRegistry.add("sheet_URL", {
6978
7022
  },
6979
7023
  open(url, env) {
6980
7024
  const sheetId = parseSheetUrl(url);
6981
- env.model.dispatch("ACTIVATE_SHEET", {
7025
+ const result = env.model.dispatch("ACTIVATE_SHEET", {
6982
7026
  sheetIdFrom: env.model.getters.getActiveSheetId(),
6983
7027
  sheetIdTo: sheetId,
6984
7028
  });
7029
+ if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
7030
+ env.notifyUser({
7031
+ type: "warning",
7032
+ sticky: false,
7033
+ text: _t("Cannot open the link because the linked sheet is hidden."),
7034
+ });
7035
+ }
6985
7036
  },
6986
7037
  sequence: 0,
6987
7038
  });
@@ -7097,7 +7148,7 @@ function textCell(value, format, formattedValue) {
7097
7148
  }
7098
7149
  function numberCell(value, format, formattedValue) {
7099
7150
  return {
7100
- value: value || 0,
7151
+ value: value || 0, // necessary to avoid "-0" and NaN values,
7101
7152
  format,
7102
7153
  formattedValue,
7103
7154
  type: CellValueType.number,
@@ -7425,9 +7476,11 @@ const TableTerms = {
7425
7476
  bandedColumns: _t("Banded columns"),
7426
7477
  automaticAutofill: _t("Automatically autofill formulas"),
7427
7478
  totalRow: _t("Total row"),
7479
+ isDynamic: _t("Auto-adjust to formula result"),
7428
7480
  },
7429
7481
  Tooltips: {
7430
7482
  filterWithoutHeader: _t("Cannot have filters without a header row"),
7483
+ isDynamic: _t("For tables based on array formulas only"),
7431
7484
  },
7432
7485
  };
7433
7486
 
@@ -8003,6 +8056,9 @@ class DependencyContainer {
8003
8056
  instantiate(Store, ...args) {
8004
8057
  return this.factory.build(Store, ...args);
8005
8058
  }
8059
+ resetStores() {
8060
+ this.dependencies.clear();
8061
+ }
8006
8062
  }
8007
8063
  class StoreFactory {
8008
8064
  get;
@@ -9936,11 +9992,11 @@ autoCompleteProviders.add("dataValidation", {
9936
9992
  }
9937
9993
  else {
9938
9994
  const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
9939
- values = this.getters
9995
+ values = Array.from(new Set(this.getters
9940
9996
  .getRangeValues(range)
9941
9997
  .filter(isNotNull)
9942
9998
  .map((value) => value.toString())
9943
- .filter((val) => val !== "");
9999
+ .filter((val) => val !== "")));
9944
10000
  }
9945
10001
  return values.map((value) => ({ text: value }));
9946
10002
  },
@@ -19387,10 +19443,10 @@ function aggregateDataForLabels(labels, datasets) {
19387
19443
  }
19388
19444
  }
19389
19445
  return {
19390
- labels: Object.keys(labelMap),
19446
+ labels: Array.from(labelSet),
19391
19447
  dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
19392
19448
  ...dataset,
19393
- data: Object.values(labelMap).map((dataOfLabel) => dataOfLabel[indexOfDataset]),
19449
+ data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
19394
19450
  })),
19395
19451
  };
19396
19452
  }
@@ -19409,8 +19465,8 @@ function truncateLabel(label) {
19409
19465
  function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale }) {
19410
19466
  const options = {
19411
19467
  // https://www.chartjs.org/docs/latest/general/responsive.html
19412
- responsive: true,
19413
- maintainAspectRatio: false,
19468
+ responsive: true, // will resize when its container is resized
19469
+ maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
19414
19470
  layout: {
19415
19471
  padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
19416
19472
  },
@@ -19456,7 +19512,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale })
19456
19512
  labels: labels.map(truncateLabel),
19457
19513
  datasets: [],
19458
19514
  },
19459
- platform: undefined,
19515
+ platform: undefined, // This key is optional and will be set by chart.js
19460
19516
  plugins: [],
19461
19517
  };
19462
19518
  }
@@ -19733,7 +19789,7 @@ function getBarConfiguration(chart, labels, localeFormat) {
19733
19789
  },
19734
19790
  y: {
19735
19791
  position: chart.verticalAxisPosition,
19736
- beginAtZero: true,
19792
+ beginAtZero: true, // the origin of the y axis is always zero
19737
19793
  ticks: {
19738
19794
  color: fontColor,
19739
19795
  callback: (value) => {
@@ -19784,6 +19840,204 @@ function createBarChartRuntime(chart, getters) {
19784
19840
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
19785
19841
  }
19786
19842
 
19843
+ class ComboChart extends AbstractChart {
19844
+ useBothYAxis;
19845
+ dataSets;
19846
+ labelRange;
19847
+ background;
19848
+ verticalAxisPosition;
19849
+ legendPosition;
19850
+ aggregated;
19851
+ dataSetsHaveTitle;
19852
+ type = "combo";
19853
+ constructor(definition, sheetId, getters) {
19854
+ super(definition, sheetId, getters);
19855
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
19856
+ this.labelRange = createRange(getters, sheetId, definition.labelRange);
19857
+ this.background = definition.background;
19858
+ this.verticalAxisPosition = definition.verticalAxisPosition;
19859
+ this.legendPosition = definition.legendPosition;
19860
+ this.aggregated = definition.aggregated;
19861
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
19862
+ this.useBothYAxis = definition.useBothYAxis;
19863
+ }
19864
+ static transformDefinition(definition, executed) {
19865
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
19866
+ }
19867
+ static validateChartDefinition(validator, definition) {
19868
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
19869
+ }
19870
+ getContextCreation() {
19871
+ return {
19872
+ background: this.background,
19873
+ title: this.title,
19874
+ range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
19875
+ auxiliaryRange: this.labelRange
19876
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
19877
+ : undefined,
19878
+ aggregated: this.aggregated,
19879
+ };
19880
+ }
19881
+ getDefinition() {
19882
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
19883
+ }
19884
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
19885
+ return {
19886
+ type: "combo",
19887
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
19888
+ background: this.background,
19889
+ dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
19890
+ legendPosition: this.legendPosition,
19891
+ verticalAxisPosition: this.verticalAxisPosition,
19892
+ labelRange: labelRange
19893
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
19894
+ : undefined,
19895
+ title: this.title,
19896
+ aggregated: this.aggregated,
19897
+ useBothYAxis: this.useBothYAxis,
19898
+ };
19899
+ }
19900
+ getDefinitionForExcel() {
19901
+ // Excel does not support aggregating labels
19902
+ if (this.aggregated) {
19903
+ return undefined;
19904
+ }
19905
+ const dataSets = this.dataSets
19906
+ .map((ds) => toExcelDataset(this.getters, ds))
19907
+ .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
19908
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
19909
+ return {
19910
+ ...this.getDefinition(),
19911
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
19912
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
19913
+ dataSets,
19914
+ labelRange,
19915
+ };
19916
+ }
19917
+ updateRanges(applyChange) {
19918
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
19919
+ if (!isStale) {
19920
+ return this;
19921
+ }
19922
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
19923
+ return new ComboChart(definition, this.sheetId, this.getters);
19924
+ }
19925
+ static getDefinitionFromContextCreation(context) {
19926
+ return {
19927
+ background: context.background,
19928
+ dataSets: context.range ? context.range : [],
19929
+ dataSetsHaveTitle: false,
19930
+ aggregated: context.aggregated,
19931
+ legendPosition: "top",
19932
+ title: context.title || "",
19933
+ verticalAxisPosition: "left",
19934
+ labelRange: context.auxiliaryRange || undefined,
19935
+ type: "combo",
19936
+ useBothYAxis: false,
19937
+ };
19938
+ }
19939
+ copyForSheetId(sheetId) {
19940
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
19941
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
19942
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
19943
+ return new ComboChart(definition, sheetId, this.getters);
19944
+ }
19945
+ copyInSheetId(sheetId) {
19946
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
19947
+ return new ComboChart(definition, sheetId, this.getters);
19948
+ }
19949
+ }
19950
+ function createComboChartRuntime(chart, getters) {
19951
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
19952
+ const locale = getters.getLocale();
19953
+ const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
19954
+ let labels = labelValues.formattedValues;
19955
+ let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
19956
+ if (chart.dataSetsHaveTitle &&
19957
+ dataSetsValues[0] &&
19958
+ labels.length > dataSetsValues[0].data.length) {
19959
+ labels.shift();
19960
+ }
19961
+ ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
19962
+ if (chart.aggregated) {
19963
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
19964
+ }
19965
+ const localeFormat = { format: dataSetFormat, locale };
19966
+ const fontColor = chartFontColor(chart.background);
19967
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
19968
+ const legend = {
19969
+ labels: { color: fontColor },
19970
+ };
19971
+ if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
19972
+ legend.display = false;
19973
+ }
19974
+ else {
19975
+ legend.position = chart.legendPosition;
19976
+ }
19977
+ config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
19978
+ config.options.layout = {
19979
+ padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
19980
+ };
19981
+ config.options.scales = {
19982
+ x: {
19983
+ ticks: {
19984
+ padding: 5,
19985
+ color: fontColor,
19986
+ },
19987
+ },
19988
+ };
19989
+ const verticalAxis = {
19990
+ beginAtZero: true, // the origin of the y axis is always zero
19991
+ ticks: {
19992
+ color: fontColor,
19993
+ callback: (value) => {
19994
+ value = Number(value);
19995
+ if (isNaN(value))
19996
+ return value;
19997
+ const { locale, format } = localeFormat;
19998
+ return formatValue(value, {
19999
+ locale,
20000
+ format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
20001
+ });
20002
+ },
20003
+ },
20004
+ };
20005
+ if (chart.useBothYAxis) {
20006
+ config.options.scales.y = {
20007
+ ...verticalAxis,
20008
+ position: "left",
20009
+ };
20010
+ config.options.scales.y1 = {
20011
+ ...verticalAxis,
20012
+ position: "right",
20013
+ grid: {
20014
+ display: false,
20015
+ },
20016
+ };
20017
+ }
20018
+ else {
20019
+ config.options.scales.y = {
20020
+ ...verticalAxis,
20021
+ position: chart.verticalAxisPosition,
20022
+ };
20023
+ }
20024
+ const colors = new ChartColors();
20025
+ for (let [index, { label, data }] of dataSetsValues.entries()) {
20026
+ const color = colors.next();
20027
+ const dataset = {
20028
+ label,
20029
+ data,
20030
+ borderColor: color,
20031
+ backgroundColor: color,
20032
+ yAxisID: index > 0 && chart.useBothYAxis ? "y1" : "y",
20033
+ type: index === 0 ? "bar" : "line",
20034
+ order: -index,
20035
+ };
20036
+ config.data.datasets.push(dataset);
20037
+ }
20038
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
20039
+ }
20040
+
19787
20041
  function isDataRangeValid(definition) {
19788
20042
  return definition.dataRange && !rangeReference.test(definition.dataRange)
19789
20043
  ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
@@ -20122,7 +20376,7 @@ function getBestTimeUnitForScale(labels, format, locale) {
20122
20376
  return undefined;
20123
20377
  }
20124
20378
  const labelsTimestamps = labelDates.map((date) => date.getTime());
20125
- const period = Math.max(...labelsTimestamps) - Math.min(...labelsTimestamps);
20379
+ const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);
20126
20380
  const minUnit = getFormatMinDisplayUnit(format);
20127
20381
  if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
20128
20382
  return "second";
@@ -20254,7 +20508,7 @@ function getLineOrScatterConfiguration(chart, labels, localeFormat) {
20254
20508
  },
20255
20509
  y: {
20256
20510
  position: chart.verticalAxisPosition,
20257
- beginAtZero: true,
20511
+ beginAtZero: true, // the origin of the y axis is always zero
20258
20512
  ticks: {
20259
20513
  color: fontColor,
20260
20514
  callback: (value) => {
@@ -20340,7 +20594,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
20340
20594
  const dataset = {
20341
20595
  label,
20342
20596
  data,
20343
- tension: 0,
20597
+ tension: 0, // 0 -> render straight lines, which is much faster
20344
20598
  borderColor: color,
20345
20599
  backgroundColor,
20346
20600
  pointBackgroundColor: color,
@@ -20562,7 +20816,7 @@ class PieChart extends AbstractChart {
20562
20816
  ...this.getDefinition(),
20563
20817
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
20564
20818
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
20565
- verticalAxisPosition: "left",
20819
+ verticalAxisPosition: "left", //TODO ExcelChartDefinition should be adapted, but can be done later
20566
20820
  dataSets,
20567
20821
  labelRange,
20568
20822
  };
@@ -20610,7 +20864,7 @@ function getPieConfiguration(chart, labels, localeFormat) {
20610
20864
  }
20611
20865
  function getPieColors(colors, dataSetsValues) {
20612
20866
  const pieColors = [];
20613
- const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
20867
+ const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
20614
20868
  for (let i = 0; i <= maxLength; i++) {
20615
20869
  pieColors.push(colors.next());
20616
20870
  }
@@ -20779,7 +21033,7 @@ function createScatterChartRuntime(chart, getters) {
20779
21033
  configOptions.elements = {
20780
21034
  point: {
20781
21035
  radius: 3,
20782
- hoverRadius: 3,
21036
+ hoverRadius: 3, // chartJS seems bugged, the point starts with the "hoverRadius" value
20783
21037
  hitRadius: 8,
20784
21038
  },
20785
21039
  };
@@ -20819,6 +21073,16 @@ chartRegistry.add("bar", {
20819
21073
  name: _t("Bar"),
20820
21074
  sequence: 10,
20821
21075
  });
21076
+ chartRegistry.add("combo", {
21077
+ match: (type) => type === "combo",
21078
+ createChart: (definition, sheetId, getters) => new ComboChart(definition, sheetId, getters),
21079
+ getChartRuntime: createComboChartRuntime,
21080
+ validateChartDefinition: (validator, definition) => ComboChart.validateChartDefinition(validator, definition),
21081
+ transformDefinition: (definition, executed) => ComboChart.transformDefinition(definition, executed),
21082
+ getChartDefinitionFromContextCreation: (context) => ComboChart.getDefinitionFromContextCreation(context),
21083
+ name: _t("Combo"),
21084
+ sequence: 15,
21085
+ });
20822
21086
  chartRegistry.add("line", {
20823
21087
  match: (type) => type === "line",
20824
21088
  createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),
@@ -20872,6 +21136,7 @@ chartRegistry.add("scatter", {
20872
21136
  const chartComponentRegistry = new Registry();
20873
21137
  chartComponentRegistry.add("line", ChartJsComponent);
20874
21138
  chartComponentRegistry.add("bar", ChartJsComponent);
21139
+ chartComponentRegistry.add("combo", ChartJsComponent);
20875
21140
  chartComponentRegistry.add("pie", ChartJsComponent);
20876
21141
  chartComponentRegistry.add("gauge", GaugeChartComponent);
20877
21142
  chartComponentRegistry.add("scatter", ChartJsComponent);
@@ -22225,6 +22490,7 @@ class LinkDisplay extends Component {
22225
22490
  }
22226
22491
  edit() {
22227
22492
  const { col, row } = this.props.cellPosition;
22493
+ this.env.model.selection.selectCell(col, row);
22228
22494
  this.cellPopovers.open({ col, row }, "LinkEditor");
22229
22495
  }
22230
22496
  unlink() {
@@ -23129,7 +23395,7 @@ const lightTemplateWithHeader = (colorSet) => ({
23129
23395
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23130
23396
  border: { bottom: { color: colorSet.highlight, style: "thin" } },
23131
23397
  },
23132
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23398
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23133
23399
  firstRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23134
23400
  secondRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23135
23401
  });
@@ -23147,7 +23413,7 @@ const lightTemplateAllBorders = (colorSet) => ({
23147
23413
  },
23148
23414
  },
23149
23415
  headerRow: { border: { bottom: { color: colorSet.highlight, style: "medium" } } },
23150
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23416
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23151
23417
  firstRowStripe: { style: { fillColor: colorSet.light } },
23152
23418
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23153
23419
  });
@@ -23166,7 +23432,7 @@ const mediumTemplateBandedBorders = (colorSet) => ({
23166
23432
  headerRow: {
23167
23433
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23168
23434
  },
23169
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23435
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23170
23436
  firstRowStripe: { style: { fillColor: colorSet.light } },
23171
23437
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23172
23438
  });
@@ -23202,7 +23468,7 @@ const mediumTemplateMinimalBorders = (colorSet) => ({
23202
23468
  bottom: { color: "#000000", style: "medium" },
23203
23469
  },
23204
23470
  },
23205
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23471
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23206
23472
  headerRow: {
23207
23473
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23208
23474
  border: { bottom: { color: "#000000", style: "medium" } },
@@ -23226,7 +23492,7 @@ const mediumTemplateAllBorders = (colorSet) => ({
23226
23492
  },
23227
23493
  style: { fillColor: colorSet.light },
23228
23494
  },
23229
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23495
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23230
23496
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23231
23497
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
23232
23498
  });
@@ -23257,7 +23523,7 @@ const darkTemplateNoBorders = (colorSet) => ({
23257
23523
  category: "dark",
23258
23524
  colorName: colorSet.name,
23259
23525
  wholeTable: { style: { fillColor: colorSet.light } },
23260
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23526
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23261
23527
  headerRow: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
23262
23528
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23263
23529
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
@@ -23341,13 +23607,20 @@ const TABLE_PRESETS = {
23341
23607
  * If a single cell is selected, expand the selection to non-empty adjacent cells to create a table.
23342
23608
  */
23343
23609
  function interactiveCreateTable(env, sheetId, tableConfig) {
23344
- const selection = env.model.getters.getSelectedZones();
23345
- if (selection.length === 1 && getZoneArea(selection[0]) === 1) {
23610
+ let target = env.model.getters.getSelectedZones();
23611
+ let isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
23612
+ if (target.length === 1 && !isDynamic && getZoneArea(target[0]) === 1) {
23346
23613
  env.model.selection.selectTableAroundSelection();
23614
+ target = env.model.getters.getSelectedZones();
23615
+ isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
23347
23616
  }
23348
- const target = env.model.getters.getSelectedZones();
23349
23617
  const ranges = target.map((zone) => env.model.getters.getRangeDataFromZone(sheetId, zone));
23350
- const result = env.model.dispatch("CREATE_TABLE", { ranges, sheetId, config: tableConfig });
23618
+ const result = env.model.dispatch("CREATE_TABLE", {
23619
+ ranges,
23620
+ sheetId,
23621
+ config: tableConfig,
23622
+ tableType: isDynamic ? "dynamic" : "static",
23623
+ });
23351
23624
  if (result.isCancelledBecause("TableOverlap" /* CommandResult.TableOverlap */)) {
23352
23625
  env.raiseError(TableTerms.Errors.TableOverlap);
23353
23626
  }
@@ -23415,8 +23688,8 @@ const DELETE_CONTENT_ROWS_NAME = (env) => {
23415
23688
  let last;
23416
23689
  const activesRows = env.model.getters.getActiveRows();
23417
23690
  if (activesRows.size !== 0) {
23418
- first = Math.min(...activesRows);
23419
- last = Math.max(...activesRows);
23691
+ first = largeMin([...activesRows]);
23692
+ last = largeMax([...activesRows]);
23420
23693
  }
23421
23694
  else {
23422
23695
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23444,8 +23717,8 @@ const DELETE_CONTENT_COLUMNS_NAME = (env) => {
23444
23717
  let last;
23445
23718
  const activeCols = env.model.getters.getActiveCols();
23446
23719
  if (activeCols.size !== 0) {
23447
- first = Math.min(...activeCols);
23448
- last = Math.max(...activeCols);
23720
+ first = largeMin([...activeCols]);
23721
+ last = largeMax([...activeCols]);
23449
23722
  }
23450
23723
  else {
23451
23724
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23473,8 +23746,8 @@ const REMOVE_ROWS_NAME = (env) => {
23473
23746
  let last;
23474
23747
  const activesRows = env.model.getters.getActiveRows();
23475
23748
  if (activesRows.size !== 0) {
23476
- first = Math.min(...activesRows);
23477
- last = Math.max(...activesRows);
23749
+ first = largeMin([...activesRows]);
23750
+ last = largeMax([...activesRows]);
23478
23751
  }
23479
23752
  else {
23480
23753
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23515,8 +23788,8 @@ const REMOVE_COLUMNS_NAME = (env) => {
23515
23788
  let last;
23516
23789
  const activeCols = env.model.getters.getActiveCols();
23517
23790
  if (activeCols.size !== 0) {
23518
- first = Math.min(...activeCols);
23519
- last = Math.max(...activeCols);
23791
+ first = largeMin([...activeCols]);
23792
+ last = largeMax([...activeCols]);
23520
23793
  }
23521
23794
  else {
23522
23795
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23557,7 +23830,7 @@ const INSERT_ROWS_BEFORE_ACTION = (env) => {
23557
23830
  let row;
23558
23831
  let quantity;
23559
23832
  if (activeRows.size) {
23560
- row = Math.min(...activeRows);
23833
+ row = largeMin([...activeRows]);
23561
23834
  quantity = activeRows.size;
23562
23835
  }
23563
23836
  else {
@@ -23578,7 +23851,7 @@ const INSERT_ROWS_AFTER_ACTION = (env) => {
23578
23851
  let row;
23579
23852
  let quantity;
23580
23853
  if (activeRows.size) {
23581
- row = Math.max(...activeRows);
23854
+ row = largeMax([...activeRows]);
23582
23855
  quantity = activeRows.size;
23583
23856
  }
23584
23857
  else {
@@ -23599,7 +23872,7 @@ const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
23599
23872
  let column;
23600
23873
  let quantity;
23601
23874
  if (activeCols.size) {
23602
- column = Math.min(...activeCols);
23875
+ column = largeMin([...activeCols]);
23603
23876
  quantity = activeCols.size;
23604
23877
  }
23605
23878
  else {
@@ -23620,7 +23893,7 @@ const INSERT_COLUMNS_AFTER_ACTION = (env) => {
23620
23893
  let column;
23621
23894
  let quantity;
23622
23895
  if (activeCols.size) {
23623
- column = Math.max(...activeCols);
23896
+ column = largeMax([...activeCols]);
23624
23897
  quantity = activeCols.size;
23625
23898
  }
23626
23899
  else {
@@ -23795,9 +24068,13 @@ const INSERT_TABLE = (env) => {
23795
24068
  };
23796
24069
  const DELETE_SELECTED_TABLE = (env) => {
23797
24070
  const position = env.model.getters.getActivePosition();
24071
+ const table = env.model.getters.getTable(position);
24072
+ if (!table) {
24073
+ return;
24074
+ }
23798
24075
  env.model.dispatch("REMOVE_TABLE", {
23799
24076
  sheetId: position.sheetId,
23800
- target: [positionToZone(position)],
24077
+ target: [table.range.zone],
23801
24078
  });
23802
24079
  };
23803
24080
  //------------------------------------------------------------------------------
@@ -24213,14 +24490,19 @@ const categorieFunctionAll = {
24213
24490
  children: [allFunctionListMenuBuilder],
24214
24491
  };
24215
24492
  function allFunctionListMenuBuilder() {
24216
- const fnNames = functionRegistry.getKeys();
24493
+ const fnNames = functionRegistry.getKeys().filter((key) => !functionRegistry.get(key).hidden);
24217
24494
  return createFormulaFunctions(fnNames);
24218
24495
  }
24219
24496
  const categoriesFunctionListMenuBuilder = () => {
24220
24497
  const functions = functionRegistry.content;
24221
- const categories = [...new Set(functionRegistry.getAll().map((fn) => fn.category))].filter(isDefined$1);
24498
+ const categories = [
24499
+ ...new Set(functionRegistry
24500
+ .getAll()
24501
+ .filter((fn) => !fn.hidden)
24502
+ .map((fn) => fn.category)),
24503
+ ].filter(isDefined$1);
24222
24504
  return categories.sort().map((category, i) => {
24223
- const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category);
24505
+ const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category && !functions[key].hidden);
24224
24506
  return {
24225
24507
  name: category,
24226
24508
  children: createFormulaFunctions(functionsInCategory),
@@ -27217,6 +27499,22 @@ class BarChartDesignPanel extends LineBarPieDesignPanel {
27217
27499
  static template = "o-spreadsheet-BarChartDesignPanel";
27218
27500
  }
27219
27501
 
27502
+ class ComboChartConfigPanel extends LineBarPieConfigPanel {
27503
+ static template = "o-spreadsheet-ComboChartConfigPanel";
27504
+ get shouldUseRightAxis() {
27505
+ return _t("Use right axis for line series");
27506
+ }
27507
+ onUpdateUseRightAxis(useBothYAxis) {
27508
+ this.props.updateChart(this.props.figureId, {
27509
+ useBothYAxis,
27510
+ });
27511
+ }
27512
+ }
27513
+
27514
+ class ComboChartDesignPanel extends LineBarPieDesignPanel {
27515
+ static template = "o-spreadsheet-ComboChartDesignPanel";
27516
+ }
27517
+
27220
27518
  class GaugeChartConfigPanel extends Component {
27221
27519
  static template = "o-spreadsheet-GaugeChartConfigPanel";
27222
27520
  static components = { ChartErrorSection, ChartDataSeries };
@@ -27574,6 +27872,10 @@ chartSidePanelComponentRegistry
27574
27872
  .add("bar", {
27575
27873
  configuration: BarConfigPanel,
27576
27874
  design: BarChartDesignPanel,
27875
+ })
27876
+ .add("combo", {
27877
+ configuration: ComboChartConfigPanel,
27878
+ design: ComboChartDesignPanel,
27577
27879
  })
27578
27880
  .add("pie", {
27579
27881
  configuration: LineBarPieConfigPanel,
@@ -28378,11 +28680,9 @@ css /* scss */ `
28378
28680
  }
28379
28681
  .o-cell-is-operator {
28380
28682
  margin-bottom: 5px;
28381
- width: 96%;
28382
28683
  }
28383
28684
  .o-cell-is-value {
28384
28685
  margin-bottom: 5px;
28385
- width: 96%;
28386
28686
  }
28387
28687
  .o-color-picker-widget .o-color-picker-button {
28388
28688
  pointer-events: all;
@@ -30216,7 +30516,11 @@ class SplitIntoColumnsPanel extends Component {
30216
30516
  const composerStore = useStore(ComposerStore);
30217
30517
  // The feature makes no sense if we are editing a cell, because then the selection isn't active
30218
30518
  // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
30219
- useEffect(this.props.onCloseSidePanel, () => [composerStore.editionMode]);
30519
+ useEffect((editionMode) => {
30520
+ if (editionMode !== "inactive") {
30521
+ this.props.onCloseSidePanel();
30522
+ }
30523
+ }, () => [composerStore.editionMode]);
30220
30524
  onMounted(() => {
30221
30525
  composerStore.stopEdition();
30222
30526
  });
@@ -30302,6 +30606,24 @@ function getTableContentZone(tableZone, tableConfig) {
30302
30606
  const contentZone = { ...tableZone, top: tableZone.top + numberOfHeaders };
30303
30607
  return contentZone.top <= contentZone.bottom ? contentZone : undefined;
30304
30608
  }
30609
+ function getTableTopLeft(table) {
30610
+ const range = table.range;
30611
+ return { row: range.zone.top, col: range.zone.left, sheetId: range.sheetId };
30612
+ }
30613
+ function createFilter(id, range, config, createRange) {
30614
+ const zone = range.zone;
30615
+ if (zone.left !== zone.right) {
30616
+ throw new Error("Can only define a filter on a single column");
30617
+ }
30618
+ const filteredZone = { ...zone, top: zone.top + config.numberOfHeaders };
30619
+ const filteredRange = createRange(range.sheetId, filteredZone);
30620
+ return {
30621
+ id,
30622
+ rangeWithHeaders: range,
30623
+ col: zone.left,
30624
+ filteredRange: filteredZone.top > filteredZone.bottom ? undefined : filteredRange,
30625
+ };
30626
+ }
30305
30627
  function getComputedTableStyle(tableConfig, numberOfCols, numberOfRows) {
30306
30628
  return {
30307
30629
  borders: getAllTableBorders(tableConfig, numberOfCols, numberOfRows),
@@ -30739,6 +31061,11 @@ css /* scss */ `
30739
31061
  color: #ffffff;
30740
31062
  background: #d94b4b;
30741
31063
  }
31064
+
31065
+ .o-info-icon {
31066
+ width: 14px;
31067
+ height: 14px;
31068
+ }
30742
31069
  }
30743
31070
  `;
30744
31071
  class TablePanel extends Component {
@@ -30770,6 +31097,29 @@ class TablePanel extends Component {
30770
31097
  const numberOfHeaders = hasHeaders ? 1 : 0;
30771
31098
  this.updateNumberOfHeaders(numberOfHeaders);
30772
31099
  }
31100
+ updateTableIsDynamic(isDynamic) {
31101
+ const newTableType = isDynamic ? "dynamic" : "forceStatic";
31102
+ if (newTableType === this.props.table.type) {
31103
+ return;
31104
+ }
31105
+ const uiTable = this.env.model.getters.getTable(getTableTopLeft(this.props.table));
31106
+ if (!uiTable) {
31107
+ return;
31108
+ }
31109
+ const sheetId = this.env.model.getters.getActiveSheetId();
31110
+ const result = this.env.model.dispatch("UPDATE_TABLE", {
31111
+ sheetId,
31112
+ zone: this.props.table.range.zone,
31113
+ newTableRange: uiTable.range.rangeData,
31114
+ tableType: newTableType,
31115
+ });
31116
+ const updatedTable = this.env.model.getters.getCoreTable(getTableTopLeft(this.props.table));
31117
+ if (result.isSuccessful && updatedTable) {
31118
+ const newTableRange = updatedTable.range;
31119
+ this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);
31120
+ this.state.tableZoneErrors = [];
31121
+ }
31122
+ }
30773
31123
  onChangeNumberOfHeaders(ev) {
30774
31124
  const input = ev.target;
30775
31125
  const numberOfHeaders = parseInt(input.value);
@@ -30793,35 +31143,59 @@ class TablePanel extends Component {
30793
31143
  }
30794
31144
  const sheetId = this.env.model.getters.getActiveSheetId();
30795
31145
  this.state.tableXc = ranges[0];
31146
+ const newTableRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
30796
31147
  this.state.tableZoneErrors = this.env.model.canDispatch("UPDATE_TABLE", {
30797
31148
  sheetId,
30798
31149
  zone: this.props.table.range.zone,
30799
31150
  newTableRange: this.env.model.getters.getRangeDataFromXc(sheetId, this.state.tableXc),
31151
+ tableType: this.getNewTableType(newTableRange.zone),
30800
31152
  }).reasons;
30801
31153
  }
30802
31154
  onRangeConfirmed() {
30803
31155
  const sheetId = this.env.model.getters.getActiveSheetId();
30804
- const newRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
31156
+ let newRange = this.env.model.getters.getRangeFromSheetXC(sheetId, this.state.tableXc);
31157
+ if (getZoneArea(newRange.zone) === 1) {
31158
+ const extendedZone = this.env.model.getters.getContiguousZone(sheetId, newRange.zone);
31159
+ newRange = this.env.model.getters.getRangeFromZone(sheetId, extendedZone);
31160
+ }
30805
31161
  const result = this.env.model.dispatch("UPDATE_TABLE", {
30806
31162
  sheetId,
30807
31163
  zone: this.props.table.range.zone,
30808
31164
  newTableRange: newRange.rangeData,
31165
+ tableType: this.getNewTableType(newRange.zone),
30809
31166
  });
30810
- if (result.isSuccessful) {
30811
- const position = { col: newRange.zone.left, row: newRange.zone.top };
31167
+ const position = { sheetId, col: newRange.zone.left, row: newRange.zone.top };
31168
+ const updatedTable = this.env.model.getters.getCoreTable(position);
31169
+ if (result.isSuccessful && updatedTable) {
31170
+ const newTopLeft = getTableTopLeft(updatedTable);
30812
31171
  this.env.model.selection.selectZone({
30813
- zone: positionToZone(position),
30814
- cell: position,
31172
+ zone: positionToZone(newTopLeft),
31173
+ cell: newTopLeft,
30815
31174
  });
31175
+ const newTableRange = updatedTable.range;
31176
+ this.state.tableXc = this.env.model.getters.getRangeString(newTableRange, sheetId);
31177
+ }
31178
+ else {
31179
+ const oldTableRange = this.props.table.range;
31180
+ this.state.tableXc = this.env.model.getters.getRangeString(oldTableRange, sheetId);
30816
31181
  }
30817
31182
  this.state.tableZoneErrors = [];
30818
- this.state.tableXc = result.isSuccessful
30819
- ? this.state.tableXc
30820
- : this.env.model.getters.getRangeString(this.props.table.range, sheetId);
30821
31183
  }
30822
31184
  deleteTable() {
30823
31185
  const sheetId = this.env.model.getters.getActiveSheetId();
30824
- this.env.model.dispatch("REMOVE_TABLE", { sheetId, target: [this.props.table.range.zone] });
31186
+ this.env.model.dispatch("REMOVE_TABLE", {
31187
+ sheetId,
31188
+ target: [this.props.table.range.zone],
31189
+ });
31190
+ }
31191
+ getNewTableType(newTableZone) {
31192
+ if (this.props.table.type === "forceStatic") {
31193
+ return "forceStatic";
31194
+ }
31195
+ const sheetId = this.env.model.getters.getActiveSheetId();
31196
+ return this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [newTableZone])
31197
+ ? "dynamic"
31198
+ : "static";
30825
31199
  }
30826
31200
  get tableConfig() {
30827
31201
  return this.props.table.config;
@@ -30839,6 +31213,14 @@ class TablePanel extends Component {
30839
31213
  get hasFilterCheckboxTooltip() {
30840
31214
  return this.canHaveFilters ? undefined : TableTerms.Tooltips.filterWithoutHeader;
30841
31215
  }
31216
+ get canBeDynamic() {
31217
+ const sheetId = this.env.model.getters.getActiveSheetId();
31218
+ return (this.props.table.type === "dynamic" ||
31219
+ this.env.model.getters.canCreateDynamicTableOnZones(sheetId, [this.props.table.range.zone]));
31220
+ }
31221
+ get dynamicTableTooltip() {
31222
+ return TableTerms.Tooltips.isDynamic;
31223
+ }
30842
31224
  }
30843
31225
 
30844
31226
  const sidePanelRegistry = new Registry();
@@ -30901,11 +31283,8 @@ sidePanelRegistry.add("TableSidePanel", {
30901
31283
  if (!table) {
30902
31284
  return { isOpen: false };
30903
31285
  }
30904
- return {
30905
- isOpen: true,
30906
- props: { table },
30907
- key: table.id,
30908
- };
31286
+ const coreTable = getters.getCoreTable(getTableTopLeft(table));
31287
+ return { isOpen: true, props: { table: coreTable }, key: table.id };
30909
31288
  },
30910
31289
  });
30911
31290
 
@@ -31084,6 +31463,9 @@ class FigureComponent extends Component {
31084
31463
  el?.focus({ preventScroll: true });
31085
31464
  }
31086
31465
  }, () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el]);
31466
+ onWillUnmount(() => {
31467
+ this.props.onFigureDeleted();
31468
+ });
31087
31469
  }
31088
31470
  clickAnchor(dirX, dirY, ev) {
31089
31471
  this.props.onClickAnchor(dirX, dirY, ev);
@@ -31102,6 +31484,7 @@ class FigureComponent extends Component {
31102
31484
  this.props.onFigureDeleted();
31103
31485
  ev.stopPropagation();
31104
31486
  ev.preventDefault();
31487
+ ev.stopPropagation();
31105
31488
  break;
31106
31489
  case "ArrowDown":
31107
31490
  case "ArrowLeft":
@@ -31122,6 +31505,7 @@ class FigureComponent extends Component {
31122
31505
  });
31123
31506
  ev.stopPropagation();
31124
31507
  ev.preventDefault();
31508
+ ev.stopPropagation();
31125
31509
  break;
31126
31510
  }
31127
31511
  }
@@ -31789,6 +32173,7 @@ function compareContentToSpanElement(content, node) {
31789
32173
  // -----------------------------------------------------------------------------
31790
32174
  css /* scss */ `
31791
32175
  .o-formula-assistant {
32176
+ background: #ffffff;
31792
32177
  .o-formula-assistant-head {
31793
32178
  background-color: #f2f2f2;
31794
32179
  padding: 10px;
@@ -31844,6 +32229,9 @@ class FunctionDescriptionProvider extends Component {
31844
32229
  this.assistantState.allowCellSelectionBehind = false;
31845
32230
  }, 2000);
31846
32231
  }
32232
+ get formulaArgSeparator() {
32233
+ return this.env.model.getters.getLocale().formulaArgSeparator + " ";
32234
+ }
31847
32235
  }
31848
32236
 
31849
32237
  const functions$2 = functionRegistry.content;
@@ -32003,6 +32391,12 @@ class Composer extends Component {
32003
32391
  useEffect(() => {
32004
32392
  this.processContent();
32005
32393
  });
32394
+ onPatched(() => {
32395
+ // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
32396
+ if (this.composerStore.editionMode === "inactive") {
32397
+ this.processTokenAtCursor();
32398
+ }
32399
+ });
32006
32400
  }
32007
32401
  // ---------------------------------------------------------------------------
32008
32402
  // Handlers
@@ -32741,8 +33135,7 @@ class FilterIconsOverlay extends Component {
32741
33135
  };
32742
33136
  getFilterHeadersPositions() {
32743
33137
  const sheetId = this.env.model.getters.getActiveSheetId();
32744
- const headerPositions = this.env.model.getters.getFilterHeaders(sheetId);
32745
- return headerPositions.map((position) => ({ sheetId, ...position }));
33138
+ return this.env.model.getters.getFilterHeaders(sheetId);
32746
33139
  }
32747
33140
  }
32748
33141
 
@@ -33983,11 +34376,6 @@ css /* scss */ `
33983
34376
  height: 10000px;
33984
34377
  background-color: ${SELECTION_BORDER_COLOR};
33985
34378
  }
33986
- .o-unhide-buttons {
33987
- width: fit-content;
33988
- gap: 5px;
33989
- transform: translate(-50%, 0);
33990
- }
33991
34379
  .o-unhide:hover {
33992
34380
  z-index: ${ComponentsImportance.Grid + 1};
33993
34381
  background-color: lightgrey;
@@ -34149,10 +34537,6 @@ css /* scss */ `
34149
34537
  height: 1px;
34150
34538
  background-color: ${SELECTION_BORDER_COLOR};
34151
34539
  }
34152
- .o-unhide-buttons {
34153
- height: fit-content;
34154
- transform: translate(0, -50%);
34155
- }
34156
34540
  .o-unhide:hover {
34157
34541
  z-index: ${ComponentsImportance.Grid + 1};
34158
34542
  background-color: lightgrey;
@@ -34613,19 +34997,16 @@ class GridRenderer {
34613
34997
  for (let col = left; col <= right; col++) {
34614
34998
  const colZone = { left: col, right: col, top: 0, bottom: numberOfRows - 1 };
34615
34999
  const { x, width } = this.getters.getVisibleRect(colZone);
34616
- const colHasFilter = this.getters.doesZonesContainFilter(sheetId, [colZone]);
34617
35000
  const isColActive = activeCols.has(col);
34618
35001
  const isColSelected = selectedCols.has(col);
34619
35002
  if (isColActive) {
34620
- ctx.fillStyle = colHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;
35003
+ ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;
34621
35004
  }
34622
35005
  else if (isColSelected) {
34623
- ctx.fillStyle = colHasFilter
34624
- ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR
34625
- : BACKGROUND_HEADER_SELECTED_COLOR;
35006
+ ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;
34626
35007
  }
34627
35008
  else {
34628
- ctx.fillStyle = colHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;
35009
+ ctx.fillStyle = BACKGROUND_HEADER_COLOR;
34629
35010
  }
34630
35011
  ctx.fillRect(x, 0, width, HEADER_HEIGHT);
34631
35012
  }
@@ -34633,19 +35014,16 @@ class GridRenderer {
34633
35014
  for (let row = top; row <= bottom; row++) {
34634
35015
  const rowZone = { top: row, bottom: row, left: 0, right: numberOfCols - 1 };
34635
35016
  const { y, height } = this.getters.getVisibleRect(rowZone);
34636
- const rowHasFilter = this.getters.doesZonesContainFilter(sheetId, [rowZone]);
34637
35017
  const isRowActive = activeRows.has(row);
34638
35018
  const isRowSelected = selectedRows.has(row);
34639
35019
  if (isRowActive) {
34640
- ctx.fillStyle = rowHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;
35020
+ ctx.fillStyle = BACKGROUND_HEADER_ACTIVE_COLOR;
34641
35021
  }
34642
35022
  else if (isRowSelected) {
34643
- ctx.fillStyle = rowHasFilter
34644
- ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR
34645
- : BACKGROUND_HEADER_SELECTED_COLOR;
35023
+ ctx.fillStyle = BACKGROUND_HEADER_SELECTED_COLOR;
34646
35024
  }
34647
35025
  else {
34648
- ctx.fillStyle = rowHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;
35026
+ ctx.fillStyle = BACKGROUND_HEADER_COLOR;
34649
35027
  }
34650
35028
  ctx.fillRect(0, y, HEADER_WIDTH, height);
34651
35029
  }
@@ -35366,7 +35744,7 @@ class VerticalScrollBar extends Component {
35366
35744
  onScroll(offset) {
35367
35745
  const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();
35368
35746
  this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
35369
- offsetX: scrollX,
35747
+ offsetX: scrollX, // offsetX is the same
35370
35748
  offsetY: offset,
35371
35749
  });
35372
35750
  }
@@ -35654,8 +36032,8 @@ class Grid extends Component {
35654
36032
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
35655
36033
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
35656
36034
  "Ctrl+Shift+V": () => PASTE_AS_VALUE_ACTION(this.env),
35657
- "Ctrl+Shift+<": () => this.clearFormatting(),
35658
- "Ctrl+<": () => this.clearFormatting(),
36035
+ "Ctrl+Shift+<": () => this.clearFormatting(), // for qwerty
36036
+ "Ctrl+<": () => this.clearFormatting(), // for azerty
35659
36037
  "Ctrl+Shift+ ": () => {
35660
36038
  this.env.model.selection.selectAll();
35661
36039
  },
@@ -36091,6 +36469,7 @@ const XLSX_CHART_TYPES = [
36091
36469
  "surfaceChart",
36092
36470
  "surface3DChart",
36093
36471
  "bubbleChart",
36472
+ "comboChart",
36094
36473
  ];
36095
36474
 
36096
36475
  /** In XLSX color format (no #) */
@@ -36422,10 +36801,10 @@ function convertCFCellIsOperator(xlsxCfOperator) {
36422
36801
  const CF_TYPE_CONVERSION_MAP = {
36423
36802
  aboveAverage: undefined,
36424
36803
  expression: undefined,
36425
- cellIs: undefined,
36426
- colorScale: undefined,
36804
+ cellIs: undefined, // exist but isn't an operator in o_spreadsheet
36805
+ colorScale: undefined, // exist but isn't an operator in o_spreadsheet
36427
36806
  dataBar: undefined,
36428
- iconSet: undefined,
36807
+ iconSet: undefined, // exist but isn't an operator in o_spreadsheet
36429
36808
  top10: undefined,
36430
36809
  uniqueValues: undefined,
36431
36810
  duplicateValues: undefined,
@@ -36502,6 +36881,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36502
36881
  surfaceChart: undefined,
36503
36882
  surface3DChart: undefined,
36504
36883
  bubbleChart: undefined,
36884
+ comboChart: "combo",
36505
36885
  };
36506
36886
  /** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/
36507
36887
  const SUBTOTAL_FUNCTION_CONVERSION_MAP = {
@@ -36660,7 +37040,7 @@ const XLSX_INDEXED_COLORS = {
36660
37040
  61: "993366",
36661
37041
  62: "333399",
36662
37042
  63: "333333",
36663
- 64: "000000",
37043
+ 64: "000000", // system foreground
36664
37044
  65: "FFFFFF", // system background
36665
37045
  };
36666
37046
  const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
@@ -37852,7 +38232,7 @@ function convertHyperlink(link, cellValue, warningManager) {
37852
38232
  function getSheetDims(sheet) {
37853
38233
  const dims = [0, 0];
37854
38234
  for (let row of sheet.rows) {
37855
- dims[0] = Math.max(dims[0], ...row.cells.map((cell) => toCartesian(cell.xc).col));
38235
+ dims[0] = Math.max(dims[0], largeMax(row.cells.map((cell) => toCartesian(cell.xc).col)));
37856
38236
  dims[1] = Math.max(dims[1], row.index);
37857
38237
  }
37858
38238
  dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
@@ -38718,6 +39098,9 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38718
39098
  if (!CHART_TYPE_CONVERSION_MAP[chartType]) {
38719
39099
  throw new Error(`Unsupported chart type ${chartType}`);
38720
39100
  }
39101
+ if (CHART_TYPE_CONVERSION_MAP[chartType] === "combo") {
39102
+ return this.extractComboChart(rootChartElement);
39103
+ }
38721
39104
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
38722
39105
  const chartTitle = this.mapOnElements({ parent: rootChartElement, query: "c:title a:t" }, (textElement) => {
38723
39106
  return textElement.textContent || "";
@@ -38746,6 +39129,37 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38746
39129
  };
38747
39130
  })[0];
38748
39131
  }
39132
+ extractComboChart(chartElement) {
39133
+ // Title can be separated into multiple xml elements (for styling and such), we only import the text
39134
+ const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
39135
+ return textElement.textContent || "";
39136
+ }).join("");
39137
+ const barChartGrouping = this.extractChildAttr(chartElement, "c:grouping", "val", {
39138
+ default: "clustered",
39139
+ }).asString();
39140
+ return {
39141
+ title: chartTitle,
39142
+ type: "combo",
39143
+ dataSets: [
39144
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:barChart`)),
39145
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:lineChart`)),
39146
+ ],
39147
+ labelRange: this.extractChildTextContent(chartElement, "c:ser c:cat c:f"),
39148
+ backgroundColor: this.extractChildAttr(chartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
39149
+ default: "ffffff",
39150
+ }).asString(),
39151
+ verticalAxisPosition: this.extractChildAttr(chartElement, "c:valAx > c:axPos", "val", {
39152
+ default: "l",
39153
+ }).asString() === "r"
39154
+ ? "right"
39155
+ : "left",
39156
+ legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
39157
+ default: "b",
39158
+ }).asString()],
39159
+ stacked: barChartGrouping === "stacked",
39160
+ fontColor: "000000",
39161
+ };
39162
+ }
38749
39163
  extractChartDatasets(chartElement) {
38750
39164
  return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
38751
39165
  return {
@@ -38763,12 +39177,21 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38763
39177
  if (!plotAreaElement) {
38764
39178
  throw new Error("Missing plot area in the chart definition.");
38765
39179
  }
39180
+ let globalTag = undefined;
38766
39181
  for (let child of plotAreaElement.children) {
38767
39182
  const tag = removeTagEscapedNamespaces(child.tagName);
38768
39183
  if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
38769
- return tag;
39184
+ if (!globalTag) {
39185
+ globalTag = tag;
39186
+ }
39187
+ else if (globalTag !== tag) {
39188
+ globalTag = "comboChart";
39189
+ }
38770
39190
  }
38771
39191
  }
39192
+ if (globalTag) {
39193
+ return globalTag;
39194
+ }
38772
39195
  throw new Error("Unknown chart type");
38773
39196
  }
38774
39197
  }
@@ -42359,6 +42782,9 @@ class DataValidationPlugin extends CorePlugin {
42359
42782
  if (newRule.criterion.type === "isBoolean") {
42360
42783
  this.setCenterStyleToBooleanCells(newRule);
42361
42784
  }
42785
+ else if (newRule.criterion.type === "isValueInList") {
42786
+ newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
42787
+ }
42362
42788
  const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
42363
42789
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
42364
42790
  if (ruleIndex !== -1) {
@@ -42755,7 +43181,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
42755
43181
  if (hiddenElements.size >= elements) {
42756
43182
  return "TooManyHiddenElements" /* CommandResult.TooManyHiddenElements */;
42757
43183
  }
42758
- else if (Math.min(...cmd.elements) < 0 || Math.max(...cmd.elements) > elements) {
43184
+ else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
42759
43185
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
42760
43186
  }
42761
43187
  else {
@@ -43084,7 +43510,6 @@ class MergePlugin extends CorePlugin {
43084
43510
  "isInSameMerge",
43085
43511
  "isMergeHidden",
43086
43512
  "getMainCellPosition",
43087
- "getBottomLeftCell",
43088
43513
  "expandZone",
43089
43514
  "doesIntersectMerge",
43090
43515
  "doesColumnsHaveCommonMerges",
@@ -43266,13 +43691,6 @@ class MergePlugin extends CorePlugin {
43266
43691
  const mergeTopLeftPos = this.getMerge(position).topLeft;
43267
43692
  return { sheetId: position.sheetId, col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };
43268
43693
  }
43269
- getBottomLeftCell(position) {
43270
- if (!this.isInMerge(position)) {
43271
- return position;
43272
- }
43273
- const { bottom, left } = this.getMerge(position);
43274
- return { sheetId: position.sheetId, col: left, row: bottom };
43275
- }
43276
43694
  isMergeHidden(sheetId, merge) {
43277
43695
  const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);
43278
43696
  const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);
@@ -43573,8 +43991,8 @@ class RangeAdapter {
43573
43991
  let newRange = range;
43574
43992
  let changeType = "NONE";
43575
43993
  for (let group of groups) {
43576
- const min = Math.min(...group);
43577
- const max = Math.max(...group);
43994
+ const min = largeMin(group);
43995
+ const max = largeMax(group);
43578
43996
  if (range.zone[start] <= min && min <= range.zone[end]) {
43579
43997
  const toRemove = Math.min(range.zone[end], max) - min + 1;
43580
43998
  changeType = "RESIZE";
@@ -43956,7 +44374,6 @@ class SheetPlugin extends CorePlugin {
43956
44374
  "getSheetIds",
43957
44375
  "getVisibleSheetIds",
43958
44376
  "isSheetVisible",
43959
- "getEvaluationSheets",
43960
44377
  "doesHeaderExist",
43961
44378
  "doesHeadersExist",
43962
44379
  "getCell",
@@ -44023,8 +44440,8 @@ class SheetPlugin extends CorePlugin {
44023
44440
  }
44024
44441
  return "Success" /* CommandResult.Success */;
44025
44442
  case "REMOVE_COLUMNS_ROWS": {
44026
- const min = Math.min(...cmd.elements);
44027
- const max = Math.max(...cmd.elements);
44443
+ const min = largeMin(cmd.elements);
44444
+ const max = largeMax(cmd.elements);
44028
44445
  if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
44029
44446
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
44030
44447
  }
@@ -44215,9 +44632,6 @@ class SheetPlugin extends CorePlugin {
44215
44632
  getVisibleSheetIds() {
44216
44633
  return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));
44217
44634
  }
44218
- getEvaluationSheets() {
44219
- return this.sheets;
44220
- }
44221
44635
  doesHeaderExist(sheetId, dimension, index) {
44222
44636
  return dimension === "COL"
44223
44637
  ? index >= 0 && index < this.getNumberCols(sheetId)
@@ -44226,13 +44640,6 @@ class SheetPlugin extends CorePlugin {
44226
44640
  doesHeadersExist(sheetId, dimension, headerIndexes) {
44227
44641
  return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));
44228
44642
  }
44229
- getRow(sheetId, index) {
44230
- const row = this.getSheet(sheetId).rows[index];
44231
- if (!row) {
44232
- throw new Error(`Row ${row} not found.`);
44233
- }
44234
- return row;
44235
- }
44236
44643
  getCell({ sheetId, col, row }) {
44237
44644
  const sheet = this.tryGetSheet(sheetId);
44238
44645
  const cellId = sheet?.rows[row]?.cells[col];
@@ -44817,23 +45224,12 @@ class SheetPlugin extends CorePlugin {
44817
45224
  }
44818
45225
 
44819
45226
  class TablePlugin extends CorePlugin {
44820
- static getters = [
44821
- "doesZonesContainFilter",
44822
- "getFilter",
44823
- "getFilters",
44824
- "getTable",
44825
- "getTables",
44826
- "getTablesInZone",
44827
- "getTablesOverlappingZones",
44828
- "getFilterId",
44829
- "getFilterHeaders",
44830
- "isFilterHeader",
44831
- ];
45227
+ static getters = ["getCoreTable", "getCoreTables"];
44832
45228
  tables = {};
44833
45229
  adaptRanges(applyChange, sheetId) {
44834
45230
  const sheetIds = sheetId ? [sheetId] : this.getters.getSheetIds();
44835
45231
  for (const sheetId of sheetIds) {
44836
- for (const table of this.getTables(sheetId)) {
45232
+ for (const table of this.getCoreTables(sheetId)) {
44837
45233
  this.applyRangeChangeOnTable(sheetId, table, applyChange);
44838
45234
  }
44839
45235
  }
@@ -44849,15 +45245,16 @@ class TablePlugin extends CorePlugin {
44849
45245
  ? "TableOverlap" /* CommandResult.TableOverlap */
44850
45246
  : "Success" /* CommandResult.Success */, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));
44851
45247
  case "UPDATE_TABLE":
44852
- const updatedTable = this.getTables(cmd.sheetId).find((table) => deepEquals(table.range.zone, cmd.zone));
45248
+ const updatedTable = this.getTableFromZone(cmd.sheetId, cmd.zone);
44853
45249
  if (!updatedTable) {
44854
45250
  return "TableNotFound" /* CommandResult.TableNotFound */;
44855
45251
  }
44856
45252
  return this.checkValidations(cmd, this.checkUpdatedTableZoneIsValid, (cmd) => this.checkTableConfigUpdateIsValid(cmd.config));
44857
45253
  case "ADD_MERGE":
44858
- for (const merge of cmd.target) {
44859
- for (const table of this.getTables(cmd.sheetId)) {
44860
- if (overlap(table.range.zone, merge)) {
45254
+ for (const table of this.getCoreTables(cmd.sheetId)) {
45255
+ const tableZone = table.range.zone;
45256
+ for (const merge of cmd.target) {
45257
+ if (overlap(tableZone, merge)) {
44861
45258
  return "MergeInTable" /* CommandResult.MergeInTable */;
44862
45259
  }
44863
45260
  }
@@ -44879,8 +45276,11 @@ class TablePlugin extends CorePlugin {
44879
45276
  }
44880
45277
  case "DUPLICATE_SHEET": {
44881
45278
  const newTables = {};
44882
- for (const table of this.getTables(cmd.sheetId)) {
44883
- newTables[table.id] = this.copyTableForSheet(cmd.sheetIdTo, table);
45279
+ for (const table of this.getCoreTables(cmd.sheetId)) {
45280
+ newTables[table.id] =
45281
+ table.type === "dynamic"
45282
+ ? this.copyDynamicTableForSheet(cmd.sheetIdTo, table)
45283
+ : this.copyStaticTableForSheet(cmd.sheetIdTo, table);
44884
45284
  }
44885
45285
  this.history.update("tables", cmd.sheetIdTo, newTables);
44886
45286
  break;
@@ -44891,14 +45291,17 @@ class TablePlugin extends CorePlugin {
44891
45291
  const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
44892
45292
  this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
44893
45293
  const id = this.uuidGenerator.uuidv4();
44894
- const newTable = this.createTable(id, union, cmd.config || DEFAULT_TABLE_CONFIG);
45294
+ const config = cmd.config || DEFAULT_TABLE_CONFIG;
45295
+ const newTable = cmd.tableType === "dynamic"
45296
+ ? this.createDynamicTable(id, union, config)
45297
+ : this.createStaticTable(id, cmd.tableType, union, config);
44895
45298
  this.history.update("tables", cmd.sheetId, newTable.id, newTable);
44896
45299
  break;
44897
45300
  }
44898
45301
  case "REMOVE_TABLE": {
44899
45302
  const tables = {};
44900
- for (const table of this.getTables(cmd.sheetId)) {
44901
- if (cmd.target.every((zone) => !intersection(zone, table.range.zone))) {
45303
+ for (const table of this.getCoreTables(cmd.sheetId)) {
45304
+ if (cmd.target.every((zone) => !intersection(table.range.zone, zone))) {
44902
45305
  tables[table.id] = table;
44903
45306
  }
44904
45307
  }
@@ -44906,23 +45309,15 @@ class TablePlugin extends CorePlugin {
44906
45309
  break;
44907
45310
  }
44908
45311
  case "UPDATE_TABLE": {
44909
- const table = this.getTables(cmd.sheetId).find((table) => deepEquals(table.range.zone, cmd.zone));
44910
- if (table) {
44911
- const newTableRange = cmd.newTableRange
44912
- ? this.getters.getRangeFromRangeData(cmd.newTableRange)
44913
- : undefined;
44914
- if (newTableRange) {
44915
- const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, newTableRange.zone);
44916
- this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
44917
- }
44918
- const newTable = this.updateTable(table, newTableRange, cmd.config);
44919
- this.history.update("tables", cmd.sheetId, table.id, newTable);
44920
- }
45312
+ this.updateTable(cmd);
44921
45313
  break;
44922
45314
  }
44923
45315
  case "UPDATE_CELL": {
44924
45316
  const sheetId = cmd.sheetId;
44925
- for (const table of this.getTables(sheetId)) {
45317
+ for (const table of this.getCoreTables(sheetId)) {
45318
+ if (table.type === "dynamic") {
45319
+ continue;
45320
+ }
44926
45321
  const direction = this.canUpdateCellCmdExtendTable(cmd, table);
44927
45322
  if (direction === "down") {
44928
45323
  this.extendTableDown(sheetId, table);
@@ -44946,66 +45341,24 @@ class TablePlugin extends CorePlugin {
44946
45341
  }
44947
45342
  }
44948
45343
  }
44949
- getFilters(sheetId) {
44950
- return this.getTables(sheetId)
44951
- .filter((table) => table.config.hasFilters)
44952
- .map((table) => table.filters)
44953
- .flat();
44954
- }
44955
- getTables(sheetId) {
45344
+ getCoreTables(sheetId) {
44956
45345
  return this.tables[sheetId] ? Object.values(this.tables[sheetId]).filter(isDefined$1) : [];
44957
45346
  }
44958
- getFilter(position) {
44959
- const table = this.getTable(position);
44960
- if (!table || !table.config.hasFilters) {
44961
- return undefined;
44962
- }
44963
- return table.filters.find((filter) => filter.col === position.col);
44964
- }
44965
- getFilterId(position) {
44966
- return this.getFilter(position)?.id;
44967
- }
44968
- getTable({ sheetId, col, row }) {
44969
- return this.getTables(sheetId).find((table) => isInside(col, row, table.range.zone));
44970
- }
44971
- /** Get the filter tables that are fully inside the given zone */
44972
- getTablesInZone(sheetId, zone) {
44973
- return this.getTables(sheetId).filter((table) => isZoneInside(table.range.zone, zone));
45347
+ getCoreTable({ sheetId, col, row }) {
45348
+ return this.getCoreTables(sheetId).find((table) => isInside(col, row, table.range.zone));
44974
45349
  }
44975
45350
  getTablesOverlappingZones(sheetId, zones) {
44976
- return this.getTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
44977
- }
44978
- doesZonesContainFilter(sheetId, zones) {
44979
- return (this.getTablesOverlappingZones(sheetId, zones).filter((table) => table.config.hasFilters)
44980
- .length > 0);
44981
- }
44982
- getFilterHeaders(sheetId) {
44983
- const headers = [];
44984
- for (const table of this.getTables(sheetId)) {
44985
- if (!table.config.hasFilters) {
44986
- continue;
44987
- }
44988
- const zone = table.range.zone;
44989
- const row = zone.top;
44990
- for (let col = zone.left; col <= zone.right; col++) {
44991
- headers.push({ col, row });
44992
- }
44993
- }
44994
- return headers;
44995
- }
44996
- isFilterHeader({ sheetId, col, row }) {
44997
- const headers = this.getFilterHeaders(sheetId);
44998
- return headers.some((header) => header.col === col && header.row === row);
45351
+ return this.getCoreTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
44999
45352
  }
45000
45353
  /** Extend a table down one row */
45001
45354
  extendTableDown(sheetId, table) {
45002
45355
  const newRange = this.getters.extendRange(table.range, "ROW", 1);
45003
- this.history.update("tables", sheetId, table.id, this.updateTable(table, newRange));
45356
+ this.history.update("tables", sheetId, table.id, this.updateStaticTable(table, newRange));
45004
45357
  }
45005
45358
  /** Extend a table right one col */
45006
45359
  extendTableRight(sheetId, table) {
45007
45360
  const newRange = this.getters.extendRange(table.range, "COL", 1);
45008
- this.history.update("tables", sheetId, table.id, this.updateTable(table, newRange));
45361
+ this.history.update("tables", sheetId, table.id, this.updateStaticTable(table, newRange));
45009
45362
  }
45010
45363
  /**
45011
45364
  * Check if an UpdateCell command should cause the given table to be extended by one row or col.
@@ -45042,12 +45395,22 @@ class TablePlugin extends CorePlugin {
45042
45395
  const cellContent = this.getters.getCell(cellPosition)?.content;
45043
45396
  if (cellContent ||
45044
45397
  this.getters.isInMerge(cellPosition) ||
45045
- this.getters.getTable(cellPosition)) {
45398
+ this.getTablesOverlappingZones(sheetId, [positionToZone(position)]).length) {
45046
45399
  return "none";
45047
45400
  }
45048
45401
  }
45049
45402
  return direction;
45050
45403
  }
45404
+ getTableFromZone(sheetId, zone) {
45405
+ for (const table of this.getCoreTables(sheetId)) {
45406
+ const tableZone = table.range.zone;
45407
+ // Only check top left to match dynamic tables
45408
+ if (tableZone.left === zone.left && tableZone.top === zone.top) {
45409
+ return table;
45410
+ }
45411
+ }
45412
+ return undefined;
45413
+ }
45051
45414
  checkUpdatedTableZoneIsValid(cmd) {
45052
45415
  if (!cmd.newTableRange) {
45053
45416
  return "Success" /* CommandResult.Success */;
@@ -45057,7 +45420,11 @@ class TablePlugin extends CorePlugin {
45057
45420
  if (zoneIsInSheet !== "Success" /* CommandResult.Success */) {
45058
45421
  return zoneIsInSheet;
45059
45422
  }
45060
- const overlappingTables = this.getTablesOverlappingZones(cmd.sheetId, [newTableZone]).filter((table) => !deepEquals(table.range.zone, cmd.zone));
45423
+ const updatedTable = this.getTableFromZone(cmd.sheetId, cmd.zone);
45424
+ if (!updatedTable) {
45425
+ return "TableNotFound" /* CommandResult.TableNotFound */;
45426
+ }
45427
+ const overlappingTables = this.getTablesOverlappingZones(cmd.sheetId, [newTableZone]).filter((table) => table.id !== updatedTable.id);
45061
45428
  return overlappingTables.length ? "TableOverlap" /* CommandResult.TableOverlap */ : "Success" /* CommandResult.Success */;
45062
45429
  }
45063
45430
  checkTableConfigUpdateIsValid(config) {
@@ -45075,7 +45442,7 @@ class TablePlugin extends CorePlugin {
45075
45442
  }
45076
45443
  return "Success" /* CommandResult.Success */;
45077
45444
  }
45078
- createTable(id, tableRange, config, filters) {
45445
+ createStaticTable(id, type, tableRange, config, filters) {
45079
45446
  const zone = tableRange.zone;
45080
45447
  if (!filters) {
45081
45448
  filters = [];
@@ -45090,9 +45457,51 @@ class TablePlugin extends CorePlugin {
45090
45457
  range: tableRange,
45091
45458
  filters,
45092
45459
  config,
45460
+ type,
45093
45461
  };
45094
45462
  }
45095
- updateTable(table, newRange, configUpdate) {
45463
+ createDynamicTable(id, tableRange, config) {
45464
+ const zone = zoneToTopLeft(tableRange.zone);
45465
+ return {
45466
+ id,
45467
+ range: this.getters.getRangeFromZone(tableRange.sheetId, zone),
45468
+ config,
45469
+ type: "dynamic",
45470
+ };
45471
+ }
45472
+ updateTable(cmd) {
45473
+ const table = this.getTableFromZone(cmd.sheetId, cmd.zone);
45474
+ if (!table) {
45475
+ return;
45476
+ }
45477
+ const newTableRange = cmd.newTableRange
45478
+ ? this.getters.getRangeFromRangeData(cmd.newTableRange)
45479
+ : undefined;
45480
+ if (newTableRange) {
45481
+ const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, newTableRange.zone);
45482
+ this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
45483
+ }
45484
+ const range = newTableRange || table.range;
45485
+ const newConfig = this.updateTableConfig(cmd.config, table.config);
45486
+ const newTableType = cmd.tableType ?? table.type;
45487
+ if ((newTableType === "dynamic" && table.type !== "dynamic") ||
45488
+ (newTableType !== "dynamic" && table.type === "dynamic")) {
45489
+ const newTable = newTableType === "dynamic"
45490
+ ? this.createDynamicTable(table.id, range, newConfig)
45491
+ : this.createStaticTable(table.id, newTableType, range, newConfig);
45492
+ this.history.update("tables", cmd.sheetId, table.id, newTable);
45493
+ }
45494
+ else {
45495
+ const updatedTable = table.type === "dynamic"
45496
+ ? this.updateDynamicTable(table, range, newConfig)
45497
+ : this.updateStaticTable(table, range, newConfig, newTableType);
45498
+ this.history.update("tables", cmd.sheetId, table.id, updatedTable);
45499
+ }
45500
+ }
45501
+ updateStaticTable(table, newRange, configUpdate, newTableType = table.type) {
45502
+ if (newTableType === "dynamic") {
45503
+ throw new Error("Cannot use updateStaticTable to update a dynamic table");
45504
+ }
45096
45505
  const tableRange = newRange ? newRange : table.range;
45097
45506
  const tableZone = tableRange.zone;
45098
45507
  const newConfig = this.updateTableConfig(configUpdate, table.config);
@@ -45113,8 +45522,16 @@ class TablePlugin extends CorePlugin {
45113
45522
  range: tableRange,
45114
45523
  config,
45115
45524
  filters: filters.length ? filters : table.filters,
45525
+ type: newTableType,
45116
45526
  };
45117
45527
  }
45528
+ updateDynamicTable(table, newRange, newConfig) {
45529
+ const range = newRange
45530
+ ? this.getters.getRangeFromZone(newRange.sheetId, zoneToTopLeft(newRange.zone))
45531
+ : table.range;
45532
+ const config = newConfig ? newConfig : table.config;
45533
+ return { ...table, range, config };
45534
+ }
45118
45535
  /**
45119
45536
  * Update the old config of a table with the new partial config from an UpdateTable command.
45120
45537
  *
@@ -45136,35 +45553,29 @@ class TablePlugin extends CorePlugin {
45136
45553
  }
45137
45554
  createFilterFromZone(id, sheetId, zone, config) {
45138
45555
  const range = this.getters.getRangeFromZone(sheetId, zone);
45139
- return this.createFilter(id, range, config);
45140
- }
45141
- createFilter(id, range, config) {
45142
- const zone = range.zone;
45143
- if (zone.left !== zone.right) {
45144
- throw new Error("Can only define a filter on a single column");
45145
- }
45146
- const contentZone = getTableContentZone(zone, config);
45147
- const filteredRange = contentZone
45148
- ? this.getters.getRangeFromZone(range.sheetId, contentZone)
45149
- : undefined;
45150
- return {
45151
- id,
45152
- rangeWithHeaders: range,
45153
- col: zone.left,
45154
- filteredRange,
45155
- };
45556
+ return createFilter(id, range, config, this.getters.getRangeFromZone);
45156
45557
  }
45157
- copyTableForSheet(sheetId, table) {
45558
+ copyStaticTableForSheet(sheetId, table) {
45158
45559
  const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);
45159
45560
  const newFilters = table.filters.map((filter) => {
45160
45561
  const newFilterRange = this.getters.getRangeFromZone(sheetId, filter.rangeWithHeaders.zone);
45161
- return this.createFilter(filter.id, newFilterRange, table.config);
45562
+ return createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);
45162
45563
  });
45163
45564
  return {
45164
45565
  id: table.id,
45165
45566
  range: newRange,
45166
45567
  filters: newFilters,
45167
45568
  config: deepCopy(table.config),
45569
+ type: table.type,
45570
+ };
45571
+ }
45572
+ copyDynamicTableForSheet(sheetId, table) {
45573
+ const newRange = this.getters.getRangeFromZone(sheetId, table.range.zone);
45574
+ return {
45575
+ id: table.id,
45576
+ range: newRange,
45577
+ config: deepCopy(table.config),
45578
+ type: "dynamic",
45168
45579
  };
45169
45580
  }
45170
45581
  applyRangeChangeOnTable(sheetId, table, applyChange) {
@@ -45179,6 +45590,11 @@ class TablePlugin extends CorePlugin {
45179
45590
  default:
45180
45591
  newTableRange = tableRangeChange.range;
45181
45592
  }
45593
+ if (table.type === "dynamic") {
45594
+ const newTable = this.updateDynamicTable(table, newTableRange);
45595
+ this.history.update("tables", sheetId, table.id, newTable);
45596
+ return;
45597
+ }
45182
45598
  const filters = [];
45183
45599
  for (const filter of table.filters) {
45184
45600
  const filterRangeChange = applyChange(filter.rangeWithHeaders);
@@ -45190,7 +45606,7 @@ class TablePlugin extends CorePlugin {
45190
45606
  break;
45191
45607
  default:
45192
45608
  const newFilterRange = filterRangeChange.range;
45193
- const newFilter = this.createFilter(filter.id, newFilterRange, table.config);
45609
+ const newFilter = createFilter(filter.id, newFilterRange, table.config, this.getters.getRangeFromZone);
45194
45610
  filters.push(newFilter);
45195
45611
  }
45196
45612
  }
@@ -45205,7 +45621,7 @@ class TablePlugin extends CorePlugin {
45205
45621
  }
45206
45622
  filters.sort((f1, f2) => f1.col - f2.col);
45207
45623
  }
45208
- const newTable = this.createTable(table.id, newTableRange, table.config, filters);
45624
+ const newTable = this.createStaticTable(table.id, table.type, newTableRange, table.config, filters);
45209
45625
  this.history.update("tables", sheetId, table.id, newTable);
45210
45626
  }
45211
45627
  // ---------------------------------------------------------------------------
@@ -45216,16 +45632,20 @@ class TablePlugin extends CorePlugin {
45216
45632
  for (const tableData of sheet.tables || []) {
45217
45633
  const uuid = this.uuidGenerator.uuidv4();
45218
45634
  const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;
45219
- const tableRange = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
45220
- const table = this.createTable(uuid, tableRange, tableConfig);
45635
+ const range = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
45636
+ const tableType = tableData.type || "static";
45637
+ const table = tableType === "dynamic"
45638
+ ? this.createDynamicTable(uuid, range, tableConfig)
45639
+ : this.createStaticTable(uuid, tableType, range, tableConfig);
45221
45640
  this.history.update("tables", sheet.id, table.id, table);
45222
45641
  }
45223
45642
  }
45224
45643
  }
45225
45644
  export(data) {
45226
45645
  for (const sheet of data.sheets) {
45227
- for (const table of this.getTables(sheet.id)) {
45228
- const tableData = { range: zoneToXc(table.range.zone) };
45646
+ for (const table of this.getCoreTables(sheet.id)) {
45647
+ const range = zoneToXc(table.range.zone);
45648
+ const tableData = { range, type: table.type };
45229
45649
  if (!deepEquals(table.config, DEFAULT_TABLE_CONFIG)) {
45230
45650
  tableData.config = table.config;
45231
45651
  }
@@ -45788,12 +46208,6 @@ class CompilationParametersBuilder {
45788
46208
  : _t("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
45789
46209
  }
45790
46210
  const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
45791
- return this.readCell(position);
45792
- }
45793
- readCell(position) {
45794
- if (!this.getters.tryGetSheet(position.sheetId)) {
45795
- throw new EvaluationError(_t("Invalid sheet name"));
45796
- }
45797
46211
  return this.computeCell(position);
45798
46212
  }
45799
46213
  /**
@@ -45829,7 +46243,7 @@ class CompilationParametersBuilder {
45829
46243
  matrix[colIndex] = new Array(height);
45830
46244
  for (let row = _zone.top; row <= _zone.bottom; row++) {
45831
46245
  const rowIndex = row - _zone.top;
45832
- matrix[colIndex][rowIndex] = this.readCell({ sheetId, col, row });
46246
+ matrix[colIndex][rowIndex] = this.computeCell({ sheetId, col, row });
45833
46247
  }
45834
46248
  }
45835
46249
  this.rangeCache[cacheKey] = matrix;
@@ -46937,18 +47351,22 @@ class Evaluator {
46937
47351
  getEvaluatedCell(position) {
46938
47352
  return this.evaluatedCells.get(position) || EMPTY_CELL;
46939
47353
  }
46940
- getSpreadPositionsOf(position) {
47354
+ getSpreadZone(position) {
46941
47355
  if (!this.spreadingRelations.isArrayFormula(position)) {
46942
- return [];
47356
+ return undefined;
46943
47357
  }
46944
- return Array.from(this.spreadingRelations.getArrayResultPositions(position));
47358
+ if (this.evaluatedCells.get(position)?.type === CellValueType.error) {
47359
+ return positionToZone(position);
47360
+ }
47361
+ const spreadPositions = Array.from(this.spreadingRelations.getArrayResultPositions(position));
47362
+ return union(positionToZone(position), unionPositionsToZone(spreadPositions));
46945
47363
  }
46946
47364
  getEvaluatedPositions() {
46947
47365
  return this.evaluatedCells.keys();
46948
47366
  }
46949
47367
  getArrayFormulaSpreadingOn(position) {
46950
47368
  if (!this.spreadingRelations.hasArrayFormulaResult(position)) {
46951
- return undefined;
47369
+ return this.spreadingRelations.isArrayFormula(position) ? position : undefined;
46952
47370
  }
46953
47371
  const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(position);
46954
47372
  return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
@@ -47077,6 +47495,7 @@ class Evaluator {
47077
47495
  if (!this.blockedArrayFormulas.has(position)) {
47078
47496
  this.invalidateSpreading(position);
47079
47497
  }
47498
+ this.spreadingRelations.removeNode(position);
47080
47499
  const cell = this.getters.getCell(position);
47081
47500
  if (cell === undefined) {
47082
47501
  return EMPTY_CELL;
@@ -47187,7 +47606,6 @@ class Evaluator {
47187
47606
  this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([child]));
47188
47607
  this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(child));
47189
47608
  }
47190
- this.spreadingRelations.removeNode(position);
47191
47609
  }
47192
47610
  // ----------------------------------------------------------
47193
47611
  // COMMON FUNCTIONALITY
@@ -47346,7 +47764,7 @@ class EvaluationPlugin extends UIPlugin {
47346
47764
  "getEvaluatedCell",
47347
47765
  "getEvaluatedCells",
47348
47766
  "getEvaluatedCellsInZone",
47349
- "getSpreadPositionsOf",
47767
+ "getSpreadZone",
47350
47768
  "getArrayFormulaSpreadingOn",
47351
47769
  "isEmpty",
47352
47770
  ];
@@ -47452,8 +47870,11 @@ class EvaluationPlugin extends UIPlugin {
47452
47870
  getEvaluatedCellsInZone(sheetId, zone) {
47453
47871
  return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
47454
47872
  }
47455
- getSpreadPositionsOf(position) {
47456
- return this.evaluator.getSpreadPositionsOf(position);
47873
+ /**
47874
+ * Return the spread zone the position is part of, if any
47875
+ */
47876
+ getSpreadZone(position) {
47877
+ return this.evaluator.getSpreadZone(position);
47457
47878
  }
47458
47879
  getArrayFormulaSpreadingOn(position) {
47459
47880
  return this.evaluator.getArrayFormulaSpreadingOn(position);
@@ -47493,7 +47914,7 @@ class EvaluationPlugin extends UIPlugin {
47493
47914
  ? getItemId(newFormat, data.formats)
47494
47915
  : exportedCellData.format;
47495
47916
  let content;
47496
- if (formulaCell instanceof FormulaCellWithDependencies) {
47917
+ if (isFormula && formulaCell instanceof FormulaCellWithDependencies) {
47497
47918
  content = formulaCell.contentWithFixedReferences;
47498
47919
  }
47499
47920
  else {
@@ -47541,17 +47962,17 @@ function isBadExpression(tokens) {
47541
47962
  */
47542
47963
  function sortWithClusters(colorsToSort) {
47543
47964
  const clusters = [
47544
- { leadColor: rgba(255, 0, 0), colors: [] },
47545
- { leadColor: rgba(255, 128, 0), colors: [] },
47546
- { leadColor: rgba(128, 128, 0), colors: [] },
47547
- { leadColor: rgba(128, 255, 0), colors: [] },
47548
- { leadColor: rgba(0, 255, 0), colors: [] },
47549
- { leadColor: rgba(0, 255, 128), colors: [] },
47550
- { leadColor: rgba(0, 255, 255), colors: [] },
47551
- { leadColor: rgba(0, 127, 255), colors: [] },
47552
- { leadColor: rgba(0, 0, 255), colors: [] },
47553
- { leadColor: rgba(127, 0, 255), colors: [] },
47554
- { leadColor: rgba(128, 0, 128), colors: [] },
47965
+ { leadColor: rgba(255, 0, 0), colors: [] }, // red
47966
+ { leadColor: rgba(255, 128, 0), colors: [] }, // orange
47967
+ { leadColor: rgba(128, 128, 0), colors: [] }, // yellow
47968
+ { leadColor: rgba(128, 255, 0), colors: [] }, // chartreuse
47969
+ { leadColor: rgba(0, 255, 0), colors: [] }, // green
47970
+ { leadColor: rgba(0, 255, 128), colors: [] }, // spring green
47971
+ { leadColor: rgba(0, 255, 255), colors: [] }, // cyan
47972
+ { leadColor: rgba(0, 127, 255), colors: [] }, // azure
47973
+ { leadColor: rgba(0, 0, 255), colors: [] }, // blue
47974
+ { leadColor: rgba(127, 0, 255), colors: [] }, // violet
47975
+ { leadColor: rgba(128, 0, 128), colors: [] }, // magenta
47555
47976
  { leadColor: rgba(255, 0, 128), colors: [] }, // rose
47556
47977
  ];
47557
47978
  for (const color of colorsToSort.map(colorToRGBA)) {
@@ -47942,13 +48363,13 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
47942
48363
  .map((cell) => cell.value);
47943
48364
  switch (threshold.type) {
47944
48365
  case "value":
47945
- const result = functionName === "max" ? Math.max(...rangeValues) : Math.min(...rangeValues);
48366
+ const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
47946
48367
  return result;
47947
48368
  case "number":
47948
48369
  return Number(threshold.value);
47949
48370
  case "percentage":
47950
- const min = Math.min(...rangeValues);
47951
- const max = Math.max(...rangeValues);
48371
+ const min = largeMin(rangeValues);
48372
+ const max = largeMax(rangeValues);
47952
48373
  const delta = max - min;
47953
48374
  return min + (delta * Number(threshold.value)) / 100;
47954
48375
  case "percentile":
@@ -48285,6 +48706,180 @@ class EvaluationDataValidationPlugin extends UIPlugin {
48285
48706
  }
48286
48707
  }
48287
48708
 
48709
+ class DynamicTablesPlugin extends UIPlugin {
48710
+ static getters = [
48711
+ "canCreateDynamicTableOnZones",
48712
+ "doesZonesContainFilter",
48713
+ "getFilter",
48714
+ "getFilters",
48715
+ "getTable",
48716
+ "getTables",
48717
+ "getTablesOverlappingZones",
48718
+ "getFilterId",
48719
+ "getFilterHeaders",
48720
+ "isFilterHeader",
48721
+ ];
48722
+ tables = {};
48723
+ handle(cmd) {
48724
+ if (invalidateEvaluationCommands.has(cmd.type) ||
48725
+ (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
48726
+ cmd.type === "EVALUATE_CELLS") {
48727
+ this.tables = {};
48728
+ return;
48729
+ }
48730
+ switch (cmd.type) {
48731
+ case "CREATE_TABLE":
48732
+ case "REMOVE_TABLE":
48733
+ case "UPDATE_TABLE":
48734
+ case "DELETE_CONTENT":
48735
+ this.tables = {};
48736
+ break;
48737
+ }
48738
+ }
48739
+ finalize() {
48740
+ for (const sheetId of this.getters.getSheetIds()) {
48741
+ if (!this.tables[sheetId]) {
48742
+ this.tables[sheetId] = this.computeTables(sheetId);
48743
+ }
48744
+ }
48745
+ }
48746
+ computeTables(sheetId) {
48747
+ const tables = [];
48748
+ const coreTables = this.getters.getCoreTables(sheetId);
48749
+ // First we create the static tables, so we can use them to compute collision with dynamic tables
48750
+ for (const table of coreTables) {
48751
+ if (table.type === "dynamic")
48752
+ continue;
48753
+ tables.push(table);
48754
+ }
48755
+ const staticTables = [...tables];
48756
+ // Then we create the dynamic tables
48757
+ for (const coreTable of coreTables) {
48758
+ if (coreTable.type !== "dynamic")
48759
+ continue;
48760
+ const table = this.coreTableToTable(sheetId, coreTable);
48761
+ let tableZone = table.range.zone;
48762
+ // Reduce the zone to avoid collision with static tables. Per design, dynamic tables can't overlap with other
48763
+ // dynamic tables, because formulas cannot spread on the same area, so we don't need to check for that.
48764
+ for (const staticTable of staticTables) {
48765
+ if (overlap(tableZone, staticTable.range.zone)) {
48766
+ tableZone = { ...tableZone, right: staticTable.range.zone.left - 1 };
48767
+ }
48768
+ }
48769
+ tables.push({ ...table, range: this.getters.getRangeFromZone(sheetId, tableZone) });
48770
+ }
48771
+ return tables;
48772
+ }
48773
+ getFilters(sheetId) {
48774
+ return this.getTables(sheetId)
48775
+ .filter((table) => table.config.hasFilters)
48776
+ .map((table) => table.filters)
48777
+ .flat();
48778
+ }
48779
+ getTables(sheetId) {
48780
+ return this.tables[sheetId] || [];
48781
+ }
48782
+ getFilter(position) {
48783
+ const table = this.getTable(position);
48784
+ if (!table || !table.config.hasFilters) {
48785
+ return undefined;
48786
+ }
48787
+ return table.filters.find((filter) => filter.col === position.col);
48788
+ }
48789
+ getFilterId(position) {
48790
+ return this.getFilter(position)?.id;
48791
+ }
48792
+ getTable({ sheetId, col, row }) {
48793
+ return this.getTables(sheetId).find((table) => isInside(col, row, table.range.zone));
48794
+ }
48795
+ getTablesOverlappingZones(sheetId, zones) {
48796
+ return this.getTables(sheetId).filter((table) => zones.some((zone) => overlap(table.range.zone, zone)));
48797
+ }
48798
+ doesZonesContainFilter(sheetId, zones) {
48799
+ return this.getTablesOverlappingZones(sheetId, zones).some((table) => table.config.hasFilters);
48800
+ }
48801
+ getFilterHeaders(sheetId) {
48802
+ const headers = [];
48803
+ for (const table of this.getTables(sheetId)) {
48804
+ if (!table.config.hasFilters) {
48805
+ continue;
48806
+ }
48807
+ const zone = table.range.zone;
48808
+ const row = zone.top;
48809
+ for (let col = zone.left; col <= zone.right; col++) {
48810
+ headers.push({ sheetId, col, row });
48811
+ }
48812
+ }
48813
+ return headers;
48814
+ }
48815
+ isFilterHeader({ sheetId, col, row }) {
48816
+ const headers = this.getFilterHeaders(sheetId);
48817
+ return headers.some((header) => header.col === col && header.row === row);
48818
+ }
48819
+ /**
48820
+ * Check if we can create a dynamic table on the given zones.
48821
+ * - The zones must be continuous
48822
+ * - The union of the zones must be either:
48823
+ * - A single cell that contains an array formula
48824
+ * - All the spread cells of a single array formula
48825
+ */
48826
+ canCreateDynamicTableOnZones(sheetId, zones) {
48827
+ if (!areZonesContinuous(zones)) {
48828
+ return false;
48829
+ }
48830
+ const unionZone = union(...zones);
48831
+ const topLeft = { col: unionZone.left, row: unionZone.top, sheetId };
48832
+ const parentSpreadingCell = this.getters.getArrayFormulaSpreadingOn(topLeft);
48833
+ if (!parentSpreadingCell) {
48834
+ return false;
48835
+ }
48836
+ else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
48837
+ return true;
48838
+ }
48839
+ const zone = this.getters.getSpreadZone(parentSpreadingCell);
48840
+ return deepEquals(unionZone, zone);
48841
+ }
48842
+ coreTableToTable(sheetId, table) {
48843
+ if (table.type !== "dynamic") {
48844
+ return table;
48845
+ }
48846
+ const tableZone = table.range.zone;
48847
+ const tablePosition = { sheetId, col: tableZone.left, row: tableZone.top };
48848
+ const zone = this.getters.getSpreadZone(tablePosition) ?? table.range.zone;
48849
+ const range = this.getters.getRangeFromZone(sheetId, zone);
48850
+ const filters = this.getDynamicTableFilters(sheetId, table, zone);
48851
+ return { id: table.id, range, filters, config: table.config };
48852
+ }
48853
+ getDynamicTableFilters(sheetId, table, tableZone) {
48854
+ const filters = [];
48855
+ const { top, bottom, left, right } = tableZone;
48856
+ for (let col = left; col <= right; col++) {
48857
+ const tableColIndex = col - left;
48858
+ const zone = { left: col, right: col, top, bottom };
48859
+ const filter = createFilter(this.getDynamicTableFilterId(table.id, tableColIndex), this.getters.getRangeFromZone(sheetId, zone), table.config, this.getters.getRangeFromZone);
48860
+ filters.push(filter);
48861
+ }
48862
+ return filters;
48863
+ }
48864
+ getDynamicTableFilterId(tableId, tableCol) {
48865
+ return tableId + "_" + tableCol;
48866
+ }
48867
+ exportForExcel(data) {
48868
+ for (const sheet of data.sheets) {
48869
+ for (const tableData of sheet.tables) {
48870
+ const zone = toZone(tableData.range);
48871
+ const topLeft = { sheetId: sheet.id, col: zone.left, row: zone.top };
48872
+ const coreTable = this.getters.getCoreTable(topLeft);
48873
+ const table = this.getTable(topLeft);
48874
+ if (coreTable?.type !== "dynamic" || !table) {
48875
+ continue;
48876
+ }
48877
+ tableData.range = zoneToXc(table.range.zone);
48878
+ }
48879
+ }
48880
+ }
48881
+ }
48882
+
48288
48883
  class HeaderSizeUIPlugin extends UIPlugin {
48289
48884
  static getters = ["getRowSize", "getHeaderSize"];
48290
48885
  tallestCellInRow = {};
@@ -48700,8 +49295,7 @@ class AutofillPlugin extends UIPlugin {
48700
49295
  let row = zone.bottom;
48701
49296
  if (col > 0) {
48702
49297
  let leftPosition = { sheetId, col: col - 1, row };
48703
- while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
48704
- this.getters.getCell(leftPosition)?.content) {
49298
+ while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty) {
48705
49299
  row += 1;
48706
49300
  leftPosition = { sheetId, col: col - 1, row };
48707
49301
  }
@@ -48710,8 +49304,7 @@ class AutofillPlugin extends UIPlugin {
48710
49304
  col = zone.right;
48711
49305
  if (col <= this.getters.getNumberCols(sheetId)) {
48712
49306
  let rightPosition = { sheetId, col: col + 1, row };
48713
- while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
48714
- this.getters.getCell(rightPosition)?.content) {
49307
+ while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty) {
48715
49308
  row += 1;
48716
49309
  rightPosition = { sheetId, col: col + 1, row };
48717
49310
  }
@@ -49021,13 +49614,13 @@ class AutomaticSumPlugin extends UIPlugin {
49021
49614
  const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
49022
49615
  const cellPositions = range(end, -1, -1);
49023
49616
  const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
49024
- const maxValidPosition = Math.max(...invalidCells);
49617
+ const maxValidPosition = largeMax(invalidCells);
49025
49618
  const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
49026
49619
  const firstSequence = numberSequences[0] || [];
49027
- if (Math.max(...firstSequence) < maxValidPosition) {
49620
+ if (largeMax(firstSequence) < maxValidPosition) {
49028
49621
  return Infinity;
49029
49622
  }
49030
- return Math.min(...firstSequence);
49623
+ return largeMin(firstSequence);
49031
49624
  }
49032
49625
  shouldFindData(sheetId, zone) {
49033
49626
  return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
@@ -50602,8 +51195,6 @@ class SheetUIPlugin extends UIPlugin {
50602
51195
  static getters = [
50603
51196
  "doesCellHaveGridIcon",
50604
51197
  "getCellWidth",
50605
- "getCellComputedBorder",
50606
- "getCellComputedStyle",
50607
51198
  "getTextWidth",
50608
51199
  "getCellText",
50609
51200
  "getCellMultiLineText",
@@ -50647,7 +51238,7 @@ class SheetUIPlugin extends UIPlugin {
50647
51238
  // Getters
50648
51239
  // ---------------------------------------------------------------------------
50649
51240
  getCellWidth(position) {
50650
- const style = this.getCellComputedStyle(position);
51241
+ const style = this.getters.getCellComputedStyle(position);
50651
51242
  let contentWidth = 0;
50652
51243
  const content = this.getters.getEvaluatedCell(position).formattedValue;
50653
51244
  if (content) {
@@ -50740,35 +51331,12 @@ class SheetUIPlugin extends UIPlugin {
50740
51331
  */
50741
51332
  isCellEmpty(position) {
50742
51333
  const mainPosition = this.getters.getMainCellPosition(position);
50743
- return !(this.getters.getCorrespondingFormulaCell(mainPosition) ||
50744
- this.getters.getCell(mainPosition)?.content);
50745
- }
50746
- getCellComputedBorder(position) {
50747
- const cellBorder = this.getters.getCellBorder(position) || {};
50748
- const cellTableBorder = this.getters.getCellTableBorder(position) || {};
50749
- // Use removeFalsyAttributes to avoid overwriting borders with undefined values
50750
- const border = { ...cellTableBorder, ...removeFalsyAttributes(cellBorder) };
50751
- return isObjectEmptyRecursive(border) ? null : border;
50752
- }
50753
- getCellComputedStyle(position) {
50754
- const cell = this.getters.getCell(position);
50755
- const cfStyle = this.getters.getCellConditionalFormatStyle(position);
50756
- const tableStyle = this.getters.getCellTableStyle(position);
50757
- const computedStyle = {
50758
- ...removeFalsyAttributes(tableStyle),
50759
- ...removeFalsyAttributes(cell?.style),
50760
- ...removeFalsyAttributes(cfStyle),
50761
- };
50762
- const evaluatedCell = this.getters.getEvaluatedCell(position);
50763
- if (evaluatedCell.link && !computedStyle.textColor) {
50764
- computedStyle.textColor = LINK_COLOR;
50765
- }
50766
- return computedStyle;
51334
+ return this.getters.getEvaluatedCell(mainPosition).type === CellValueType.empty;
50767
51335
  }
50768
51336
  getColMaxWidth(sheetId, index) {
50769
51337
  const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
50770
51338
  const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
50771
- return Math.max(0, ...sizes);
51339
+ return Math.max(0, largeMax(sizes));
50772
51340
  }
50773
51341
  /**
50774
51342
  * Check that any "sheetId" in the command matches an existing
@@ -50797,6 +51365,236 @@ class SheetUIPlugin extends UIPlugin {
50797
51365
  }
50798
51366
  }
50799
51367
 
51368
+ class TableStylePlugin extends UIPlugin {
51369
+ static getters = ["getCellTableStyle", "getCellTableBorder"];
51370
+ tableStyles = {};
51371
+ handle(cmd) {
51372
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51373
+ (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51374
+ cmd.type === "EVALUATE_CELLS") {
51375
+ this.tableStyles = {};
51376
+ return;
51377
+ }
51378
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51379
+ delete this.tableStyles[cmd.sheetId];
51380
+ return;
51381
+ }
51382
+ }
51383
+ finalize() {
51384
+ for (const sheetId of this.getters.getSheetIds()) {
51385
+ if (!this.tableStyles[sheetId]) {
51386
+ this.tableStyles[sheetId] = {};
51387
+ }
51388
+ for (const table of this.getters.getTables(sheetId)) {
51389
+ if (!this.tableStyles[sheetId][table.id]) {
51390
+ this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51391
+ }
51392
+ }
51393
+ }
51394
+ }
51395
+ getCellTableStyle(position) {
51396
+ const table = this.getters.getTable(position);
51397
+ if (!table) {
51398
+ return undefined;
51399
+ }
51400
+ return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51401
+ }
51402
+ getCellTableBorder(position) {
51403
+ const table = this.getters.getTable(position);
51404
+ if (!table) {
51405
+ return undefined;
51406
+ }
51407
+ return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51408
+ }
51409
+ computeTableStyle(sheetId, table) {
51410
+ return lazy(() => {
51411
+ const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51412
+ const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51413
+ // Return the style with sheet coordinates instead of tables coordinates
51414
+ const mapping = this.getTableMapping(sheetId, table);
51415
+ const absoluteTableStyle = { borders: {}, styles: {} };
51416
+ for (let col = 0; col < numberOfCols; col++) {
51417
+ const colInSheet = mapping.colMapping[col];
51418
+ absoluteTableStyle.borders[colInSheet] = {};
51419
+ absoluteTableStyle.styles[colInSheet] = {};
51420
+ for (let row = 0; row < numberOfRows; row++) {
51421
+ const rowInSheet = mapping.rowMapping[row];
51422
+ absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51423
+ absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51424
+ }
51425
+ }
51426
+ return absoluteTableStyle;
51427
+ });
51428
+ }
51429
+ /**
51430
+ * Get the actual table config that will be used to compute the table style. It is different from
51431
+ * the config of the table because of hidden rows and columns in the sheet. For example remove the
51432
+ * hidden rows from config.numberOfHeaders.
51433
+ */
51434
+ getTableRuntimeConfig(sheetId, table) {
51435
+ const tableZone = table.range.zone;
51436
+ const config = { ...table.config };
51437
+ let numberOfCols = tableZone.right - tableZone.left + 1;
51438
+ let numberOfRows = tableZone.bottom - tableZone.top + 1;
51439
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51440
+ if (!this.getters.isRowHidden(sheetId, row)) {
51441
+ continue;
51442
+ }
51443
+ numberOfRows--;
51444
+ if (row - tableZone.top < table.config.numberOfHeaders) {
51445
+ config.numberOfHeaders--;
51446
+ if (config.numberOfHeaders < 0) {
51447
+ config.numberOfHeaders = 0;
51448
+ }
51449
+ }
51450
+ if (row === tableZone.bottom) {
51451
+ config.totalRow = false;
51452
+ }
51453
+ }
51454
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51455
+ if (!this.getters.isColHidden(sheetId, col)) {
51456
+ continue;
51457
+ }
51458
+ numberOfCols--;
51459
+ if (col === tableZone.left) {
51460
+ config.firstColumn = false;
51461
+ }
51462
+ if (col === tableZone.right) {
51463
+ config.lastColumn = false;
51464
+ }
51465
+ }
51466
+ return {
51467
+ config,
51468
+ numberOfCols,
51469
+ numberOfRows,
51470
+ };
51471
+ }
51472
+ /**
51473
+ * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51474
+ */
51475
+ getTableMapping(sheetId, table) {
51476
+ const colMapping = {};
51477
+ const rowMapping = {};
51478
+ let colOffset = 0;
51479
+ let rowOffset = 0;
51480
+ const tableZone = table.range.zone;
51481
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51482
+ if (this.getters.isColHidden(sheetId, col)) {
51483
+ continue;
51484
+ }
51485
+ colMapping[colOffset] = col;
51486
+ colOffset++;
51487
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51488
+ if (this.getters.isRowHidden(sheetId, row)) {
51489
+ continue;
51490
+ }
51491
+ rowMapping[rowOffset] = row;
51492
+ rowOffset++;
51493
+ }
51494
+ }
51495
+ return {
51496
+ colMapping,
51497
+ rowMapping,
51498
+ };
51499
+ }
51500
+ }
51501
+ const invalidateTableStyleCommands = [
51502
+ "HIDE_COLUMNS_ROWS",
51503
+ "UNHIDE_COLUMNS_ROWS",
51504
+ "UNFOLD_HEADER_GROUP",
51505
+ "FOLD_HEADER_GROUP",
51506
+ "FOLD_ALL_HEADER_GROUPS",
51507
+ "UNFOLD_ALL_HEADER_GROUPS",
51508
+ "CREATE_TABLE",
51509
+ "UPDATE_TABLE",
51510
+ "UPDATE_FILTER",
51511
+ ];
51512
+ const invalidateTableStyleCommandsSet = new Set(invalidateTableStyleCommands);
51513
+ function doesCommandInvalidatesTableStyle(cmd) {
51514
+ return invalidateTableStyleCommandsSet.has(cmd.type);
51515
+ }
51516
+
51517
+ class CellComputedStylePlugin extends UIPlugin {
51518
+ static getters = ["getCellComputedBorder", "getCellComputedStyle"];
51519
+ styles = {};
51520
+ borders = {};
51521
+ handle(cmd) {
51522
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51523
+ cmd.type === "UPDATE_CELL" ||
51524
+ cmd.type === "EVALUATE_CELLS") {
51525
+ this.styles = {};
51526
+ this.borders = {};
51527
+ return;
51528
+ }
51529
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51530
+ delete this.styles[cmd.sheetId];
51531
+ delete this.borders[cmd.sheetId];
51532
+ return;
51533
+ }
51534
+ if (invalidateCFEvaluationCommands.has(cmd.type)) {
51535
+ this.styles = {};
51536
+ return;
51537
+ }
51538
+ }
51539
+ getCellComputedBorder(position) {
51540
+ const { sheetId, row, col } = position;
51541
+ if (this.borders[sheetId]?.[row]?.[col] !== undefined) {
51542
+ return this.borders[sheetId][row][col];
51543
+ }
51544
+ if (!this.borders[sheetId]) {
51545
+ this.borders[sheetId] = {};
51546
+ }
51547
+ if (!this.borders[sheetId][row]) {
51548
+ this.borders[sheetId][row] = {};
51549
+ }
51550
+ if (!this.borders[sheetId][row][col]) {
51551
+ this.borders[sheetId][row][col] = this.computeCellBorder(position);
51552
+ }
51553
+ return this.borders[sheetId][row][col];
51554
+ }
51555
+ getCellComputedStyle(position) {
51556
+ const { sheetId, row, col } = position;
51557
+ if (this.styles[sheetId]?.[row]?.[col] !== undefined) {
51558
+ return this.styles[sheetId][row][col];
51559
+ }
51560
+ if (!this.styles[sheetId]) {
51561
+ this.styles[sheetId] = {};
51562
+ }
51563
+ if (!this.styles[sheetId][row]) {
51564
+ this.styles[sheetId][row] = {};
51565
+ }
51566
+ if (!this.styles[sheetId][row][col]) {
51567
+ this.styles[sheetId][row][col] = this.computeCellStyle(position);
51568
+ }
51569
+ return this.styles[sheetId][row][col];
51570
+ }
51571
+ computeCellBorder(position) {
51572
+ const cellBorder = this.getters.getCellBorder(position) || {};
51573
+ const cellTableBorder = this.getters.getCellTableBorder(position) || {};
51574
+ // Use removeFalsyAttributes to avoid overwriting borders with undefined values
51575
+ const border = {
51576
+ ...removeFalsyAttributes(cellTableBorder),
51577
+ ...removeFalsyAttributes(cellBorder),
51578
+ };
51579
+ return isObjectEmptyRecursive(border) ? null : border;
51580
+ }
51581
+ computeCellStyle(position) {
51582
+ const cell = this.getters.getCell(position);
51583
+ const cfStyle = this.getters.getCellConditionalFormatStyle(position);
51584
+ const tableStyle = this.getters.getCellTableStyle(position);
51585
+ const computedStyle = {
51586
+ ...removeFalsyAttributes(tableStyle),
51587
+ ...removeFalsyAttributes(cell?.style),
51588
+ ...removeFalsyAttributes(cfStyle),
51589
+ };
51590
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
51591
+ if (evaluatedCell.link && !computedStyle.textColor) {
51592
+ computedStyle.textColor = LINK_COLOR;
51593
+ }
51594
+ return computedStyle;
51595
+ }
51596
+ }
51597
+
50800
51598
  const genericRepeatsTransforms = [
50801
51599
  repeatSheetDependantCommand,
50802
51600
  repeatTargetDependantCommand,
@@ -51332,10 +52130,11 @@ class TableAutofillPlugin extends UIPlugin {
51332
52130
  handle(cmd) {
51333
52131
  switch (cmd.type) {
51334
52132
  case "AUTOFILL_TABLE_COLUMN":
51335
- const table = this.getters.getTable(cmd);
52133
+ const table = this.getters.getCoreTable(cmd);
51336
52134
  const cell = this.getters.getCell(cmd);
51337
- if (!table || !table.config.automaticAutofill || !cell?.isFormula)
52135
+ if (!table?.config.automaticAutofill || table.type === "dynamic" || !cell?.isFormula) {
51338
52136
  return;
52137
+ }
51339
52138
  const { col, row } = cmd;
51340
52139
  const tableContentZone = getTableContentZone(table.range.zone, table.config);
51341
52140
  if (tableContentZone && isInside(col, row, tableContentZone)) {
@@ -51373,148 +52172,6 @@ class TableAutofillPlugin extends UIPlugin {
51373
52172
  }
51374
52173
  }
51375
52174
 
51376
- class TableStylePlugin extends UIPlugin {
51377
- static getters = ["getCellTableStyle", "getCellTableBorder"];
51378
- tableStyles = {};
51379
- handle(cmd) {
51380
- if (invalidateEvaluationCommands.has(cmd.type) ||
51381
- (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51382
- cmd.type === "EVALUATE_CELLS") {
51383
- this.tableStyles = {};
51384
- return;
51385
- }
51386
- switch (cmd.type) {
51387
- case "HIDE_COLUMNS_ROWS":
51388
- case "UNHIDE_COLUMNS_ROWS":
51389
- case "UNFOLD_HEADER_GROUP":
51390
- case "FOLD_HEADER_GROUP":
51391
- case "FOLD_ALL_HEADER_GROUPS":
51392
- case "UNFOLD_ALL_HEADER_GROUPS":
51393
- case "UPDATE_TABLE":
51394
- case "UPDATE_FILTER":
51395
- delete this.tableStyles[cmd.sheetId];
51396
- break;
51397
- }
51398
- }
51399
- finalize() {
51400
- for (const sheetId of this.getters.getSheetIds()) {
51401
- if (!this.tableStyles[sheetId]) {
51402
- this.tableStyles[sheetId] = {};
51403
- }
51404
- for (const table of this.getters.getTables(sheetId)) {
51405
- if (!this.tableStyles[sheetId][table.id]) {
51406
- this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51407
- }
51408
- }
51409
- }
51410
- }
51411
- getCellTableStyle(position) {
51412
- const table = this.getters.getTable(position);
51413
- if (!table) {
51414
- return undefined;
51415
- }
51416
- return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51417
- }
51418
- getCellTableBorder(position) {
51419
- const table = this.getters.getTable(position);
51420
- if (!table) {
51421
- return undefined;
51422
- }
51423
- return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51424
- }
51425
- computeTableStyle(sheetId, table) {
51426
- return lazy(() => {
51427
- const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51428
- const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51429
- // Return the style with sheet coordinates instead of tables coordinates
51430
- const mapping = this.getTableMapping(sheetId, table);
51431
- const absoluteTableStyle = { borders: {}, styles: {} };
51432
- for (let col = 0; col < numberOfCols; col++) {
51433
- const colInSheet = mapping.colMapping[col];
51434
- absoluteTableStyle.borders[colInSheet] = {};
51435
- absoluteTableStyle.styles[colInSheet] = {};
51436
- for (let row = 0; row < numberOfRows; row++) {
51437
- const rowInSheet = mapping.rowMapping[row];
51438
- absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51439
- absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51440
- }
51441
- }
51442
- return absoluteTableStyle;
51443
- });
51444
- }
51445
- /**
51446
- * Get the actual table config that will be used to compute the table style. It is different from
51447
- * the config of the table because of hidden rows and columns in the sheet. For example remove the
51448
- * hidden rows from config.numberOfHeaders.
51449
- */
51450
- getTableRuntimeConfig(sheetId, table) {
51451
- const tableZone = table.range.zone;
51452
- const config = { ...table.config };
51453
- let numberOfCols = tableZone.right - tableZone.left + 1;
51454
- let numberOfRows = tableZone.bottom - tableZone.top + 1;
51455
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51456
- if (!this.getters.isRowHidden(sheetId, row)) {
51457
- continue;
51458
- }
51459
- numberOfRows--;
51460
- if (row - tableZone.top < table.config.numberOfHeaders) {
51461
- config.numberOfHeaders--;
51462
- if (config.numberOfHeaders < 0) {
51463
- config.numberOfHeaders = 0;
51464
- }
51465
- }
51466
- if (row === tableZone.bottom) {
51467
- config.totalRow = false;
51468
- }
51469
- }
51470
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51471
- if (!this.getters.isColHidden(sheetId, col)) {
51472
- continue;
51473
- }
51474
- numberOfCols--;
51475
- if (col === tableZone.left) {
51476
- config.firstColumn = false;
51477
- }
51478
- if (col === tableZone.right) {
51479
- config.lastColumn = false;
51480
- }
51481
- }
51482
- return {
51483
- config,
51484
- numberOfCols,
51485
- numberOfRows,
51486
- };
51487
- }
51488
- /**
51489
- * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51490
- */
51491
- getTableMapping(sheetId, table) {
51492
- const colMapping = {};
51493
- const rowMapping = {};
51494
- let colOffset = 0;
51495
- let rowOffset = 0;
51496
- const tableZone = table.range.zone;
51497
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51498
- if (this.getters.isColHidden(sheetId, col)) {
51499
- continue;
51500
- }
51501
- colMapping[colOffset] = col;
51502
- colOffset++;
51503
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51504
- if (this.getters.isRowHidden(sheetId, row)) {
51505
- continue;
51506
- }
51507
- rowMapping[rowOffset] = row;
51508
- rowOffset++;
51509
- }
51510
- }
51511
- return {
51512
- colMapping,
51513
- rowMapping,
51514
- };
51515
- }
51516
- }
51517
-
51518
52175
  /**
51519
52176
  * Clipboard Plugin
51520
52177
  *
@@ -52064,9 +52721,6 @@ class FilterEvaluationPlugin extends UIPlugin {
52064
52721
  case "START":
52065
52722
  for (const sheetId of this.getters.getSheetIds()) {
52066
52723
  this.filterValues[sheetId] = {};
52067
- for (const filter of this.getters.getFilters(sheetId)) {
52068
- this.filterValues[sheetId][filter.id] = [];
52069
- }
52070
52724
  }
52071
52725
  break;
52072
52726
  case "CREATE_SHEET":
@@ -52087,16 +52741,7 @@ class FilterEvaluationPlugin extends UIPlugin {
52087
52741
  this.updateHiddenRows();
52088
52742
  break;
52089
52743
  case "DUPLICATE_SHEET":
52090
- const filterValues = {};
52091
- for (const newFilter of this.getters.getFilters(cmd.sheetIdTo)) {
52092
- const zone = newFilter.rangeWithHeaders.zone;
52093
- filterValues[newFilter.id] = this.getFilterHiddenValues({
52094
- sheetId: cmd.sheetId,
52095
- col: zone.left,
52096
- row: zone.top,
52097
- });
52098
- }
52099
- this.filterValues[cmd.sheetIdTo] = filterValues;
52744
+ this.filterValues[cmd.sheetIdTo] = deepCopy(this.filterValues[cmd.sheetId]);
52100
52745
  break;
52101
52746
  // If we don't handle DELETE_SHEET, on one hand we will have some residual data, on the other hand we keep the data
52102
52747
  // on DELETE_SHEET followed by undo
@@ -52236,38 +52881,6 @@ class FilterEvaluationPlugin extends UIPlugin {
52236
52881
  }
52237
52882
  }
52238
52883
 
52239
- const selectionStatisticFunctions = [
52240
- {
52241
- name: _t("Sum"),
52242
- types: [CellValueType.number],
52243
- compute: (values, locale) => sum([[values]], locale),
52244
- },
52245
- {
52246
- name: _t("Avg"),
52247
- types: [CellValueType.number],
52248
- compute: (values, locale) => average([[values]], locale),
52249
- },
52250
- {
52251
- name: _t("Min"),
52252
- types: [CellValueType.number],
52253
- compute: (values, locale) => min([[values]], locale),
52254
- },
52255
- {
52256
- name: _t("Max"),
52257
- types: [CellValueType.number],
52258
- compute: (values, locale) => max([[values]], locale),
52259
- },
52260
- {
52261
- name: _t("Count"),
52262
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52263
- compute: (values) => countAny([[values]]),
52264
- },
52265
- {
52266
- name: _t("Count Numbers"),
52267
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52268
- compute: (values, locale) => countNumbers([[values]], locale),
52269
- },
52270
- ];
52271
52884
  /**
52272
52885
  * SelectionPlugin
52273
52886
  */
@@ -52283,8 +52896,6 @@ class GridSelectionPlugin extends UIPlugin {
52283
52896
  "getSelectedZones",
52284
52897
  "getSelectedZone",
52285
52898
  "getSelectedCells",
52286
- "getStatisticFnResults",
52287
- "getAggregate",
52288
52899
  "getSelectedFigureId",
52289
52900
  "getSelection",
52290
52901
  "getActivePosition",
@@ -52319,7 +52930,10 @@ class GridSelectionPlugin extends UIPlugin {
52319
52930
  switch (cmd.type) {
52320
52931
  case "ACTIVATE_SHEET":
52321
52932
  try {
52322
- this.getters.getSheet(cmd.sheetIdTo);
52933
+ const sheet = this.getters.getSheet(cmd.sheetIdTo);
52934
+ if (!sheet.isVisible) {
52935
+ return "SheetIsHidden" /* CommandResult.SheetIsHidden */;
52936
+ }
52323
52937
  break;
52324
52938
  }
52325
52939
  catch (error) {
@@ -52473,6 +53087,7 @@ class GridSelectionPlugin extends UIPlugin {
52473
53087
  this.gridSelection.zones = this.gridSelection.zones.map((z) => this.getters.expandZone(sheetId, z));
52474
53088
  this.gridSelection.anchor.zone = this.getters.expandZone(sheetId, this.gridSelection.anchor.zone);
52475
53089
  this.setSelectionMixin(this.gridSelection.anchor, this.gridSelection.zones);
53090
+ this.selectedFigureId = null;
52476
53091
  break;
52477
53092
  }
52478
53093
  /** Any change to the selection has to be reflected in the selection processor. */
@@ -52567,52 +53182,6 @@ class GridSelectionPlugin extends UIPlugin {
52567
53182
  : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });
52568
53183
  }
52569
53184
  }
52570
- getStatisticFnResults() {
52571
- const sheetId = this.getters.getActiveSheetId();
52572
- const cells = new Set();
52573
- for (const zone of this.gridSelection.zones) {
52574
- for (const { col, row } of positions(zone)) {
52575
- if (this.getters.isRowHidden(sheetId, row) || this.getters.isColHidden(sheetId, col)) {
52576
- continue; // Skip hidden cells
52577
- }
52578
- const evaluatedCell = this.getters.getEvaluatedCell({ sheetId, col, row });
52579
- if (evaluatedCell.type !== CellValueType.empty) {
52580
- cells.add(evaluatedCell);
52581
- }
52582
- }
52583
- }
52584
- const locale = this.getters.getLocale();
52585
- let statisticFnResults = {};
52586
- for (let fn of selectionStatisticFunctions) {
52587
- // We don't want to display statistical information when there is no interest:
52588
- // We set the statistical result to undefined if the data handled by the selection
52589
- // does not match the data handled by the function.
52590
- // Ex: if there are only texts in the selection, we prefer that the SUM result
52591
- // be displayed as undefined rather than 0.
52592
- let fnResult = undefined;
52593
- const evaluatedCells = [...cells].filter((c) => fn.types.includes(c.type));
52594
- if (evaluatedCells.length) {
52595
- fnResult = fn.compute(evaluatedCells, locale);
52596
- }
52597
- statisticFnResults[fn.name] = fnResult;
52598
- }
52599
- return statisticFnResults;
52600
- }
52601
- getAggregate() {
52602
- let aggregate = 0;
52603
- let n = 0;
52604
- const sheetId = this.getters.getActiveSheetId();
52605
- const cellPositions = this.gridSelection.zones.map(positions).flat();
52606
- for (const { col, row } of cellPositions) {
52607
- const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
52608
- if (cell.type === CellValueType.number) {
52609
- n++;
52610
- aggregate += cell.value;
52611
- }
52612
- }
52613
- const locale = this.getters.getLocale();
52614
- return n < 2 ? null : formatValue(aggregate, { locale });
52615
- }
52616
53185
  isSelected(zone) {
52617
53186
  return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));
52618
53187
  }
@@ -52654,9 +53223,6 @@ class GridSelectionPlugin extends UIPlugin {
52654
53223
  // Other
52655
53224
  // ---------------------------------------------------------------------------
52656
53225
  activateSheet(sheetIdFrom, sheetIdTo) {
52657
- if (!this.getters.isSheetVisible(sheetIdTo)) {
52658
- this.dispatch("SHOW_SHEET", { sheetId: sheetIdTo });
52659
- }
52660
53226
  this.setActiveSheet(sheetIdTo);
52661
53227
  this.sheetsData[sheetIdFrom] = {
52662
53228
  gridSelection: deepCopy(this.gridSelection),
@@ -53663,7 +54229,7 @@ class SheetViewPlugin extends UIPlugin {
53663
54229
  * column of the current viewport
53664
54230
  */
53665
54231
  getColDimensionsInViewport(sheetId, col) {
53666
- const left = Math.min(...this.getters.getSheetViewVisibleCols());
54232
+ const left = largeMin(this.getters.getSheetViewVisibleCols());
53667
54233
  const start = this.getters.getColRowOffsetInViewport("COL", left, col);
53668
54234
  const size = this.getters.getColSize(sheetId, col);
53669
54235
  const isColHidden = this.getters.isColHidden(sheetId, col);
@@ -53678,7 +54244,7 @@ class SheetViewPlugin extends UIPlugin {
53678
54244
  * of the current viewport
53679
54245
  */
53680
54246
  getRowDimensionsInViewport(sheetId, row) {
53681
- const top = Math.min(...this.getters.getSheetViewVisibleRows());
54247
+ const top = largeMin(this.getters.getSheetViewVisibleRows());
53682
54248
  const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
53683
54249
  const size = this.getters.getRowSize(sheetId, row);
53684
54250
  const isRowHidden = this.getters.isRowHidden(sheetId, row);
@@ -54030,6 +54596,7 @@ const statefulUIPluginRegistry = new Registry()
54030
54596
  .add("evaluation_filter", FilterEvaluationPlugin)
54031
54597
  .add("header_visibility_ui", HeaderVisibilityUIPlugin)
54032
54598
  .add("table_style", TableStylePlugin)
54599
+ .add("cell_computed_style", CellComputedStylePlugin)
54033
54600
  .add("header_positions", HeaderPositionsUIPlugin)
54034
54601
  .add("viewport", SheetViewPlugin)
54035
54602
  .add("clipboard", ClipboardPlugin);
@@ -54039,8 +54606,9 @@ const coreViewsPluginRegistry = new Registry()
54039
54606
  .add("evaluation_chart", EvaluationChartPlugin)
54040
54607
  .add("evaluation_cf", EvaluationConditionalFormatPlugin)
54041
54608
  .add("row_size", HeaderSizeUIPlugin)
54042
- .add("custom_colors", CustomColorsPlugin)
54043
- .add("data_validation_ui", EvaluationDataValidationPlugin);
54609
+ .add("data_validation_ui", EvaluationDataValidationPlugin)
54610
+ .add("dynamic_tables", DynamicTablesPlugin)
54611
+ .add("custom_colors", CustomColorsPlugin);
54044
54612
 
54045
54613
  const clickableCellRegistry = new Registry();
54046
54614
  clickableCellRegistry.add("link", {
@@ -54089,6 +54657,38 @@ class ImageProvider {
54089
54657
  }
54090
54658
  }
54091
54659
 
54660
+ class ArrayFormulaHighlight extends SpreadsheetStore {
54661
+ highlightStore = this.get(HighlightStore);
54662
+ constructor(get) {
54663
+ super(get);
54664
+ this.highlightStore.register(this);
54665
+ }
54666
+ get highlights() {
54667
+ const zone = this.getHighlightZone();
54668
+ if (!zone) {
54669
+ return [];
54670
+ }
54671
+ const sheetId = this.model.getters.getActiveSheetId();
54672
+ return [
54673
+ {
54674
+ sheetId,
54675
+ zone,
54676
+ color: "#17A2B8",
54677
+ noFill: true,
54678
+ thinLine: true,
54679
+ },
54680
+ ];
54681
+ }
54682
+ getHighlightZone() {
54683
+ const position = this.model.getters.getActivePosition();
54684
+ const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
54685
+ const spreadZone = spreader
54686
+ ? this.model.getters.getSpreadZone(spreader)
54687
+ : this.model.getters.getSpreadZone(position);
54688
+ return spreadZone;
54689
+ }
54690
+ }
54691
+
54092
54692
  const RIPPLE_KEY_FRAMES = [
54093
54693
  { transform: "scale(0)" },
54094
54694
  { transform: "scale(0.8)", offset: 0.33 },
@@ -54391,12 +54991,14 @@ class BottomBarSheet extends Component {
54391
54991
  this.editionState = "initializing";
54392
54992
  }
54393
54993
  stopEdition() {
54394
- if (!this.state.isEditing)
54994
+ const input = this.sheetNameRef.el;
54995
+ if (!this.state.isEditing || !input)
54395
54996
  return;
54396
54997
  this.state.isEditing = false;
54397
54998
  this.editionState = "initializing";
54398
- this.sheetNameRef.el?.blur();
54999
+ input.blur();
54399
55000
  const inputValue = this.getInputContent() || "";
55001
+ input.innerText = inputValue;
54400
55002
  interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
54401
55003
  }
54402
55004
  cancelEdition() {
@@ -54440,6 +55042,115 @@ class BottomBarSheet extends Component {
54440
55042
  }
54441
55043
  }
54442
55044
 
55045
+ const selectionStatisticFunctions = [
55046
+ {
55047
+ name: _t("Sum"),
55048
+ types: [CellValueType.number],
55049
+ compute: (values, locale) => sum([[values]], locale),
55050
+ },
55051
+ {
55052
+ name: _t("Avg"),
55053
+ types: [CellValueType.number],
55054
+ compute: (values, locale) => average([[values]], locale),
55055
+ },
55056
+ {
55057
+ name: _t("Min"),
55058
+ types: [CellValueType.number],
55059
+ compute: (values, locale) => min([[values]], locale),
55060
+ },
55061
+ {
55062
+ name: _t("Max"),
55063
+ types: [CellValueType.number],
55064
+ compute: (values, locale) => max([[values]], locale),
55065
+ },
55066
+ {
55067
+ name: _t("Count"),
55068
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55069
+ compute: (values) => countAny([[values]]),
55070
+ },
55071
+ {
55072
+ name: _t("Count Numbers"),
55073
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55074
+ compute: (values, locale) => countNumbers([[values]], locale),
55075
+ },
55076
+ ];
55077
+ class AggregateStatisticsStore extends SpreadsheetStore {
55078
+ statisticFnResults = this._computeStatisticFnResults();
55079
+ isDirty = false;
55080
+ constructor(get) {
55081
+ super(get);
55082
+ this.model.selection.observe(this, {
55083
+ handleEvent: this.handleEvent.bind(this),
55084
+ });
55085
+ }
55086
+ handle(cmd) {
55087
+ if (invalidateEvaluationCommands.has(cmd.type) ||
55088
+ (cmd.type === "UPDATE_CELL" && "content" in cmd)) {
55089
+ this.isDirty = true;
55090
+ }
55091
+ switch (cmd.type) {
55092
+ case "HIDE_COLUMNS_ROWS":
55093
+ case "UNHIDE_COLUMNS_ROWS":
55094
+ case "GROUP_HEADERS":
55095
+ case "UNGROUP_HEADERS":
55096
+ case "ACTIVATE_SHEET":
55097
+ case "ACTIVATE_NEXT_SHEET":
55098
+ case "ACTIVATE_PREVIOUS_SHEET":
55099
+ case "EVALUATE_CELLS":
55100
+ case "UNDO":
55101
+ case "REDO":
55102
+ this.isDirty = true;
55103
+ }
55104
+ }
55105
+ finalize() {
55106
+ if (this.isDirty) {
55107
+ this.isDirty = false;
55108
+ this.statisticFnResults = this._computeStatisticFnResults();
55109
+ }
55110
+ }
55111
+ handleEvent() {
55112
+ if (this.getters.isGridSelectionActive()) {
55113
+ this.statisticFnResults = this._computeStatisticFnResults();
55114
+ }
55115
+ }
55116
+ _computeStatisticFnResults() {
55117
+ const getters = this.getters;
55118
+ const sheetId = getters.getActiveSheetId();
55119
+ const cells = new Set();
55120
+ const zones = getters.getSelectedZones();
55121
+ for (const zone of zones) {
55122
+ for (let col = zone.left; col <= zone.right; col++) {
55123
+ for (let row = zone.top; row <= zone.bottom; row++) {
55124
+ if (getters.isRowHidden(sheetId, row) || getters.isColHidden(sheetId, col)) {
55125
+ continue; // Skip hidden cells
55126
+ }
55127
+ const evaluatedCell = getters.getEvaluatedCell({ sheetId, col, row });
55128
+ if (evaluatedCell.type !== CellValueType.empty) {
55129
+ cells.add(evaluatedCell);
55130
+ }
55131
+ }
55132
+ }
55133
+ }
55134
+ const locale = getters.getLocale();
55135
+ let statisticFnResults = {};
55136
+ const cellsArray = [...cells];
55137
+ for (let fn of selectionStatisticFunctions) {
55138
+ // We don't want to display statistical information when there is no interest:
55139
+ // We set the statistical result to undefined if the data handled by the selection
55140
+ // does not match the data handled by the function.
55141
+ // Ex: if there are only texts in the selection, we prefer that the SUM result
55142
+ // be displayed as undefined rather than 0.
55143
+ let fnResult = undefined;
55144
+ const evaluatedCells = cellsArray.filter((c) => fn.types.includes(c.type));
55145
+ if (evaluatedCells.length) {
55146
+ fnResult = fn.compute(evaluatedCells, locale);
55147
+ }
55148
+ statisticFnResults[fn.name] = fnResult;
55149
+ }
55150
+ return statisticFnResults;
55151
+ }
55152
+ }
55153
+
54443
55154
  // -----------------------------------------------------------------------------
54444
55155
  // SpreadSheet
54445
55156
  // -----------------------------------------------------------------------------
@@ -54455,40 +55166,38 @@ css /* scss */ `
54455
55166
  }
54456
55167
  `;
54457
55168
  class BottomBarStatistic extends Component {
54458
- static template = "o-spreadsheet-BottomBarStatisic";
55169
+ static template = "o-spreadsheet-BottomBarStatistic";
54459
55170
  static props = {
54460
55171
  openContextMenu: Function,
54461
55172
  closeContextMenu: Function,
54462
55173
  };
54463
55174
  static components = { Ripple };
54464
55175
  selectedStatisticFn = "";
54465
- statisticFnResults = {};
55176
+ store;
54466
55177
  setup() {
54467
- this.statisticFnResults = this.env.model.getters.getStatisticFnResults();
55178
+ this.store = useStore(AggregateStatisticsStore);
54468
55179
  onWillUpdateProps(() => {
54469
- const newStatisticFnResults = this.env.model.getters.getStatisticFnResults();
54470
- if (!deepEquals(newStatisticFnResults, this.statisticFnResults)) {
55180
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54471
55181
  this.props.closeContextMenu();
54472
55182
  }
54473
- this.statisticFnResults = newStatisticFnResults;
54474
55183
  });
54475
55184
  }
54476
55185
  getSelectedStatistic() {
54477
55186
  // don't display button if no function has a result
54478
- if (Object.values(this.statisticFnResults).every((result) => result === undefined)) {
55187
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54479
55188
  return undefined;
54480
55189
  }
54481
55190
  if (this.selectedStatisticFn === "") {
54482
- this.selectedStatisticFn = Object.keys(this.statisticFnResults)[0];
55191
+ this.selectedStatisticFn = Object.keys(this.store.statisticFnResults)[0];
54483
55192
  }
54484
- return this.getComposedFnName(this.selectedStatisticFn, this.statisticFnResults[this.selectedStatisticFn]);
55193
+ return this.getComposedFnName(this.selectedStatisticFn);
54485
55194
  }
54486
55195
  listSelectionStatistics(ev) {
54487
55196
  const registry = new MenuItemRegistry();
54488
55197
  let i = 0;
54489
- for (let [fnName, fnValue] of Object.entries(this.statisticFnResults)) {
55198
+ for (let [fnName] of Object.entries(this.store.statisticFnResults)) {
54490
55199
  registry.add(fnName, {
54491
- name: this.getComposedFnName(fnName, fnValue),
55200
+ name: () => this.getComposedFnName(fnName),
54492
55201
  sequence: i,
54493
55202
  isReadonlyAllowed: true,
54494
55203
  execute: () => {
@@ -54501,8 +55210,9 @@ class BottomBarStatistic extends Component {
54501
55210
  const { top, left, width } = target.getBoundingClientRect();
54502
55211
  this.props.openContextMenu(left + width, top, registry);
54503
55212
  }
54504
- getComposedFnName(fnName, fnValue) {
55213
+ getComposedFnName(fnName) {
54505
55214
  const locale = this.env.model.getters.getLocale();
55215
+ const fnValue = this.store.statisticFnResults[fnName];
54506
55216
  return fnName + ": " + (fnValue !== undefined ? formatValue(fnValue, { locale }) : "__");
54507
55217
  }
54508
55218
  }
@@ -54611,10 +55321,14 @@ class BottomBar extends Component {
54611
55321
  name: sheet.name,
54612
55322
  sequence: i,
54613
55323
  isReadonlyAllowed: true,
54614
- textColor: sheet.isVisible ? undefined : "grey",
55324
+ textColor: sheet.isVisible ? undefined : "#808080",
54615
55325
  execute: (env) => {
55326
+ if (!this.env.model.getters.isSheetVisible(sheetId)) {
55327
+ this.env.model.dispatch("SHOW_SHEET", { sheetId });
55328
+ }
54616
55329
  env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: from, sheetIdTo: sheetId });
54617
55330
  },
55331
+ isEnabled: (env) => (env.model.getters.isReadonly() ? sheet.isVisible : true),
54618
55332
  });
54619
55333
  i++;
54620
55334
  }
@@ -54687,7 +55401,7 @@ class BottomBar extends Component {
54687
55401
  this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
54688
55402
  }
54689
55403
  onSheetMouseDown(sheetId, event) {
54690
- if (event.button !== 0)
55404
+ if (event.button !== 0 || this.env.model.getters.isReadonly())
54691
55405
  return;
54692
55406
  this.closeMenu();
54693
55407
  const visibleSheets = this.getVisibleSheets();
@@ -54723,7 +55437,7 @@ class BottomBar extends Component {
54723
55437
  .map((sheetEl) => sheetEl.getBoundingClientRect())
54724
55438
  .map((rect) => ({
54725
55439
  x: rect.x,
54726
- width: rect.width - 1,
55440
+ width: rect.width - 1, // -1 to compensate negative margin
54727
55441
  y: rect.y,
54728
55442
  height: rect.height,
54729
55443
  }));
@@ -54950,7 +55664,7 @@ class RowGroup extends AbstractHeaderGroup {
54950
55664
  }
54951
55665
  return cssPropertiesToCss({
54952
55666
  top: `${groupBox.headerRect.height / 2}px`,
54953
- left: `calc(50% - 1px)`,
55667
+ left: `calc(50% - 1px)`, // -1px: we want the border to be on the center
54954
55668
  width: `30%`,
54955
55669
  height: `calc(100% - ${groupBox.headerRect.height / 2}px)`,
54956
55670
  "border-left": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
@@ -55002,7 +55716,7 @@ class ColGroup extends AbstractHeaderGroup {
55002
55716
  return "";
55003
55717
  }
55004
55718
  return cssPropertiesToCss({
55005
- top: `calc(50% - 1px)`,
55719
+ top: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55006
55720
  left: `${groupBox.headerRect.width / 2}px`,
55007
55721
  width: `calc(100% - ${groupBox.headerRect.width / 2}px)`,
55008
55722
  height: `30%`,
@@ -56091,12 +56805,9 @@ css /* scss */ `
56091
56805
  .text-muted {
56092
56806
  color: grey !important;
56093
56807
  }
56094
- button {
56095
- color: #333;
56096
- }
56097
56808
  .o-disabled {
56098
56809
  opacity: 0.4;
56099
- pointer: default;
56810
+ cursor: default;
56100
56811
  pointer-events: none;
56101
56812
  }
56102
56813
 
@@ -56214,17 +56925,17 @@ css /* scss */ `
56214
56925
  }
56215
56926
 
56216
56927
  .o-button {
56217
- border: 1px solid lightgrey;
56928
+ border: 1px solid;
56218
56929
  padding: 0px 20px 0px 20px;
56219
56930
  border-radius: 4px;
56220
56931
  font-weight: 500;
56221
56932
  font-size: 14px;
56222
56933
  height: 30px;
56223
56934
  line-height: 16px;
56224
- background: white;
56225
56935
  margin-right: 8px;
56226
- &:hover:enabled {
56227
- background-color: rgba(0, 0, 0, 0.08);
56936
+
56937
+ &:not(:hover) {
56938
+ background-color: transparent;
56228
56939
  }
56229
56940
 
56230
56941
  &:enabled {
@@ -56238,6 +56949,15 @@ css /* scss */ `
56238
56949
  &:last-child {
56239
56950
  margin-right: 0px;
56240
56951
  }
56952
+
56953
+ &.o-button-grey {
56954
+ border-color: lightgrey;
56955
+ background: #ffffff;
56956
+ color: #333;
56957
+ &:hover:enabled {
56958
+ background-color: rgba(0, 0, 0, 0.08);
56959
+ }
56960
+ }
56241
56961
  }
56242
56962
 
56243
56963
  .o-input {
@@ -56253,7 +56973,7 @@ css /* scss */ `
56253
56973
 
56254
56974
  .o-number-input {
56255
56975
  /* Remove number input arrows */
56256
- -moz-appearance: textfield;
56976
+ appearance: textfield;
56257
56977
  &::-webkit-outer-spin-button,
56258
56978
  &::-webkit-inner-spin-button {
56259
56979
  -webkit-appearance: none;
@@ -56296,6 +57016,7 @@ class Spreadsheet extends Component {
56296
57016
  this.notificationStore = useStore(NotificationStore);
56297
57017
  this.composerFocusStore = useStore(ComposerFocusStore);
56298
57018
  this.sidePanel = useStore(SidePanelStore);
57019
+ useStore(ArrayFormulaHighlight);
56299
57020
  this.keyDownMapping = {
56300
57021
  "CTRL+H": () => this.sidePanel.toggle("FindAndReplace", {}),
56301
57022
  "CTRL+F": () => this.sidePanel.toggle("FindAndReplace", {}),
@@ -56398,7 +57119,7 @@ class Spreadsheet extends Component {
56398
57119
  const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
56399
57120
  const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;
56400
57121
  return cssPropertiesToCss({
56401
- "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`,
57122
+ "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`, // +2: margins
56402
57123
  "grid-template-rows": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,
56403
57124
  });
56404
57125
  }
@@ -57130,14 +57851,6 @@ class SelectiveHistory {
57130
57851
  this.revertBefore(operationId);
57131
57852
  this.tree.drop(operationId);
57132
57853
  }
57133
- getRevertedExecution() {
57134
- const data = [];
57135
- const operations = this.tree.revertedExecution(this.HEAD_BRANCH);
57136
- for (const { operation } of operations) {
57137
- data.push(operation.data);
57138
- }
57139
- return data;
57140
- }
57141
57854
  /**
57142
57855
  * Revert the state as it was *before* the given operation was executed.
57143
57856
  */
@@ -57926,6 +58639,9 @@ function createChart(chart, chartSheetIndex, data) {
57926
58639
  case "bar":
57927
58640
  plot = addBarChart(chart.data);
57928
58641
  break;
58642
+ case "combo":
58643
+ plot = addComboChart(chart.data);
58644
+ break;
57929
58645
  case "line":
57930
58646
  plot = addLineChart(chart.data);
57931
58647
  break;
@@ -58088,6 +58804,79 @@ function addBarChart(chart) {
58088
58804
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58089
58805
  `;
58090
58806
  }
58807
+ function addComboChart(chart) {
58808
+ // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)
58809
+ // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB
58810
+ // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB
58811
+ //
58812
+ // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.
58813
+ // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage
58814
+ const colors = new ChartColors();
58815
+ const dataSetsNodes = [];
58816
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
58817
+ const color = toXlsxHexColor(colors.next());
58818
+ const dataShapeProperty = shapeProperty({
58819
+ backgroundColor: color,
58820
+ line: { color },
58821
+ });
58822
+ dataSetsNodes.push(dsIndex === "0"
58823
+ ? escapeXml /*xml*/ `
58824
+ <c:ser>
58825
+ <c:idx val="${dsIndex}"/>
58826
+ <c:order val="${dsIndex}"/>
58827
+ ${dataset.label ? escapeXml /*xml*/ `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
58828
+ ${dataShapeProperty}
58829
+ ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
58830
+ <c:val> <!-- x-coordinate values -->
58831
+ ${numberRef(dataset.range)}
58832
+ </c:val>
58833
+ </c:ser>
58834
+ `
58835
+ : escapeXml /*xml*/ `
58836
+ <c:ser>
58837
+ <c:idx val="${dsIndex}"/>
58838
+ <c:order val="${dsIndex}"/>
58839
+ <c:smooth val="0"/>
58840
+ <c:marker>
58841
+ <c:symbol val="circle" />
58842
+ <c:size val="5"/>
58843
+ </c:marker>
58844
+ ${dataset.label ? escapeXml `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
58845
+ ${dataShapeProperty}
58846
+ ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
58847
+ <c:val> <!-- x-coordinate values -->
58848
+ ${numberRef(dataset.range)}
58849
+ </c:val>
58850
+ </c:ser>
58851
+ `);
58852
+ }
58853
+ // Excel does not support this feature
58854
+ const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
58855
+ const overlap = chart.stacked ? 100 : -20;
58856
+ return escapeXml /*xml*/ `
58857
+ <c:barChart>
58858
+ <c:barDir val="col"/>
58859
+ <c:grouping val="clustered"/>
58860
+ <c:overlap val="${overlap}"/>
58861
+ <c:gapWidth val="70"/>
58862
+ <!-- each data marker in the series does not have a different color -->
58863
+ <c:varyColors val="0"/>
58864
+ ${dataSetsNodes[0]}
58865
+ <c:axId val="${catAxId}" />
58866
+ <c:axId val="${valAxId}" />
58867
+ </c:barChart>
58868
+ <c:lineChart>
58869
+ <c:grouping val="standard"/>
58870
+ <!-- each data marker in the series does not have a different color -->
58871
+ <c:varyColors val="0"/>
58872
+ ${joinXmlNodes(dataSetsNodes.slice(1))}
58873
+ <c:axId val="${catAxId}" />
58874
+ <c:axId val="${valAxId}" />
58875
+ </c:lineChart>
58876
+ ${addAx("b", "c:catAx", catAxId, valAxId, { fontColor: chart.fontColor })}
58877
+ ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58878
+ `;
58879
+ }
58091
58880
  function addLineChart(chart) {
58092
58881
  const colors = new ChartColors();
58093
58882
  const dataSetsNodes = [];
@@ -58135,7 +58924,7 @@ function addLineChart(chart) {
58135
58924
  }
58136
58925
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
58137
58926
  const colors = new ChartColors();
58138
- const maxLength = Math.max(...chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
58927
+ const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
58139
58928
  const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
58140
58929
  const dataSetsNodes = [];
58141
58930
  for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
@@ -59013,7 +59802,7 @@ function addTableColumns(table, sheetData) {
59013
59802
  const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
59014
59803
  const colName = sheetData.cells[colHeaderXc]?.content || `col${i}`;
59015
59804
  const colAttributes = [
59016
- ["id", i + 1],
59805
+ ["id", i + 1], // id cannot be 0
59017
59806
  ["name", colName],
59018
59807
  ];
59019
59808
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
@@ -59229,6 +60018,7 @@ function addSheetViews(sheet) {
59229
60018
  * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
59230
60019
  */
59231
60020
  function getXLSX(data) {
60021
+ data = fixLengthySheetNames(data);
59232
60022
  const files = [];
59233
60023
  const construct = getDefaultXLSXStructure();
59234
60024
  files.push(createWorkbook(data, construct));
@@ -59476,6 +60266,40 @@ function createRelRoot() {
59476
60266
  `;
59477
60267
  return createXMLFile(parseXML(xml), "_rels/.rels");
59478
60268
  }
60269
+ /**
60270
+ * Excel sheet names are maximum 31 characters while o-spreadsheet do not have this limit.
60271
+ * This method converts the sheet names to be within the 31 characters limit.
60272
+ * The cells/charts referencing this sheet will be updated accordingly.
60273
+ */
60274
+ function fixLengthySheetNames(data) {
60275
+ const nameMapping = {};
60276
+ const newNames = new Set();
60277
+ for (const sheet of data.sheets) {
60278
+ let newName = sheet.name.slice(0, 31);
60279
+ let i = 1;
60280
+ while (newNames.has(newName)) {
60281
+ newName = newName.slice(0, 31 - String(i).length) + i++;
60282
+ }
60283
+ newNames.add(newName);
60284
+ if (newName !== sheet.name) {
60285
+ nameMapping[sheet.name] = newName;
60286
+ sheet.name = newName;
60287
+ }
60288
+ }
60289
+ if (!Object.keys(nameMapping).length) {
60290
+ return data;
60291
+ }
60292
+ const sheetWithNewNames = Object.keys(nameMapping).sort((a, b) => b.length - a.length);
60293
+ let stringifiedData = JSON.stringify(data);
60294
+ for (const sheetName of sheetWithNewNames) {
60295
+ const regex = new RegExp(`'?${escapeRegExp(sheetName)}'?!`, "g");
60296
+ stringifiedData = stringifiedData.replaceAll(regex, (match) => {
60297
+ const newName = nameMapping[sheetName];
60298
+ return match.replace(sheetName, newName);
60299
+ });
60300
+ }
60301
+ return JSON.parse(stringifiedData);
60302
+ }
59479
60303
 
59480
60304
  var Status;
59481
60305
  (function (Status) {
@@ -60124,6 +60948,6 @@ const constants = {
60124
60948
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
60125
60949
 
60126
60950
 
60127
- __info__.version = "17.3.0-alpha.0";
60128
- __info__.date = "2024-03-20T13:42:32.042Z";
60129
- __info__.hash = "073e154";
60951
+ __info__.version = "17.3.0-alpha.2";
60952
+ __info__.date = "2024-04-05T14:01:07.060Z";
60953
+ __info__.hash = "8c5a229";