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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * This file is generated by o-spreadsheet build tools. Do not edit it.
5
5
  * @see https://github.com/odoo/o-spreadsheet
6
- * @version 17.3.0-alpha.1
7
- * @date 2024-03-25T09:43:36.072Z
8
- * @hash 4095c41
6
+ * @version 17.3.0-alpha.3
7
+ * @date 2024-04-10T12:28:23.658Z
8
+ * @hash 80b5056
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';
@@ -188,7 +188,7 @@ const DEFAULT_GAUGE_LOWER_COLOR = "#cc0000";
188
188
  const DEFAULT_GAUGE_MIDDLE_COLOR = "#f1c232";
189
189
  const DEFAULT_GAUGE_UPPER_COLOR = "#6aa84f";
190
190
  const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
191
- const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#6aa84f";
191
+ const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#6AA84F";
192
192
  const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#E06666";
193
193
  const LINE_FILL_TRANSPARENCY = 0.4;
194
194
  // session
@@ -464,7 +464,7 @@ function getItemId(item, itemsDic) {
464
464
  }
465
465
  // Generate new Id if the item didn't exist in the dictionary
466
466
  const ids = Object.keys(itemsDic);
467
- 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)));
468
468
  itemsDic[maxId + 1] = item;
469
469
  return maxId + 1;
470
470
  }
@@ -482,7 +482,7 @@ function debounce(func, wait, immediate) {
482
482
  let timeout = undefined;
483
483
  const debounced = function () {
484
484
  const context = this;
485
- const args = arguments;
485
+ const args = Array.from(arguments);
486
486
  function later() {
487
487
  timeout = undefined;
488
488
  if (!immediate) {
@@ -560,7 +560,7 @@ function deepEquals(o1, o2) {
560
560
  if (typeof o1 !== typeof o2)
561
561
  return false;
562
562
  if (typeof o1 !== "object")
563
- return o1 === o2;
563
+ return false;
564
564
  // Objects can have different keys if the values are undefined
565
565
  for (const key in o2) {
566
566
  if (!(key in o1) && o2[key] !== undefined) {
@@ -684,6 +684,34 @@ function getSearchRegex(searchStr, searchOptions) {
684
684
  }
685
685
  return RegExp(searchValue, flags);
686
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
+ }
687
715
 
688
716
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
689
717
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
@@ -1856,6 +1884,11 @@ const invalidateCFEvaluationCommands = new Set([
1856
1884
  "REMOVE_CONDITIONAL_FORMAT",
1857
1885
  "CHANGE_CONDITIONAL_FORMAT_PRIORITY",
1858
1886
  ]);
1887
+ const invalidateBordersCommands = new Set([
1888
+ "AUTOFILL_CELL",
1889
+ "SET_BORDER",
1890
+ "SET_ZONE_BORDERS",
1891
+ ]);
1859
1892
  const readonlyAllowedCommands = new Set([
1860
1893
  "START",
1861
1894
  "ACTIVATE_SHEET",
@@ -2086,6 +2119,7 @@ var CommandResult;
2086
2119
  CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
2087
2120
  CommandResult["NoChanges"] = "NoChanges";
2088
2121
  CommandResult["InvalidInputId"] = "InvalidInputId";
2122
+ CommandResult["SheetIsHidden"] = "SheetIsHidden";
2089
2123
  })(CommandResult || (CommandResult = {}));
2090
2124
 
2091
2125
  const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
@@ -2152,6 +2186,7 @@ const CellErrorType = {
2152
2186
  BadExpression: "#BAD_EXPR",
2153
2187
  CircularDependency: "#CYCLE",
2154
2188
  UnknownFunction: "#NAME?",
2189
+ DivisionByZero: "#DIV/0!",
2155
2190
  GenericError: "#ERROR",
2156
2191
  };
2157
2192
  const errorTypes = new Set(Object.values(CellErrorType));
@@ -2190,9 +2225,9 @@ class UnknownFunctionError extends EvaluationError {
2190
2225
 
2191
2226
  // HELPERS
2192
2227
  const SORT_TYPES_ORDER = ["number", "string", "boolean", "undefined"];
2193
- function assert(condition, message) {
2228
+ function assert(condition, message, value) {
2194
2229
  if (!condition()) {
2195
- throw new EvaluationError(message);
2230
+ throw new EvaluationError(message, value);
2196
2231
  }
2197
2232
  }
2198
2233
  function inferFormat(data) {
@@ -2270,6 +2305,9 @@ function strictToInteger(value, locale) {
2270
2305
  function assertNumberGreaterThanOrEqualToOne(value) {
2271
2306
  assert(() => value >= 1, _t("The function [[FUNCTION_NAME]] expects a number value to be greater than or equal to 1, but receives %s.", value.toString()));
2272
2307
  }
2308
+ function assertNotZero(value) {
2309
+ assert(() => value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
2310
+ }
2273
2311
  function toString(data) {
2274
2312
  const value = toValue(data);
2275
2313
  switch (typeof value) {
@@ -3035,6 +3073,9 @@ function applyInternalFormat(value, internalFormat, locale) {
3035
3073
  return formattedValue;
3036
3074
  }
3037
3075
  function applyInternalNumberFormat(value, format, locale) {
3076
+ if (value === Infinity) {
3077
+ return "∞" + (format.isPercent ? "%" : "");
3078
+ }
3038
3079
  if (format.isPercent) {
3039
3080
  value = value * 100;
3040
3081
  }
@@ -3378,6 +3419,46 @@ function roundFormat(format) {
3378
3419
  });
3379
3420
  return convertInternalFormatToFormat(roundedFormat);
3380
3421
  }
3422
+ function humanizeNumber({ value, format }, locale) {
3423
+ const numberFormat = formatLargeNumber({
3424
+ value,
3425
+ format,
3426
+ }, undefined, locale);
3427
+ return formatValue(value, { format: numberFormat, locale });
3428
+ }
3429
+ function formatLargeNumber(arg, unit, locale) {
3430
+ let value = 0;
3431
+ try {
3432
+ value = Math.abs(toNumber(arg?.value, locale));
3433
+ }
3434
+ catch (e) {
3435
+ return "";
3436
+ }
3437
+ const format = arg?.format;
3438
+ if (unit !== undefined) {
3439
+ const postFix = unit?.value;
3440
+ switch (postFix) {
3441
+ case "k":
3442
+ return createLargeNumberFormat(format, 1e3, "k");
3443
+ case "m":
3444
+ return createLargeNumberFormat(format, 1e6, "m");
3445
+ case "b":
3446
+ return createLargeNumberFormat(format, 1e9, "b");
3447
+ default:
3448
+ throw new EvaluationError(_t("The formatting unit should be 'k', 'm' or 'b'."));
3449
+ }
3450
+ }
3451
+ if (value < 1e5) {
3452
+ return createLargeNumberFormat(format, 0, "");
3453
+ }
3454
+ else if (value < 1e8) {
3455
+ return createLargeNumberFormat(format, 1e3, "k");
3456
+ }
3457
+ else if (value < 1e11) {
3458
+ return createLargeNumberFormat(format, 1e6, "m");
3459
+ }
3460
+ return createLargeNumberFormat(format, 1e9, "b");
3461
+ }
3381
3462
  function createLargeNumberFormat(format, magnitude, postFix, locale) {
3382
3463
  const internalFormat = parseFormat(format || "#,##0");
3383
3464
  const largeNumberFormat = [];
@@ -4557,8 +4638,9 @@ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
4557
4638
  * Get the default height of the cell given its style.
4558
4639
  */
4559
4640
  function getDefaultCellHeight(ctx, cell, colSize) {
4560
- if (!cell || !cell.content)
4641
+ if (!cell || (!cell.isFormula && !cell.content)) {
4561
4642
  return DEFAULT_CELL_HEIGHT;
4643
+ }
4562
4644
  const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
4563
4645
  const numberOfLines = cell.isFormula
4564
4646
  ? 1
@@ -4569,14 +4651,19 @@ function getDefaultCellHeight(ctx, cell, colSize) {
4569
4651
  const textWidthCache = {};
4570
4652
  function computeTextWidth(context, text, style, fontUnit = "pt") {
4571
4653
  const font = computeTextFont(style, fontUnit);
4654
+ context.save();
4655
+ context.font = font;
4656
+ const width = computeCachedTextWidth(context, text);
4657
+ context.restore();
4658
+ return width;
4659
+ }
4660
+ function computeCachedTextWidth(context, text) {
4661
+ const font = context.font;
4572
4662
  if (!textWidthCache[font]) {
4573
4663
  textWidthCache[font] = {};
4574
4664
  }
4575
4665
  if (textWidthCache[font][text] === undefined) {
4576
- context.save();
4577
- context.font = font;
4578
4666
  const textWidth = context.measureText(text).width;
4579
- context.restore();
4580
4667
  textWidthCache[font][text] = textWidth;
4581
4668
  }
4582
4669
  return textWidthCache[font][text];
@@ -4721,6 +4808,42 @@ const pxRegex = /([0-9\.]*)px/;
4721
4808
  function getContextFontSize(font) {
4722
4809
  return Number(font.match(pxRegex)?.[1]);
4723
4810
  }
4811
+ // Inspired from https://stackoverflow.com/a/10511598
4812
+ function clipTextWithEllipsis(ctx, text, maxWidth) {
4813
+ let width = computeCachedTextWidth(ctx, text);
4814
+ if (width <= maxWidth) {
4815
+ return text;
4816
+ }
4817
+ const ellipsis = "…";
4818
+ const ellipsisWidth = computeCachedTextWidth(ctx, text);
4819
+ if (width <= ellipsisWidth) {
4820
+ return text;
4821
+ }
4822
+ let len = text.length;
4823
+ while (width >= maxWidth - ellipsisWidth && len-- > 0) {
4824
+ text = text.substring(0, len);
4825
+ width = computeCachedTextWidth(ctx, text);
4826
+ }
4827
+ return text + ellipsis;
4828
+ }
4829
+ function splitTextInTwoLines(text) {
4830
+ let spaces = "";
4831
+ while (text[0] === " ") {
4832
+ spaces += " ";
4833
+ text = text.slice(1);
4834
+ }
4835
+ const length = text.length;
4836
+ const middle = Math.floor(length / 2);
4837
+ const leftSpace = text.substring(0, middle).lastIndexOf(" ");
4838
+ const rightSpace = text.substring(middle).indexOf(" ") + middle;
4839
+ if (leftSpace === -1 && rightSpace === middle - 1) {
4840
+ return [spaces + text, ""];
4841
+ }
4842
+ if (leftSpace > length - rightSpace || rightSpace === middle - 1) {
4843
+ return [spaces + text.slice(0, leftSpace), spaces + text.slice(leftSpace + 1)];
4844
+ }
4845
+ return [spaces + text.slice(0, rightSpace), spaces + text.slice(rightSpace + 1)];
4846
+ }
4724
4847
  function drawDecoratedText(context, text, position, underline = false, strikethrough = false, strokeWidth = getContextFontSize(context.font) / 10 //This value is defined to get a good looking stroke
4725
4848
  ) {
4726
4849
  context.fillText(text, position.x, position.y);
@@ -6992,10 +7115,17 @@ urlRegistry.add("sheet_URL", {
6992
7115
  },
6993
7116
  open(url, env) {
6994
7117
  const sheetId = parseSheetUrl(url);
6995
- env.model.dispatch("ACTIVATE_SHEET", {
7118
+ const result = env.model.dispatch("ACTIVATE_SHEET", {
6996
7119
  sheetIdFrom: env.model.getters.getActiveSheetId(),
6997
7120
  sheetIdTo: sheetId,
6998
7121
  });
7122
+ if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
7123
+ env.notifyUser({
7124
+ type: "warning",
7125
+ sticky: false,
7126
+ text: _t("Cannot open the link because the linked sheet is hidden."),
7127
+ });
7128
+ }
6999
7129
  },
7000
7130
  sequence: 0,
7001
7131
  });
@@ -7111,7 +7241,7 @@ function textCell(value, format, formattedValue) {
7111
7241
  }
7112
7242
  function numberCell(value, format, formattedValue) {
7113
7243
  return {
7114
- value: value || 0,
7244
+ value: value || 0, // necessary to avoid "-0" and NaN values,
7115
7245
  format,
7116
7246
  formattedValue,
7117
7247
  type: CellValueType.number,
@@ -7357,6 +7487,7 @@ const CellIsOperators = {
7357
7487
  };
7358
7488
  const ChartTerms = {
7359
7489
  Series: _t("Series"),
7490
+ BackgroundColor: _t("Background color"),
7360
7491
  Errors: {
7361
7492
  Unexpected: _t("The chart definition is invalid for an unknown reason"),
7362
7493
  // BASIC CHART ERRORS (LINE | BAR | PIE)
@@ -9392,32 +9523,63 @@ function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
9392
9523
  }
9393
9524
  return true;
9394
9525
  }
9395
- // ---------------------------------------------------------------------------
9396
- // Scorecard
9397
- // ---------------------------------------------------------------------------
9398
- function getBaselineText(baseline, keyValue, baselineMode, locale) {
9526
+ function getChartPositionAtCenterOfViewport(getters, chartSize) {
9527
+ const { x, y } = getters.getMainViewportCoordinates();
9528
+ const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
9529
+ const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
9530
+ const position = {
9531
+ x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
9532
+ y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
9533
+ }; // Position at the center of the scrollable viewport
9534
+ return position;
9535
+ }
9536
+
9537
+ function getBaselineText(baseline, keyValue, baselineMode, humanize, locale) {
9399
9538
  if (!baseline) {
9400
9539
  return "";
9401
9540
  }
9402
9541
  else if (baselineMode === "text" ||
9403
9542
  keyValue?.type !== CellValueType.number ||
9404
9543
  baseline.type !== CellValueType.number) {
9544
+ if (humanize) {
9545
+ return humanizeNumber(baseline, locale);
9546
+ }
9405
9547
  return baseline.formattedValue;
9406
9548
  }
9549
+ let { value, format } = baseline;
9550
+ if (baselineMode === "progress") {
9551
+ value = keyValue.value / value;
9552
+ format = "0.0%";
9553
+ }
9407
9554
  else {
9408
- let diff = keyValue.value - baseline.value;
9409
- if (baselineMode === "percentage" && diff !== 0) {
9410
- diff = (diff / baseline.value) * 100;
9555
+ value = Math.abs(keyValue.value - value);
9556
+ if (baselineMode === "percentage" && value !== 0) {
9557
+ value = value / baseline.value;
9558
+ }
9559
+ if (baselineMode === "percentage") {
9560
+ format = "0.0%";
9411
9561
  }
9412
- if (baselineMode !== "percentage" && baseline.format) {
9413
- return formatValue(diff, { format: baseline.format, locale });
9562
+ if (!format) {
9563
+ value = Math.round(value * 100) / 100;
9414
9564
  }
9415
- const baselineStr = Math.abs(parseFloat(diff.toFixed(2))).toLocaleString();
9416
- return baselineMode === "percentage" ? baselineStr + "%" : baselineStr;
9417
9565
  }
9566
+ if (humanize) {
9567
+ return humanizeNumber({ value, format }, locale);
9568
+ }
9569
+ return formatValue(value, { format, locale });
9570
+ }
9571
+ function getKeyValueText(keyValueCell, humanize, locale) {
9572
+ if (!keyValueCell) {
9573
+ return "";
9574
+ }
9575
+ if (humanize) {
9576
+ return humanizeNumber(keyValueCell, locale);
9577
+ }
9578
+ return keyValueCell.formattedValue ?? String(keyValueCell.value ?? "");
9418
9579
  }
9419
9580
  function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
9420
9581
  if (baselineMode === "text" ||
9582
+ baselineMode === "progress" ||
9421
9583
  baseline?.type !== CellValueType.number ||
9422
9584
  keyValue?.type !== CellValueType.number) {
9423
9585
  return undefined;
@@ -9446,17 +9608,6 @@ function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
9446
9608
  }
9447
9609
  return "neutral";
9448
9610
  }
9449
- function getChartPositionAtCenterOfViewport(getters, chartSize) {
9450
- const { x, y } = getters.getMainViewportCoordinates();
9451
- const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
9452
- const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
9453
- const position = {
9454
- x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
9455
- y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
9456
- }; // Position at the center of the scrollable viewport
9457
- return position;
9458
- }
9459
-
9460
9611
  function checkKeyValue(definition) {
9461
9612
  return definition.keyValue && !rangeReference.test(definition.keyValue)
9462
9613
  ? "InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */
@@ -9474,10 +9625,12 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9474
9625
  baseline;
9475
9626
  baselineMode;
9476
9627
  baselineDescr;
9628
+ progressBar = false;
9477
9629
  background;
9478
9630
  baselineColorUp;
9479
9631
  baselineColorDown;
9480
9632
  fontColor;
9633
+ humanize;
9481
9634
  type = "scorecard";
9482
9635
  constructor(definition, sheetId, getters) {
9483
9636
  super(definition, sheetId, getters);
@@ -9488,6 +9641,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9488
9641
  this.background = definition.background;
9489
9642
  this.baselineColorUp = definition.baselineColorUp;
9490
9643
  this.baselineColorDown = definition.baselineColorDown;
9644
+ this.humanize = definition.humanize ?? false;
9491
9645
  }
9492
9646
  static validateChartDefinition(validator, definition) {
9493
9647
  return validator.checkValidations(definition, checkKeyValue, checkBaseline);
@@ -9557,6 +9711,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9557
9711
  keyValue: keyValue
9558
9712
  ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
9559
9713
  : undefined,
9714
+ humanize: this.humanize,
9560
9715
  };
9561
9716
  }
9562
9717
  getDefinitionForExcel() {
@@ -9582,7 +9737,7 @@ function drawScoreChart(structure, canvas) {
9582
9737
  if (structure.title) {
9583
9738
  ctx.font = structure.title.style.font;
9584
9739
  ctx.fillStyle = structure.title.style.color;
9585
- ctx.fillText(structure.title.text, structure.title.position.x, structure.title.position.y);
9740
+ ctx.fillText(clipTextWithEllipsis(ctx, structure.title.text, canvas.width - structure.title.position.x), structure.title.position.x, structure.title.position.y);
9586
9741
  }
9587
9742
  if (structure.baseline) {
9588
9743
  ctx.font = structure.baseline.style.font;
@@ -9609,20 +9764,41 @@ function drawScoreChart(structure, canvas) {
9609
9764
  ctx.restore();
9610
9765
  }
9611
9766
  if (structure.baselineDescr) {
9612
- ctx.font = structure.baselineDescr.style.font;
9613
- ctx.fillStyle = structure.baselineDescr.style.color;
9614
- ctx.fillText(structure.baselineDescr.text, structure.baselineDescr.position.x, structure.baselineDescr.position.y);
9767
+ const descr = structure.baselineDescr[0];
9768
+ ctx.font = descr.style.font;
9769
+ ctx.fillStyle = descr.style.color;
9770
+ for (const description of structure.baselineDescr) {
9771
+ ctx.fillText(clipTextWithEllipsis(ctx, description.text, canvas.width - description.position.x), description.position.x, description.position.y);
9772
+ }
9615
9773
  }
9616
9774
  if (structure.key) {
9617
9775
  ctx.font = structure.key.style.font;
9618
9776
  ctx.fillStyle = structure.key.style.color;
9619
9777
  drawDecoratedText(ctx, structure.key.text, structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
9620
9778
  }
9779
+ if (structure.progressBar) {
9780
+ ctx.fillStyle = structure.progressBar.style.backgroundColor;
9781
+ ctx.beginPath();
9782
+ ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
9783
+ ctx.fill();
9784
+ ctx.fillStyle = structure.progressBar.style.color;
9785
+ ctx.beginPath();
9786
+ if (structure.progressBar.value > 0) {
9787
+ ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width *
9788
+ Math.max(0, Math.min(1.0, structure.progressBar.value)), structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
9789
+ }
9790
+ else {
9791
+ const width = structure.progressBar.dimension.width *
9792
+ Math.max(0, Math.min(1.0, -structure.progressBar.value));
9793
+ ctx.roundRect(structure.progressBar.position.x + structure.progressBar.dimension.width - width, structure.progressBar.position.y, width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
9794
+ }
9795
+ ctx.fill();
9796
+ }
9621
9797
  }
9622
9798
  function createScorecardChartRuntime(chart, getters) {
9623
- let keyValue = "";
9624
9799
  let formattedKeyValue = "";
9625
9800
  let keyValueCell;
9801
+ const locale = getters.getLocale();
9626
9802
  if (chart.keyValue) {
9627
9803
  const keyValuePosition = {
9628
9804
  sheetId: chart.keyValue.sheetId,
@@ -9630,31 +9806,33 @@ function createScorecardChartRuntime(chart, getters) {
9630
9806
  row: chart.keyValue.zone.top,
9631
9807
  };
9632
9808
  keyValueCell = getters.getEvaluatedCell(keyValuePosition);
9633
- keyValue = String(keyValueCell.value ?? "");
9634
- formattedKeyValue = keyValueCell.formattedValue;
9809
+ formattedKeyValue = getKeyValueText(keyValueCell, chart.humanize ?? false, locale);
9635
9810
  }
9636
9811
  let baselineCell;
9637
9812
  const baseline = chart.baseline;
9638
9813
  if (baseline) {
9639
9814
  const baselinePosition = {
9640
- sheetId: chart.baseline.sheetId,
9641
- col: chart.baseline.zone.left,
9642
- row: chart.baseline.zone.top,
9815
+ sheetId: baseline.sheetId,
9816
+ col: baseline.zone.left,
9817
+ row: baseline.zone.top,
9643
9818
  };
9644
9819
  baselineCell = getters.getEvaluatedCell(baselinePosition);
9645
9820
  }
9646
9821
  const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
9647
- const locale = getters.getLocale();
9822
+ const baselineDisplay = getBaselineText(baselineCell, keyValueCell, chart.baselineMode, chart.humanize ?? false, locale);
9823
+ const baselineValue = chart.baselineMode === "progress" && isNumber(baselineDisplay, locale)
9824
+ ? toNumber(baselineDisplay, locale)
9825
+ : 0;
9648
9826
  return {
9649
9827
  title: _t(chart.title),
9650
- keyValue: formattedKeyValue || keyValue,
9651
- baselineDisplay: getBaselineText(baselineCell, keyValueCell, chart.baselineMode, locale),
9828
+ keyValue: formattedKeyValue,
9829
+ baselineDisplay,
9652
9830
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
9653
9831
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
9654
- baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
9832
+ baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr ? _t(chart.baselineDescr) : "",
9655
9833
  fontColor,
9656
9834
  background,
9657
- baselineStyle: chart.baselineMode !== "percentage" && baseline
9835
+ baselineStyle: chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
9658
9836
  ? getters.getCellStyle({
9659
9837
  sheetId: baseline.sheetId,
9660
9838
  col: baseline.zone.left,
@@ -9668,17 +9846,21 @@ function createScorecardChartRuntime(chart, getters) {
9668
9846
  row: chart.keyValue.zone.top,
9669
9847
  })
9670
9848
  : undefined,
9849
+ progressBar: chart.baselineMode === "progress"
9850
+ ? {
9851
+ value: baselineValue,
9852
+ color: baselineValue > 0 ? chart.baselineColorUp : chart.baselineColorDown,
9853
+ }
9854
+ : undefined,
9671
9855
  };
9672
9856
  }
9673
9857
 
9674
9858
  /* Sizes of boxes containing the texts, in percentage of the Chart size */
9675
9859
  const TITLE_FONT_SIZE = 18;
9676
- const BASELINE_BOX_HEIGHT_RATIO = 0.35;
9677
- const KEY_BOX_HEIGHT_RATIO = 0.65;
9678
- /** Baseline description should have a smaller font than the baseline */
9679
- const BASELINE_DESCR_FONT_RATIO = 0.9;
9680
- /* Padding at the border of the chart, in percentage of the chart width */
9681
- const CHART_PADDING_RATIO = 0.02;
9860
+ const KEY_BOX_HEIGHT_RATIO = 0.8;
9861
+ /* Padding at the border of the chart */
9862
+ const CHART_PADDING = 10;
9863
+ const BOTTOM_PADDING_RATIO = 0.05;
9682
9864
  /**
9683
9865
  * Line height (in em)
9684
9866
  * Having a line heigh =1em (=font size) don't work, the font will overflow.
@@ -9718,31 +9900,37 @@ class ScorecardChartConfigBuilder {
9718
9900
  },
9719
9901
  };
9720
9902
  const style = this.getTextStyles();
9721
- const { height: titleHeight } = this.getTextDimensions(this.title, style.title.font);
9903
+ let titleHeight = 0;
9722
9904
  if (this.title) {
9905
+ ({ height: titleHeight } = this.getFullTextDimensions(this.title, style.title.font));
9723
9906
  structure.title = {
9724
9907
  text: this.title,
9725
9908
  style: style.title,
9726
9909
  position: {
9727
- x: this.chartPadding,
9728
- y: this.chartPadding + titleHeight,
9910
+ x: CHART_PADDING,
9911
+ y: CHART_PADDING / 2 + titleHeight,
9729
9912
  },
9730
9913
  };
9731
9914
  }
9732
9915
  const baselineArrowSize = style.baselineArrow?.size ?? 0;
9733
- const { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
9734
- const { width: baselineDescrWidth } = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font);
9916
+ let { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
9917
+ if (!this.baseline) {
9918
+ baselineHeight = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).height;
9919
+ }
9920
+ const baselineDescrWidth = style.baselineDescr.isSplit
9921
+ ? Math.max(...splitTextInTwoLines(this.baselineDescr).map((line) => this.getTextDimensions(line, style.baselineDescr.font).width))
9922
+ : this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).width;
9735
9923
  structure.baseline = {
9736
9924
  text: this.baseline,
9737
9925
  style: style.baselineValue,
9738
9926
  position: {
9739
9927
  x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2,
9740
9928
  y: this.keyValue
9741
- ? this.height - 2 * this.chartPadding
9742
- : this.height - (this.height - titleHeight - baselineHeight) / 2 - this.chartPadding,
9929
+ ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))
9930
+ : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING,
9743
9931
  },
9744
9932
  };
9745
- if (style.baselineArrow) {
9933
+ if (style.baselineArrow && !this.runtime.progressBar) {
9746
9934
  structure.baselineArrow = {
9747
9935
  direction: this.baselineArrow,
9748
9936
  style: style.baselineArrow,
@@ -9753,23 +9941,68 @@ class ScorecardChartConfigBuilder {
9753
9941
  };
9754
9942
  }
9755
9943
  if (this.baselineDescr) {
9756
- structure.baselineDescr = {
9757
- text: this.baselineDescr,
9758
- style: style.baselineDescr,
9944
+ const position = {
9945
+ x: structure.baseline.position.x + baselineWidth,
9946
+ y: structure.baseline.position.y,
9947
+ };
9948
+ if (style.baselineDescr.isSplit) {
9949
+ const description = splitTextInTwoLines(this.baselineDescr);
9950
+ const measure = this.context.measureText(description[1]);
9951
+ const deltaY = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
9952
+ structure.baselineDescr = [
9953
+ {
9954
+ text: description[0],
9955
+ style: style.baselineDescr,
9956
+ position: {
9957
+ x: position.x,
9958
+ y: position.y - deltaY,
9959
+ },
9960
+ },
9961
+ {
9962
+ text: description[1],
9963
+ style: style.baselineDescr,
9964
+ position,
9965
+ },
9966
+ ];
9967
+ }
9968
+ else {
9969
+ structure.baselineDescr = [
9970
+ {
9971
+ text: this.baselineDescr,
9972
+ style: style.baselineDescr,
9973
+ position,
9974
+ },
9975
+ ];
9976
+ }
9977
+ }
9978
+ let progressBarHeight = 0;
9979
+ if (this.runtime.progressBar) {
9980
+ progressBarHeight = this.height * 0.05;
9981
+ structure.progressBar = {
9759
9982
  position: {
9760
- x: structure.baseline.position.x + baselineWidth,
9761
- y: structure.baseline.position.y,
9983
+ x: 2 * CHART_PADDING,
9984
+ y: this.height * (1 - 2 * BOTTOM_PADDING_RATIO) - baselineHeight - progressBarHeight,
9985
+ },
9986
+ dimension: {
9987
+ height: progressBarHeight,
9988
+ width: this.width - 4 * CHART_PADDING,
9989
+ },
9990
+ value: this.runtime.progressBar.value,
9991
+ style: {
9992
+ color: this.runtime.progressBar.color,
9993
+ backgroundColor: this.secondaryFontColor,
9762
9994
  },
9763
9995
  };
9764
9996
  }
9765
- const { height: keyHeight, width: keyWidth } = this.getTextDimensions(this.keyValue, style.keyValue.font);
9997
+ const { width: keyWidth, height: keyHeight } = this.getFullTextDimensions(this.keyValue, style.keyValue.font);
9766
9998
  if (this.keyValue) {
9767
9999
  structure.key = {
9768
10000
  text: this.keyValue,
9769
10001
  style: style.keyValue,
9770
10002
  position: {
9771
10003
  x: (this.width - keyWidth) / 2,
9772
- y: (this.height - baselineHeight + titleHeight + keyHeight) / 2 - this.chartPadding,
10004
+ y: this.height * (0.5 - BOTTOM_PADDING_RATIO * 2) +
10005
+ (titleHeight + keyHeight / (this.baseline || this.baselineDescr ? 2 : 1.2)) / 2,
9773
10006
  },
9774
10007
  };
9775
10008
  }
@@ -9796,9 +10029,6 @@ class ScorecardChartConfigBuilder {
9796
10029
  get secondaryFontColor() {
9797
10030
  return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
9798
10031
  }
9799
- get chartPadding() {
9800
- return this.width * CHART_PADDING_RATIO;
9801
- }
9802
10032
  getTextDimensions(text, font) {
9803
10033
  this.context.font = font;
9804
10034
  const measure = this.context.measureText(text);
@@ -9807,16 +10037,44 @@ class ScorecardChartConfigBuilder {
9807
10037
  height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,
9808
10038
  };
9809
10039
  }
10040
+ getFullTextDimensions(text, font) {
10041
+ this.context.font = font;
10042
+ const measure = this.context.measureText(text);
10043
+ return {
10044
+ width: measure.width,
10045
+ height: measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent,
10046
+ };
10047
+ }
9810
10048
  getTextStyles() {
9811
10049
  // If the widest text overflows horizontally, scale it down, and apply the same scaling factors to all the other fonts.
9812
- const maxLineWidth = this.width * (1 - 2 * CHART_PADDING_RATIO);
9813
- const widestElement = this.getWidestElement();
9814
- const baseFontSize = widestElement.getElementMaxFontSize(this.getDrawableHeight(), this);
9815
- const fontSizeMatchingWidth = getFontSizeMatchingWidth(maxLineWidth, baseFontSize, (fontSize) => widestElement.getElementWidth(fontSize, this.context, this));
9816
- let scalingFactor = fontSizeMatchingWidth / baseFontSize;
10050
+ const maxLineWidth = this.width - 2 * CHART_PADDING;
10051
+ const drawableHeight = this.getDrawableHeight();
9817
10052
  // Fonts sizes in px
9818
- const keyFontSize = new KeyValueElement(this.runtime.keyValueStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
9819
- const baselineFontSize = new BaselineElement(this.runtime.baselineStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
10053
+ const keyValueElement = new KeyValueElement(this.runtime.keyValueStyle);
10054
+ const heightFont = keyValueElement.getElementMaxFontSize(drawableHeight, this);
10055
+ const widthFont = getFontSizeMatchingWidth(maxLineWidth, 600, (fontSize) => keyValueElement.getElementWidth(fontSize, this.context, this));
10056
+ const keyFontSize = Math.min(heightFont, widthFont);
10057
+ let baselineValueFontSize = Math.floor(keyFontSize * 0.5);
10058
+ this.context.font = getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic);
10059
+ const baselineText = this.baselineArrow !== "neutral" ? "A " + this.baseline : this.baseline;
10060
+ const baselineValueWidth = computeCachedTextWidth(this.context, baselineText);
10061
+ const remainingWidth = maxLineWidth - baselineValueWidth;
10062
+ let baselineDescrFontSize = getFontSizeMatchingWidth(remainingWidth, baselineValueFontSize, (fontSize) => computeTextWidth(this.context, this.baselineDescr, { fontSize }));
10063
+ let isBaselineSplit = false;
10064
+ if (baselineDescrFontSize < baselineValueFontSize / 2.5) {
10065
+ isBaselineSplit = true;
10066
+ baselineDescrFontSize = Math.floor(baselineValueFontSize / 2.5);
10067
+ for (const line of splitTextInTwoLines(this.baselineDescr)) {
10068
+ const lineWidth = getFontSizeMatchingWidth(remainingWidth, baselineValueFontSize, (fontSize) => {
10069
+ this.context.font = getDefaultContextFont(fontSize);
10070
+ return this.context.measureText(line).width;
10071
+ });
10072
+ baselineDescrFontSize = Math.min(baselineDescrFontSize, lineWidth);
10073
+ }
10074
+ }
10075
+ if (this.runtime.progressBar) {
10076
+ baselineValueFontSize /= 1.5;
10077
+ }
9820
10078
  return {
9821
10079
  title: {
9822
10080
  font: getDefaultContextFont(TITLE_FONT_SIZE),
@@ -9829,7 +10087,7 @@ class ScorecardChartConfigBuilder {
9829
10087
  underline: this.runtime.keyValueStyle?.underline,
9830
10088
  },
9831
10089
  baselineValue: {
9832
- font: getDefaultContextFont(baselineFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
10090
+ font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
9833
10091
  strikethrough: this.runtime.baselineStyle?.strikethrough,
9834
10092
  underline: this.runtime.baselineStyle?.underline,
9835
10093
  color: this.runtime.baselineStyle?.textColor ||
@@ -9837,33 +10095,25 @@ class ScorecardChartConfigBuilder {
9837
10095
  this.secondaryFontColor,
9838
10096
  },
9839
10097
  baselineDescr: {
9840
- font: getDefaultContextFont(baselineFontSize * BASELINE_DESCR_FONT_RATIO),
10098
+ font: getDefaultContextFont(baselineDescrFontSize),
10099
+ isSplit: isBaselineSplit,
9841
10100
  color: this.secondaryFontColor,
9842
10101
  },
9843
- baselineArrow: this.baselineArrow === "neutral"
10102
+ baselineArrow: this.baselineArrow === "neutral" || this.runtime.progressBar
9844
10103
  ? undefined
9845
10104
  : {
9846
- size: this.keyValue ? 0.8 * baselineFontSize : 0,
10105
+ size: this.keyValue ? 0.8 * baselineValueFontSize : 0,
9847
10106
  color: this.runtime.baselineColor || this.secondaryFontColor,
9848
10107
  },
9849
10108
  };
9850
10109
  }
9851
10110
  /** Get the height of the chart minus all the vertical paddings */
9852
10111
  getDrawableHeight() {
9853
- const verticalPadding = 2 * this.chartPadding;
9854
- let availableHeight = this.height - verticalPadding;
10112
+ const verticalPadding = CHART_PADDING + this.height * BOTTOM_PADDING_RATIO;
10113
+ let availableHeight = this.height - 2 * verticalPadding;
9855
10114
  availableHeight -= this.title ? TITLE_FONT_SIZE * LINE_HEIGHT : 0;
9856
10115
  return availableHeight;
9857
10116
  }
9858
- /** Return the element with he widest text in the chart */
9859
- getWidestElement() {
9860
- const baseline = new BaselineElement(this.runtime.baselineStyle);
9861
- const keyValue = new KeyValueElement(this.runtime.keyValueStyle);
9862
- return baseline.getElementWidth(BASELINE_BOX_HEIGHT_RATIO, this.context, this) >
9863
- keyValue.getElementWidth(KEY_BOX_HEIGHT_RATIO, this.context, this)
9864
- ? baseline
9865
- : keyValue;
9866
- }
9867
10117
  }
9868
10118
  class ScorecardScalableElement {
9869
10119
  style;
@@ -9872,29 +10122,7 @@ class ScorecardScalableElement {
9872
10122
  }
9873
10123
  measureTextWidth(ctx, text, fontSize) {
9874
10124
  ctx.font = getDefaultContextFont(fontSize, this.style.bold, this.style.italic);
9875
- return ctx.measureText(text).width;
9876
- }
9877
- }
9878
- class BaselineElement extends ScorecardScalableElement {
9879
- getElementWidth(fontSize, ctx, chart) {
9880
- if (!chart.runtime) {
9881
- return 0;
9882
- }
9883
- const baselineStr = chart.baseline;
9884
- // Put mock text to simulate the width of the up/down arrow
9885
- const largeText = chart.baselineArrow !== "neutral" ? "A " + baselineStr : baselineStr;
9886
- let textWidth = this.measureTextWidth(ctx, largeText, fontSize);
9887
- // Baseline descr font size should be smaller than baseline font size
9888
- textWidth += this.measureTextWidth(ctx, chart.baselineDescr, fontSize * BASELINE_DESCR_FONT_RATIO);
9889
- return textWidth;
9890
- }
9891
- getElementMaxFontSize(availableHeight, chart) {
9892
- if (!chart.runtime) {
9893
- return 0;
9894
- }
9895
- const haveBaseline = chart.baseline !== "" || chart.baselineDescr;
9896
- const maxHeight = haveBaseline ? BASELINE_BOX_HEIGHT_RATIO * availableHeight : 0;
9897
- return maxHeight / LINE_HEIGHT;
10125
+ return computeCachedTextWidth(ctx, text);
9898
10126
  }
9899
10127
  }
9900
10128
  class KeyValueElement extends ScorecardScalableElement {
@@ -9955,11 +10183,11 @@ autoCompleteProviders.add("dataValidation", {
9955
10183
  }
9956
10184
  else {
9957
10185
  const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
9958
- values = this.getters
10186
+ values = Array.from(new Set(this.getters
9959
10187
  .getRangeValues(range)
9960
10188
  .filter(isNotNull)
9961
10189
  .map((value) => value.toString())
9962
- .filter((val) => val !== "");
10190
+ .filter((val) => val !== "")));
9963
10191
  }
9964
10192
  return values.map((value) => ({ text: value }));
9965
10193
  },
@@ -10824,33 +11052,6 @@ var array = /*#__PURE__*/Object.freeze({
10824
11052
  // -----------------------------------------------------------------------------
10825
11053
  // FORMAT.LARGE.NUMBER
10826
11054
  // -----------------------------------------------------------------------------
10827
- function formatLargeNumber(arg, unit, locale) {
10828
- const value = Math.abs(toNumber(arg?.value, locale));
10829
- const format = arg?.format;
10830
- if (unit !== undefined) {
10831
- const postFix = unit?.value;
10832
- switch (postFix) {
10833
- case "k":
10834
- return createLargeNumberFormat(format, 1e3, "k");
10835
- case "m":
10836
- return createLargeNumberFormat(format, 1e6, "m");
10837
- case "b":
10838
- return createLargeNumberFormat(format, 1e9, "b");
10839
- default:
10840
- throw new EvaluationError(_t("The formatting unit should be 'k', 'm' or 'b'."));
10841
- }
10842
- }
10843
- if (value < 1e5) {
10844
- return createLargeNumberFormat(format, 0, "");
10845
- }
10846
- else if (value < 1e8) {
10847
- return createLargeNumberFormat(format, 1e3, "k");
10848
- }
10849
- else if (value < 1e11) {
10850
- return createLargeNumberFormat(format, 1e6, "m");
10851
- }
10852
- return createLargeNumberFormat(format, 1e9, "b");
10853
- }
10854
11055
  const FORMAT_LARGE_NUMBER = {
10855
11056
  description: _t("Apply a large number format"),
10856
11057
  args: [
@@ -11012,7 +11213,7 @@ const ATAN2 = {
11012
11213
  compute: function (x, y) {
11013
11214
  const _x = toNumber(x, this.locale);
11014
11215
  const _y = toNumber(y, this.locale);
11015
- assert(() => _x !== 0 || _y !== 0, _t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
11216
+ assert(() => _x !== 0 || _y !== 0, _t("Function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
11016
11217
  return Math.atan2(_y, _x);
11017
11218
  },
11018
11219
  isExported: true,
@@ -11142,7 +11343,7 @@ const COT = {
11142
11343
  returns: ["NUMBER"],
11143
11344
  compute: function (angle) {
11144
11345
  const _angle = toNumber(angle, this.locale);
11145
- assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11346
+ assertNotZero(_angle);
11146
11347
  return 1 / Math.tan(_angle);
11147
11348
  },
11148
11349
  isExported: true,
@@ -11156,7 +11357,7 @@ const COTH = {
11156
11357
  returns: ["NUMBER"],
11157
11358
  compute: function (value) {
11158
11359
  const _value = toNumber(value, this.locale);
11159
- assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11360
+ assertNotZero(_value);
11160
11361
  return 1 / Math.tanh(_value);
11161
11362
  },
11162
11363
  isExported: true,
@@ -11284,7 +11485,7 @@ const CSC = {
11284
11485
  returns: ["NUMBER"],
11285
11486
  compute: function (angle) {
11286
11487
  const _angle = toNumber(angle, this.locale);
11287
- assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11488
+ assertNotZero(_angle);
11288
11489
  return 1 / Math.sin(_angle);
11289
11490
  },
11290
11491
  isExported: true,
@@ -11298,7 +11499,7 @@ const CSCH = {
11298
11499
  returns: ["NUMBER"],
11299
11500
  compute: function (value) {
11300
11501
  const _value = toNumber(value, this.locale);
11301
- assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11502
+ assertNotZero(_value);
11302
11503
  return 1 / Math.sinh(_value);
11303
11504
  },
11304
11505
  isExported: true,
@@ -11497,7 +11698,7 @@ const LN = {
11497
11698
  // MOD
11498
11699
  // -----------------------------------------------------------------------------
11499
11700
  function mod(dividend, divisor) {
11500
- assert(() => divisor !== 0, _t("The divisor must be different from 0."));
11701
+ assert(() => divisor !== 0, _t("The divisor must be different from 0."), CellErrorType.DivisionByZero);
11501
11702
  const modulus = dividend % divisor;
11502
11703
  // -42 % 10 = -2 but we want 8, so need the code below
11503
11704
  if ((modulus > 0 && divisor < 0) || (modulus < 0 && divisor > 0)) {
@@ -12066,7 +12267,7 @@ function average(values, locale) {
12066
12267
  count += 1;
12067
12268
  return acc + a;
12068
12269
  }, 0, locale);
12069
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12270
+ assertNotZero(count);
12070
12271
  return sum / count;
12071
12272
  }
12072
12273
  function countNumbers(values, locale) {
@@ -12133,7 +12334,7 @@ function filterAndFlatData(dataY, dataX) {
12133
12334
  function covariance(dataY, dataX, isSample) {
12134
12335
  const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
12135
12336
  const count = flatDataY.length;
12136
- assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12337
+ assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
12137
12338
  let sumY = 0;
12138
12339
  let sumX = 0;
12139
12340
  for (let i = 0; i < count; i++) {
@@ -12156,7 +12357,7 @@ function variance(args, isSample, textAs0, locale) {
12156
12357
  count += 1;
12157
12358
  return acc + a;
12158
12359
  }, 0, locale);
12159
- assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12360
+ assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
12160
12361
  const average = sum / count;
12161
12362
  return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) /
12162
12363
  (count - (isSample ? 1 : 0)));
@@ -12353,7 +12554,7 @@ const AVEDEV = {
12353
12554
  count += 1;
12354
12555
  return acc + a;
12355
12556
  }, 0, this.locale);
12356
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12557
+ assertNotZero(count);
12357
12558
  const average = sum / count;
12358
12559
  return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count;
12359
12560
  },
@@ -12425,7 +12626,7 @@ const AVERAGE_WEIGHTED = {
12425
12626
  }
12426
12627
  }
12427
12628
  }
12428
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12629
+ assertNotZero(count);
12429
12630
  return { value: sum / count, format: inferFormat(args[0]) };
12430
12631
  },
12431
12632
  };
@@ -12445,7 +12646,7 @@ const AVERAGEA = {
12445
12646
  count += 1;
12446
12647
  return acc + a;
12447
12648
  }, 0, this.locale);
12448
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12649
+ assertNotZero(count);
12449
12650
  return {
12450
12651
  value: sum / count,
12451
12652
  format: inferFormat(args[0]),
@@ -12475,7 +12676,7 @@ const AVERAGEIF = {
12475
12676
  sum += value;
12476
12677
  }
12477
12678
  }, this.locale);
12478
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12679
+ assertNotZero(count);
12479
12680
  return sum / count;
12480
12681
  },
12481
12682
  isExported: true,
@@ -12504,7 +12705,7 @@ const AVERAGEIFS = {
12504
12705
  sum += value;
12505
12706
  }
12506
12707
  }, this.locale);
12507
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12708
+ assertNotZero(count);
12508
12709
  return sum / count;
12509
12710
  },
12510
12711
  isExported: true,
@@ -17929,7 +18130,7 @@ const DIVIDE = {
17929
18130
  returns: ["NUMBER"],
17930
18131
  compute: function (dividend, divisor) {
17931
18132
  const _divisor = toNumber(divisor, this.locale);
17932
- assert(() => _divisor !== 0, _t("The divisor must be different from zero."));
18133
+ assert(() => _divisor !== 0, _t("The divisor must be different from zero."), CellErrorType.DivisionByZero);
17933
18134
  return {
17934
18135
  value: toNumber(dividend, this.locale) / _divisor,
17935
18136
  format: dividend?.format || divisor?.format,
@@ -19406,10 +19607,10 @@ function aggregateDataForLabels(labels, datasets) {
19406
19607
  }
19407
19608
  }
19408
19609
  return {
19409
- labels: Object.keys(labelMap),
19610
+ labels: Array.from(labelSet),
19410
19611
  dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
19411
19612
  ...dataset,
19412
- data: Object.values(labelMap).map((dataOfLabel) => dataOfLabel[indexOfDataset]),
19613
+ data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
19413
19614
  })),
19414
19615
  };
19415
19616
  }
@@ -19428,8 +19629,8 @@ function truncateLabel(label) {
19428
19629
  function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale }) {
19429
19630
  const options = {
19430
19631
  // https://www.chartjs.org/docs/latest/general/responsive.html
19431
- responsive: true,
19432
- maintainAspectRatio: false,
19632
+ responsive: true, // will resize when its container is resized
19633
+ maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
19433
19634
  layout: {
19434
19635
  padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
19435
19636
  },
@@ -19475,7 +19676,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale })
19475
19676
  labels: labels.map(truncateLabel),
19476
19677
  datasets: [],
19477
19678
  },
19478
- platform: undefined,
19679
+ platform: undefined, // This key is optional and will be set by chart.js
19479
19680
  plugins: [],
19480
19681
  };
19481
19682
  }
@@ -19752,7 +19953,7 @@ function getBarConfiguration(chart, labels, localeFormat) {
19752
19953
  },
19753
19954
  y: {
19754
19955
  position: chart.verticalAxisPosition,
19755
- beginAtZero: true,
19956
+ beginAtZero: true, // the origin of the y axis is always zero
19756
19957
  ticks: {
19757
19958
  color: fontColor,
19758
19959
  callback: (value) => {
@@ -19803,6 +20004,204 @@ function createBarChartRuntime(chart, getters) {
19803
20004
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
19804
20005
  }
19805
20006
 
20007
+ class ComboChart extends AbstractChart {
20008
+ useBothYAxis;
20009
+ dataSets;
20010
+ labelRange;
20011
+ background;
20012
+ verticalAxisPosition;
20013
+ legendPosition;
20014
+ aggregated;
20015
+ dataSetsHaveTitle;
20016
+ type = "combo";
20017
+ constructor(definition, sheetId, getters) {
20018
+ super(definition, sheetId, getters);
20019
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
20020
+ this.labelRange = createRange(getters, sheetId, definition.labelRange);
20021
+ this.background = definition.background;
20022
+ this.verticalAxisPosition = definition.verticalAxisPosition;
20023
+ this.legendPosition = definition.legendPosition;
20024
+ this.aggregated = definition.aggregated;
20025
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
20026
+ this.useBothYAxis = definition.useBothYAxis;
20027
+ }
20028
+ static transformDefinition(definition, executed) {
20029
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
20030
+ }
20031
+ static validateChartDefinition(validator, definition) {
20032
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
20033
+ }
20034
+ getContextCreation() {
20035
+ return {
20036
+ background: this.background,
20037
+ title: this.title,
20038
+ range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
20039
+ auxiliaryRange: this.labelRange
20040
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
20041
+ : undefined,
20042
+ aggregated: this.aggregated,
20043
+ };
20044
+ }
20045
+ getDefinition() {
20046
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
20047
+ }
20048
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
20049
+ return {
20050
+ type: "combo",
20051
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
20052
+ background: this.background,
20053
+ dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
20054
+ legendPosition: this.legendPosition,
20055
+ verticalAxisPosition: this.verticalAxisPosition,
20056
+ labelRange: labelRange
20057
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
20058
+ : undefined,
20059
+ title: this.title,
20060
+ aggregated: this.aggregated,
20061
+ useBothYAxis: this.useBothYAxis,
20062
+ };
20063
+ }
20064
+ getDefinitionForExcel() {
20065
+ // Excel does not support aggregating labels
20066
+ if (this.aggregated) {
20067
+ return undefined;
20068
+ }
20069
+ const dataSets = this.dataSets
20070
+ .map((ds) => toExcelDataset(this.getters, ds))
20071
+ .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
20072
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
20073
+ return {
20074
+ ...this.getDefinition(),
20075
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
20076
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
20077
+ dataSets,
20078
+ labelRange,
20079
+ };
20080
+ }
20081
+ updateRanges(applyChange) {
20082
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
20083
+ if (!isStale) {
20084
+ return this;
20085
+ }
20086
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
20087
+ return new ComboChart(definition, this.sheetId, this.getters);
20088
+ }
20089
+ static getDefinitionFromContextCreation(context) {
20090
+ return {
20091
+ background: context.background,
20092
+ dataSets: context.range ? context.range : [],
20093
+ dataSetsHaveTitle: false,
20094
+ aggregated: context.aggregated,
20095
+ legendPosition: "top",
20096
+ title: context.title || "",
20097
+ verticalAxisPosition: "left",
20098
+ labelRange: context.auxiliaryRange || undefined,
20099
+ type: "combo",
20100
+ useBothYAxis: false,
20101
+ };
20102
+ }
20103
+ copyForSheetId(sheetId) {
20104
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
20105
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
20106
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
20107
+ return new ComboChart(definition, sheetId, this.getters);
20108
+ }
20109
+ copyInSheetId(sheetId) {
20110
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
20111
+ return new ComboChart(definition, sheetId, this.getters);
20112
+ }
20113
+ }
20114
+ function createComboChartRuntime(chart, getters) {
20115
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
20116
+ const locale = getters.getLocale();
20117
+ const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
20118
+ let labels = labelValues.formattedValues;
20119
+ let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
20120
+ if (chart.dataSetsHaveTitle &&
20121
+ dataSetsValues[0] &&
20122
+ labels.length > dataSetsValues[0].data.length) {
20123
+ labels.shift();
20124
+ }
20125
+ ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
20126
+ if (chart.aggregated) {
20127
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
20128
+ }
20129
+ const localeFormat = { format: dataSetFormat, locale };
20130
+ const fontColor = chartFontColor(chart.background);
20131
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
20132
+ const legend = {
20133
+ labels: { color: fontColor },
20134
+ };
20135
+ if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
20136
+ legend.display = false;
20137
+ }
20138
+ else {
20139
+ legend.position = chart.legendPosition;
20140
+ }
20141
+ config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
20142
+ config.options.layout = {
20143
+ padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
20144
+ };
20145
+ config.options.scales = {
20146
+ x: {
20147
+ ticks: {
20148
+ padding: 5,
20149
+ color: fontColor,
20150
+ },
20151
+ },
20152
+ };
20153
+ const verticalAxis = {
20154
+ beginAtZero: true, // the origin of the y axis is always zero
20155
+ ticks: {
20156
+ color: fontColor,
20157
+ callback: (value) => {
20158
+ value = Number(value);
20159
+ if (isNaN(value))
20160
+ return value;
20161
+ const { locale, format } = localeFormat;
20162
+ return formatValue(value, {
20163
+ locale,
20164
+ format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
20165
+ });
20166
+ },
20167
+ },
20168
+ };
20169
+ if (chart.useBothYAxis) {
20170
+ config.options.scales.y = {
20171
+ ...verticalAxis,
20172
+ position: "left",
20173
+ };
20174
+ config.options.scales.y1 = {
20175
+ ...verticalAxis,
20176
+ position: "right",
20177
+ grid: {
20178
+ display: false,
20179
+ },
20180
+ };
20181
+ }
20182
+ else {
20183
+ config.options.scales.y = {
20184
+ ...verticalAxis,
20185
+ position: chart.verticalAxisPosition,
20186
+ };
20187
+ }
20188
+ const colors = new ChartColors();
20189
+ for (let [index, { label, data }] of dataSetsValues.entries()) {
20190
+ const color = colors.next();
20191
+ const dataset = {
20192
+ label,
20193
+ data,
20194
+ borderColor: color,
20195
+ backgroundColor: color,
20196
+ yAxisID: index > 0 && chart.useBothYAxis ? "y1" : "y",
20197
+ type: index === 0 ? "bar" : "line",
20198
+ order: -index,
20199
+ };
20200
+ config.data.datasets.push(dataset);
20201
+ }
20202
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
20203
+ }
20204
+
19806
20205
  function isDataRangeValid(definition) {
19807
20206
  return definition.dataRange && !rangeReference.test(definition.dataRange)
19808
20207
  ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
@@ -20141,7 +20540,7 @@ function getBestTimeUnitForScale(labels, format, locale) {
20141
20540
  return undefined;
20142
20541
  }
20143
20542
  const labelsTimestamps = labelDates.map((date) => date.getTime());
20144
- const period = Math.max(...labelsTimestamps) - Math.min(...labelsTimestamps);
20543
+ const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);
20145
20544
  const minUnit = getFormatMinDisplayUnit(format);
20146
20545
  if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
20147
20546
  return "second";
@@ -20273,7 +20672,7 @@ function getLineOrScatterConfiguration(chart, labels, localeFormat) {
20273
20672
  },
20274
20673
  y: {
20275
20674
  position: chart.verticalAxisPosition,
20276
- beginAtZero: true,
20675
+ beginAtZero: true, // the origin of the y axis is always zero
20277
20676
  ticks: {
20278
20677
  color: fontColor,
20279
20678
  callback: (value) => {
@@ -20359,7 +20758,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
20359
20758
  const dataset = {
20360
20759
  label,
20361
20760
  data,
20362
- tension: 0,
20761
+ tension: 0, // 0 -> render straight lines, which is much faster
20363
20762
  borderColor: color,
20364
20763
  backgroundColor,
20365
20764
  pointBackgroundColor: color,
@@ -20581,7 +20980,7 @@ class PieChart extends AbstractChart {
20581
20980
  ...this.getDefinition(),
20582
20981
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
20583
20982
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
20584
- verticalAxisPosition: "left",
20983
+ verticalAxisPosition: "left", //TODO ExcelChartDefinition should be adapted, but can be done later
20585
20984
  dataSets,
20586
20985
  labelRange,
20587
20986
  };
@@ -20629,7 +21028,7 @@ function getPieConfiguration(chart, labels, localeFormat) {
20629
21028
  }
20630
21029
  function getPieColors(colors, dataSetsValues) {
20631
21030
  const pieColors = [];
20632
- const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
21031
+ const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
20633
21032
  for (let i = 0; i <= maxLength; i++) {
20634
21033
  pieColors.push(colors.next());
20635
21034
  }
@@ -20776,7 +21175,21 @@ class ScatterChart extends AbstractChart {
20776
21175
  return new ScatterChart(definition, this.sheetId, this.getters);
20777
21176
  }
20778
21177
  getDefinitionForExcel() {
20779
- return undefined; // TODO
21178
+ // Excel does not support aggregating labels
21179
+ if (this.aggregated) {
21180
+ return undefined;
21181
+ }
21182
+ const dataSets = this.dataSets
21183
+ .map((ds) => toExcelDataset(this.getters, ds))
21184
+ .filter((ds) => ds.range !== "");
21185
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
21186
+ return {
21187
+ ...this.getDefinition(),
21188
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
21189
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
21190
+ dataSets,
21191
+ labelRange,
21192
+ };
20780
21193
  }
20781
21194
  copyForSheetId(sheetId) {
20782
21195
  const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
@@ -20798,7 +21211,7 @@ function createScatterChartRuntime(chart, getters) {
20798
21211
  configOptions.elements = {
20799
21212
  point: {
20800
21213
  radius: 3,
20801
- hoverRadius: 3,
21214
+ hoverRadius: 3, // chartJS seems bugged, the point starts with the "hoverRadius" value
20802
21215
  hitRadius: 8,
20803
21216
  },
20804
21217
  };
@@ -20838,6 +21251,16 @@ chartRegistry.add("bar", {
20838
21251
  name: _t("Bar"),
20839
21252
  sequence: 10,
20840
21253
  });
21254
+ chartRegistry.add("combo", {
21255
+ match: (type) => type === "combo",
21256
+ createChart: (definition, sheetId, getters) => new ComboChart(definition, sheetId, getters),
21257
+ getChartRuntime: createComboChartRuntime,
21258
+ validateChartDefinition: (validator, definition) => ComboChart.validateChartDefinition(validator, definition),
21259
+ transformDefinition: (definition, executed) => ComboChart.transformDefinition(definition, executed),
21260
+ getChartDefinitionFromContextCreation: (context) => ComboChart.getDefinitionFromContextCreation(context),
21261
+ name: _t("Combo"),
21262
+ sequence: 15,
21263
+ });
20841
21264
  chartRegistry.add("line", {
20842
21265
  match: (type) => type === "line",
20843
21266
  createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),
@@ -20891,6 +21314,7 @@ chartRegistry.add("scatter", {
20891
21314
  const chartComponentRegistry = new Registry();
20892
21315
  chartComponentRegistry.add("line", ChartJsComponent);
20893
21316
  chartComponentRegistry.add("bar", ChartJsComponent);
21317
+ chartComponentRegistry.add("combo", ChartJsComponent);
20894
21318
  chartComponentRegistry.add("pie", ChartJsComponent);
20895
21319
  chartComponentRegistry.add("gauge", GaugeChartComponent);
20896
21320
  chartComponentRegistry.add("scatter", ChartJsComponent);
@@ -23149,7 +23573,7 @@ const lightTemplateWithHeader = (colorSet) => ({
23149
23573
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23150
23574
  border: { bottom: { color: colorSet.highlight, style: "thin" } },
23151
23575
  },
23152
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23576
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23153
23577
  firstRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23154
23578
  secondRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23155
23579
  });
@@ -23167,7 +23591,7 @@ const lightTemplateAllBorders = (colorSet) => ({
23167
23591
  },
23168
23592
  },
23169
23593
  headerRow: { border: { bottom: { color: colorSet.highlight, style: "medium" } } },
23170
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23594
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23171
23595
  firstRowStripe: { style: { fillColor: colorSet.light } },
23172
23596
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23173
23597
  });
@@ -23186,7 +23610,7 @@ const mediumTemplateBandedBorders = (colorSet) => ({
23186
23610
  headerRow: {
23187
23611
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23188
23612
  },
23189
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23613
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23190
23614
  firstRowStripe: { style: { fillColor: colorSet.light } },
23191
23615
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23192
23616
  });
@@ -23222,7 +23646,7 @@ const mediumTemplateMinimalBorders = (colorSet) => ({
23222
23646
  bottom: { color: "#000000", style: "medium" },
23223
23647
  },
23224
23648
  },
23225
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23649
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23226
23650
  headerRow: {
23227
23651
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23228
23652
  border: { bottom: { color: "#000000", style: "medium" } },
@@ -23246,7 +23670,7 @@ const mediumTemplateAllBorders = (colorSet) => ({
23246
23670
  },
23247
23671
  style: { fillColor: colorSet.light },
23248
23672
  },
23249
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23673
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23250
23674
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23251
23675
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
23252
23676
  });
@@ -23277,7 +23701,7 @@ const darkTemplateNoBorders = (colorSet) => ({
23277
23701
  category: "dark",
23278
23702
  colorName: colorSet.name,
23279
23703
  wholeTable: { style: { fillColor: colorSet.light } },
23280
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23704
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23281
23705
  headerRow: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
23282
23706
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23283
23707
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
@@ -23442,8 +23866,8 @@ const DELETE_CONTENT_ROWS_NAME = (env) => {
23442
23866
  let last;
23443
23867
  const activesRows = env.model.getters.getActiveRows();
23444
23868
  if (activesRows.size !== 0) {
23445
- first = Math.min(...activesRows);
23446
- last = Math.max(...activesRows);
23869
+ first = largeMin([...activesRows]);
23870
+ last = largeMax([...activesRows]);
23447
23871
  }
23448
23872
  else {
23449
23873
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23471,8 +23895,8 @@ const DELETE_CONTENT_COLUMNS_NAME = (env) => {
23471
23895
  let last;
23472
23896
  const activeCols = env.model.getters.getActiveCols();
23473
23897
  if (activeCols.size !== 0) {
23474
- first = Math.min(...activeCols);
23475
- last = Math.max(...activeCols);
23898
+ first = largeMin([...activeCols]);
23899
+ last = largeMax([...activeCols]);
23476
23900
  }
23477
23901
  else {
23478
23902
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23500,8 +23924,8 @@ const REMOVE_ROWS_NAME = (env) => {
23500
23924
  let last;
23501
23925
  const activesRows = env.model.getters.getActiveRows();
23502
23926
  if (activesRows.size !== 0) {
23503
- first = Math.min(...activesRows);
23504
- last = Math.max(...activesRows);
23927
+ first = largeMin([...activesRows]);
23928
+ last = largeMax([...activesRows]);
23505
23929
  }
23506
23930
  else {
23507
23931
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23542,8 +23966,8 @@ const REMOVE_COLUMNS_NAME = (env) => {
23542
23966
  let last;
23543
23967
  const activeCols = env.model.getters.getActiveCols();
23544
23968
  if (activeCols.size !== 0) {
23545
- first = Math.min(...activeCols);
23546
- last = Math.max(...activeCols);
23969
+ first = largeMin([...activeCols]);
23970
+ last = largeMax([...activeCols]);
23547
23971
  }
23548
23972
  else {
23549
23973
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23584,7 +24008,7 @@ const INSERT_ROWS_BEFORE_ACTION = (env) => {
23584
24008
  let row;
23585
24009
  let quantity;
23586
24010
  if (activeRows.size) {
23587
- row = Math.min(...activeRows);
24011
+ row = largeMin([...activeRows]);
23588
24012
  quantity = activeRows.size;
23589
24013
  }
23590
24014
  else {
@@ -23605,7 +24029,7 @@ const INSERT_ROWS_AFTER_ACTION = (env) => {
23605
24029
  let row;
23606
24030
  let quantity;
23607
24031
  if (activeRows.size) {
23608
- row = Math.max(...activeRows);
24032
+ row = largeMax([...activeRows]);
23609
24033
  quantity = activeRows.size;
23610
24034
  }
23611
24035
  else {
@@ -23626,7 +24050,7 @@ const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
23626
24050
  let column;
23627
24051
  let quantity;
23628
24052
  if (activeCols.size) {
23629
- column = Math.min(...activeCols);
24053
+ column = largeMin([...activeCols]);
23630
24054
  quantity = activeCols.size;
23631
24055
  }
23632
24056
  else {
@@ -23647,7 +24071,7 @@ const INSERT_COLUMNS_AFTER_ACTION = (env) => {
23647
24071
  let column;
23648
24072
  let quantity;
23649
24073
  if (activeCols.size) {
23650
- column = Math.max(...activeCols);
24074
+ column = largeMax([...activeCols]);
23651
24075
  quantity = activeCols.size;
23652
24076
  }
23653
24077
  else {
@@ -27193,13 +27617,30 @@ class ColorPickerWidget extends Component {
27193
27617
  }
27194
27618
  }
27195
27619
 
27196
- class ChartColor extends Component {
27197
- static template = "o-spreadsheet.ChartColor";
27198
- static components = { ColorPickerWidget, Section };
27620
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
27621
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
27622
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
27623
+ </svg>
27624
+ `;
27625
+ css /* scss */ `
27626
+ .o-round-color-picker-button {
27627
+ width: 15px;
27628
+ height: 15px;
27629
+ cursor: pointer;
27630
+ border: 1px solid #aaa;
27631
+ background-position: 1px 1px;
27632
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
27633
+ }
27634
+ `;
27635
+ class RoundColorPicker extends Component {
27636
+ static template = "o-spreadsheet.RoundColorPicker";
27637
+ static components = { ColorPickerWidget, Section, ColorPicker };
27199
27638
  static props = {
27200
27639
  currentColor: { type: String, optional: true },
27640
+ title: { type: String, optional: true },
27201
27641
  onColorPicked: Function,
27202
27642
  };
27643
+ colorPickerButtonRef = useRef("colorPickerButton");
27203
27644
  state;
27204
27645
  setup() {
27205
27646
  this.state = useState({ pickerOpened: false });
@@ -27211,6 +27652,19 @@ class ChartColor extends Component {
27211
27652
  togglePicker() {
27212
27653
  this.state.pickerOpened = !this.state.pickerOpened;
27213
27654
  }
27655
+ onColorPicked(color) {
27656
+ this.props.onColorPicked(color);
27657
+ this.state.pickerOpened = false;
27658
+ }
27659
+ get colorPickerAnchorRect() {
27660
+ const button = this.colorPickerButtonRef.el;
27661
+ return getBoundingRectAsPOJO(button);
27662
+ }
27663
+ get buttonStyle() {
27664
+ return cssPropertiesToCss({
27665
+ background: this.props.currentColor,
27666
+ });
27667
+ }
27214
27668
  }
27215
27669
 
27216
27670
  class ChartTitle extends Component {
@@ -27224,7 +27678,7 @@ class ChartTitle extends Component {
27224
27678
 
27225
27679
  class LineBarPieDesignPanel extends Component {
27226
27680
  static template = "o-spreadsheet-LineBarPieDesignPanel";
27227
- static components = { ChartColor, ChartTitle, Section };
27681
+ static components = { RoundColorPicker, ChartTitle, Section };
27228
27682
  static props = {
27229
27683
  figureId: String,
27230
27684
  definition: Object,
@@ -27247,12 +27701,31 @@ class LineBarPieDesignPanel extends Component {
27247
27701
  [attr]: ev.target.value,
27248
27702
  });
27249
27703
  }
27704
+ get backgroundColorTitle() {
27705
+ return ChartTerms.BackgroundColor;
27706
+ }
27250
27707
  }
27251
27708
 
27252
27709
  class BarChartDesignPanel extends LineBarPieDesignPanel {
27253
27710
  static template = "o-spreadsheet-BarChartDesignPanel";
27254
27711
  }
27255
27712
 
27713
+ class ComboChartConfigPanel extends LineBarPieConfigPanel {
27714
+ static template = "o-spreadsheet-ComboChartConfigPanel";
27715
+ get shouldUseRightAxis() {
27716
+ return _t("Use right axis for line series");
27717
+ }
27718
+ onUpdateUseRightAxis(useBothYAxis) {
27719
+ this.props.updateChart(this.props.figureId, {
27720
+ useBothYAxis,
27721
+ });
27722
+ }
27723
+ }
27724
+
27725
+ class ComboChartDesignPanel extends LineBarPieDesignPanel {
27726
+ static template = "o-spreadsheet-ComboChartDesignPanel";
27727
+ }
27728
+
27256
27729
  class GaugeChartConfigPanel extends Component {
27257
27730
  static template = "o-spreadsheet-GaugeChartConfigPanel";
27258
27731
  static components = { ChartErrorSection, ChartDataSeries };
@@ -27300,6 +27773,10 @@ css /* scss */ `
27300
27773
  line-height: 18px;
27301
27774
  width: 100%;
27302
27775
  }
27776
+ td {
27777
+ box-sizing: border-box;
27778
+ height: 30px;
27779
+ }
27303
27780
  th.o-gauge-color-set-colorPicker {
27304
27781
  width: 8%;
27305
27782
  }
@@ -27322,7 +27799,12 @@ css /* scss */ `
27322
27799
  `;
27323
27800
  class GaugeChartDesignPanel extends Component {
27324
27801
  static template = "o-spreadsheet-GaugeChartDesignPanel";
27325
- static components = { ColorPickerWidget, ChartErrorSection, ChartColor, ChartTitle, Section };
27802
+ static components = {
27803
+ ChartErrorSection,
27804
+ RoundColorPicker,
27805
+ ChartTitle,
27806
+ Section,
27807
+ };
27326
27808
  static props = {
27327
27809
  figureId: String,
27328
27810
  definition: Object,
@@ -27334,9 +27816,6 @@ class GaugeChartDesignPanel extends Component {
27334
27816
  sectionRuleDispatchResult: undefined,
27335
27817
  sectionRule: deepCopy(this.props.definition.sectionRule),
27336
27818
  });
27337
- setup() {
27338
- useExternalListener(window, "click", this.closeMenus);
27339
- }
27340
27819
  get title() {
27341
27820
  return _t(this.props.definition.title);
27342
27821
  }
@@ -27377,27 +27856,22 @@ class GaugeChartDesignPanel extends Component {
27377
27856
  const sectionRule = deepCopy(this.state.sectionRule);
27378
27857
  sectionRule.colors[target] = color;
27379
27858
  this.updateSectionRule(sectionRule);
27380
- this.closeMenus();
27381
- }
27382
- toggleMenu(menu) {
27383
- const isSelected = this.state.openedMenu === menu;
27384
- this.closeMenus();
27385
- if (!isSelected) {
27386
- this.state.openedMenu = menu;
27387
- }
27388
27859
  }
27389
27860
  updateSectionRule(sectionRule) {
27390
27861
  this.state.sectionRuleDispatchResult = this.props.updateChart(this.props.figureId, {
27391
27862
  sectionRule,
27392
27863
  });
27864
+ if (this.state.sectionRuleDispatchResult.isSuccessful) {
27865
+ this.state.sectionRule = deepCopy(sectionRule);
27866
+ }
27393
27867
  }
27394
27868
  canUpdateSectionRule(sectionRule) {
27395
27869
  this.state.sectionRuleDispatchResult = this.props.canUpdateChart(this.props.figureId, {
27396
27870
  sectionRule,
27397
27871
  });
27398
27872
  }
27399
- closeMenus() {
27400
- this.state.openedMenu = undefined;
27873
+ get backgroundColorTitle() {
27874
+ return ChartTerms.BackgroundColor;
27401
27875
  }
27402
27876
  }
27403
27877
 
@@ -27545,39 +28019,36 @@ class ScorecardChartConfigPanel extends Component {
27545
28019
 
27546
28020
  class ScorecardChartDesignPanel extends Component {
27547
28021
  static template = "o-spreadsheet-ScorecardChartDesignPanel";
27548
- static components = { ColorPickerWidget, ChartColor, ChartTitle, Section };
28022
+ static components = { RoundColorPicker, ChartTitle, Section, Checkbox };
27549
28023
  static props = {
27550
28024
  figureId: String,
27551
28025
  definition: Object,
27552
28026
  updateChart: Function,
27553
28027
  canUpdateChart: Function,
27554
28028
  };
27555
- state = useState({
27556
- openedColorPicker: undefined,
27557
- });
27558
- setup() {
27559
- useExternalListener(window, "click", this.closeMenus);
27560
- }
27561
28029
  get title() {
27562
28030
  return _t(this.props.definition.title);
27563
28031
  }
28032
+ get colorsSectionTitle() {
28033
+ return this.props.definition.baselineMode === "progress"
28034
+ ? _t("Progress bar colors")
28035
+ : _t("Baseline colors");
28036
+ }
28037
+ get humanizeNumbersLabel() {
28038
+ return _t("Humanize numbers");
28039
+ }
27564
28040
  updateTitle(title) {
27565
28041
  this.props.updateChart(this.props.figureId, { title });
27566
28042
  }
28043
+ updateHumanizeNumbers(humanize) {
28044
+ this.props.updateChart(this.props.figureId, { humanize });
28045
+ }
27567
28046
  translate(term) {
27568
28047
  return _t(term);
27569
28048
  }
27570
28049
  updateBaselineDescr(ev) {
27571
28050
  this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
27572
28051
  }
27573
- toggleColorPicker(colorPickerId) {
27574
- if (this.state.openedColorPicker === colorPickerId) {
27575
- this.state.openedColorPicker = undefined;
27576
- }
27577
- else {
27578
- this.state.openedColorPicker = colorPickerId;
27579
- }
27580
- }
27581
28052
  setColor(color, colorPickerId) {
27582
28053
  switch (colorPickerId) {
27583
28054
  case "backgroundColor":
@@ -27590,10 +28061,9 @@ class ScorecardChartDesignPanel extends Component {
27590
28061
  this.props.updateChart(this.props.figureId, { baselineColorUp: color });
27591
28062
  break;
27592
28063
  }
27593
- this.closeMenus();
27594
28064
  }
27595
- closeMenus() {
27596
- this.state.openedColorPicker = undefined;
28065
+ get backgroundColorTitle() {
28066
+ return ChartTerms.BackgroundColor;
27597
28067
  }
27598
28068
  }
27599
28069
 
@@ -27610,6 +28080,10 @@ chartSidePanelComponentRegistry
27610
28080
  .add("bar", {
27611
28081
  configuration: BarConfigPanel,
27612
28082
  design: BarChartDesignPanel,
28083
+ })
28084
+ .add("combo", {
28085
+ configuration: ComboChartConfigPanel,
28086
+ design: ComboChartDesignPanel,
27613
28087
  })
27614
28088
  .add("pie", {
27615
28089
  configuration: LineBarPieConfigPanel,
@@ -28538,6 +29012,7 @@ class ConditionalFormattingEditor extends Component {
28538
29012
  ColorPickerWidget,
28539
29013
  ConditionalFormatPreviewList,
28540
29014
  Section,
29015
+ RoundColorPicker,
28541
29016
  };
28542
29017
  icons = ICONS;
28543
29018
  cellIsOperators = CellIsOperators;
@@ -30250,7 +30725,11 @@ class SplitIntoColumnsPanel extends Component {
30250
30725
  const composerStore = useStore(ComposerStore);
30251
30726
  // The feature makes no sense if we are editing a cell, because then the selection isn't active
30252
30727
  // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
30253
- useEffect(this.props.onCloseSidePanel, () => [composerStore.editionMode]);
30728
+ useEffect((editionMode) => {
30729
+ if (editionMode !== "inactive") {
30730
+ this.props.onCloseSidePanel();
30731
+ }
30732
+ }, () => [composerStore.editionMode]);
30254
30733
  onMounted(() => {
30255
30734
  composerStore.stopEdition();
30256
30735
  });
@@ -31959,6 +32438,9 @@ class FunctionDescriptionProvider extends Component {
31959
32438
  this.assistantState.allowCellSelectionBehind = false;
31960
32439
  }, 2000);
31961
32440
  }
32441
+ get formulaArgSeparator() {
32442
+ return this.env.model.getters.getLocale().formulaArgSeparator + " ";
32443
+ }
31962
32444
  }
31963
32445
 
31964
32446
  const functions$2 = functionRegistry.content;
@@ -32035,6 +32517,7 @@ class Composer extends Component {
32035
32517
  onComposerCellFocused: { type: Function, optional: true },
32036
32518
  onComposerContentFocused: Function,
32037
32519
  isDefaultFocus: { type: Boolean, optional: true },
32520
+ onInputContextMenu: { type: Function, optional: true },
32038
32521
  };
32039
32522
  static components = { TextValueProvider, FunctionDescriptionProvider };
32040
32523
  static defaultProps = {
@@ -32085,6 +32568,9 @@ class Composer extends Component {
32085
32568
  assistantStyle.right = `0px`;
32086
32569
  }
32087
32570
  }
32571
+ else if (this.props.delimitation) {
32572
+ assistantStyle["max-height"] = `${this.props.delimitation.height}px`;
32573
+ }
32088
32574
  return cssPropertiesToCss(assistantStyle);
32089
32575
  }
32090
32576
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
@@ -32118,6 +32604,12 @@ class Composer extends Component {
32118
32604
  useEffect(() => {
32119
32605
  this.processContent();
32120
32606
  });
32607
+ onPatched(() => {
32608
+ // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
32609
+ if (this.composerStore.editionMode === "inactive") {
32610
+ this.processTokenAtCursor();
32611
+ }
32612
+ });
32121
32613
  }
32122
32614
  // ---------------------------------------------------------------------------
32123
32615
  // Handlers
@@ -32365,6 +32857,11 @@ class Composer extends Component {
32365
32857
  }
32366
32858
  }
32367
32859
  }
32860
+ onContextMenu(ev) {
32861
+ if (this.composerStore.editionMode === "inactive") {
32862
+ this.props.onInputContextMenu?.(ev);
32863
+ }
32864
+ }
32368
32865
  // ---------------------------------------------------------------------------
32369
32866
  // Private
32370
32867
  // ---------------------------------------------------------------------------
@@ -32586,6 +33083,7 @@ class GridComposer extends Component {
32586
33083
  static template = "o-spreadsheet-GridComposer";
32587
33084
  static props = {
32588
33085
  gridDims: Object,
33086
+ onInputContextMenu: Function,
32589
33087
  };
32590
33088
  static components = { Composer };
32591
33089
  rect = this.defaultRect;
@@ -32633,6 +33131,7 @@ class GridComposer extends Component {
32633
33131
  isDefaultFocus: true,
32634
33132
  onComposerContentFocused: () => this.composerFocusStore.focusGridComposerContent(),
32635
33133
  onComposerCellFocused: (content) => this.composerFocusStore.focusGridComposerCell(content),
33134
+ onInputContextMenu: this.props.onInputContextMenu,
32636
33135
  };
32637
33136
  }
32638
33137
  get containerStyle() {
@@ -32731,7 +33230,6 @@ class GridCellIcon extends Component {
32731
33230
  cellPosition: Object,
32732
33231
  horizontalAlign: { type: String, optional: true },
32733
33232
  verticalAlign: { type: String, optional: true },
32734
- offset: { type: Object, optional: true },
32735
33233
  slots: Object,
32736
33234
  };
32737
33235
  get iconStyle() {
@@ -32742,8 +33240,8 @@ class GridCellIcon extends Component {
32742
33240
  const x = this.getIconHorizontalPosition(rect, cellPosition);
32743
33241
  const y = this.getIconVerticalPosition(rect, cellPosition);
32744
33242
  return cssPropertiesToCss({
32745
- top: `${y + (this.props.offset?.y || 0)}px`,
32746
- left: `${x + (this.props.offset?.x || 0)}px`,
33243
+ top: `${y}px`,
33244
+ left: `${x}px`,
32747
33245
  });
32748
33246
  }
32749
33247
  getIconVerticalPosition(rect, cellPosition) {
@@ -32783,83 +33281,6 @@ class GridCellIcon extends Component {
32783
33281
  }
32784
33282
  }
32785
33283
 
32786
- css /* scss */ `
32787
- .o-filter-icon {
32788
- color: ${FILTERS_COLOR};
32789
- display: flex;
32790
- align-items: center;
32791
- justify-content: center;
32792
- width: ${GRID_ICON_EDGE_LENGTH}px;
32793
- height: ${GRID_ICON_EDGE_LENGTH}px;
32794
-
32795
- &:hover {
32796
- background: ${FILTERS_COLOR};
32797
- color: #fff;
32798
- }
32799
-
32800
- &.o-high-contrast {
32801
- color: #defade;
32802
- }
32803
- &.o-high-contrast:hover {
32804
- color: ${FILTERS_COLOR};
32805
- background: #fff;
32806
- }
32807
- }
32808
- .o-filter-icon:hover {
32809
- background: ${FILTERS_COLOR};
32810
- color: #fff;
32811
- }
32812
- `;
32813
- class FilterIcon extends Component {
32814
- static template = "o-spreadsheet-FilterIcon";
32815
- static props = {
32816
- cellPosition: Object,
32817
- };
32818
- cellPopovers;
32819
- setup() {
32820
- this.cellPopovers = useStore(CellPopoverStore);
32821
- }
32822
- onClick() {
32823
- const position = this.props.cellPosition;
32824
- const activePopover = this.cellPopovers.persistentCellPopover;
32825
- const { col, row } = position;
32826
- if (activePopover.isOpen &&
32827
- activePopover.col === col &&
32828
- activePopover.row === row &&
32829
- activePopover.type === "FilterMenu") {
32830
- this.cellPopovers.close();
32831
- return;
32832
- }
32833
- this.cellPopovers.open({ col, row }, "FilterMenu");
32834
- }
32835
- get isFilterActive() {
32836
- return this.env.model.getters.isFilterActive(this.props.cellPosition);
32837
- }
32838
- get iconClass() {
32839
- const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
32840
- const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
32841
- return luminance < 0.45 ? "o-high-contrast" : "";
32842
- }
32843
- }
32844
-
32845
- class FilterIconsOverlay extends Component {
32846
- static template = "o-spreadsheet-FilterIconsOverlay";
32847
- static props = {
32848
- gridPosition: { type: Object, optional: true },
32849
- };
32850
- static components = {
32851
- GridCellIcon,
32852
- FilterIcon,
32853
- };
32854
- static defaultProps = {
32855
- gridPosition: { x: 0, y: 0 },
32856
- };
32857
- getFilterHeadersPositions() {
32858
- const sheetId = this.env.model.getters.getActiveSheetId();
32859
- return this.env.model.getters.getFilterHeaders(sheetId);
32860
- }
32861
- }
32862
-
32863
33284
  const CHECKBOX_WIDTH = 15;
32864
33285
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
32865
33286
  css /* scss */ `
@@ -33527,6 +33948,80 @@ class FiguresContainer extends Component {
33527
33948
  }
33528
33949
  }
33529
33950
 
33951
+ css /* scss */ `
33952
+ .o-filter-icon {
33953
+ color: ${FILTERS_COLOR};
33954
+ display: flex;
33955
+ align-items: center;
33956
+ justify-content: center;
33957
+ width: ${GRID_ICON_EDGE_LENGTH}px;
33958
+ height: ${GRID_ICON_EDGE_LENGTH}px;
33959
+
33960
+ &:hover {
33961
+ background: ${FILTERS_COLOR};
33962
+ color: #fff;
33963
+ }
33964
+
33965
+ &.o-high-contrast {
33966
+ color: #defade;
33967
+ }
33968
+ &.o-high-contrast:hover {
33969
+ color: ${FILTERS_COLOR};
33970
+ background: #fff;
33971
+ }
33972
+ }
33973
+ .o-filter-icon:hover {
33974
+ background: ${FILTERS_COLOR};
33975
+ color: #fff;
33976
+ }
33977
+ `;
33978
+ class FilterIcon extends Component {
33979
+ static template = "o-spreadsheet-FilterIcon";
33980
+ static props = {
33981
+ cellPosition: Object,
33982
+ };
33983
+ cellPopovers;
33984
+ setup() {
33985
+ this.cellPopovers = useStore(CellPopoverStore);
33986
+ }
33987
+ onClick() {
33988
+ const position = this.props.cellPosition;
33989
+ const activePopover = this.cellPopovers.persistentCellPopover;
33990
+ const { col, row } = position;
33991
+ if (activePopover.isOpen &&
33992
+ activePopover.col === col &&
33993
+ activePopover.row === row &&
33994
+ activePopover.type === "FilterMenu") {
33995
+ this.cellPopovers.close();
33996
+ return;
33997
+ }
33998
+ this.cellPopovers.open({ col, row }, "FilterMenu");
33999
+ }
34000
+ get isFilterActive() {
34001
+ return this.env.model.getters.isFilterActive(this.props.cellPosition);
34002
+ }
34003
+ get iconClass() {
34004
+ const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
34005
+ const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
34006
+ return luminance < 0.45 ? "o-high-contrast" : "";
34007
+ }
34008
+ }
34009
+
34010
+ class FilterIconsOverlay extends Component {
34011
+ static template = "o-spreadsheet-FilterIconsOverlay";
34012
+ static props = {
34013
+ onMouseDown: Function,
34014
+ };
34015
+ static components = {
34016
+ GridCellIcon,
34017
+ FilterIcon,
34018
+ };
34019
+ getFilterHeadersPositions() {
34020
+ const sheetId = this.env.model.getters.getActiveSheetId();
34021
+ return this.env.model.getters.getFilterHeaders(sheetId);
34022
+ }
34023
+ }
34024
+
33530
34025
  css /* scss */ `
33531
34026
  .o-grid-add-rows {
33532
34027
  input {
@@ -33740,7 +34235,12 @@ class GridOverlay extends Component {
33740
34235
  onGridMoved: Function,
33741
34236
  gridOverlayDimensions: String,
33742
34237
  };
33743
- static components = { FiguresContainer, DataValidationOverlay, GridAddRowsFooter };
34238
+ static components = {
34239
+ FiguresContainer,
34240
+ DataValidationOverlay,
34241
+ GridAddRowsFooter,
34242
+ FilterIconsOverlay,
34243
+ };
33744
34244
  static defaultProps = {
33745
34245
  onCellHovered: () => { },
33746
34246
  onCellDoubleClicked: () => { },
@@ -33785,7 +34285,7 @@ class GridOverlay extends Component {
33785
34285
  get isPaintingFormat() {
33786
34286
  return this.env.model.getters.isPaintingFormat();
33787
34287
  }
33788
- onMouseDown(ev) {
34288
+ onMouseDown(ev, modifiers) {
33789
34289
  if (ev.button > 0) {
33790
34290
  // not main button, probably a context menu
33791
34291
  return;
@@ -33794,6 +34294,7 @@ class GridOverlay extends Component {
33794
34294
  this.props.onCellClicked(col, row, {
33795
34295
  expandZone: ev.shiftKey,
33796
34296
  addZone: isCtrlKey(ev),
34297
+ closePopover: modifiers?.closePopover ?? true,
33797
34298
  });
33798
34299
  }
33799
34300
  onDoubleClick(ev) {
@@ -34097,11 +34598,6 @@ css /* scss */ `
34097
34598
  height: 10000px;
34098
34599
  background-color: ${SELECTION_BORDER_COLOR};
34099
34600
  }
34100
- .o-unhide-buttons {
34101
- width: fit-content;
34102
- gap: 5px;
34103
- transform: translate(-50%, 0);
34104
- }
34105
34601
  .o-unhide:hover {
34106
34602
  z-index: ${ComponentsImportance.Grid + 1};
34107
34603
  background-color: lightgrey;
@@ -34263,10 +34759,6 @@ css /* scss */ `
34263
34759
  height: 1px;
34264
34760
  background-color: ${SELECTION_BORDER_COLOR};
34265
34761
  }
34266
- .o-unhide-buttons {
34267
- height: fit-content;
34268
- transform: translate(0, -50%);
34269
- }
34270
34762
  .o-unhide:hover {
34271
34763
  z-index: ${ComponentsImportance.Grid + 1};
34272
34764
  background-color: lightgrey;
@@ -35474,7 +35966,7 @@ class VerticalScrollBar extends Component {
35474
35966
  onScroll(offset) {
35475
35967
  const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();
35476
35968
  this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
35477
- offsetX: scrollX,
35969
+ offsetX: scrollX, // offsetX is the same
35478
35970
  offsetY: offset,
35479
35971
  });
35480
35972
  }
@@ -35568,7 +36060,6 @@ class Grid extends Component {
35568
36060
  Popover,
35569
36061
  VerticalScrollBar,
35570
36062
  HorizontalScrollBar,
35571
- FilterIconsOverlay,
35572
36063
  };
35573
36064
  HEADER_HEIGHT = HEADER_HEIGHT;
35574
36065
  HEADER_WIDTH = HEADER_WIDTH;
@@ -35762,8 +36253,8 @@ class Grid extends Component {
35762
36253
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
35763
36254
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
35764
36255
  "Ctrl+Shift+V": () => PASTE_AS_VALUE_ACTION(this.env),
35765
- "Ctrl+Shift+<": () => this.clearFormatting(),
35766
- "Ctrl+<": () => this.clearFormatting(),
36256
+ "Ctrl+Shift+<": () => this.clearFormatting(), // for qwerty
36257
+ "Ctrl+<": () => this.clearFormatting(), // for azerty
35767
36258
  "Ctrl+Shift+ ": () => {
35768
36259
  this.env.model.selection.selectAll();
35769
36260
  },
@@ -35869,17 +36360,17 @@ class Grid extends Component {
35869
36360
  // ---------------------------------------------------------------------------
35870
36361
  // Zone selection with mouse
35871
36362
  // ---------------------------------------------------------------------------
35872
- onCellClicked(col, row, { addZone, expandZone }) {
35873
- if (this.cellPopovers.isOpen) {
36363
+ onCellClicked(col, row, modifiers) {
36364
+ if (modifiers.closePopover && this.cellPopovers.isOpen) {
35874
36365
  this.cellPopovers.close();
35875
36366
  }
35876
36367
  if (this.composerStore.editionMode === "editing") {
35877
36368
  this.composerStore.stopEdition();
35878
36369
  }
35879
- if (expandZone) {
36370
+ if (modifiers.expandZone) {
35880
36371
  this.env.model.selection.setAnchorCorner(col, row);
35881
36372
  }
35882
- else if (addZone) {
36373
+ else if (modifiers.addZone) {
35883
36374
  this.env.model.selection.addCellToSelection(col, row);
35884
36375
  }
35885
36376
  else {
@@ -36199,6 +36690,7 @@ const XLSX_CHART_TYPES = [
36199
36690
  "surfaceChart",
36200
36691
  "surface3DChart",
36201
36692
  "bubbleChart",
36693
+ "comboChart",
36202
36694
  ];
36203
36695
 
36204
36696
  /** In XLSX color format (no #) */
@@ -36530,10 +37022,10 @@ function convertCFCellIsOperator(xlsxCfOperator) {
36530
37022
  const CF_TYPE_CONVERSION_MAP = {
36531
37023
  aboveAverage: undefined,
36532
37024
  expression: undefined,
36533
- cellIs: undefined,
36534
- colorScale: undefined,
37025
+ cellIs: undefined, // exist but isn't an operator in o_spreadsheet
37026
+ colorScale: undefined, // exist but isn't an operator in o_spreadsheet
36535
37027
  dataBar: undefined,
36536
- iconSet: undefined,
37028
+ iconSet: undefined, // exist but isn't an operator in o_spreadsheet
36537
37029
  top10: undefined,
36538
37030
  uniqueValues: undefined,
36539
37031
  duplicateValues: undefined,
@@ -36600,7 +37092,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36600
37092
  line3DChart: undefined,
36601
37093
  stockChart: undefined,
36602
37094
  radarChart: undefined,
36603
- scatterChart: undefined,
37095
+ scatterChart: "scatter",
36604
37096
  pieChart: "pie",
36605
37097
  pie3DChart: undefined,
36606
37098
  doughnutChart: "pie",
@@ -36610,6 +37102,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36610
37102
  surfaceChart: undefined,
36611
37103
  surface3DChart: undefined,
36612
37104
  bubbleChart: undefined,
37105
+ comboChart: "combo",
36613
37106
  };
36614
37107
  /** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/
36615
37108
  const SUBTOTAL_FUNCTION_CONVERSION_MAP = {
@@ -36768,7 +37261,7 @@ const XLSX_INDEXED_COLORS = {
36768
37261
  61: "993366",
36769
37262
  62: "333399",
36770
37263
  63: "333333",
36771
- 64: "000000",
37264
+ 64: "000000", // system foreground
36772
37265
  65: "FFFFFF", // system background
36773
37266
  };
36774
37267
  const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
@@ -37363,29 +37856,12 @@ function convertWidthFromExcel(width) {
37363
37856
  return width;
37364
37857
  return Math.round((width / WIDTH_FACTOR) * 100) / 100;
37365
37858
  }
37366
- function convertBorderDescr(descr) {
37367
- if (!descr) {
37368
- return undefined;
37369
- }
37370
- return {
37371
- style: descr.style,
37372
- color: { rgb: descr.color },
37373
- };
37374
- }
37375
37859
  function extractStyle(cell, data) {
37376
37860
  let style = {};
37377
37861
  if (cell.style) {
37378
37862
  style = data.styles[cell.style];
37379
37863
  }
37380
37864
  const format = extractFormat(cell, data);
37381
- const exportedBorder = {};
37382
- if (cell.border) {
37383
- const border = data.borders[cell.border];
37384
- exportedBorder.left = convertBorderDescr(border.left);
37385
- exportedBorder.right = convertBorderDescr(border.right);
37386
- exportedBorder.bottom = convertBorderDescr(border.bottom);
37387
- exportedBorder.top = convertBorderDescr(border.top);
37388
- }
37389
37865
  const styles = {
37390
37866
  font: {
37391
37867
  size: style?.fontSize || DEFAULT_FONT_SIZE,
@@ -37399,7 +37875,7 @@ function extractStyle(cell, data) {
37399
37875
  }
37400
37876
  : { reservedAttribute: "none" },
37401
37877
  numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,
37402
- border: exportedBorder || {},
37878
+ border: cell.border || 0,
37403
37879
  alignment: {
37404
37880
  horizontal: style.align,
37405
37881
  vertical: style.verticalAlign
@@ -37421,15 +37897,12 @@ function extractFormat(cell, data) {
37421
37897
  return undefined;
37422
37898
  }
37423
37899
  function normalizeStyle(construct, styles) {
37424
- const { id: fontId } = pushElement(styles["font"], construct.fonts);
37425
- const { id: fillId } = pushElement(styles["fill"], construct.fills);
37426
- const { id: borderId } = pushElement(styles["border"], construct.borders);
37427
37900
  // Normalize this
37428
37901
  const numFmtId = convertFormat(styles["numFmt"], construct.numFmts);
37429
37902
  const style = {
37430
- fontId,
37431
- fillId,
37432
- borderId,
37903
+ fontId: pushElement(styles.font, construct.fonts),
37904
+ fillId: pushElement(styles.fill, construct.fills),
37905
+ borderId: styles.border,
37433
37906
  numFmtId,
37434
37907
  alignment: {
37435
37908
  vertical: styles.alignment.vertical,
@@ -37437,8 +37910,7 @@ function normalizeStyle(construct, styles) {
37437
37910
  wrapText: styles.alignment.wrapText,
37438
37911
  },
37439
37912
  };
37440
- const { id } = pushElement(style, construct.styles);
37441
- return id;
37913
+ return pushElement(style, construct.styles);
37442
37914
  }
37443
37915
  function convertFormat(format, numFmtStructure) {
37444
37916
  if (!format) {
@@ -37446,8 +37918,7 @@ function convertFormat(format, numFmtStructure) {
37446
37918
  }
37447
37919
  let formatId = XLSX_FORMAT_MAP[format.format];
37448
37920
  if (!formatId) {
37449
- const { id } = pushElement(format, numFmtStructure);
37450
- formatId = id + FIRST_NUMFMT_ID;
37921
+ formatId = pushElement(format, numFmtStructure) + FIRST_NUMFMT_ID;
37451
37922
  }
37452
37923
  return formatId;
37453
37924
  }
@@ -37472,20 +37943,15 @@ function addRelsToFile(relsFiles, path, rel) {
37472
37943
  return id;
37473
37944
  }
37474
37945
  function pushElement(property, propertyList) {
37475
- for (let [key, value] of Object.entries(propertyList)) {
37476
- if (JSON.stringify(value) === JSON.stringify(property)) {
37477
- return { id: parseInt(key, 10), list: propertyList };
37946
+ let len = propertyList.length;
37947
+ const operator = typeof property === "object" ? deepEquals : (a, b) => a === b;
37948
+ for (let i = 0; i < len; i++) {
37949
+ if (operator(property, propertyList[i])) {
37950
+ return i;
37478
37951
  }
37479
37952
  }
37480
- let elemId = propertyList.findIndex((elem) => JSON.stringify(elem) === JSON.stringify(property));
37481
- if (elemId === -1) {
37482
- propertyList.push(property);
37483
- elemId = propertyList.length - 1;
37484
- }
37485
- return {
37486
- id: elemId,
37487
- list: propertyList,
37488
- };
37953
+ propertyList[propertyList.length] = property;
37954
+ return propertyList.length - 1;
37489
37955
  }
37490
37956
  const chartIds = [];
37491
37957
  /**
@@ -37960,7 +38426,7 @@ function convertHyperlink(link, cellValue, warningManager) {
37960
38426
  function getSheetDims(sheet) {
37961
38427
  const dims = [0, 0];
37962
38428
  for (let row of sheet.rows) {
37963
- dims[0] = Math.max(dims[0], ...row.cells.map((cell) => toCartesian(cell.xc).col));
38429
+ dims[0] = Math.max(dims[0], largeMax(row.cells.map((cell) => toCartesian(cell.xc).col)));
37964
38430
  dims[1] = Math.max(dims[1], row.index);
37965
38431
  }
37966
38432
  dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
@@ -38280,7 +38746,25 @@ function parseXML(xmlString, mimeType = "text/xml") {
38280
38746
  }
38281
38747
  return document;
38282
38748
  }
38283
- function getDefaultXLSXStructure() {
38749
+ function convertBorderDescr(descr) {
38750
+ if (!descr) {
38751
+ return undefined;
38752
+ }
38753
+ return {
38754
+ style: descr.style,
38755
+ color: { rgb: descr.color },
38756
+ };
38757
+ }
38758
+ function getDefaultXLSXStructure(data) {
38759
+ const xlsxBorders = Object.values(data.borders).map((border) => {
38760
+ return {
38761
+ left: convertBorderDescr(border.left),
38762
+ right: convertBorderDescr(border.right),
38763
+ bottom: convertBorderDescr(border.bottom),
38764
+ top: convertBorderDescr(border.top),
38765
+ };
38766
+ });
38767
+ const borders = [{}, ...xlsxBorders];
38284
38768
  return {
38285
38769
  relsFiles: [],
38286
38770
  sharedStrings: [],
@@ -38303,7 +38787,7 @@ function getDefaultXLSXStructure() {
38303
38787
  },
38304
38788
  ],
38305
38789
  fills: [{ reservedAttribute: "none" }, { reservedAttribute: "gray125" }],
38306
- borders: [{}],
38790
+ borders,
38307
38791
  numFmts: [],
38308
38792
  dxfs: [],
38309
38793
  };
@@ -38826,6 +39310,9 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38826
39310
  if (!CHART_TYPE_CONVERSION_MAP[chartType]) {
38827
39311
  throw new Error(`Unsupported chart type ${chartType}`);
38828
39312
  }
39313
+ if (CHART_TYPE_CONVERSION_MAP[chartType] === "combo") {
39314
+ return this.extractComboChart(rootChartElement);
39315
+ }
38829
39316
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
38830
39317
  const chartTitle = this.mapOnElements({ parent: rootChartElement, query: "c:title a:t" }, (textElement) => {
38831
39318
  return textElement.textContent || "";
@@ -38836,8 +39323,8 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38836
39323
  return {
38837
39324
  title: chartTitle,
38838
39325
  type: CHART_TYPE_CONVERSION_MAP[chartType],
38839
- dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`)),
38840
- labelRange: this.extractChildTextContent(rootChartElement, "c:ser c:cat c:f"),
39326
+ dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`), chartType),
39327
+ labelRange: this.extractChildTextContent(rootChartElement, `c:ser ${chartType === "scatterChart" ? "c:numRef" : "c:cat"} c:f`),
38841
39328
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
38842
39329
  default: "ffffff",
38843
39330
  }).asString(),
@@ -38854,7 +39341,41 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38854
39341
  };
38855
39342
  })[0];
38856
39343
  }
38857
- extractChartDatasets(chartElement) {
39344
+ extractComboChart(chartElement) {
39345
+ // Title can be separated into multiple xml elements (for styling and such), we only import the text
39346
+ const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
39347
+ return textElement.textContent || "";
39348
+ }).join("");
39349
+ const barChartGrouping = this.extractChildAttr(chartElement, "c:grouping", "val", {
39350
+ default: "clustered",
39351
+ }).asString();
39352
+ return {
39353
+ title: chartTitle,
39354
+ type: "combo",
39355
+ dataSets: [
39356
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:barChart`), "comboChart"),
39357
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:lineChart`), "comboChart"),
39358
+ ],
39359
+ labelRange: this.extractChildTextContent(chartElement, "c:ser c:cat c:f"),
39360
+ backgroundColor: this.extractChildAttr(chartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
39361
+ default: "ffffff",
39362
+ }).asString(),
39363
+ verticalAxisPosition: this.extractChildAttr(chartElement, "c:valAx > c:axPos", "val", {
39364
+ default: "l",
39365
+ }).asString() === "r"
39366
+ ? "right"
39367
+ : "left",
39368
+ legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
39369
+ default: "b",
39370
+ }).asString()],
39371
+ stacked: barChartGrouping === "stacked",
39372
+ fontColor: "000000",
39373
+ };
39374
+ }
39375
+ extractChartDatasets(chartElement, chartType) {
39376
+ if (chartType === "scatterChart") {
39377
+ return this.extractScatterChartDatasets(chartElement);
39378
+ }
38858
39379
  return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
38859
39380
  return {
38860
39381
  label: this.extractChildTextContent(chartDataElement, "c:tx c:f"),
@@ -38862,6 +39383,14 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38862
39383
  };
38863
39384
  });
38864
39385
  }
39386
+ extractScatterChartDatasets(chartElement) {
39387
+ return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
39388
+ return {
39389
+ label: this.extractChildTextContent(chartDataElement, "c:xVal c:f", { required: false }),
39390
+ range: this.extractChildTextContent(chartDataElement, "c:yVal c:f", { required: true }),
39391
+ };
39392
+ });
39393
+ }
38865
39394
  /**
38866
39395
  * The chart type in the XML isn't explicitly defined, but there is an XML element that define the
38867
39396
  * chart, and this element tag name tells us which type of chart it is. We just need to find this XML element.
@@ -38871,12 +39400,21 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38871
39400
  if (!plotAreaElement) {
38872
39401
  throw new Error("Missing plot area in the chart definition.");
38873
39402
  }
39403
+ let globalTag = undefined;
38874
39404
  for (let child of plotAreaElement.children) {
38875
39405
  const tag = removeTagEscapedNamespaces(child.tagName);
38876
39406
  if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
38877
- return tag;
39407
+ if (!globalTag) {
39408
+ globalTag = tag;
39409
+ }
39410
+ else if (globalTag !== tag) {
39411
+ globalTag = "comboChart";
39412
+ }
38878
39413
  }
38879
39414
  }
39415
+ if (globalTag) {
39416
+ return globalTag;
39417
+ }
38880
39418
  throw new Error("Unknown chart type");
38881
39419
  }
38882
39420
  }
@@ -40246,12 +40784,14 @@ class BasePlugin {
40246
40784
  static getters = [];
40247
40785
  history;
40248
40786
  dispatch;
40249
- constructor(stateObserver, dispatch) {
40787
+ canDispatch;
40788
+ constructor(stateObserver, dispatch, canDispatch) {
40250
40789
  this.history = Object.assign(Object.create(stateObserver), {
40251
40790
  update: stateObserver.addChange.bind(stateObserver, this),
40252
40791
  selectCell: () => { },
40253
40792
  });
40254
40793
  this.dispatch = dispatch;
40794
+ this.canDispatch = canDispatch;
40255
40795
  }
40256
40796
  /**
40257
40797
  * Export for excel should be available for all plugins, even for the UI.
@@ -40330,8 +40870,8 @@ class BasePlugin {
40330
40870
  class CorePlugin extends BasePlugin {
40331
40871
  getters;
40332
40872
  uuidGenerator;
40333
- constructor({ getters, stateObserver, range, dispatch, uuidGenerator }) {
40334
- super(stateObserver, dispatch);
40873
+ constructor({ getters, stateObserver, range, dispatch, canDispatch, uuidGenerator, }) {
40874
+ super(stateObserver, dispatch, canDispatch);
40335
40875
  range.addRangeProvider(this.adaptRanges.bind(this));
40336
40876
  this.getters = getters;
40337
40877
  this.uuidGenerator = uuidGenerator;
@@ -42467,6 +43007,9 @@ class DataValidationPlugin extends CorePlugin {
42467
43007
  if (newRule.criterion.type === "isBoolean") {
42468
43008
  this.setCenterStyleToBooleanCells(newRule);
42469
43009
  }
43010
+ else if (newRule.criterion.type === "isValueInList") {
43011
+ newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
43012
+ }
42470
43013
  const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
42471
43014
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
42472
43015
  if (ruleIndex !== -1) {
@@ -42863,7 +43406,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
42863
43406
  if (hiddenElements.size >= elements) {
42864
43407
  return "TooManyHiddenElements" /* CommandResult.TooManyHiddenElements */;
42865
43408
  }
42866
- else if (Math.min(...cmd.elements) < 0 || Math.max(...cmd.elements) > elements) {
43409
+ else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
42867
43410
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
42868
43411
  }
42869
43412
  else {
@@ -43192,7 +43735,6 @@ class MergePlugin extends CorePlugin {
43192
43735
  "isInSameMerge",
43193
43736
  "isMergeHidden",
43194
43737
  "getMainCellPosition",
43195
- "getBottomLeftCell",
43196
43738
  "expandZone",
43197
43739
  "doesIntersectMerge",
43198
43740
  "doesColumnsHaveCommonMerges",
@@ -43374,13 +43916,6 @@ class MergePlugin extends CorePlugin {
43374
43916
  const mergeTopLeftPos = this.getMerge(position).topLeft;
43375
43917
  return { sheetId: position.sheetId, col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };
43376
43918
  }
43377
- getBottomLeftCell(position) {
43378
- if (!this.isInMerge(position)) {
43379
- return position;
43380
- }
43381
- const { bottom, left } = this.getMerge(position);
43382
- return { sheetId: position.sheetId, col: left, row: bottom };
43383
- }
43384
43919
  isMergeHidden(sheetId, merge) {
43385
43920
  const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);
43386
43921
  const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);
@@ -43681,8 +44216,8 @@ class RangeAdapter {
43681
44216
  let newRange = range;
43682
44217
  let changeType = "NONE";
43683
44218
  for (let group of groups) {
43684
- const min = Math.min(...group);
43685
- const max = Math.max(...group);
44219
+ const min = largeMin(group);
44220
+ const max = largeMax(group);
43686
44221
  if (range.zone[start] <= min && min <= range.zone[end]) {
43687
44222
  const toRemove = Math.min(range.zone[end], max) - min + 1;
43688
44223
  changeType = "RESIZE";
@@ -44064,7 +44599,6 @@ class SheetPlugin extends CorePlugin {
44064
44599
  "getSheetIds",
44065
44600
  "getVisibleSheetIds",
44066
44601
  "isSheetVisible",
44067
- "getEvaluationSheets",
44068
44602
  "doesHeaderExist",
44069
44603
  "doesHeadersExist",
44070
44604
  "getCell",
@@ -44131,8 +44665,8 @@ class SheetPlugin extends CorePlugin {
44131
44665
  }
44132
44666
  return "Success" /* CommandResult.Success */;
44133
44667
  case "REMOVE_COLUMNS_ROWS": {
44134
- const min = Math.min(...cmd.elements);
44135
- const max = Math.max(...cmd.elements);
44668
+ const min = largeMin(cmd.elements);
44669
+ const max = largeMax(cmd.elements);
44136
44670
  if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
44137
44671
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
44138
44672
  }
@@ -44323,9 +44857,6 @@ class SheetPlugin extends CorePlugin {
44323
44857
  getVisibleSheetIds() {
44324
44858
  return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));
44325
44859
  }
44326
- getEvaluationSheets() {
44327
- return this.sheets;
44328
- }
44329
44860
  doesHeaderExist(sheetId, dimension, index) {
44330
44861
  return dimension === "COL"
44331
44862
  ? index >= 0 && index < this.getNumberCols(sheetId)
@@ -44334,13 +44865,6 @@ class SheetPlugin extends CorePlugin {
44334
44865
  doesHeadersExist(sheetId, dimension, headerIndexes) {
44335
44866
  return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));
44336
44867
  }
44337
- getRow(sheetId, index) {
44338
- const row = this.getSheet(sheetId).rows[index];
44339
- if (!row) {
44340
- throw new Error(`Row ${row} not found.`);
44341
- }
44342
- return row;
44343
- }
44344
44868
  getCell({ sheetId, col, row }) {
44345
44869
  const sheet = this.tryGetSheet(sheetId);
44346
44870
  const cellId = sheet?.rows[row]?.cells[col];
@@ -45844,8 +46368,8 @@ class UIPlugin extends BasePlugin {
45844
46368
  getters;
45845
46369
  ui;
45846
46370
  selection;
45847
- constructor({ getters, stateObserver, dispatch, uiActions, selection }) {
45848
- super(stateObserver, dispatch);
46371
+ constructor({ getters, stateObserver, dispatch, canDispatch, uiActions, selection, }) {
46372
+ super(stateObserver, dispatch, canDispatch);
45849
46373
  this.getters = getters;
45850
46374
  this.ui = uiActions;
45851
46375
  this.selection = selection;
@@ -45909,12 +46433,6 @@ class CompilationParametersBuilder {
45909
46433
  : _t("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
45910
46434
  }
45911
46435
  const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
45912
- return this.readCell(position);
45913
- }
45914
- readCell(position) {
45915
- if (!this.getters.tryGetSheet(position.sheetId)) {
45916
- throw new EvaluationError(_t("Invalid sheet name"));
45917
- }
45918
46436
  return this.computeCell(position);
45919
46437
  }
45920
46438
  /**
@@ -45950,7 +46468,7 @@ class CompilationParametersBuilder {
45950
46468
  matrix[colIndex] = new Array(height);
45951
46469
  for (let row = _zone.top; row <= _zone.bottom; row++) {
45952
46470
  const rowIndex = row - _zone.top;
45953
- matrix[colIndex][rowIndex] = this.readCell({ sheetId, col, row });
46471
+ matrix[colIndex][rowIndex] = this.computeCell({ sheetId, col, row });
45954
46472
  }
45955
46473
  }
45956
46474
  this.rangeCache[cacheKey] = matrix;
@@ -47058,15 +47576,15 @@ class Evaluator {
47058
47576
  getEvaluatedCell(position) {
47059
47577
  return this.evaluatedCells.get(position) || EMPTY_CELL;
47060
47578
  }
47061
- getSpreadPositionsOf(position) {
47579
+ getSpreadZone(position) {
47062
47580
  if (!this.spreadingRelations.isArrayFormula(position)) {
47063
- return [];
47581
+ return undefined;
47064
47582
  }
47065
47583
  if (this.evaluatedCells.get(position)?.type === CellValueType.error) {
47066
- return [position];
47584
+ return positionToZone(position);
47067
47585
  }
47068
47586
  const spreadPositions = Array.from(this.spreadingRelations.getArrayResultPositions(position));
47069
- return [position, ...spreadPositions];
47587
+ return union(positionToZone(position), unionPositionsToZone(spreadPositions));
47070
47588
  }
47071
47589
  getEvaluatedPositions() {
47072
47590
  return this.evaluatedCells.keys();
@@ -47131,7 +47649,9 @@ class Evaluator {
47131
47649
  this.blockedArrayFormulas = this.createEmptyPositionSet();
47132
47650
  this.spreadingRelations = new SpreadingRelation(this.createEmptyPositionSet.bind(this));
47133
47651
  this.formulaDependencies = lazy(() => {
47134
- const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position).map((range) => ({
47652
+ const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position)
47653
+ .filter((range) => !range.invalidSheetName && !range.invalidXc)
47654
+ .map((range) => ({
47135
47655
  data: position,
47136
47656
  boundingBox: {
47137
47657
  zone: range.zone,
@@ -47471,7 +47991,7 @@ class EvaluationPlugin extends UIPlugin {
47471
47991
  "getEvaluatedCell",
47472
47992
  "getEvaluatedCells",
47473
47993
  "getEvaluatedCellsInZone",
47474
- "getSpreadPositionsOf",
47994
+ "getSpreadZone",
47475
47995
  "getArrayFormulaSpreadingOn",
47476
47996
  "isEmpty",
47477
47997
  ];
@@ -47577,8 +48097,11 @@ class EvaluationPlugin extends UIPlugin {
47577
48097
  getEvaluatedCellsInZone(sheetId, zone) {
47578
48098
  return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
47579
48099
  }
47580
- getSpreadPositionsOf(position) {
47581
- return this.evaluator.getSpreadPositionsOf(position);
48100
+ /**
48101
+ * Return the spread zone the position is part of, if any
48102
+ */
48103
+ getSpreadZone(position) {
48104
+ return this.evaluator.getSpreadZone(position);
47582
48105
  }
47583
48106
  getArrayFormulaSpreadingOn(position) {
47584
48107
  return this.evaluator.getArrayFormulaSpreadingOn(position);
@@ -47618,7 +48141,7 @@ class EvaluationPlugin extends UIPlugin {
47618
48141
  ? getItemId(newFormat, data.formats)
47619
48142
  : exportedCellData.format;
47620
48143
  let content;
47621
- if (formulaCell instanceof FormulaCellWithDependencies) {
48144
+ if (isFormula && formulaCell instanceof FormulaCellWithDependencies) {
47622
48145
  content = formulaCell.contentWithFixedReferences;
47623
48146
  }
47624
48147
  else {
@@ -47666,17 +48189,17 @@ function isBadExpression(tokens) {
47666
48189
  */
47667
48190
  function sortWithClusters(colorsToSort) {
47668
48191
  const clusters = [
47669
- { leadColor: rgba(255, 0, 0), colors: [] },
47670
- { leadColor: rgba(255, 128, 0), colors: [] },
47671
- { leadColor: rgba(128, 128, 0), colors: [] },
47672
- { leadColor: rgba(128, 255, 0), colors: [] },
47673
- { leadColor: rgba(0, 255, 0), colors: [] },
47674
- { leadColor: rgba(0, 255, 128), colors: [] },
47675
- { leadColor: rgba(0, 255, 255), colors: [] },
47676
- { leadColor: rgba(0, 127, 255), colors: [] },
47677
- { leadColor: rgba(0, 0, 255), colors: [] },
47678
- { leadColor: rgba(127, 0, 255), colors: [] },
47679
- { leadColor: rgba(128, 0, 128), colors: [] },
48192
+ { leadColor: rgba(255, 0, 0), colors: [] }, // red
48193
+ { leadColor: rgba(255, 128, 0), colors: [] }, // orange
48194
+ { leadColor: rgba(128, 128, 0), colors: [] }, // yellow
48195
+ { leadColor: rgba(128, 255, 0), colors: [] }, // chartreuse
48196
+ { leadColor: rgba(0, 255, 0), colors: [] }, // green
48197
+ { leadColor: rgba(0, 255, 128), colors: [] }, // spring green
48198
+ { leadColor: rgba(0, 255, 255), colors: [] }, // cyan
48199
+ { leadColor: rgba(0, 127, 255), colors: [] }, // azure
48200
+ { leadColor: rgba(0, 0, 255), colors: [] }, // blue
48201
+ { leadColor: rgba(127, 0, 255), colors: [] }, // violet
48202
+ { leadColor: rgba(128, 0, 128), colors: [] }, // magenta
47680
48203
  { leadColor: rgba(255, 0, 128), colors: [] }, // rose
47681
48204
  ];
47682
48205
  for (const color of colorsToSort.map(colorToRGBA)) {
@@ -48067,13 +48590,13 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
48067
48590
  .map((cell) => cell.value);
48068
48591
  switch (threshold.type) {
48069
48592
  case "value":
48070
- const result = functionName === "max" ? Math.max(...rangeValues) : Math.min(...rangeValues);
48593
+ const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
48071
48594
  return result;
48072
48595
  case "number":
48073
48596
  return Number(threshold.value);
48074
48597
  case "percentage":
48075
- const min = Math.min(...rangeValues);
48076
- const max = Math.max(...rangeValues);
48598
+ const min = largeMin(rangeValues);
48599
+ const max = largeMax(rangeValues);
48077
48600
  const delta = max - min;
48078
48601
  return min + (delta * Number(threshold.value)) / 100;
48079
48602
  case "percentile":
@@ -48540,8 +49063,8 @@ class DynamicTablesPlugin extends UIPlugin {
48540
49063
  else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
48541
49064
  return true;
48542
49065
  }
48543
- const spreadPositions = this.getters.getSpreadPositionsOf(parentSpreadingCell);
48544
- return deepEquals(unionZone, unionPositionsToZone(spreadPositions));
49066
+ const zone = this.getters.getSpreadZone(parentSpreadingCell);
49067
+ return deepEquals(unionZone, zone);
48545
49068
  }
48546
49069
  coreTableToTable(sheetId, table) {
48547
49070
  if (table.type !== "dynamic") {
@@ -48549,8 +49072,7 @@ class DynamicTablesPlugin extends UIPlugin {
48549
49072
  }
48550
49073
  const tableZone = table.range.zone;
48551
49074
  const tablePosition = { sheetId, col: tableZone.left, row: tableZone.top };
48552
- const spreadPositions = this.getters.getSpreadPositionsOf(tablePosition);
48553
- const zone = spreadPositions.length ? unionPositionsToZone(spreadPositions) : table.range.zone;
49075
+ const zone = this.getters.getSpreadZone(tablePosition) ?? table.range.zone;
48554
49076
  const range = this.getters.getRangeFromZone(sheetId, zone);
48555
49077
  const filters = this.getDynamicTableFilters(sheetId, table, zone);
48556
49078
  return { id: table.id, range, filters, config: table.config };
@@ -49000,8 +49522,7 @@ class AutofillPlugin extends UIPlugin {
49000
49522
  let row = zone.bottom;
49001
49523
  if (col > 0) {
49002
49524
  let leftPosition = { sheetId, col: col - 1, row };
49003
- while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
49004
- this.getters.getCell(leftPosition)?.content) {
49525
+ while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty) {
49005
49526
  row += 1;
49006
49527
  leftPosition = { sheetId, col: col - 1, row };
49007
49528
  }
@@ -49010,8 +49531,7 @@ class AutofillPlugin extends UIPlugin {
49010
49531
  col = zone.right;
49011
49532
  if (col <= this.getters.getNumberCols(sheetId)) {
49012
49533
  let rightPosition = { sheetId, col: col + 1, row };
49013
- while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
49014
- this.getters.getCell(rightPosition)?.content) {
49534
+ while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty) {
49015
49535
  row += 1;
49016
49536
  rightPosition = { sheetId, col: col + 1, row };
49017
49537
  }
@@ -49321,13 +49841,13 @@ class AutomaticSumPlugin extends UIPlugin {
49321
49841
  const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
49322
49842
  const cellPositions = range(end, -1, -1);
49323
49843
  const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
49324
- const maxValidPosition = Math.max(...invalidCells);
49844
+ const maxValidPosition = largeMax(invalidCells);
49325
49845
  const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
49326
49846
  const firstSequence = numberSequences[0] || [];
49327
- if (Math.max(...firstSequence) < maxValidPosition) {
49847
+ if (largeMax(firstSequence) < maxValidPosition) {
49328
49848
  return Infinity;
49329
49849
  }
49330
- return Math.min(...firstSequence);
49850
+ return largeMin(firstSequence);
49331
49851
  }
49332
49852
  shouldFindData(sheetId, zone) {
49333
49853
  return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
@@ -50902,8 +51422,6 @@ class SheetUIPlugin extends UIPlugin {
50902
51422
  static getters = [
50903
51423
  "doesCellHaveGridIcon",
50904
51424
  "getCellWidth",
50905
- "getCellComputedBorder",
50906
- "getCellComputedStyle",
50907
51425
  "getTextWidth",
50908
51426
  "getCellText",
50909
51427
  "getCellMultiLineText",
@@ -50947,7 +51465,7 @@ class SheetUIPlugin extends UIPlugin {
50947
51465
  // Getters
50948
51466
  // ---------------------------------------------------------------------------
50949
51467
  getCellWidth(position) {
50950
- const style = this.getCellComputedStyle(position);
51468
+ const style = this.getters.getCellComputedStyle(position);
50951
51469
  let contentWidth = 0;
50952
51470
  const content = this.getters.getEvaluatedCell(position).formattedValue;
50953
51471
  if (content) {
@@ -51040,35 +51558,12 @@ class SheetUIPlugin extends UIPlugin {
51040
51558
  */
51041
51559
  isCellEmpty(position) {
51042
51560
  const mainPosition = this.getters.getMainCellPosition(position);
51043
- return !(this.getters.getCorrespondingFormulaCell(mainPosition) ||
51044
- this.getters.getCell(mainPosition)?.content);
51045
- }
51046
- getCellComputedBorder(position) {
51047
- const cellBorder = this.getters.getCellBorder(position) || {};
51048
- const cellTableBorder = this.getters.getCellTableBorder(position) || {};
51049
- // Use removeFalsyAttributes to avoid overwriting borders with undefined values
51050
- const border = { ...cellTableBorder, ...removeFalsyAttributes(cellBorder) };
51051
- return isObjectEmptyRecursive(border) ? null : border;
51052
- }
51053
- getCellComputedStyle(position) {
51054
- const cell = this.getters.getCell(position);
51055
- const cfStyle = this.getters.getCellConditionalFormatStyle(position);
51056
- const tableStyle = this.getters.getCellTableStyle(position);
51057
- const computedStyle = {
51058
- ...removeFalsyAttributes(tableStyle),
51059
- ...removeFalsyAttributes(cell?.style),
51060
- ...removeFalsyAttributes(cfStyle),
51061
- };
51062
- const evaluatedCell = this.getters.getEvaluatedCell(position);
51063
- if (evaluatedCell.link && !computedStyle.textColor) {
51064
- computedStyle.textColor = LINK_COLOR;
51065
- }
51066
- return computedStyle;
51561
+ return this.getters.getEvaluatedCell(mainPosition).type === CellValueType.empty;
51067
51562
  }
51068
51563
  getColMaxWidth(sheetId, index) {
51069
51564
  const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
51070
51565
  const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
51071
- return Math.max(0, ...sizes);
51566
+ return Math.max(0, largeMax(sizes));
51072
51567
  }
51073
51568
  /**
51074
51569
  * Check that any "sheetId" in the command matches an existing
@@ -51097,6 +51592,241 @@ class SheetUIPlugin extends UIPlugin {
51097
51592
  }
51098
51593
  }
51099
51594
 
51595
+ class TableStylePlugin extends UIPlugin {
51596
+ static getters = ["getCellTableStyle", "getCellTableBorder"];
51597
+ tableStyles = {};
51598
+ handle(cmd) {
51599
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51600
+ (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51601
+ cmd.type === "EVALUATE_CELLS") {
51602
+ this.tableStyles = {};
51603
+ return;
51604
+ }
51605
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51606
+ delete this.tableStyles[cmd.sheetId];
51607
+ return;
51608
+ }
51609
+ }
51610
+ finalize() {
51611
+ for (const sheetId of this.getters.getSheetIds()) {
51612
+ if (!this.tableStyles[sheetId]) {
51613
+ this.tableStyles[sheetId] = {};
51614
+ }
51615
+ for (const table of this.getters.getTables(sheetId)) {
51616
+ if (!this.tableStyles[sheetId][table.id]) {
51617
+ this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51618
+ }
51619
+ }
51620
+ }
51621
+ }
51622
+ getCellTableStyle(position) {
51623
+ const table = this.getters.getTable(position);
51624
+ if (!table) {
51625
+ return undefined;
51626
+ }
51627
+ return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51628
+ }
51629
+ getCellTableBorder(position) {
51630
+ const table = this.getters.getTable(position);
51631
+ if (!table) {
51632
+ return undefined;
51633
+ }
51634
+ return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51635
+ }
51636
+ computeTableStyle(sheetId, table) {
51637
+ return lazy(() => {
51638
+ const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51639
+ const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51640
+ // Return the style with sheet coordinates instead of tables coordinates
51641
+ const mapping = this.getTableMapping(sheetId, table);
51642
+ const absoluteTableStyle = { borders: {}, styles: {} };
51643
+ for (let col = 0; col < numberOfCols; col++) {
51644
+ const colInSheet = mapping.colMapping[col];
51645
+ absoluteTableStyle.borders[colInSheet] = {};
51646
+ absoluteTableStyle.styles[colInSheet] = {};
51647
+ for (let row = 0; row < numberOfRows; row++) {
51648
+ const rowInSheet = mapping.rowMapping[row];
51649
+ absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51650
+ absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51651
+ }
51652
+ }
51653
+ return absoluteTableStyle;
51654
+ });
51655
+ }
51656
+ /**
51657
+ * Get the actual table config that will be used to compute the table style. It is different from
51658
+ * the config of the table because of hidden rows and columns in the sheet. For example remove the
51659
+ * hidden rows from config.numberOfHeaders.
51660
+ */
51661
+ getTableRuntimeConfig(sheetId, table) {
51662
+ const tableZone = table.range.zone;
51663
+ const config = { ...table.config };
51664
+ let numberOfCols = tableZone.right - tableZone.left + 1;
51665
+ let numberOfRows = tableZone.bottom - tableZone.top + 1;
51666
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51667
+ if (!this.getters.isRowHidden(sheetId, row)) {
51668
+ continue;
51669
+ }
51670
+ numberOfRows--;
51671
+ if (row - tableZone.top < table.config.numberOfHeaders) {
51672
+ config.numberOfHeaders--;
51673
+ if (config.numberOfHeaders < 0) {
51674
+ config.numberOfHeaders = 0;
51675
+ }
51676
+ }
51677
+ if (row === tableZone.bottom) {
51678
+ config.totalRow = false;
51679
+ }
51680
+ }
51681
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51682
+ if (!this.getters.isColHidden(sheetId, col)) {
51683
+ continue;
51684
+ }
51685
+ numberOfCols--;
51686
+ if (col === tableZone.left) {
51687
+ config.firstColumn = false;
51688
+ }
51689
+ if (col === tableZone.right) {
51690
+ config.lastColumn = false;
51691
+ }
51692
+ }
51693
+ return {
51694
+ config,
51695
+ numberOfCols,
51696
+ numberOfRows,
51697
+ };
51698
+ }
51699
+ /**
51700
+ * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51701
+ */
51702
+ getTableMapping(sheetId, table) {
51703
+ const colMapping = {};
51704
+ const rowMapping = {};
51705
+ let colOffset = 0;
51706
+ let rowOffset = 0;
51707
+ const tableZone = table.range.zone;
51708
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51709
+ if (this.getters.isColHidden(sheetId, col)) {
51710
+ continue;
51711
+ }
51712
+ colMapping[colOffset] = col;
51713
+ colOffset++;
51714
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51715
+ if (this.getters.isRowHidden(sheetId, row)) {
51716
+ continue;
51717
+ }
51718
+ rowMapping[rowOffset] = row;
51719
+ rowOffset++;
51720
+ }
51721
+ }
51722
+ return {
51723
+ colMapping,
51724
+ rowMapping,
51725
+ };
51726
+ }
51727
+ }
51728
+ const invalidateTableStyleCommands = [
51729
+ "HIDE_COLUMNS_ROWS",
51730
+ "UNHIDE_COLUMNS_ROWS",
51731
+ "UNFOLD_HEADER_GROUP",
51732
+ "FOLD_HEADER_GROUP",
51733
+ "FOLD_ALL_HEADER_GROUPS",
51734
+ "UNFOLD_ALL_HEADER_GROUPS",
51735
+ "CREATE_TABLE",
51736
+ "UPDATE_TABLE",
51737
+ "UPDATE_FILTER",
51738
+ "REMOVE_TABLE",
51739
+ ];
51740
+ const invalidateTableStyleCommandsSet = new Set(invalidateTableStyleCommands);
51741
+ function doesCommandInvalidatesTableStyle(cmd) {
51742
+ return invalidateTableStyleCommandsSet.has(cmd.type);
51743
+ }
51744
+
51745
+ class CellComputedStylePlugin extends UIPlugin {
51746
+ static getters = ["getCellComputedBorder", "getCellComputedStyle"];
51747
+ styles = {};
51748
+ borders = {};
51749
+ handle(cmd) {
51750
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51751
+ cmd.type === "UPDATE_CELL" ||
51752
+ cmd.type === "EVALUATE_CELLS") {
51753
+ this.styles = {};
51754
+ this.borders = {};
51755
+ return;
51756
+ }
51757
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51758
+ delete this.styles[cmd.sheetId];
51759
+ delete this.borders[cmd.sheetId];
51760
+ return;
51761
+ }
51762
+ if (invalidateCFEvaluationCommands.has(cmd.type)) {
51763
+ this.styles = {};
51764
+ return;
51765
+ }
51766
+ if (invalidateBordersCommands.has(cmd.type)) {
51767
+ this.borders = {};
51768
+ return;
51769
+ }
51770
+ }
51771
+ getCellComputedBorder(position) {
51772
+ const { sheetId, row, col } = position;
51773
+ if (this.borders[sheetId]?.[row]?.[col] !== undefined) {
51774
+ return this.borders[sheetId][row][col];
51775
+ }
51776
+ if (!this.borders[sheetId]) {
51777
+ this.borders[sheetId] = {};
51778
+ }
51779
+ if (!this.borders[sheetId][row]) {
51780
+ this.borders[sheetId][row] = {};
51781
+ }
51782
+ if (!this.borders[sheetId][row][col]) {
51783
+ this.borders[sheetId][row][col] = this.computeCellBorder(position);
51784
+ }
51785
+ return this.borders[sheetId][row][col];
51786
+ }
51787
+ getCellComputedStyle(position) {
51788
+ const { sheetId, row, col } = position;
51789
+ if (this.styles[sheetId]?.[row]?.[col] !== undefined) {
51790
+ return this.styles[sheetId][row][col];
51791
+ }
51792
+ if (!this.styles[sheetId]) {
51793
+ this.styles[sheetId] = {};
51794
+ }
51795
+ if (!this.styles[sheetId][row]) {
51796
+ this.styles[sheetId][row] = {};
51797
+ }
51798
+ if (!this.styles[sheetId][row][col]) {
51799
+ this.styles[sheetId][row][col] = this.computeCellStyle(position);
51800
+ }
51801
+ return this.styles[sheetId][row][col];
51802
+ }
51803
+ computeCellBorder(position) {
51804
+ const cellBorder = this.getters.getCellBorder(position) || {};
51805
+ const cellTableBorder = this.getters.getCellTableBorder(position) || {};
51806
+ // Use removeFalsyAttributes to avoid overwriting borders with undefined values
51807
+ const border = {
51808
+ ...removeFalsyAttributes(cellTableBorder),
51809
+ ...removeFalsyAttributes(cellBorder),
51810
+ };
51811
+ return isObjectEmptyRecursive(border) ? null : border;
51812
+ }
51813
+ computeCellStyle(position) {
51814
+ const cell = this.getters.getCell(position);
51815
+ const cfStyle = this.getters.getCellConditionalFormatStyle(position);
51816
+ const tableStyle = this.getters.getCellTableStyle(position);
51817
+ const computedStyle = {
51818
+ ...removeFalsyAttributes(tableStyle),
51819
+ ...removeFalsyAttributes(cell?.style),
51820
+ ...removeFalsyAttributes(cfStyle),
51821
+ };
51822
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
51823
+ if (evaluatedCell.link && !computedStyle.textColor) {
51824
+ computedStyle.textColor = LINK_COLOR;
51825
+ }
51826
+ return computedStyle;
51827
+ }
51828
+ }
51829
+
51100
51830
  const genericRepeatsTransforms = [
51101
51831
  repeatSheetDependantCommand,
51102
51832
  repeatTargetDependantCommand,
@@ -51674,148 +52404,6 @@ class TableAutofillPlugin extends UIPlugin {
51674
52404
  }
51675
52405
  }
51676
52406
 
51677
- class TableStylePlugin extends UIPlugin {
51678
- static getters = ["getCellTableStyle", "getCellTableBorder"];
51679
- tableStyles = {};
51680
- handle(cmd) {
51681
- if (invalidateEvaluationCommands.has(cmd.type) ||
51682
- (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51683
- cmd.type === "EVALUATE_CELLS") {
51684
- this.tableStyles = {};
51685
- return;
51686
- }
51687
- switch (cmd.type) {
51688
- case "HIDE_COLUMNS_ROWS":
51689
- case "UNHIDE_COLUMNS_ROWS":
51690
- case "UNFOLD_HEADER_GROUP":
51691
- case "FOLD_HEADER_GROUP":
51692
- case "FOLD_ALL_HEADER_GROUPS":
51693
- case "UNFOLD_ALL_HEADER_GROUPS":
51694
- case "UPDATE_TABLE":
51695
- case "UPDATE_FILTER":
51696
- delete this.tableStyles[cmd.sheetId];
51697
- break;
51698
- }
51699
- }
51700
- finalize() {
51701
- for (const sheetId of this.getters.getSheetIds()) {
51702
- if (!this.tableStyles[sheetId]) {
51703
- this.tableStyles[sheetId] = {};
51704
- }
51705
- for (const table of this.getters.getTables(sheetId)) {
51706
- if (!this.tableStyles[sheetId][table.id]) {
51707
- this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51708
- }
51709
- }
51710
- }
51711
- }
51712
- getCellTableStyle(position) {
51713
- const table = this.getters.getTable(position);
51714
- if (!table) {
51715
- return undefined;
51716
- }
51717
- return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51718
- }
51719
- getCellTableBorder(position) {
51720
- const table = this.getters.getTable(position);
51721
- if (!table) {
51722
- return undefined;
51723
- }
51724
- return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51725
- }
51726
- computeTableStyle(sheetId, table) {
51727
- return lazy(() => {
51728
- const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51729
- const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51730
- // Return the style with sheet coordinates instead of tables coordinates
51731
- const mapping = this.getTableMapping(sheetId, table);
51732
- const absoluteTableStyle = { borders: {}, styles: {} };
51733
- for (let col = 0; col < numberOfCols; col++) {
51734
- const colInSheet = mapping.colMapping[col];
51735
- absoluteTableStyle.borders[colInSheet] = {};
51736
- absoluteTableStyle.styles[colInSheet] = {};
51737
- for (let row = 0; row < numberOfRows; row++) {
51738
- const rowInSheet = mapping.rowMapping[row];
51739
- absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51740
- absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51741
- }
51742
- }
51743
- return absoluteTableStyle;
51744
- });
51745
- }
51746
- /**
51747
- * Get the actual table config that will be used to compute the table style. It is different from
51748
- * the config of the table because of hidden rows and columns in the sheet. For example remove the
51749
- * hidden rows from config.numberOfHeaders.
51750
- */
51751
- getTableRuntimeConfig(sheetId, table) {
51752
- const tableZone = table.range.zone;
51753
- const config = { ...table.config };
51754
- let numberOfCols = tableZone.right - tableZone.left + 1;
51755
- let numberOfRows = tableZone.bottom - tableZone.top + 1;
51756
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51757
- if (!this.getters.isRowHidden(sheetId, row)) {
51758
- continue;
51759
- }
51760
- numberOfRows--;
51761
- if (row - tableZone.top < table.config.numberOfHeaders) {
51762
- config.numberOfHeaders--;
51763
- if (config.numberOfHeaders < 0) {
51764
- config.numberOfHeaders = 0;
51765
- }
51766
- }
51767
- if (row === tableZone.bottom) {
51768
- config.totalRow = false;
51769
- }
51770
- }
51771
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51772
- if (!this.getters.isColHidden(sheetId, col)) {
51773
- continue;
51774
- }
51775
- numberOfCols--;
51776
- if (col === tableZone.left) {
51777
- config.firstColumn = false;
51778
- }
51779
- if (col === tableZone.right) {
51780
- config.lastColumn = false;
51781
- }
51782
- }
51783
- return {
51784
- config,
51785
- numberOfCols,
51786
- numberOfRows,
51787
- };
51788
- }
51789
- /**
51790
- * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51791
- */
51792
- getTableMapping(sheetId, table) {
51793
- const colMapping = {};
51794
- const rowMapping = {};
51795
- let colOffset = 0;
51796
- let rowOffset = 0;
51797
- const tableZone = table.range.zone;
51798
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51799
- if (this.getters.isColHidden(sheetId, col)) {
51800
- continue;
51801
- }
51802
- colMapping[colOffset] = col;
51803
- colOffset++;
51804
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51805
- if (this.getters.isRowHidden(sheetId, row)) {
51806
- continue;
51807
- }
51808
- rowMapping[rowOffset] = row;
51809
- rowOffset++;
51810
- }
51811
- }
51812
- return {
51813
- colMapping,
51814
- rowMapping,
51815
- };
51816
- }
51817
- }
51818
-
51819
52407
  /**
51820
52408
  * Clipboard Plugin
51821
52409
  *
@@ -52525,38 +53113,6 @@ class FilterEvaluationPlugin extends UIPlugin {
52525
53113
  }
52526
53114
  }
52527
53115
 
52528
- const selectionStatisticFunctions = [
52529
- {
52530
- name: _t("Sum"),
52531
- types: [CellValueType.number],
52532
- compute: (values, locale) => sum([[values]], locale),
52533
- },
52534
- {
52535
- name: _t("Avg"),
52536
- types: [CellValueType.number],
52537
- compute: (values, locale) => average([[values]], locale),
52538
- },
52539
- {
52540
- name: _t("Min"),
52541
- types: [CellValueType.number],
52542
- compute: (values, locale) => min([[values]], locale),
52543
- },
52544
- {
52545
- name: _t("Max"),
52546
- types: [CellValueType.number],
52547
- compute: (values, locale) => max([[values]], locale),
52548
- },
52549
- {
52550
- name: _t("Count"),
52551
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52552
- compute: (values) => countAny([[values]]),
52553
- },
52554
- {
52555
- name: _t("Count Numbers"),
52556
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52557
- compute: (values, locale) => countNumbers([[values]], locale),
52558
- },
52559
- ];
52560
53116
  /**
52561
53117
  * SelectionPlugin
52562
53118
  */
@@ -52572,8 +53128,6 @@ class GridSelectionPlugin extends UIPlugin {
52572
53128
  "getSelectedZones",
52573
53129
  "getSelectedZone",
52574
53130
  "getSelectedCells",
52575
- "getStatisticFnResults",
52576
- "getAggregate",
52577
53131
  "getSelectedFigureId",
52578
53132
  "getSelection",
52579
53133
  "getActivePosition",
@@ -52608,7 +53162,10 @@ class GridSelectionPlugin extends UIPlugin {
52608
53162
  switch (cmd.type) {
52609
53163
  case "ACTIVATE_SHEET":
52610
53164
  try {
52611
- this.getters.getSheet(cmd.sheetIdTo);
53165
+ const sheet = this.getters.getSheet(cmd.sheetIdTo);
53166
+ if (!sheet.isVisible) {
53167
+ return "SheetIsHidden" /* CommandResult.SheetIsHidden */;
53168
+ }
52612
53169
  break;
52613
53170
  }
52614
53171
  catch (error) {
@@ -52857,52 +53414,6 @@ class GridSelectionPlugin extends UIPlugin {
52857
53414
  : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });
52858
53415
  }
52859
53416
  }
52860
- getStatisticFnResults() {
52861
- const sheetId = this.getters.getActiveSheetId();
52862
- const cells = new Set();
52863
- for (const zone of this.gridSelection.zones) {
52864
- for (const { col, row } of positions(zone)) {
52865
- if (this.getters.isRowHidden(sheetId, row) || this.getters.isColHidden(sheetId, col)) {
52866
- continue; // Skip hidden cells
52867
- }
52868
- const evaluatedCell = this.getters.getEvaluatedCell({ sheetId, col, row });
52869
- if (evaluatedCell.type !== CellValueType.empty) {
52870
- cells.add(evaluatedCell);
52871
- }
52872
- }
52873
- }
52874
- const locale = this.getters.getLocale();
52875
- let statisticFnResults = {};
52876
- for (let fn of selectionStatisticFunctions) {
52877
- // We don't want to display statistical information when there is no interest:
52878
- // We set the statistical result to undefined if the data handled by the selection
52879
- // does not match the data handled by the function.
52880
- // Ex: if there are only texts in the selection, we prefer that the SUM result
52881
- // be displayed as undefined rather than 0.
52882
- let fnResult = undefined;
52883
- const evaluatedCells = [...cells].filter((c) => fn.types.includes(c.type));
52884
- if (evaluatedCells.length) {
52885
- fnResult = fn.compute(evaluatedCells, locale);
52886
- }
52887
- statisticFnResults[fn.name] = fnResult;
52888
- }
52889
- return statisticFnResults;
52890
- }
52891
- getAggregate() {
52892
- let aggregate = 0;
52893
- let n = 0;
52894
- const sheetId = this.getters.getActiveSheetId();
52895
- const cellPositions = this.gridSelection.zones.map(positions).flat();
52896
- for (const { col, row } of cellPositions) {
52897
- const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
52898
- if (cell.type === CellValueType.number) {
52899
- n++;
52900
- aggregate += cell.value;
52901
- }
52902
- }
52903
- const locale = this.getters.getLocale();
52904
- return n < 2 ? null : formatValue(aggregate, { locale });
52905
- }
52906
53417
  isSelected(zone) {
52907
53418
  return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));
52908
53419
  }
@@ -52944,9 +53455,6 @@ class GridSelectionPlugin extends UIPlugin {
52944
53455
  // Other
52945
53456
  // ---------------------------------------------------------------------------
52946
53457
  activateSheet(sheetIdFrom, sheetIdTo) {
52947
- if (!this.getters.isSheetVisible(sheetIdTo)) {
52948
- this.dispatch("SHOW_SHEET", { sheetId: sheetIdTo });
52949
- }
52950
53458
  this.setActiveSheet(sheetIdTo);
52951
53459
  this.sheetsData[sheetIdFrom] = {
52952
53460
  gridSelection: deepCopy(this.gridSelection),
@@ -53953,7 +54461,7 @@ class SheetViewPlugin extends UIPlugin {
53953
54461
  * column of the current viewport
53954
54462
  */
53955
54463
  getColDimensionsInViewport(sheetId, col) {
53956
- const left = Math.min(...this.getters.getSheetViewVisibleCols());
54464
+ const left = largeMin(this.getters.getSheetViewVisibleCols());
53957
54465
  const start = this.getters.getColRowOffsetInViewport("COL", left, col);
53958
54466
  const size = this.getters.getColSize(sheetId, col);
53959
54467
  const isColHidden = this.getters.isColHidden(sheetId, col);
@@ -53968,7 +54476,7 @@ class SheetViewPlugin extends UIPlugin {
53968
54476
  * of the current viewport
53969
54477
  */
53970
54478
  getRowDimensionsInViewport(sheetId, row) {
53971
- const top = Math.min(...this.getters.getSheetViewVisibleRows());
54479
+ const top = largeMin(this.getters.getSheetViewVisibleRows());
53972
54480
  const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
53973
54481
  const size = this.getters.getRowSize(sheetId, row);
53974
54482
  const isRowHidden = this.getters.isRowHidden(sheetId, row);
@@ -54320,6 +54828,7 @@ const statefulUIPluginRegistry = new Registry()
54320
54828
  .add("evaluation_filter", FilterEvaluationPlugin)
54321
54829
  .add("header_visibility_ui", HeaderVisibilityUIPlugin)
54322
54830
  .add("table_style", TableStylePlugin)
54831
+ .add("cell_computed_style", CellComputedStylePlugin)
54323
54832
  .add("header_positions", HeaderPositionsUIPlugin)
54324
54833
  .add("viewport", SheetViewPlugin)
54325
54834
  .add("clipboard", ClipboardPlugin);
@@ -54380,6 +54889,38 @@ class ImageProvider {
54380
54889
  }
54381
54890
  }
54382
54891
 
54892
+ class ArrayFormulaHighlight extends SpreadsheetStore {
54893
+ highlightStore = this.get(HighlightStore);
54894
+ constructor(get) {
54895
+ super(get);
54896
+ this.highlightStore.register(this);
54897
+ }
54898
+ get highlights() {
54899
+ const zone = this.getHighlightZone();
54900
+ if (!zone) {
54901
+ return [];
54902
+ }
54903
+ const sheetId = this.model.getters.getActiveSheetId();
54904
+ return [
54905
+ {
54906
+ sheetId,
54907
+ zone,
54908
+ color: "#17A2B8",
54909
+ noFill: true,
54910
+ thinLine: true,
54911
+ },
54912
+ ];
54913
+ }
54914
+ getHighlightZone() {
54915
+ const position = this.model.getters.getActivePosition();
54916
+ const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
54917
+ const spreadZone = spreader
54918
+ ? this.model.getters.getSpreadZone(spreader)
54919
+ : this.model.getters.getSpreadZone(position);
54920
+ return spreadZone;
54921
+ }
54922
+ }
54923
+
54383
54924
  const RIPPLE_KEY_FRAMES = [
54384
54925
  { transform: "scale(0)" },
54385
54926
  { transform: "scale(0.8)", offset: 0.33 },
@@ -54682,12 +55223,14 @@ class BottomBarSheet extends Component {
54682
55223
  this.editionState = "initializing";
54683
55224
  }
54684
55225
  stopEdition() {
54685
- if (!this.state.isEditing)
55226
+ const input = this.sheetNameRef.el;
55227
+ if (!this.state.isEditing || !input)
54686
55228
  return;
54687
55229
  this.state.isEditing = false;
54688
55230
  this.editionState = "initializing";
54689
- this.sheetNameRef.el?.blur();
55231
+ input.blur();
54690
55232
  const inputValue = this.getInputContent() || "";
55233
+ input.innerText = inputValue;
54691
55234
  interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
54692
55235
  }
54693
55236
  cancelEdition() {
@@ -54731,6 +55274,115 @@ class BottomBarSheet extends Component {
54731
55274
  }
54732
55275
  }
54733
55276
 
55277
+ const selectionStatisticFunctions = [
55278
+ {
55279
+ name: _t("Sum"),
55280
+ types: [CellValueType.number],
55281
+ compute: (values, locale) => sum([[values]], locale),
55282
+ },
55283
+ {
55284
+ name: _t("Avg"),
55285
+ types: [CellValueType.number],
55286
+ compute: (values, locale) => average([[values]], locale),
55287
+ },
55288
+ {
55289
+ name: _t("Min"),
55290
+ types: [CellValueType.number],
55291
+ compute: (values, locale) => min([[values]], locale),
55292
+ },
55293
+ {
55294
+ name: _t("Max"),
55295
+ types: [CellValueType.number],
55296
+ compute: (values, locale) => max([[values]], locale),
55297
+ },
55298
+ {
55299
+ name: _t("Count"),
55300
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55301
+ compute: (values) => countAny([[values]]),
55302
+ },
55303
+ {
55304
+ name: _t("Count Numbers"),
55305
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55306
+ compute: (values, locale) => countNumbers([[values]], locale),
55307
+ },
55308
+ ];
55309
+ class AggregateStatisticsStore extends SpreadsheetStore {
55310
+ statisticFnResults = this._computeStatisticFnResults();
55311
+ isDirty = false;
55312
+ constructor(get) {
55313
+ super(get);
55314
+ this.model.selection.observe(this, {
55315
+ handleEvent: this.handleEvent.bind(this),
55316
+ });
55317
+ }
55318
+ handle(cmd) {
55319
+ if (invalidateEvaluationCommands.has(cmd.type) ||
55320
+ (cmd.type === "UPDATE_CELL" && "content" in cmd)) {
55321
+ this.isDirty = true;
55322
+ }
55323
+ switch (cmd.type) {
55324
+ case "HIDE_COLUMNS_ROWS":
55325
+ case "UNHIDE_COLUMNS_ROWS":
55326
+ case "GROUP_HEADERS":
55327
+ case "UNGROUP_HEADERS":
55328
+ case "ACTIVATE_SHEET":
55329
+ case "ACTIVATE_NEXT_SHEET":
55330
+ case "ACTIVATE_PREVIOUS_SHEET":
55331
+ case "EVALUATE_CELLS":
55332
+ case "UNDO":
55333
+ case "REDO":
55334
+ this.isDirty = true;
55335
+ }
55336
+ }
55337
+ finalize() {
55338
+ if (this.isDirty) {
55339
+ this.isDirty = false;
55340
+ this.statisticFnResults = this._computeStatisticFnResults();
55341
+ }
55342
+ }
55343
+ handleEvent() {
55344
+ if (this.getters.isGridSelectionActive()) {
55345
+ this.statisticFnResults = this._computeStatisticFnResults();
55346
+ }
55347
+ }
55348
+ _computeStatisticFnResults() {
55349
+ const getters = this.getters;
55350
+ const sheetId = getters.getActiveSheetId();
55351
+ const cells = new Set();
55352
+ const zones = getters.getSelectedZones();
55353
+ for (const zone of zones) {
55354
+ for (let col = zone.left; col <= zone.right; col++) {
55355
+ for (let row = zone.top; row <= zone.bottom; row++) {
55356
+ if (getters.isRowHidden(sheetId, row) || getters.isColHidden(sheetId, col)) {
55357
+ continue; // Skip hidden cells
55358
+ }
55359
+ const evaluatedCell = getters.getEvaluatedCell({ sheetId, col, row });
55360
+ if (evaluatedCell.type !== CellValueType.empty) {
55361
+ cells.add(evaluatedCell);
55362
+ }
55363
+ }
55364
+ }
55365
+ }
55366
+ const locale = getters.getLocale();
55367
+ let statisticFnResults = {};
55368
+ const cellsArray = [...cells];
55369
+ for (let fn of selectionStatisticFunctions) {
55370
+ // We don't want to display statistical information when there is no interest:
55371
+ // We set the statistical result to undefined if the data handled by the selection
55372
+ // does not match the data handled by the function.
55373
+ // Ex: if there are only texts in the selection, we prefer that the SUM result
55374
+ // be displayed as undefined rather than 0.
55375
+ let fnResult = undefined;
55376
+ const evaluatedCells = cellsArray.filter((c) => fn.types.includes(c.type));
55377
+ if (evaluatedCells.length) {
55378
+ fnResult = fn.compute(evaluatedCells, locale);
55379
+ }
55380
+ statisticFnResults[fn.name] = fnResult;
55381
+ }
55382
+ return statisticFnResults;
55383
+ }
55384
+ }
55385
+
54734
55386
  // -----------------------------------------------------------------------------
54735
55387
  // SpreadSheet
54736
55388
  // -----------------------------------------------------------------------------
@@ -54746,40 +55398,38 @@ css /* scss */ `
54746
55398
  }
54747
55399
  `;
54748
55400
  class BottomBarStatistic extends Component {
54749
- static template = "o-spreadsheet-BottomBarStatisic";
55401
+ static template = "o-spreadsheet-BottomBarStatistic";
54750
55402
  static props = {
54751
55403
  openContextMenu: Function,
54752
55404
  closeContextMenu: Function,
54753
55405
  };
54754
55406
  static components = { Ripple };
54755
55407
  selectedStatisticFn = "";
54756
- statisticFnResults = {};
55408
+ store;
54757
55409
  setup() {
54758
- this.statisticFnResults = this.env.model.getters.getStatisticFnResults();
55410
+ this.store = useStore(AggregateStatisticsStore);
54759
55411
  onWillUpdateProps(() => {
54760
- const newStatisticFnResults = this.env.model.getters.getStatisticFnResults();
54761
- if (!deepEquals(newStatisticFnResults, this.statisticFnResults)) {
55412
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54762
55413
  this.props.closeContextMenu();
54763
55414
  }
54764
- this.statisticFnResults = newStatisticFnResults;
54765
55415
  });
54766
55416
  }
54767
55417
  getSelectedStatistic() {
54768
55418
  // don't display button if no function has a result
54769
- if (Object.values(this.statisticFnResults).every((result) => result === undefined)) {
55419
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54770
55420
  return undefined;
54771
55421
  }
54772
55422
  if (this.selectedStatisticFn === "") {
54773
- this.selectedStatisticFn = Object.keys(this.statisticFnResults)[0];
55423
+ this.selectedStatisticFn = Object.keys(this.store.statisticFnResults)[0];
54774
55424
  }
54775
- return this.getComposedFnName(this.selectedStatisticFn, this.statisticFnResults[this.selectedStatisticFn]);
55425
+ return this.getComposedFnName(this.selectedStatisticFn);
54776
55426
  }
54777
55427
  listSelectionStatistics(ev) {
54778
55428
  const registry = new MenuItemRegistry();
54779
55429
  let i = 0;
54780
- for (let [fnName, fnValue] of Object.entries(this.statisticFnResults)) {
55430
+ for (let [fnName] of Object.entries(this.store.statisticFnResults)) {
54781
55431
  registry.add(fnName, {
54782
- name: this.getComposedFnName(fnName, fnValue),
55432
+ name: () => this.getComposedFnName(fnName),
54783
55433
  sequence: i,
54784
55434
  isReadonlyAllowed: true,
54785
55435
  execute: () => {
@@ -54792,8 +55442,9 @@ class BottomBarStatistic extends Component {
54792
55442
  const { top, left, width } = target.getBoundingClientRect();
54793
55443
  this.props.openContextMenu(left + width, top, registry);
54794
55444
  }
54795
- getComposedFnName(fnName, fnValue) {
55445
+ getComposedFnName(fnName) {
54796
55446
  const locale = this.env.model.getters.getLocale();
55447
+ const fnValue = this.store.statisticFnResults[fnName];
54797
55448
  return fnName + ": " + (fnValue !== undefined ? formatValue(fnValue, { locale }) : "__");
54798
55449
  }
54799
55450
  }
@@ -54902,10 +55553,14 @@ class BottomBar extends Component {
54902
55553
  name: sheet.name,
54903
55554
  sequence: i,
54904
55555
  isReadonlyAllowed: true,
54905
- textColor: sheet.isVisible ? undefined : "grey",
55556
+ textColor: sheet.isVisible ? undefined : "#808080",
54906
55557
  execute: (env) => {
55558
+ if (!this.env.model.getters.isSheetVisible(sheetId)) {
55559
+ this.env.model.dispatch("SHOW_SHEET", { sheetId });
55560
+ }
54907
55561
  env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: from, sheetIdTo: sheetId });
54908
55562
  },
55563
+ isEnabled: (env) => (env.model.getters.isReadonly() ? sheet.isVisible : true),
54909
55564
  });
54910
55565
  i++;
54911
55566
  }
@@ -54978,7 +55633,7 @@ class BottomBar extends Component {
54978
55633
  this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
54979
55634
  }
54980
55635
  onSheetMouseDown(sheetId, event) {
54981
- if (event.button !== 0)
55636
+ if (event.button !== 0 || this.env.model.getters.isReadonly())
54982
55637
  return;
54983
55638
  this.closeMenu();
54984
55639
  const visibleSheets = this.getVisibleSheets();
@@ -55014,7 +55669,7 @@ class BottomBar extends Component {
55014
55669
  .map((sheetEl) => sheetEl.getBoundingClientRect())
55015
55670
  .map((rect) => ({
55016
55671
  x: rect.x,
55017
- width: rect.width - 1,
55672
+ width: rect.width - 1, // -1 to compensate negative margin
55018
55673
  y: rect.y,
55019
55674
  height: rect.height,
55020
55675
  }));
@@ -55052,7 +55707,6 @@ class SpreadsheetDashboard extends Component {
55052
55707
  Popover,
55053
55708
  VerticalScrollBar,
55054
55709
  HorizontalScrollBar,
55055
- FilterIconsOverlay,
55056
55710
  };
55057
55711
  cellPopovers;
55058
55712
  onMouseWheel;
@@ -55241,7 +55895,7 @@ class RowGroup extends AbstractHeaderGroup {
55241
55895
  }
55242
55896
  return cssPropertiesToCss({
55243
55897
  top: `${groupBox.headerRect.height / 2}px`,
55244
- left: `calc(50% - 1px)`,
55898
+ left: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55245
55899
  width: `30%`,
55246
55900
  height: `calc(100% - ${groupBox.headerRect.height / 2}px)`,
55247
55901
  "border-left": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
@@ -55293,7 +55947,7 @@ class ColGroup extends AbstractHeaderGroup {
55293
55947
  return "";
55294
55948
  }
55295
55949
  return cssPropertiesToCss({
55296
- top: `calc(50% - 1px)`,
55950
+ top: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55297
55951
  left: `${groupBox.headerRect.width / 2}px`,
55298
55952
  width: `calc(100% - ${groupBox.headerRect.width / 2}px)`,
55299
55953
  height: `30%`,
@@ -55939,6 +56593,13 @@ class TopBarComposer extends Component {
55939
56593
  "border-color": SELECTION_BORDER_COLOR,
55940
56594
  });
55941
56595
  }
56596
+ get delimitation() {
56597
+ const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
56598
+ return {
56599
+ width,
56600
+ height,
56601
+ };
56602
+ }
55942
56603
  onFocus(selection) {
55943
56604
  this.composerFocusStore.focusTopBarComposer(selection);
55944
56605
  }
@@ -56384,7 +57045,7 @@ css /* scss */ `
56384
57045
  }
56385
57046
  .o-disabled {
56386
57047
  opacity: 0.4;
56387
- pointer: default;
57048
+ cursor: default;
56388
57049
  pointer-events: none;
56389
57050
  }
56390
57051
 
@@ -56550,7 +57211,7 @@ css /* scss */ `
56550
57211
 
56551
57212
  .o-number-input {
56552
57213
  /* Remove number input arrows */
56553
- -moz-appearance: textfield;
57214
+ appearance: textfield;
56554
57215
  &::-webkit-outer-spin-button,
56555
57216
  &::-webkit-inner-spin-button {
56556
57217
  -webkit-appearance: none;
@@ -56593,6 +57254,7 @@ class Spreadsheet extends Component {
56593
57254
  this.notificationStore = useStore(NotificationStore);
56594
57255
  this.composerFocusStore = useStore(ComposerFocusStore);
56595
57256
  this.sidePanel = useStore(SidePanelStore);
57257
+ useStore(ArrayFormulaHighlight);
56596
57258
  this.keyDownMapping = {
56597
57259
  "CTRL+H": () => this.sidePanel.toggle("FindAndReplace", {}),
56598
57260
  "CTRL+F": () => this.sidePanel.toggle("FindAndReplace", {}),
@@ -56695,7 +57357,7 @@ class Spreadsheet extends Component {
56695
57357
  const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
56696
57358
  const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;
56697
57359
  return cssPropertiesToCss({
56698
- "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`,
57360
+ "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`, // +2: margins
56699
57361
  "grid-template-rows": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,
56700
57362
  });
56701
57363
  }
@@ -57427,14 +58089,6 @@ class SelectiveHistory {
57427
58089
  this.revertBefore(operationId);
57428
58090
  this.tree.drop(operationId);
57429
58091
  }
57430
- getRevertedExecution() {
57431
- const data = [];
57432
- const operations = this.tree.revertedExecution(this.HEAD_BRANCH);
57433
- for (const { operation } of operations) {
57434
- data.push(operation.data);
57435
- }
57436
- return data;
57437
- }
57438
58092
  /**
57439
58093
  * Revert the state as it was *before* the given operation was executed.
57440
58094
  */
@@ -58223,9 +58877,15 @@ function createChart(chart, chartSheetIndex, data) {
58223
58877
  case "bar":
58224
58878
  plot = addBarChart(chart.data);
58225
58879
  break;
58880
+ case "combo":
58881
+ plot = addComboChart(chart.data);
58882
+ break;
58226
58883
  case "line":
58227
58884
  plot = addLineChart(chart.data);
58228
58885
  break;
58886
+ case "scatter":
58887
+ plot = addScatterChart(chart.data);
58888
+ break;
58229
58889
  case "pie":
58230
58890
  plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });
58231
58891
  break;
@@ -58385,6 +59045,79 @@ function addBarChart(chart) {
58385
59045
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58386
59046
  `;
58387
59047
  }
59048
+ function addComboChart(chart) {
59049
+ // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)
59050
+ // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB
59051
+ // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB
59052
+ //
59053
+ // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.
59054
+ // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage
59055
+ const colors = new ChartColors();
59056
+ const dataSetsNodes = [];
59057
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
59058
+ const color = toXlsxHexColor(colors.next());
59059
+ const dataShapeProperty = shapeProperty({
59060
+ backgroundColor: color,
59061
+ line: { color },
59062
+ });
59063
+ dataSetsNodes.push(dsIndex === "0"
59064
+ ? escapeXml /*xml*/ `
59065
+ <c:ser>
59066
+ <c:idx val="${dsIndex}"/>
59067
+ <c:order val="${dsIndex}"/>
59068
+ ${dataset.label ? escapeXml /*xml*/ `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
59069
+ ${dataShapeProperty}
59070
+ ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
59071
+ <c:val> <!-- x-coordinate values -->
59072
+ ${numberRef(dataset.range)}
59073
+ </c:val>
59074
+ </c:ser>
59075
+ `
59076
+ : escapeXml /*xml*/ `
59077
+ <c:ser>
59078
+ <c:idx val="${dsIndex}"/>
59079
+ <c:order val="${dsIndex}"/>
59080
+ <c:smooth val="0"/>
59081
+ <c:marker>
59082
+ <c:symbol val="circle" />
59083
+ <c:size val="5"/>
59084
+ </c:marker>
59085
+ ${dataset.label ? escapeXml `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
59086
+ ${dataShapeProperty}
59087
+ ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
59088
+ <c:val> <!-- x-coordinate values -->
59089
+ ${numberRef(dataset.range)}
59090
+ </c:val>
59091
+ </c:ser>
59092
+ `);
59093
+ }
59094
+ // Excel does not support this feature
59095
+ const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
59096
+ const overlap = chart.stacked ? 100 : -20;
59097
+ return escapeXml /*xml*/ `
59098
+ <c:barChart>
59099
+ <c:barDir val="col"/>
59100
+ <c:grouping val="clustered"/>
59101
+ <c:overlap val="${overlap}"/>
59102
+ <c:gapWidth val="70"/>
59103
+ <!-- each data marker in the series does not have a different color -->
59104
+ <c:varyColors val="0"/>
59105
+ ${dataSetsNodes[0]}
59106
+ <c:axId val="${catAxId}" />
59107
+ <c:axId val="${valAxId}" />
59108
+ </c:barChart>
59109
+ <c:lineChart>
59110
+ <c:grouping val="standard"/>
59111
+ <!-- each data marker in the series does not have a different color -->
59112
+ <c:varyColors val="0"/>
59113
+ ${joinXmlNodes(dataSetsNodes.slice(1))}
59114
+ <c:axId val="${catAxId}" />
59115
+ <c:axId val="${valAxId}" />
59116
+ </c:lineChart>
59117
+ ${addAx("b", "c:catAx", catAxId, valAxId, { fontColor: chart.fontColor })}
59118
+ ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
59119
+ `;
59120
+ }
58388
59121
  function addLineChart(chart) {
58389
59122
  const colors = new ChartColors();
58390
59123
  const dataSetsNodes = [];
@@ -58430,9 +59163,55 @@ function addLineChart(chart) {
58430
59163
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58431
59164
  `;
58432
59165
  }
59166
+ function addScatterChart(chart) {
59167
+ const colors = new ChartColors();
59168
+ const dataSetsNodes = [];
59169
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
59170
+ dataSetsNodes.push(escapeXml /*xml*/ `
59171
+ <c:ser>
59172
+ <c:idx val="${dsIndex}"/>
59173
+ <c:order val="${dsIndex}"/>
59174
+ <c:smooth val="0"/>
59175
+ <c:spPr>
59176
+ <a:ln w="19050" cap="rnd">
59177
+ <a:noFill/>
59178
+ <a:round/>
59179
+ </a:ln>
59180
+ <a:effectLst/>
59181
+ </c:spPr>
59182
+ <c:marker>
59183
+ <c:symbol val="circle" />
59184
+ <c:size val="5"/>
59185
+ ${shapeProperty({ backgroundColor: toXlsxHexColor(colors.next()) })}
59186
+ </c:marker>
59187
+ ${chart.labelRange
59188
+ ? escapeXml /*xml*/ `<c:xVal> <!-- x-coordinate values -->
59189
+ ${numberRef(chart.labelRange)}
59190
+ </c:xVal>`
59191
+ : ""}
59192
+ <c:yVal> <!-- y-coordinate values -->
59193
+ ${numberRef(dataset.range)}
59194
+ </c:yVal>
59195
+ </c:ser>
59196
+ `);
59197
+ }
59198
+ const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
59199
+ return escapeXml /*xml*/ `
59200
+ <c:scatterChart>
59201
+ <!-- each data marker in the series does not have a different color -->
59202
+ <c:varyColors val="0"/>
59203
+ <c:scatterStyle val="lineMarker"/>
59204
+ ${joinXmlNodes(dataSetsNodes)}
59205
+ <c:axId val="${catAxId}" />
59206
+ <c:axId val="${valAxId}" />
59207
+ </c:scatterChart>
59208
+ ${addAx("b", "c:valAx", catAxId, valAxId, { fontColor: chart.fontColor })}
59209
+ ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
59210
+ `;
59211
+ }
58433
59212
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
58434
59213
  const colors = new ChartColors();
58435
- const maxLength = Math.max(...chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
59214
+ const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
58436
59215
  const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
58437
59216
  const dataSetsNodes = [];
58438
59217
  for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
@@ -58570,8 +59349,7 @@ function addContent(content, sharedStrings, forceString = false) {
58570
59349
  attrs.push(["t", "b"]);
58571
59350
  }
58572
59351
  else if (forceString || !isNumber(value, DEFAULT_LOCALE)) {
58573
- const { id } = pushElement(content, sharedStrings);
58574
- value = id.toString();
59352
+ value = pushElement(content, sharedStrings);
58575
59353
  attrs.push(["t", "s"]);
58576
59354
  }
58577
59355
  return { attrs, node: escapeXml /*xml*/ `<v>${value}</v>` };
@@ -58696,8 +59474,7 @@ function addCellIsRule(cf, rule, dxfs) {
58696
59474
  if (rule.style.fillColor) {
58697
59475
  dxf.fill = { fgColor: { rgb: rule.style.fillColor } };
58698
59476
  }
58699
- const { id } = pushElement(dxf, dxfs);
58700
- ruleAttributes.push(["dxfId", id]);
59477
+ ruleAttributes.push(["dxfId", pushElement(dxf, dxfs)]);
58701
59478
  return escapeXml /*xml*/ `
58702
59479
  <conditionalFormatting sqref="${cf.ranges.join(" ")}">
58703
59480
  <cfRule ${formatAttributes(ruleAttributes)}>
@@ -59310,7 +60087,7 @@ function addTableColumns(table, sheetData) {
59310
60087
  const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
59311
60088
  const colName = sheetData.cells[colHeaderXc]?.content || `col${i}`;
59312
60089
  const colAttributes = [
59313
- ["id", i + 1],
60090
+ ["id", i + 1], // id cannot be 0
59314
60091
  ["name", colName],
59315
60092
  ];
59316
60093
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
@@ -59526,8 +60303,10 @@ function addSheetViews(sheet) {
59526
60303
  * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
59527
60304
  */
59528
60305
  function getXLSX(data) {
60306
+ data = fixLengthySheetNames(data);
60307
+ data = purgeSingleRowTables(data);
59529
60308
  const files = [];
59530
- const construct = getDefaultXLSXStructure();
60309
+ const construct = getDefaultXLSXStructure(data);
59531
60310
  files.push(createWorkbook(data, construct));
59532
60311
  files.push(...createWorksheets(data, construct));
59533
60312
  files.push(createStylesSheet(construct));
@@ -59773,6 +60552,50 @@ function createRelRoot() {
59773
60552
  `;
59774
60553
  return createXMLFile(parseXML(xml), "_rels/.rels");
59775
60554
  }
60555
+ /**
60556
+ * Excel sheet names are maximum 31 characters while o-spreadsheet do not have this limit.
60557
+ * This method converts the sheet names to be within the 31 characters limit.
60558
+ * The cells/charts referencing this sheet will be updated accordingly.
60559
+ */
60560
+ function fixLengthySheetNames(data) {
60561
+ const nameMapping = {};
60562
+ const newNames = new Set();
60563
+ for (const sheet of data.sheets) {
60564
+ let newName = sheet.name.slice(0, 31);
60565
+ let i = 1;
60566
+ while (newNames.has(newName)) {
60567
+ newName = newName.slice(0, 31 - String(i).length) + i++;
60568
+ }
60569
+ newNames.add(newName);
60570
+ if (newName !== sheet.name) {
60571
+ nameMapping[sheet.name] = newName;
60572
+ sheet.name = newName;
60573
+ }
60574
+ }
60575
+ if (!Object.keys(nameMapping).length) {
60576
+ return data;
60577
+ }
60578
+ const sheetWithNewNames = Object.keys(nameMapping).sort((a, b) => b.length - a.length);
60579
+ let stringifiedData = JSON.stringify(data);
60580
+ for (const sheetName of sheetWithNewNames) {
60581
+ const regex = new RegExp(`'?${escapeRegExp(sheetName)}'?!`, "g");
60582
+ stringifiedData = stringifiedData.replaceAll(regex, (match) => {
60583
+ const newName = nameMapping[sheetName];
60584
+ return match.replace(sheetName, newName);
60585
+ });
60586
+ }
60587
+ return JSON.parse(stringifiedData);
60588
+ }
60589
+ /** Excel files do not support tables with a single row the defined range
60590
+ * Since those tables are not really useful (no filtering/limited styling)
60591
+ * This function filters out all tables with a single row.
60592
+ */
60593
+ function purgeSingleRowTables(data) {
60594
+ for (const sheet of data.sheets) {
60595
+ sheet.tables = sheet.tables.filter((table) => zoneToDimension(toZone(table.range)).numberOfRows > 1);
60596
+ }
60597
+ return data;
60598
+ }
59776
60599
 
59777
60600
  var Status;
59778
60601
  (function (Status) {
@@ -60026,6 +60849,7 @@ class Model extends EventBus {
60026
60849
  stateObserver: this.state,
60027
60850
  range: this.range,
60028
60851
  dispatch: this.dispatchFromCorePlugin,
60852
+ canDispatch: this.canDispatch,
60029
60853
  uuidGenerator: this.uuidGenerator,
60030
60854
  custom: this.config.custom,
60031
60855
  external: this.config.external,
@@ -60036,6 +60860,7 @@ class Model extends EventBus {
60036
60860
  getters: this.getters,
60037
60861
  stateObserver: this.state,
60038
60862
  dispatch: this.dispatch,
60863
+ canDispatch: this.canDispatch,
60039
60864
  selection: this.selection,
60040
60865
  moveClient: this.session.move.bind(this.session),
60041
60866
  custom: this.config.custom,
@@ -60359,7 +61184,7 @@ const links = {
60359
61184
  const components = {
60360
61185
  Checkbox,
60361
61186
  Section,
60362
- ChartColor,
61187
+ RoundColorPicker,
60363
61188
  ChartDataSeries,
60364
61189
  ChartErrorSection,
60365
61190
  ChartLabelRange,
@@ -60421,6 +61246,6 @@ const constants = {
60421
61246
  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 };
60422
61247
 
60423
61248
 
60424
- __info__.version = "17.3.0-alpha.1";
60425
- __info__.date = "2024-03-25T09:43:36.072Z";
60426
- __info__.hash = "4095c41";
61249
+ __info__.version = "17.3.0-alpha.3";
61250
+ __info__.date = "2024-04-10T12:28:23.658Z";
61251
+ __info__.hash = "80b5056";