@odoo/o-spreadsheet 18.1.0-alpha.0 → 18.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,15 +2,121 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.1.0-alpha.0
6
- * @date 2024-09-25T13:17:49.636Z
7
- * @hash 288f0b7
5
+ * @version 18.1.0-alpha.2
6
+ * @date 2024-10-24T08:53:21.828Z
7
+ * @hash 2a01250
8
8
  */
9
9
 
10
10
  'use strict';
11
11
 
12
12
  var owl = require('@odoo/owl');
13
13
 
14
+ function createActions(menuItems) {
15
+ return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
16
+ }
17
+ let nextItemId = 1;
18
+ function createAction(item) {
19
+ const name = item.name;
20
+ const children = item.children;
21
+ const description = item.description;
22
+ const icon = item.icon;
23
+ const secondaryIcon = item.secondaryIcon;
24
+ const itemId = item.id || nextItemId++;
25
+ return {
26
+ id: itemId.toString(),
27
+ name: typeof name === "function" ? name : () => name,
28
+ isVisible: item.isVisible ? item.isVisible : () => true,
29
+ isEnabled: item.isEnabled ? item.isEnabled : () => true,
30
+ isActive: item.isActive,
31
+ execute: item.execute,
32
+ children: children
33
+ ? (env) => {
34
+ return children
35
+ .map((child) => (typeof child === "function" ? child(env) : child))
36
+ .flat()
37
+ .map(createAction);
38
+ }
39
+ : () => [],
40
+ isReadonlyAllowed: item.isReadonlyAllowed || false,
41
+ separator: item.separator || false,
42
+ icon: typeof icon === "function" ? icon : () => icon || "",
43
+ iconColor: item.iconColor,
44
+ secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
45
+ description: typeof description === "function" ? description : () => description || "",
46
+ textColor: item.textColor,
47
+ sequence: item.sequence || 0,
48
+ onStartHover: item.onStartHover,
49
+ onStopHover: item.onStopHover,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Registry
55
+ *
56
+ * The Registry class is basically just a mapping from a string key to an object.
57
+ * It is really not much more than an object. It is however useful for the
58
+ * following reasons:
59
+ *
60
+ * 1. it let us react and execute code when someone add something to the registry
61
+ * (for example, the FunctionRegistry subclass this for this purpose)
62
+ * 2. it throws an error when the get operation fails
63
+ * 3. it provides a chained API to add items to the registry.
64
+ */
65
+ class Registry {
66
+ content = {};
67
+ /**
68
+ * Add an item to the registry
69
+ *
70
+ * Note that this also returns the registry, so another add method call can
71
+ * be chained
72
+ */
73
+ add(key, value) {
74
+ this.content[key] = value;
75
+ return this;
76
+ }
77
+ /**
78
+ * Get an item from the registry
79
+ */
80
+ get(key) {
81
+ /**
82
+ * Note: key in {} is ~12 times slower than {}[key].
83
+ * So, we check the absence of key only when the direct access returns
84
+ * a falsy value. It's done to ensure that the registry can contains falsy values
85
+ */
86
+ const content = this.content[key];
87
+ if (!content) {
88
+ if (!(key in this.content)) {
89
+ throw new Error(`Cannot find ${key} in this registry!`);
90
+ }
91
+ }
92
+ return content;
93
+ }
94
+ /**
95
+ * Check if the key is already in the registry
96
+ */
97
+ contains(key) {
98
+ return key in this.content;
99
+ }
100
+ /**
101
+ * Get a list of all elements in the registry
102
+ */
103
+ getAll() {
104
+ return Object.values(this.content);
105
+ }
106
+ /**
107
+ * Get a list of all keys in the registry
108
+ */
109
+ getKeys() {
110
+ return Object.keys(this.content);
111
+ }
112
+ /**
113
+ * Remove an item from the registry
114
+ */
115
+ remove(key) {
116
+ delete this.content[key];
117
+ }
118
+ }
119
+
14
120
  const CANVAS_SHIFT = 0.5;
15
121
  // Colors
16
122
  const HIGHLIGHT_COLOR = "#37A850";
@@ -225,6 +331,7 @@ const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
225
331
  const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1";
226
332
  const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175";
227
333
  const LINE_FILL_TRANSPARENCY = 0.4;
334
+ const DEFAULT_WINDOW_SIZE = 2;
228
335
  // session
229
336
  const DEBOUNCE_TIME = 200;
230
337
  const MESSAGE_VERSION = 1;
@@ -270,7 +377,7 @@ const PIVOT_TABLE_CONFIG = {
270
377
  bandedRows: true,
271
378
  bandedColumns: false,
272
379
  styleId: "TableStyleMedium5",
273
- automaticAutofill: true,
380
+ automaticAutofill: false,
274
381
  };
275
382
  const DEFAULT_CURRENCY = {
276
383
  symbol: "$",
@@ -486,7 +593,7 @@ function buildSheetLink(sheetId) {
486
593
  */
487
594
  function parseSheetUrl(sheetLink) {
488
595
  if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) {
489
- return sheetLink.substr(O_SPREADSHEET_LINK_PREFIX.length);
596
+ return sheetLink.slice(O_SPREADSHEET_LINK_PREFIX.length);
490
597
  }
491
598
  throw new Error(`${sheetLink} is not a valid sheet link`);
492
599
  }
@@ -3051,8 +3158,7 @@ const getNumberRegex = memoize(function getNumberRegex(locale) {
3051
3158
  const p2 = pMinus + pNumber + pCurrencyFormat;
3052
3159
  const p3 = pCurrencyFormat + pMinus + pNumber;
3053
3160
  const pNumberExp = "^(?:(?:" + [p1, p2, p3].join(")|(?:") + "))$";
3054
- const numberRegexp = new RegExp(pNumberExp, "i");
3055
- return numberRegexp;
3161
+ return new RegExp(pNumberExp, "i");
3056
3162
  });
3057
3163
  /**
3058
3164
  * Return true if the argument is a "number string".
@@ -5726,8 +5832,7 @@ function computeCachedTextWidth(context, text) {
5726
5832
  textWidthCache[font] = {};
5727
5833
  }
5728
5834
  if (textWidthCache[font][text] === undefined) {
5729
- const textWidth = context.measureText(text).width;
5730
- textWidthCache[font][text] = textWidth;
5835
+ textWidthCache[font][text] = context.measureText(text).width;
5731
5836
  }
5732
5837
  return textWidthCache[font][text];
5733
5838
  }
@@ -5970,136 +6075,22 @@ function drawDecoratedText(context, text, position, underline = false, strikethr
5970
6075
  * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
5971
6076
  * */
5972
6077
  class UuidGenerator {
5973
- isFastIdStrategy = false;
5974
- fastIdStart = 0;
5975
- setIsFastStrategy(isFast) {
5976
- this.isFastIdStrategy = isFast;
5977
- }
5978
6078
  uuidv4() {
5979
- if (this.isFastIdStrategy) {
5980
- this.fastIdStart++;
5981
- return String(this.fastIdStart);
5982
- //@ts-ignore
5983
- }
5984
- else if (window.crypto && window.crypto.getRandomValues) {
6079
+ //@ts-ignore
6080
+ if (window.crypto && window.crypto.getRandomValues) {
5985
6081
  //@ts-ignore
5986
6082
  return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
5987
6083
  }
5988
6084
  else {
5989
6085
  // mainly for jest and other browsers that do not have the crypto functionality
5990
6086
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
5991
- var r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
6087
+ const r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
5992
6088
  return v.toString(16);
5993
6089
  });
5994
6090
  }
5995
6091
  }
5996
6092
  }
5997
6093
 
5998
- function createActions(menuItems) {
5999
- return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
6000
- }
6001
- const uuidGenerator$1 = new UuidGenerator();
6002
- function createAction(item) {
6003
- const name = item.name;
6004
- const children = item.children;
6005
- const description = item.description;
6006
- const icon = item.icon;
6007
- const secondaryIcon = item.secondaryIcon;
6008
- return {
6009
- id: item.id || uuidGenerator$1.uuidv4(),
6010
- name: typeof name === "function" ? name : () => name,
6011
- isVisible: item.isVisible ? item.isVisible : () => true,
6012
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
6013
- isActive: item.isActive,
6014
- execute: item.execute,
6015
- children: children
6016
- ? (env) => {
6017
- return children
6018
- .map((child) => (typeof child === "function" ? child(env) : child))
6019
- .flat()
6020
- .map(createAction);
6021
- }
6022
- : () => [],
6023
- isReadonlyAllowed: item.isReadonlyAllowed || false,
6024
- separator: item.separator || false,
6025
- icon: typeof icon === "function" ? icon : () => icon || "",
6026
- iconColor: item.iconColor,
6027
- secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
6028
- description: typeof description === "function" ? description : () => description || "",
6029
- textColor: item.textColor,
6030
- sequence: item.sequence || 0,
6031
- onStartHover: item.onStartHover,
6032
- onStopHover: item.onStopHover,
6033
- };
6034
- }
6035
-
6036
- /**
6037
- * Registry
6038
- *
6039
- * The Registry class is basically just a mapping from a string key to an object.
6040
- * It is really not much more than an object. It is however useful for the
6041
- * following reasons:
6042
- *
6043
- * 1. it let us react and execute code when someone add something to the registry
6044
- * (for example, the FunctionRegistry subclass this for this purpose)
6045
- * 2. it throws an error when the get operation fails
6046
- * 3. it provides a chained API to add items to the registry.
6047
- */
6048
- class Registry {
6049
- content = {};
6050
- /**
6051
- * Add an item to the registry
6052
- *
6053
- * Note that this also returns the registry, so another add method call can
6054
- * be chained
6055
- */
6056
- add(key, value) {
6057
- this.content[key] = value;
6058
- return this;
6059
- }
6060
- /**
6061
- * Get an item from the registry
6062
- */
6063
- get(key) {
6064
- /**
6065
- * Note: key in {} is ~12 times slower than {}[key].
6066
- * So, we check the absence of key only when the direct access returns
6067
- * a falsy value. It's done to ensure that the registry can contains falsy values
6068
- */
6069
- const content = this.content[key];
6070
- if (!content) {
6071
- if (!(key in this.content)) {
6072
- throw new Error(`Cannot find ${key} in this registry!`);
6073
- }
6074
- }
6075
- return content;
6076
- }
6077
- /**
6078
- * Check if the key is already in the registry
6079
- */
6080
- contains(key) {
6081
- return key in this.content;
6082
- }
6083
- /**
6084
- * Get a list of all elements in the registry
6085
- */
6086
- getAll() {
6087
- return Object.values(this.content);
6088
- }
6089
- /**
6090
- * Get a list of all keys in the registry
6091
- */
6092
- getKeys() {
6093
- return Object.keys(this.content);
6094
- }
6095
- /**
6096
- * Remove an item from the registry
6097
- */
6098
- remove(key) {
6099
- delete this.content[key];
6100
- }
6101
- }
6102
-
6103
6094
  function getClipboardDataPositions(sheetId, zones) {
6104
6095
  const lefts = new Set(zones.map((z) => z.left));
6105
6096
  const rights = new Set(zones.map((z) => z.right));
@@ -6590,10 +6581,9 @@ function localizeNumberLiteral(literal, locale) {
6590
6581
  return literal;
6591
6582
  }
6592
6583
  const decimalNumberRegex = getDecimalNumberRegex(DEFAULT_LOCALE);
6593
- const localized = literal.replace(decimalNumberRegex, (match) => {
6584
+ return literal.replace(decimalNumberRegex, (match) => {
6594
6585
  return match.replace(".", locale.decimalSeparator);
6595
6586
  });
6596
- return localized;
6597
6587
  }
6598
6588
  /**
6599
6589
  * Change a literal string from its canonical form (en_US locale) to the given locale. Also convert date string.
@@ -7078,6 +7068,21 @@ function predictLinearValues(Y, X, newX, computeIntercept) {
7078
7068
  });
7079
7069
  return newY.length === newX.length ? newY : transposeMatrix(newY);
7080
7070
  }
7071
+ function getMovingAverageValues(dataset, windowSize = DEFAULT_WINDOW_SIZE) {
7072
+ const values = [];
7073
+ // Fill the starting values with null until we have a full window
7074
+ for (let i = 0; i < windowSize - 1; i++) {
7075
+ values.push(null);
7076
+ }
7077
+ for (let i = 0; i <= dataset.length - windowSize; i++) {
7078
+ let sum = 0;
7079
+ for (let j = i; j < i + windowSize; j++) {
7080
+ sum += dataset[j];
7081
+ }
7082
+ values.push(sum / windowSize);
7083
+ }
7084
+ return values;
7085
+ }
7081
7086
 
7082
7087
  const PREVIOUS_VALUE = "(previous)";
7083
7088
  const NEXT_VALUE = "(next)";
@@ -7613,8 +7618,7 @@ function getMaxObjectId(o) {
7613
7618
  return 0;
7614
7619
  }
7615
7620
  const nums = keys.map((id) => parseInt(id, 10));
7616
- const max = Math.max(...nums);
7617
- return max;
7621
+ return Math.max(...nums);
7618
7622
  }
7619
7623
  const ALL_PERIODS = {
7620
7624
  year: _t("Year"),
@@ -8454,31 +8458,30 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8454
8458
  for (let col of columnsIndexes) {
8455
8459
  const position = { col, row, sheetId };
8456
8460
  const table = this.getters.getTable(position);
8457
- if (!table || copiedTablesIds.has(table.id)) {
8461
+ if (!table) {
8458
8462
  tableCellsInRow.push({});
8459
8463
  continue;
8460
8464
  }
8461
8465
  const coreTable = this.getters.getCoreTable(position);
8462
8466
  const tableZone = coreTable?.range.zone;
8467
+ let copiedTable = undefined;
8463
8468
  // Copy whole table
8464
- if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
8465
- copiedTablesIds.add(coreTable.id);
8466
- const values = [];
8467
- for (const col of range(tableZone.left, tableZone.right + 1)) {
8468
- values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
8469
- }
8470
- tableCellsInRow.push({
8471
- table: {
8472
- range: coreTable.range.rangeData,
8473
- config: coreTable.config,
8474
- type: coreTable.type,
8475
- },
8476
- });
8477
- }
8478
- // Copy only style of cell
8479
- else if (table) {
8480
- tableCellsInRow.push({ style: this.getTableStyleToCopy(position) });
8469
+ if (!copiedTablesIds.has(table.id) &&
8470
+ coreTable &&
8471
+ tableZone &&
8472
+ zones.some((z) => isZoneInside(tableZone, z))) {
8473
+ copiedTablesIds.add(table.id);
8474
+ copiedTable = {
8475
+ range: coreTable.range.rangeData,
8476
+ config: coreTable.config,
8477
+ type: coreTable.type,
8478
+ };
8481
8479
  }
8480
+ tableCellsInRow.push({
8481
+ table: copiedTable,
8482
+ style: this.getTableStyleToCopy(position),
8483
+ isWholeTableCopied: copiedTablesIds.has(table.id),
8484
+ });
8482
8485
  }
8483
8486
  }
8484
8487
  return {
@@ -8559,11 +8562,14 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8559
8562
  tableType: tableCell.table.type,
8560
8563
  });
8561
8564
  }
8562
- // Do not paste table style if we're inside another table
8563
8565
  // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
8564
8566
  // dynamic tables are not yet computed
8565
- if (!this.getters.getCoreTable(position)) {
8566
- if (tableCell.style?.style && options?.pasteOption !== "asValue") {
8567
+ if (this.getters.getCoreTable(position) || options?.pasteOption === "asValue") {
8568
+ return;
8569
+ }
8570
+ if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||
8571
+ options?.pasteOption === "onlyFormat") {
8572
+ if (tableCell.style?.style) {
8567
8573
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
8568
8574
  }
8569
8575
  if (tableCell.style?.border) {
@@ -9245,11 +9251,10 @@ function getChartPositionAtCenterOfViewport(getters, chartSize) {
9245
9251
  const { x, y } = getters.getMainViewportCoordinates();
9246
9252
  const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
9247
9253
  const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
9248
- const position = {
9254
+ return {
9249
9255
  x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
9250
9256
  y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
9251
9257
  }; // Position at the center of the scrollable viewport
9252
- return position;
9253
9258
  }
9254
9259
  function getChartAxisTitleRuntime(design) {
9255
9260
  if (design?.title?.text) {
@@ -9283,6 +9288,49 @@ function getDefinedAxis(definition) {
9283
9288
  useLeftAxis ||= !useRightAxis;
9284
9289
  return { useLeftAxis, useRightAxis };
9285
9290
  }
9291
+ function getChartAxis(definition, position, type, options) {
9292
+ const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);
9293
+ if ((position === "left" && !useLeftAxis) || (position === "right" && !useRightAxis)) {
9294
+ return undefined;
9295
+ }
9296
+ const fontColor = chartFontColor(definition.background);
9297
+ let design;
9298
+ if (position === "bottom") {
9299
+ design = definition.axesDesign?.x;
9300
+ }
9301
+ else if (position === "left") {
9302
+ design = definition.axesDesign?.y;
9303
+ }
9304
+ else {
9305
+ design = definition.axesDesign?.y1;
9306
+ }
9307
+ if (type === "values") {
9308
+ const displayGridLines = position === "left" || (position === "right" && !useLeftAxis);
9309
+ return {
9310
+ position: position,
9311
+ title: getChartAxisTitleRuntime(design),
9312
+ grid: {
9313
+ display: displayGridLines,
9314
+ },
9315
+ beginAtZero: true,
9316
+ stacked: options?.stacked,
9317
+ ticks: {
9318
+ color: fontColor,
9319
+ callback: formatTickValue(options),
9320
+ },
9321
+ };
9322
+ }
9323
+ else {
9324
+ return {
9325
+ ticks: {
9326
+ padding: 5,
9327
+ color: fontColor,
9328
+ },
9329
+ stacked: options?.stacked,
9330
+ title: getChartAxisTitleRuntime(design),
9331
+ };
9332
+ }
9333
+ }
9286
9334
  function computeChartPadding({ displayTitle, displayLegend, }) {
9287
9335
  let top = 25;
9288
9336
  if (displayTitle) {
@@ -9323,7 +9371,7 @@ function getFullTrendingLineDataSet(dataset, config, data) {
9323
9371
  return {
9324
9372
  ...dataset,
9325
9373
  type: "line",
9326
- xAxisID: TREND_LINE_XAXIS_ID,
9374
+ xAxisID: config.type !== "trailingMovingAverage" ? TREND_LINE_XAXIS_ID : "x",
9327
9375
  label: dataset.label ? _t("Trend line for %s", dataset.label) : "",
9328
9376
  data,
9329
9377
  order: -1,
@@ -9365,10 +9413,19 @@ function interpolateData(config, values, labels, newLabels) {
9365
9413
  case "logarithmic": {
9366
9414
  return predictLinearValues([values], logM([labels]), logM([newLabels]), true)[0];
9367
9415
  }
9416
+ case "trailingMovingAverage": {
9417
+ return getMovingAverageValues(values, config.window);
9418
+ }
9368
9419
  default:
9369
9420
  return [];
9370
9421
  }
9371
9422
  }
9423
+ function formatChartDatasetValue(axisFormats, locale) {
9424
+ return (value, axisId) => {
9425
+ const format = axisId ? axisFormats?.[axisId] : undefined;
9426
+ return formatTickValue({ format, locale })(value);
9427
+ };
9428
+ }
9372
9429
  function formatTickValue(localeFormat) {
9373
9430
  return (value) => {
9374
9431
  value = Number(value);
@@ -9404,70 +9461,115 @@ const chartShowValuesPlugin = {
9404
9461
  ctx.save();
9405
9462
  ctx.textAlign = "center";
9406
9463
  ctx.textBaseline = "middle";
9407
- ctx.fillStyle = chartFontColor(options.background);
9408
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9409
- chart._metasets.forEach(function (dataset) {
9410
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9411
- return; // ignore trend lines
9412
- }
9413
- switch (dataset.type) {
9414
- case "doughnut":
9415
- case "pie": {
9416
- for (let i = 0; i < dataset._parsed.length; i++) {
9417
- const bar = dataset.data[i];
9418
- const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9419
- const midAngle = (startAngle + endAngle) / 2;
9420
- const midRadius = (innerRadius + outerRadius) / 2;
9421
- const x = bar.x + midRadius * Math.cos(midAngle);
9422
- const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9423
- ctx.fillStyle = chartFontColor(bar.options.backgroundColor);
9424
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9425
- const value = options.callback(dataset._parsed[i]);
9426
- ctx.strokeText(value, x, y);
9427
- ctx.fillText(value, x, y);
9428
- }
9429
- break;
9430
- }
9431
- case "bar":
9432
- case "line": {
9433
- const yOffset = dataset.type === "bar" && !options.horizontal ? 0 : 3;
9434
- for (let i = 0; i < dataset._parsed.length; i++) {
9435
- const point = dataset.data[i];
9436
- const value = options.horizontal ? dataset._parsed[i].x : dataset._parsed[i].y;
9437
- const displayedValue = options.callback(value - 0);
9438
- let xPosition = 0, yPosition = 0;
9439
- if (options.horizontal) {
9440
- yPosition = point.y;
9441
- if (value < 0) {
9442
- ctx.textAlign = "right";
9443
- xPosition = point.x - yOffset;
9444
- }
9445
- else {
9446
- ctx.textAlign = "left";
9447
- xPosition = point.x + yOffset;
9448
- }
9449
- }
9450
- else {
9451
- xPosition = point.x;
9452
- if (value < 0) {
9453
- ctx.textBaseline = "top";
9454
- yPosition = point.y + yOffset;
9455
- }
9456
- else {
9457
- ctx.textBaseline = "bottom";
9458
- yPosition = point.y - yOffset;
9459
- }
9460
- }
9461
- ctx.strokeText(displayedValue, xPosition, yPosition);
9462
- ctx.fillText(displayedValue, xPosition, yPosition);
9463
- }
9464
- break;
9465
- }
9466
- }
9467
- });
9464
+ ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
9465
+ switch (chart.config.type) {
9466
+ case "pie":
9467
+ case "doughnut":
9468
+ drawPieChartValues(chart, options, ctx);
9469
+ break;
9470
+ case "bar":
9471
+ case "line":
9472
+ options.horizontal
9473
+ ? drawHorizontalBarChartValues(chart, options, ctx)
9474
+ : drawLineOrBarChartValues(chart, options, ctx);
9475
+ break;
9476
+ }
9468
9477
  ctx.restore();
9469
9478
  },
9470
9479
  };
9480
+ function drawTextWithBackground(text, x, y, ctx) {
9481
+ ctx.lineWidth = 3; // Stroke the text with a big lineWidth width to have some kind of background
9482
+ ctx.strokeText(text, x, y);
9483
+ ctx.lineWidth = 1;
9484
+ ctx.fillText(text, x, y);
9485
+ }
9486
+ function drawLineOrBarChartValues(chart, options, ctx) {
9487
+ const yMax = chart.chartArea.bottom;
9488
+ const yMin = chart.chartArea.top;
9489
+ const textsPositions = {};
9490
+ for (const dataset of chart._metasets) {
9491
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9492
+ return; // ignore trend lines
9493
+ }
9494
+ for (let i = 0; i < dataset._parsed.length; i++) {
9495
+ const value = dataset._parsed[i].y;
9496
+ const displayValue = options.callback(value - 0, dataset.yAxisID);
9497
+ const point = dataset.data[i];
9498
+ const xPosition = point.x;
9499
+ let yPosition = 0;
9500
+ if (chart.config.type === "line") {
9501
+ yPosition = point.y - 10;
9502
+ }
9503
+ else {
9504
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
9505
+ }
9506
+ yPosition = Math.min(yPosition, yMax);
9507
+ yPosition = Math.max(yPosition, yMin);
9508
+ // Avoid overlapping texts with same X
9509
+ if (!textsPositions[xPosition]) {
9510
+ textsPositions[xPosition] = [];
9511
+ }
9512
+ for (const otherPosition of textsPositions[xPosition] || []) {
9513
+ if (Math.abs(otherPosition - yPosition) < 13) {
9514
+ yPosition = otherPosition - 13;
9515
+ }
9516
+ }
9517
+ textsPositions[xPosition].push(yPosition);
9518
+ ctx.fillStyle = point.options.backgroundColor;
9519
+ ctx.strokeStyle = options.background || "#ffffff";
9520
+ drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
9521
+ }
9522
+ }
9523
+ }
9524
+ function drawHorizontalBarChartValues(chart, options, ctx) {
9525
+ const xMax = chart.chartArea.right;
9526
+ const xMin = chart.chartArea.left;
9527
+ const textsPositions = {};
9528
+ for (const dataset of chart._metasets) {
9529
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9530
+ return; // ignore trend lines
9531
+ }
9532
+ for (let i = 0; i < dataset._parsed.length; i++) {
9533
+ const value = dataset._parsed[i].x;
9534
+ const displayValue = options.callback(value - 0, dataset.xAxisID);
9535
+ const point = dataset.data[i];
9536
+ const yPosition = point.y;
9537
+ let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
9538
+ xPosition = Math.min(xPosition, xMax);
9539
+ xPosition = Math.max(xPosition, xMin);
9540
+ // Avoid overlapping texts with same Y
9541
+ if (!textsPositions[yPosition]) {
9542
+ textsPositions[yPosition] = [];
9543
+ }
9544
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
9545
+ for (const otherPosition of textsPositions[yPosition]) {
9546
+ if (Math.abs(otherPosition - xPosition) < textWidth) {
9547
+ xPosition = otherPosition + textWidth + 3;
9548
+ }
9549
+ }
9550
+ textsPositions[yPosition].push(xPosition);
9551
+ ctx.fillStyle = point.options.backgroundColor;
9552
+ ctx.strokeStyle = options.background || "#ffffff";
9553
+ drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
9554
+ }
9555
+ }
9556
+ }
9557
+ function drawPieChartValues(chart, options, ctx) {
9558
+ for (const dataset of chart._metasets) {
9559
+ for (let i = 0; i < dataset._parsed.length; i++) {
9560
+ const bar = dataset.data[i];
9561
+ const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9562
+ const midAngle = (startAngle + endAngle) / 2;
9563
+ const midRadius = (innerRadius + outerRadius) / 2;
9564
+ const x = bar.x + midRadius * Math.cos(midAngle);
9565
+ const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9566
+ ctx.fillStyle = chartFontColor(options.background);
9567
+ ctx.strokeStyle = options.background || "#ffffff";
9568
+ const value = options.callback(dataset._parsed[i]);
9569
+ drawTextWithBackground(value, x, y, ctx);
9570
+ }
9571
+ }
9572
+ }
9471
9573
 
9472
9574
  /** This is a chartJS plugin that will draw connector lines between the bars of a Waterfall chart */
9473
9575
  const waterfallLinesPlugin = {
@@ -10245,8 +10347,7 @@ function getHtmlContentFromPattern(pattern, value, highlightColor, className) {
10245
10347
  value = value.slice(index + 1);
10246
10348
  }
10247
10349
  pendingHtmlContent.push({ value });
10248
- const htmlContent = pendingHtmlContent.filter((content) => content.value);
10249
- return htmlContent;
10350
+ return pendingHtmlContent.filter((content) => content.value);
10250
10351
  }
10251
10352
 
10252
10353
  //------------------------------------------------------------------------------
@@ -10650,7 +10751,7 @@ const MINVERSE = {
10650
10751
  assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix);
10651
10752
  const { inverted } = invertMatrix(_matrix);
10652
10753
  if (!inverted) {
10653
- throw new EvaluationError(_t("The matrix is not invertible."));
10754
+ return new EvaluationError(_t("The matrix is not invertible."));
10654
10755
  }
10655
10756
  return inverted;
10656
10757
  },
@@ -10729,7 +10830,7 @@ function getSumXAndY(arrayX, arrayY, cb) {
10729
10830
  }
10730
10831
  }
10731
10832
  if (!validPairFound) {
10732
- throw new EvaluationError(_t("The arguments array_x and array_y must contain at least one pair of numbers."));
10833
+ return new EvaluationError(_t("The arguments array_x and array_y must contain at least one pair of numbers."));
10733
10834
  }
10734
10835
  return result;
10735
10836
  }
@@ -10810,7 +10911,7 @@ const TOCOL = {
10810
10911
  .flat()
10811
10912
  .filter(shouldKeepValue(_ignore));
10812
10913
  if (result.length === 0) {
10813
- throw new NotAvailableError(_t("No results for the given arguments of TOCOL."));
10914
+ return new NotAvailableError(_t("No results for the given arguments of TOCOL."));
10814
10915
  }
10815
10916
  return [result];
10816
10917
  },
@@ -10831,7 +10932,7 @@ const TOROW = {
10831
10932
  .filter(shouldKeepValue(_ignore))
10832
10933
  .map((item) => [item]);
10833
10934
  if (result.length === 0 || result[0].length === 0) {
10834
- throw new NotAvailableError(_t("No results for the given arguments of TOROW."));
10935
+ return new NotAvailableError(_t("No results for the given arguments of TOROW."));
10835
10936
  }
10836
10937
  return result;
10837
10938
  },
@@ -11391,7 +11492,7 @@ const DECIMAL = {
11391
11492
  * Return error if 'value' is positive.
11392
11493
  * Remove '-?' in the next regex to catch this error.
11393
11494
  */
11394
- assert(() => !!DECIMAL_REPRESENTATION.test(_value), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
11495
+ assert(() => DECIMAL_REPRESENTATION.test(_value), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
11395
11496
  const deci = parseInt(_value, _base);
11396
11497
  assert(() => !isNaN(deci), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
11397
11498
  return deci;
@@ -11659,7 +11760,7 @@ const PRODUCT = {
11659
11760
  count += 1;
11660
11761
  }
11661
11762
  if (isEvaluationError(f)) {
11662
- throw j;
11763
+ return j;
11663
11764
  }
11664
11765
  }
11665
11766
  }
@@ -12162,9 +12263,8 @@ function covariance(dataY, dataX, isSample) {
12162
12263
  }
12163
12264
  function variance(args, isSample, textAs0, locale) {
12164
12265
  let count = 0;
12165
- let sum = 0;
12166
12266
  const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers;
12167
- sum = reduceFunction(args, (acc, a) => {
12267
+ const sum = reduceFunction(args, (acc, a) => {
12168
12268
  count += 1;
12169
12269
  return acc + a;
12170
12270
  }, 0, locale);
@@ -12561,7 +12661,7 @@ const MATTHEWS = {
12561
12661
  const flatY = dataY.flat();
12562
12662
  assertSameNumberOfElements(flatX, flatY);
12563
12663
  if (flatX.length === 0) {
12564
- throw new EvaluationError(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters."));
12664
+ return new EvaluationError(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters."));
12565
12665
  }
12566
12666
  const n = flatX.length;
12567
12667
  let trueN = 0, trueP = 0, falseP = 0, falseN = 0;
@@ -13361,9 +13461,8 @@ function getMatchingCells(database, field, criteria, locale) {
13361
13461
  // 4 - return for each database row corresponding, the cells corresponding to the field parameter
13362
13462
  const fieldCol = database[index];
13363
13463
  // Example continuation:: fieldCol = ["C", "j", "k", 7]
13364
- const matchingCells = [...matchingRows].map((x) => fieldCol[x + 1]);
13365
13464
  // Example continuation:: matchingCells = ["j", 7]
13366
- return matchingCells;
13465
+ return [...matchingRows].map((x) => fieldCol[x + 1]);
13367
13466
  }
13368
13467
  const databaseArgs = [
13369
13468
  arg("database (range)", _t("The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.")),
@@ -14548,7 +14647,7 @@ const FILTER = {
14548
14647
  }
14549
14648
  }
14550
14649
  if (!result.length) {
14551
- throw new NotAvailableError(_t("No match found in FILTER evaluation"));
14650
+ return new NotAvailableError(_t("No match found in FILTER evaluation"));
14552
14651
  }
14553
14652
  return mode === "row" ? transposeMatrix(result) : result;
14554
14653
  },
@@ -17246,7 +17345,7 @@ function mapParentFunction(tokens) {
17246
17345
  argsTokens[argPosition].push({ value: token.value, type: token.type });
17247
17346
  }
17248
17347
  }
17249
- const res = tokens.map((token, i) => {
17348
+ return tokens.map((token, i) => {
17250
17349
  if (!["SPACE", "LEFT_PAREN"].includes(token.type)) {
17251
17350
  functionStarted = "";
17252
17351
  }
@@ -17284,7 +17383,6 @@ function mapParentFunction(tokens) {
17284
17383
  }
17285
17384
  return token;
17286
17385
  });
17287
- return res;
17288
17386
  }
17289
17387
  /**
17290
17388
  * Parse the list of tokens that compose the arguments of a function to
@@ -17740,7 +17838,7 @@ const IFS = {
17740
17838
  return result;
17741
17839
  }
17742
17840
  }
17743
- throw new EvaluationError(_t("No match."));
17841
+ return new EvaluationError(_t("No match."));
17744
17842
  },
17745
17843
  isExported: true,
17746
17844
  };
@@ -17843,7 +17941,7 @@ function assertDomainLength(domain) {
17843
17941
  throw new EvaluationError(_t("Function PIVOT takes an even number of arguments."));
17844
17942
  }
17845
17943
  }
17846
- function addPivotDependencies(evalContext, coreDefinition) {
17944
+ function addPivotDependencies(evalContext, coreDefinition, forMeasures) {
17847
17945
  //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
17848
17946
  const dependencies = [];
17849
17947
  if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
@@ -17855,7 +17953,7 @@ function addPivotDependencies(evalContext, coreDefinition) {
17855
17953
  }
17856
17954
  dependencies.push(range);
17857
17955
  }
17858
- for (const measure of coreDefinition.measures) {
17956
+ for (const measure of forMeasures) {
17859
17957
  if (measure.computedBy) {
17860
17958
  const formula = evalContext.getters.getMeasureCompiledFormula(measure);
17861
17959
  dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
@@ -17904,8 +18002,8 @@ const ADDRESS = {
17904
18002
  let cellReference;
17905
18003
  if (_useA1Notation) {
17906
18004
  const rangePart = {
17907
- rowFixed: [1, 2].includes(_absoluteRelativeMode) ? true : false,
17908
- colFixed: [1, 3].includes(_absoluteRelativeMode) ? true : false,
18005
+ rowFixed: [1, 2].includes(_absoluteRelativeMode),
18006
+ colFixed: [1, 3].includes(_absoluteRelativeMode),
17909
18007
  };
17910
18008
  cellReference = toXC(colNumber - 1, rowNumber - 1, rangePart);
17911
18009
  }
@@ -17931,7 +18029,7 @@ const COLUMN = {
17931
18029
  ],
17932
18030
  compute: function (cellReference) {
17933
18031
  if (isEvaluationError(cellReference?.value)) {
17934
- throw cellReference;
18032
+ return cellReference;
17935
18033
  }
17936
18034
  const column = cellReference === undefined
17937
18035
  ? this.__originCellPosition?.col
@@ -17949,7 +18047,7 @@ const COLUMNS = {
17949
18047
  args: [arg("range (meta)", _t("The range whose column count will be returned."))],
17950
18048
  compute: function (range) {
17951
18049
  if (isEvaluationError(range?.value)) {
17952
- throw range;
18050
+ return range;
17953
18051
  }
17954
18052
  const zone = toZone(range.value);
17955
18053
  return zone.right - zone.left + 1;
@@ -18029,11 +18127,11 @@ const INDIRECT = {
18029
18127
  compute: function (reference, useA1Notation = { value: true }) {
18030
18128
  let _reference = reference?.value?.toString();
18031
18129
  if (!_reference) {
18032
- throw new InvalidReferenceError(_t("Reference should be defined."));
18130
+ return new InvalidReferenceError(_t("Reference should be defined."));
18033
18131
  }
18034
18132
  const _useA1Notation = toBoolean(useA1Notation);
18035
18133
  if (!_useA1Notation) {
18036
- throw new EvaluationError(_t("R1C1 notation is not supported."));
18134
+ return new EvaluationError(_t("R1C1 notation is not supported."));
18037
18135
  }
18038
18136
  const sheetId = this.__originSheetId;
18039
18137
  const originPosition = this.__originCellPosition;
@@ -18045,7 +18143,7 @@ const INDIRECT = {
18045
18143
  }
18046
18144
  const range = this.getters.getRangeFromSheetXC(sheetId, _reference);
18047
18145
  if (range === undefined || range.invalidXc || range.invalidSheetName) {
18048
- throw new InvalidReferenceError();
18146
+ return new InvalidReferenceError();
18049
18147
  }
18050
18148
  if (originPosition) {
18051
18149
  this.addDependencies?.(originPosition, [range]);
@@ -18153,7 +18251,7 @@ const ROW = {
18153
18251
  ],
18154
18252
  compute: function (cellReference) {
18155
18253
  if (isEvaluationError(cellReference?.value)) {
18156
- throw cellReference;
18254
+ return cellReference;
18157
18255
  }
18158
18256
  const row = cellReference === undefined
18159
18257
  ? this.__originCellPosition?.row
@@ -18171,7 +18269,7 @@ const ROWS = {
18171
18269
  args: [arg("range (meta)", _t("The range whose row count will be returned."))],
18172
18270
  compute: function (range) {
18173
18271
  if (isEvaluationError(range?.value)) {
18174
- throw range;
18272
+ return range;
18175
18273
  }
18176
18274
  const zone = toZone(range.value);
18177
18275
  return zone.bottom - zone.top + 1;
@@ -18288,7 +18386,7 @@ const PIVOT_VALUE = {
18288
18386
  assertDomainLength(domainArgs);
18289
18387
  const pivot = this.getters.getPivot(pivotId);
18290
18388
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18291
- addPivotDependencies(this, coreDefinition);
18389
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures.filter((m) => m.id === _measure));
18292
18390
  pivot.init({ reload: pivot.needsReevaluation });
18293
18391
  const error = pivot.assertIsValid({ throwOnError: false });
18294
18392
  if (error) {
@@ -18318,7 +18416,7 @@ const PIVOT_HEADER = {
18318
18416
  assertDomainLength(domainArgs);
18319
18417
  const pivot = this.getters.getPivot(_pivotId);
18320
18418
  const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
18321
- addPivotDependencies(this, coreDefinition);
18419
+ addPivotDependencies(this, coreDefinition, []);
18322
18420
  pivot.init({ reload: pivot.needsReevaluation });
18323
18421
  const error = pivot.assertIsValid({ throwOnError: false });
18324
18422
  if (error) {
@@ -18358,18 +18456,18 @@ const PIVOT = {
18358
18456
  const _pivotFormulaId = toString(pivotFormulaId);
18359
18457
  const _rowCount = toNumber(rowCount, this.locale);
18360
18458
  if (_rowCount < 0) {
18361
- throw new EvaluationError(_t("The number of rows must be positive."));
18459
+ return new EvaluationError(_t("The number of rows must be positive."));
18362
18460
  }
18363
18461
  const _columnCount = toNumber(columnCount, this.locale);
18364
18462
  if (_columnCount < 0) {
18365
- throw new EvaluationError(_t("The number of columns must be positive."));
18463
+ return new EvaluationError(_t("The number of columns must be positive."));
18366
18464
  }
18367
18465
  const _includeColumnHeaders = toBoolean(includeColumnHeaders);
18368
18466
  const _includedTotal = toBoolean(includeTotal);
18369
18467
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
18370
18468
  const pivot = this.getters.getPivot(pivotId);
18371
18469
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18372
- addPivotDependencies(this, coreDefinition);
18470
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures);
18373
18471
  pivot.init({ reload: pivot.needsReevaluation });
18374
18472
  const error = pivot.assertIsValid({ throwOnError: false });
18375
18473
  if (error) {
@@ -18561,6 +18659,12 @@ const EQ = {
18561
18659
  arg("value2 (any)", _t("The value to test against value1 for equality.")),
18562
18660
  ],
18563
18661
  compute: function (value1, value2) {
18662
+ if (isEvaluationError(value1?.value)) {
18663
+ return value1;
18664
+ }
18665
+ if (isEvaluationError(value2?.value)) {
18666
+ return value2;
18667
+ }
18564
18668
  let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
18565
18669
  let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
18566
18670
  if (typeof _value1 === "string") {
@@ -18569,27 +18673,21 @@ const EQ = {
18569
18673
  if (typeof _value2 === "string") {
18570
18674
  _value2 = _value2.toUpperCase();
18571
18675
  }
18572
- if (isEvaluationError(_value1)) {
18573
- throw value1;
18574
- }
18575
- if (isEvaluationError(_value2)) {
18576
- throw value2;
18577
- }
18578
- return _value1 === _value2;
18676
+ return { value: _value1 === _value2 };
18579
18677
  },
18580
18678
  };
18581
18679
  // -----------------------------------------------------------------------------
18582
18680
  // GT
18583
18681
  // -----------------------------------------------------------------------------
18584
18682
  function applyRelationalOperator(value1, value2, cb) {
18585
- let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
18586
- let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
18587
- if (isEvaluationError(_value1)) {
18588
- throw value1;
18683
+ if (isEvaluationError(value1?.value)) {
18684
+ return value1;
18589
18685
  }
18590
- if (isEvaluationError(_value2)) {
18591
- throw value2;
18686
+ if (isEvaluationError(value2?.value)) {
18687
+ return value2;
18592
18688
  }
18689
+ let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
18690
+ let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
18593
18691
  if (typeof _value1 !== "number") {
18594
18692
  _value1 = toString(_value1).toUpperCase();
18595
18693
  }
@@ -18599,12 +18697,12 @@ function applyRelationalOperator(value1, value2, cb) {
18599
18697
  const tV1 = typeof _value1;
18600
18698
  const tV2 = typeof _value2;
18601
18699
  if (tV1 === "string" && tV2 === "number") {
18602
- return true;
18700
+ return { value: true };
18603
18701
  }
18604
18702
  if (tV2 === "string" && tV1 === "number") {
18605
- return false;
18703
+ return { value: false };
18606
18704
  }
18607
- return cb(_value1, _value2);
18705
+ return { value: cb(_value1, _value2) };
18608
18706
  }
18609
18707
  const GT = {
18610
18708
  description: _t("Strictly greater than."),
@@ -18643,7 +18741,11 @@ const LT = {
18643
18741
  arg("value2 (any)", _t("The second value.")),
18644
18742
  ],
18645
18743
  compute: function (value1, value2) {
18646
- return !GTE.compute.bind(this)(value1, value2);
18744
+ const result = GTE.compute.bind(this)(value1, value2);
18745
+ if (isEvaluationError(result.value)) {
18746
+ return result;
18747
+ }
18748
+ return { value: !result.value };
18647
18749
  },
18648
18750
  };
18649
18751
  // -----------------------------------------------------------------------------
@@ -18656,7 +18758,11 @@ const LTE = {
18656
18758
  arg("value2 (any)", _t("The second value.")),
18657
18759
  ],
18658
18760
  compute: function (value1, value2) {
18659
- return !GT.compute.bind(this)(value1, value2);
18761
+ const result = GT.compute.bind(this)(value1, value2);
18762
+ if (isEvaluationError(result.value)) {
18763
+ return result;
18764
+ }
18765
+ return { value: !result.value };
18660
18766
  },
18661
18767
  };
18662
18768
  // -----------------------------------------------------------------------------
@@ -18701,7 +18807,11 @@ const NE = {
18701
18807
  arg("value2 (any)", _t("The value to test against value1 for inequality.")),
18702
18808
  ],
18703
18809
  compute: function (value1, value2) {
18704
- return !EQ.compute.bind(this)(value1, value2);
18810
+ const result = EQ.compute.bind(this)(value1, value2);
18811
+ if (isEvaluationError(result.value)) {
18812
+ return result;
18813
+ }
18814
+ return { value: !result.value };
18705
18815
  },
18706
18816
  };
18707
18817
  // -----------------------------------------------------------------------------
@@ -19860,13 +19970,12 @@ function cssPropertiesToCss(attributes) {
19860
19970
  }
19861
19971
  function getElementMargins(el) {
19862
19972
  const style = window.getComputedStyle(el);
19863
- const margins = {
19973
+ return {
19864
19974
  top: parseInt(style.marginTop, 10) || 0,
19865
19975
  bottom: parseInt(style.marginBottom, 10) || 0,
19866
19976
  left: parseInt(style.marginLeft, 10) || 0,
19867
19977
  right: parseInt(style.marginRight, 10) || 0,
19868
19978
  };
19869
- return margins;
19870
19979
  }
19871
19980
 
19872
19981
  const macRegex = /Mac/i;
@@ -20165,7 +20274,7 @@ class ContentEditableHelper {
20165
20274
  let startNode = this.findChildAtCharacterIndex(start);
20166
20275
  let endNode = this.findChildAtCharacterIndex(end);
20167
20276
  range.setStart(startNode.node, startNode.offset);
20168
- selection.extend(endNode.node, endNode.offset);
20277
+ range.setEnd(endNode.node, endNode.offset);
20169
20278
  }
20170
20279
  }
20171
20280
  /**
@@ -20663,7 +20772,7 @@ class Composer extends owl.Component {
20663
20772
  "Alt+Enter": this.processNewLineEvent,
20664
20773
  "Ctrl+Enter": this.processNewLineEvent,
20665
20774
  Escape: this.processEscapeKey,
20666
- F2: () => console.warn("Not implemented"),
20775
+ F2: (ev) => this.toggleEditionMode(ev),
20667
20776
  F4: (ev) => this.processF4Key(ev),
20668
20777
  Tab: (ev) => this.processTabKey(ev, "right"),
20669
20778
  "Shift+Tab": (ev) => this.processTabKey(ev, "left"),
@@ -20782,6 +20891,11 @@ class Composer extends owl.Component {
20782
20891
  this.props.composerStore.cycleReferences();
20783
20892
  this.processContent();
20784
20893
  }
20894
+ toggleEditionMode(ev) {
20895
+ ev.stopPropagation();
20896
+ this.props.composerStore.toggleEditionMode();
20897
+ this.processContent();
20898
+ }
20785
20899
  processNumpadDecimal(ev) {
20786
20900
  ev.stopPropagation();
20787
20901
  ev.preventDefault();
@@ -21007,7 +21121,13 @@ class Composer extends owl.Component {
21007
21121
  break;
21008
21122
  case "REFERENCE":
21009
21123
  const { xc, sheetName } = splitReference(token.value);
21010
- result.push({ value: token.value, color: this.rangeColor(xc, sheetName) || "#000" });
21124
+ result.push({
21125
+ value: token.value,
21126
+ color: this.rangeColor(xc, sheetName) || "#000",
21127
+ class: tokenAtCursor === token && this.props.composerStore.editionMode === "selecting"
21128
+ ? "text-decoration-underline"
21129
+ : undefined,
21130
+ });
21011
21131
  break;
21012
21132
  case "SYMBOL":
21013
21133
  const value = token.value;
@@ -21578,6 +21698,7 @@ function insertTokenAfterArgSeparator(tokenAtCursor, value) {
21578
21698
  // replace the whole token
21579
21699
  start = tokenAtCursor.start;
21580
21700
  }
21701
+ this.composer.stopComposerRangeSelection();
21581
21702
  this.composer.changeComposerCursorSelection(start, end);
21582
21703
  this.composer.replaceComposerCursorSelection(value);
21583
21704
  }
@@ -21595,6 +21716,7 @@ function insertTokenAfterLeftParenthesis(tokenAtCursor, value) {
21595
21716
  // replace the whole token
21596
21717
  start = tokenAtCursor.start;
21597
21718
  }
21719
+ this.composer.stopComposerRangeSelection();
21598
21720
  this.composer.changeComposerCursorSelection(start, end);
21599
21721
  this.composer.replaceComposerCursorSelection(value);
21600
21722
  }
@@ -21728,9 +21850,13 @@ autoCompleteProviders.add("pivot_group_fields", {
21728
21850
  const colFields = columns.map((groupBy) => groupBy.nameWithGranularity);
21729
21851
  const rowFields = rows.map((groupBy) => groupBy.nameWithGranularity);
21730
21852
  const proposals = [];
21731
- const previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21853
+ let previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21732
21854
  ? argGroupBys.at(-1)
21733
21855
  : argGroupBys.at(-2);
21856
+ const isPositionalSupported = supportedPivotPositionalFormulaRegistry.get(pivot.type);
21857
+ if (isPositionalSupported && previousGroupBy?.startsWith("#")) {
21858
+ previousGroupBy = previousGroupBy.slice(1);
21859
+ }
21734
21860
  if (previousGroupBy === undefined) {
21735
21861
  proposals.push(colFields[0]);
21736
21862
  proposals.push(rowFields[0]);
@@ -21752,7 +21878,7 @@ autoCompleteProviders.add("pivot_group_fields", {
21752
21878
  return field ? makeFieldProposal(field, granularity) : undefined;
21753
21879
  })
21754
21880
  .concat(groupBys.map((groupBy) => {
21755
- if (!supportedPivotPositionalFormulaRegistry.get(pivot.type)) {
21881
+ if (!isPositionalSupported) {
21756
21882
  return undefined;
21757
21883
  }
21758
21884
  const fieldName = groupBy.split(":")[0];
@@ -21761,13 +21887,12 @@ autoCompleteProviders.add("pivot_group_fields", {
21761
21887
  return undefined;
21762
21888
  }
21763
21889
  const positionalFieldArg = `"#${groupBy}"`;
21764
- const positionalProposal = {
21890
+ return {
21765
21891
  text: positionalFieldArg,
21766
21892
  description: _t("%s (positional)", field.string) + (field.help ? ` (${field.help})` : ""),
21767
21893
  htmlContent: [{ value: positionalFieldArg, color: tokenColors.STRING }],
21768
21894
  fuzzySearchKey: field.string + positionalFieldArg, // search on translated name and on technical name
21769
21895
  };
21770
- return positionalProposal;
21771
21896
  }))
21772
21897
  .filter(isDefined);
21773
21898
  },
@@ -22036,7 +22161,7 @@ function parseLiteral(content, locale) {
22036
22161
  return internalDate.value;
22037
22162
  }
22038
22163
  if (isBoolean(content)) {
22039
- return content.toUpperCase() === "TRUE" ? true : false;
22164
+ return content.toUpperCase() === "TRUE";
22040
22165
  }
22041
22166
  return content;
22042
22167
  }
@@ -22187,6 +22312,27 @@ autofillModifiersRegistry
22187
22312
  tooltip: content ? { props: { content: tooltipValue } } : undefined,
22188
22313
  };
22189
22314
  },
22315
+ })
22316
+ .add("DATE_INCREMENT_MODIFIER", {
22317
+ apply: (rule, data, getters) => {
22318
+ const date = toJsDate(rule.current, getters.getLocale());
22319
+ date.setFullYear(date.getFullYear() + rule.increment.years || 0);
22320
+ date.setMonth(date.getMonth() + rule.increment.months || 0);
22321
+ date.setDate(date.getDate() + rule.increment.days || 0);
22322
+ const value = jsDateToNumber(date);
22323
+ rule.current = value;
22324
+ const locale = getters.getLocale();
22325
+ const tooltipValue = formatValue(value, { format: data.cell?.format, locale });
22326
+ return {
22327
+ cellData: {
22328
+ border: data.border,
22329
+ style: data.cell && data.cell.style,
22330
+ format: data.cell && data.cell.format,
22331
+ content: value.toString(),
22332
+ },
22333
+ tooltip: value ? { props: { content: tooltipValue } } : undefined,
22334
+ };
22335
+ },
22190
22336
  })
22191
22337
  .add("COPY_MODIFIER", {
22192
22338
  apply: (rule, data, getters) => {
@@ -22267,7 +22413,9 @@ function getGroup(cell, cells, filter) {
22267
22413
  if (x === cell) {
22268
22414
  found = true;
22269
22415
  }
22270
- const cellValue = x === undefined || x.isFormula ? undefined : evaluateLiteral(x, { locale: DEFAULT_LOCALE });
22416
+ const cellValue = x === undefined || x.isFormula
22417
+ ? undefined
22418
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22271
22419
  if (cellValue && filter(cellValue)) {
22272
22420
  group.push(cellValue);
22273
22421
  }
@@ -22303,6 +22451,71 @@ function calculateIncrementBasedOnGroup(group) {
22303
22451
  }
22304
22452
  return increment;
22305
22453
  }
22454
+ /**
22455
+ * Iterates on a list of date intervals.
22456
+ * if every interval is the same, return the interval
22457
+ * Otherwise return undefined
22458
+ *
22459
+ */
22460
+ function getEqualInterval(intervals) {
22461
+ if (intervals.length < 2) {
22462
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22463
+ }
22464
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22465
+ interval.months === intervals[0].months &&
22466
+ interval.days === intervals[0].days);
22467
+ return equal ? intervals[0] : undefined;
22468
+ }
22469
+ /**
22470
+ * Based on a group of dates, calculate the increment that should be applied
22471
+ * to the next date.
22472
+ *
22473
+ * This will compute the date difference in calendar terms (years, months, days)
22474
+ * In order to make abstraction of leap years and months with different number of days.
22475
+ *
22476
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22477
+ * In case of equidistant dates, we either have in that order:
22478
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22479
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22480
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22481
+ *
22482
+ * */
22483
+ function calculateDateIncrementBasedOnGroup(group) {
22484
+ if (group.length < 2) {
22485
+ return 1;
22486
+ }
22487
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22488
+ const datesIntervals = getDateIntervals(jsDates);
22489
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22490
+ if (datesEquidistantInterval === undefined) {
22491
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22492
+ return undefined;
22493
+ }
22494
+ // The dates are apart by an exact interval of years, months or days
22495
+ // but not a combination of them
22496
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22497
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22498
+ if (!exactDateInterval || isSameDay) {
22499
+ const timeIntervals = jsDates
22500
+ .map((date, index) => {
22501
+ if (index === 0) {
22502
+ return 0;
22503
+ }
22504
+ const previous = jsDates[index - 1];
22505
+ return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22506
+ })
22507
+ .slice(1);
22508
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22509
+ if (equidistantDates) {
22510
+ return group.length * (group[1] - group[0]);
22511
+ }
22512
+ }
22513
+ return {
22514
+ years: datesEquidistantInterval.years * group.length,
22515
+ months: datesEquidistantInterval.months * group.length,
22516
+ days: datesEquidistantInterval.days * group.length,
22517
+ };
22518
+ }
22306
22519
  autofillRulesRegistry
22307
22520
  .add("simple_value_copy", {
22308
22521
  condition: (cell, cells) => {
@@ -22350,12 +22563,47 @@ autofillRulesRegistry
22350
22563
  return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22351
22564
  },
22352
22565
  sequence: 30,
22566
+ })
22567
+ .add("increment_dates", {
22568
+ condition: (cell, cells) => {
22569
+ return (!cell.isFormula &&
22570
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22571
+ !!cell.format &&
22572
+ isDateTimeFormat(cell.format));
22573
+ },
22574
+ generateRule: (cell, cells) => {
22575
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22576
+ !!evaluatedCell.format &&
22577
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22578
+ const increment = calculateDateIncrementBasedOnGroup(group);
22579
+ if (increment === undefined) {
22580
+ return { type: "COPY_MODIFIER" };
22581
+ }
22582
+ /** requires to detect the current date (requires to be an integer value with the right format)
22583
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22584
+ */
22585
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22586
+ if (typeof increment === "object") {
22587
+ return {
22588
+ type: "DATE_INCREMENT_MODIFIER",
22589
+ increment,
22590
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22591
+ };
22592
+ }
22593
+ return {
22594
+ type: "INCREMENT_MODIFIER",
22595
+ increment,
22596
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22597
+ };
22598
+ },
22599
+ sequence: 25,
22353
22600
  })
22354
22601
  .add("increment_number", {
22355
22602
  condition: (cell) => !cell.isFormula &&
22356
22603
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22357
22604
  generateRule: (cell, cells) => {
22358
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
22605
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22606
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22359
22607
  const increment = calculateIncrementBasedOnGroup(group);
22360
22608
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22361
22609
  return {
@@ -22366,6 +22614,37 @@ autofillRulesRegistry
22366
22614
  },
22367
22615
  sequence: 40,
22368
22616
  });
22617
+ /**
22618
+ * Returns the date intervals between consecutive dates of an array
22619
+ * in the format of { years: number, months: number, days: number }
22620
+ *
22621
+ * The split is necessary to make abstraction of leap years and
22622
+ * months with different number of days.
22623
+ *
22624
+ * @param dates
22625
+ */
22626
+ function getDateIntervals(dates) {
22627
+ if (dates.length < 2) {
22628
+ return [{ years: 0, months: 0, days: 0 }];
22629
+ }
22630
+ const res = dates.map((date, index) => {
22631
+ if (index === 0) {
22632
+ return { years: 0, months: 0, days: 0 };
22633
+ }
22634
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22635
+ const years = getTimeDifferenceInWholeYears(previous, date);
22636
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22637
+ previous.setFullYear(previous.getFullYear() + years);
22638
+ previous.setMonth(previous.getMonth() + months);
22639
+ const days = getTimeDifferenceInWholeDays(previous, date);
22640
+ return {
22641
+ years,
22642
+ months,
22643
+ days,
22644
+ };
22645
+ });
22646
+ return res.slice(1);
22647
+ }
22369
22648
 
22370
22649
  const cellPopoverRegistry = new Registry();
22371
22650
 
@@ -22782,6 +23061,7 @@ const XLSX_CHART_TYPES = [
22782
23061
  "surface3DChart",
22783
23062
  "bubbleChart",
22784
23063
  "comboChart",
23064
+ "radarChart",
22785
23065
  ];
22786
23066
 
22787
23067
  /** In XLSX color format (no #) */
@@ -23322,7 +23602,7 @@ const CHART_TYPE_CONVERSION_MAP = {
23322
23602
  lineChart: "line",
23323
23603
  line3DChart: undefined,
23324
23604
  stockChart: undefined,
23325
- radarChart: undefined,
23605
+ radarChart: "radar",
23326
23606
  scatterChart: "scatter",
23327
23607
  pieChart: "pie",
23328
23608
  pie3DChart: undefined,
@@ -24735,7 +25015,7 @@ const TABLE_STYLE_CATEGORIES = {
24735
25015
  custom: _t("Custom"),
24736
25016
  };
24737
25017
  const DEFAULT_TABLE_CONFIG = {
24738
- hasFilters: true,
25018
+ hasFilters: false,
24739
25019
  totalRow: false,
24740
25020
  firstColumn: false,
24741
25021
  lastColumn: false,
@@ -25404,12 +25684,11 @@ class XlsxBaseExtractor {
25404
25684
  * Get the list of all the XLSX files in the XLSX file structure
25405
25685
  */
25406
25686
  getListOfXMLFiles() {
25407
- const XMLFiles = Object.entries(this.xlsxFileStructure)
25687
+ return Object.entries(this.xlsxFileStructure)
25408
25688
  .filter(([key]) => key !== "images")
25409
25689
  .map(([_, value]) => value)
25410
25690
  .flat()
25411
25691
  .filter(isDefined);
25412
- return XMLFiles;
25413
25692
  }
25414
25693
  /**
25415
25694
  * Return an array containing the return value of the given function applied to all the XML elements
@@ -25573,13 +25852,12 @@ class XlsxBaseExtractor {
25573
25852
  rgb = this.extractAttr(colorElement, "rgb")?.asString();
25574
25853
  rgb = rgb === DEFAULT_SYSTEM_COLOR ? undefined : rgb;
25575
25854
  }
25576
- const color = {
25855
+ return {
25577
25856
  rgb: rgb || defaultColor,
25578
25857
  auto: this.extractAttr(colorElement, "auto")?.asBool(),
25579
25858
  indexed: this.extractAttr(colorElement, "indexed")?.asNum(),
25580
25859
  tint: this.extractAttr(colorElement, "tint")?.asNum(),
25581
25860
  };
25582
- return color;
25583
25861
  }
25584
25862
  /**
25585
25863
  * Returns the xml file targeted by a relationship.
@@ -26171,7 +26449,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
26171
26449
  hyperlinks: this.extractHyperLinks(sheetElement),
26172
26450
  tables: this.extractTables(sheetElement),
26173
26451
  pivotTables: this.extractPivotTables(),
26174
- isVisible: sheetWorkbookInfo.state === "visible" ? true : false,
26452
+ isVisible: sheetWorkbookInfo.state === "visible",
26175
26453
  };
26176
26454
  })[0];
26177
26455
  }
@@ -26242,8 +26520,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
26242
26520
  const figures = this.mapOnElements({ parent: worksheet, query: "drawing" }, (drawingElement) => {
26243
26521
  const drawingId = this.extractAttr(drawingElement, "r:id", { required: true })?.asString();
26244
26522
  const drawingFile = this.getTargetXmlFile(this.relationships[drawingId]);
26245
- const figures = new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();
26246
- return figures;
26523
+ return new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();
26247
26524
  })[0];
26248
26525
  return figures || [];
26249
26526
  }
@@ -26261,8 +26538,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
26261
26538
  .filter((relationship) => relationship.type.endsWith("pivotTable"))
26262
26539
  .map((pivotRelationship) => {
26263
26540
  const pivotFile = this.getTargetXmlFile(pivotRelationship);
26264
- const pivot = new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();
26265
- return pivot;
26541
+ return new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();
26266
26542
  });
26267
26543
  }
26268
26544
  catch (e) {
@@ -26661,8 +26937,7 @@ class XlsxReader {
26661
26937
  }
26662
26938
  convertXlsx() {
26663
26939
  const xlsxData = this.getXlsxData();
26664
- const convertedData = this.convertImportedData(xlsxData);
26665
- return convertedData;
26940
+ return this.convertImportedData(xlsxData);
26666
26941
  }
26667
26942
  // ---------------------------------------------------------------------------
26668
26943
  // Parsing XMLs
@@ -27253,6 +27528,20 @@ migrationStepRegistry
27253
27528
  }
27254
27529
  return data;
27255
27530
  },
27531
+ })
27532
+ .add("migration_22", {
27533
+ // "tables are no longer inserted with filters by default",
27534
+ versionFrom: "22",
27535
+ migrate(data) {
27536
+ for (const sheet of data.sheets || []) {
27537
+ for (const table of sheet.tables || []) {
27538
+ if (!table.config) {
27539
+ table.config = { ...DEFAULT_TABLE_CONFIG, hasFilters: true };
27540
+ }
27541
+ }
27542
+ }
27543
+ return data;
27544
+ },
27256
27545
  });
27257
27546
  function fixOverlappingFilters(data) {
27258
27547
  for (let sheet of data.sheets || []) {
@@ -27280,7 +27569,7 @@ function fixOverlappingFilters(data) {
27280
27569
  * a breaking change is made in the way the state is handled, and an upgrade
27281
27570
  * function should be defined
27282
27571
  */
27283
- const CURRENT_VERSION = 22;
27572
+ const CURRENT_VERSION = 23;
27284
27573
  const INITIAL_SHEET_ID = "Sheet1";
27285
27574
  /**
27286
27575
  * This function tries to load anything that could look like a valid
@@ -27293,7 +27582,7 @@ function load(data, verboseImport) {
27293
27582
  if (!data) {
27294
27583
  return createEmptyWorkbookData();
27295
27584
  }
27296
- console.group("Loading data");
27585
+ console.debug("### Loading data ###");
27297
27586
  const start = performance.now();
27298
27587
  if (data["[Content_Types].xml"]) {
27299
27588
  const reader = new XlsxReader(data);
@@ -27307,13 +27596,13 @@ function load(data, verboseImport) {
27307
27596
  // apply migrations, if needed
27308
27597
  if ("version" in data) {
27309
27598
  if (data.version < CURRENT_VERSION) {
27310
- console.info("Migrating data from version", data.version);
27599
+ console.debug("Migrating data from version", data.version);
27311
27600
  data = migrate(data);
27312
27601
  }
27313
27602
  }
27314
27603
  data = repairData(data);
27315
- console.info("Data loaded in", performance.now() - start, "ms");
27316
- console.groupEnd();
27604
+ console.debug("Data loaded in", performance.now() - start, "ms");
27605
+ console.debug("###");
27317
27606
  return data;
27318
27607
  }
27319
27608
  // -----------------------------------------------------------------------------
@@ -27343,7 +27632,7 @@ function migrate(data) {
27343
27632
  for (let i = index; i < steps.length; i++) {
27344
27633
  data = steps[i].migrate(data);
27345
27634
  }
27346
- console.info("Data migrated in", performance.now() - start, "ms");
27635
+ console.debug("Data migrated in", performance.now() - start, "ms");
27347
27636
  return data;
27348
27637
  }
27349
27638
  /**
@@ -27525,7 +27814,7 @@ function createEmptySheet(sheetId, name) {
27525
27814
  };
27526
27815
  }
27527
27816
  function createEmptyWorkbookData(sheetName = "Sheet1") {
27528
- const data = {
27817
+ return {
27529
27818
  version: CURRENT_VERSION,
27530
27819
  sheets: [createEmptySheet(INITIAL_SHEET_ID, sheetName)],
27531
27820
  styles: {},
@@ -27538,7 +27827,6 @@ function createEmptyWorkbookData(sheetName = "Sheet1") {
27538
27827
  pivotNextId: 1,
27539
27828
  customTableStyles: {},
27540
27829
  };
27541
- return data;
27542
27830
  }
27543
27831
  function createEmptyExcelSheet(sheetId, name) {
27544
27832
  return {
@@ -27876,9 +28164,9 @@ function truncateLabel(label) {
27876
28164
  /**
27877
28165
  * Get a default chart js configuration
27878
28166
  */
27879
- function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, truncateLabels = true, horizontalChart, }) {
28167
+ function getDefaultChartJsRuntime(chart, labels, fontColor, { axisFormats, locale, truncateLabels = true, horizontalChart }) {
27880
28168
  const chartTitle = chart.title.text ? chart.title : { ...chart.title, content: "" };
27881
- const options = {
28169
+ const chartOptions = {
27882
28170
  // https://www.chartjs.org/docs/latest/general/responsive.html
27883
28171
  responsive: true, // will resize when its container is resized
27884
28172
  maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
@@ -27922,11 +28210,13 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27922
28210
  const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
27923
28211
  // tooltipItem.parsed can be an object or a number for pie charts
27924
28212
  let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
27925
- if (!yLabel) {
28213
+ if (yLabel === undefined || yLabel === null) {
27926
28214
  yLabel = tooltipItem.parsed;
27927
28215
  }
27928
- const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
27929
- const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
28216
+ const axisId = horizontalChart
28217
+ ? tooltipItem.dataset.xAxisID
28218
+ : tooltipItem.dataset.yAxisID;
28219
+ const yLabelStr = formatChartDatasetValue(axisFormats, locale)(yLabel, axisId);
27930
28220
  return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
27931
28221
  },
27932
28222
  },
@@ -27935,7 +28225,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27935
28225
  };
27936
28226
  return {
27937
28227
  type: chart.type,
27938
- options,
28228
+ options: chartOptions,
27939
28229
  data: {
27940
28230
  labels: truncateLabels ? labels.map(truncateLabel) : labels,
27941
28231
  datasets: [],
@@ -27994,7 +28284,8 @@ function getChartLabelValues(getters, dataSets, labelRange) {
27994
28284
  * Get the format to apply to the the dataset values. This format is defined as the first format
27995
28285
  * found in the dataset ranges that isn't a date format.
27996
28286
  */
27997
- function getChartDatasetFormat(getters, dataSets) {
28287
+ function getChartDatasetFormat(getters, allDataSets, axis) {
28288
+ const dataSets = allDataSets.filter((ds) => (axis === "right") === !!ds.rightYAxis);
27998
28289
  for (const ds of dataSets) {
27999
28290
  const formatsInDataset = getters.getRangeFormats(ds.dataRange);
28000
28291
  const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
@@ -28248,12 +28539,16 @@ function createBarChartRuntime(chart, getters) {
28248
28539
  if (chart.aggregated) {
28249
28540
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28250
28541
  }
28251
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
28542
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
28543
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28252
28544
  const locale = getters.getLocale();
28253
- const localeFormat = { format: dataSetFormat, locale };
28254
28545
  const fontColor = chartFontColor(chart.background);
28546
+ const axisFormats = chart.horizontal
28547
+ ? { x: leftAxisFormat || rightAxisFormat }
28548
+ : { y: leftAxisFormat, y1: rightAxisFormat };
28255
28549
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, {
28256
- ...localeFormat,
28550
+ locale,
28551
+ axisFormats,
28257
28552
  horizontalChart: chart.horizontal,
28258
28553
  });
28259
28554
  const legend = {
@@ -28274,51 +28569,27 @@ function createBarChartRuntime(chart, getters) {
28274
28569
  };
28275
28570
  config.options.indexAxis = chart.horizontal ? "y" : "x";
28276
28571
  config.options.scales = {};
28277
- const labelsAxis = { ticks: { padding: 5, color: fontColor } };
28278
- const valuesAxis = {
28279
- beginAtZero: true, // the origin of the y axis is always zero
28280
- ticks: {
28281
- color: fontColor,
28282
- callback: formatTickValue(localeFormat),
28283
- },
28284
- };
28285
- const xAxis = chart.horizontal ? valuesAxis : labelsAxis;
28286
- const yAxis = chart.horizontal ? labelsAxis : valuesAxis;
28287
- const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
28288
- config.options.scales.x = { ...xAxis, title: getChartAxisTitleRuntime(chart.axesDesign?.x) };
28289
- if (useLeftAxis) {
28290
- config.options.scales.y = {
28291
- ...yAxis,
28292
- position: "left",
28293
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
28294
- };
28295
- }
28296
- if (useRightAxis) {
28297
- config.options.scales.y1 = {
28298
- ...yAxis,
28299
- position: "right",
28300
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
28301
- };
28572
+ const definition = chart.getDefinition();
28573
+ const options = { stacked: chart.stacked, locale };
28574
+ if (chart.horizontal) {
28575
+ const format = leftAxisFormat || rightAxisFormat;
28576
+ config.options.scales.x = getChartAxis(definition, "bottom", "values", { ...options, format });
28577
+ config.options.scales.y = getChartAxis(definition, "left", "labels", options);
28302
28578
  }
28303
- if (chart.stacked) {
28304
- // @ts-ignore chart.js type is broken
28305
- config.options.scales.x.stacked = true;
28306
- if (useLeftAxis) {
28307
- // @ts-ignore chart.js type is broken
28308
- config.options.scales.y.stacked = true;
28309
- }
28310
- if (useRightAxis) {
28311
- // @ts-ignore chart.js type is broken
28312
- config.options.scales.y1.stacked = true;
28313
- }
28579
+ else {
28580
+ config.options.scales.x = getChartAxis(definition, "bottom", "labels", options);
28581
+ const leftAxisOptions = { ...options, format: leftAxisFormat };
28582
+ config.options.scales.y = getChartAxis(definition, "left", "values", leftAxisOptions);
28583
+ const rightAxisOptions = { ...options, format: rightAxisFormat };
28584
+ config.options.scales.y1 = getChartAxis(definition, "right", "values", rightAxisOptions);
28314
28585
  }
28586
+ config.options.scales = removeFalsyAttributes(config.options.scales);
28315
28587
  config.options.plugins.chartShowValuesPlugin = {
28316
28588
  showValues: chart.showValues,
28317
28589
  background: chart.background,
28318
28590
  horizontal: chart.horizontal,
28319
- callback: formatTickValue(localeFormat),
28591
+ callback: formatChartDatasetValue(axisFormats, locale),
28320
28592
  };
28321
- const definition = chart.getDefinition();
28322
28593
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28323
28594
  const trendDatasets = [];
28324
28595
  for (const index in dataSetsValues) {
@@ -28333,12 +28604,10 @@ function createBarChartRuntime(chart, getters) {
28333
28604
  };
28334
28605
  config.data.datasets.push(dataset);
28335
28606
  if (definition.dataSets?.[index]?.label) {
28336
- const label = definition.dataSets[index].label;
28337
- dataset.label = label;
28338
- }
28339
- if (definition.dataSets?.[index]?.yAxisId && !chart.horizontal) {
28340
- dataset["yAxisID"] = definition.dataSets[index].yAxisId;
28607
+ dataset.label = definition.dataSets[index].label;
28341
28608
  }
28609
+ dataset.yAxisID = chart.horizontal ? "y" : definition.dataSets[index].yAxisId || "y";
28610
+ dataset.xAxisID = "x";
28342
28611
  const trend = definition.dataSets?.[index].trend;
28343
28612
  if (!trend?.display || chart.horizontal) {
28344
28613
  continue;
@@ -28354,7 +28623,7 @@ function createBarChartRuntime(chart, getters) {
28354
28623
  */
28355
28624
  const maxLength = Math.max(...trendDatasets.map((trendDataset) => trendDataset.data.length));
28356
28625
  config.options.scales[TREND_LINE_XAXIS_ID] = {
28357
- ...xAxis,
28626
+ ...config.options.scales.x,
28358
28627
  labels: Array(maxLength).fill(""),
28359
28628
  offset: false,
28360
28629
  display: false,
@@ -28363,13 +28632,10 @@ function createBarChartRuntime(chart, getters) {
28363
28632
  * datasets to ensure the way we distinguish the originals and trendLine datasets after
28364
28633
  */
28365
28634
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28366
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28367
28635
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28368
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28369
- // @ts-expect-error
28370
- return originalTooltipTitle?.(tooltipItems);
28371
- }
28372
- return "";
28636
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
28637
+ ? undefined
28638
+ : "";
28373
28639
  };
28374
28640
  }
28375
28641
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -28644,8 +28910,10 @@ function createLineOrScatterChartRuntime(chart, getters) {
28644
28910
  }
28645
28911
  const locale = getters.getLocale();
28646
28912
  const truncateLabels = axisType === "category";
28647
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
28648
- const options = { format: dataSetFormat, locale, truncateLabels };
28913
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
28914
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28915
+ const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat };
28916
+ const options = { locale, truncateLabels, axisFormats };
28649
28917
  const fontColor = chartFontColor(chart.background);
28650
28918
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, options);
28651
28919
  const legend = {
@@ -28675,52 +28943,18 @@ function createLineOrScatterChartRuntime(chart, getters) {
28675
28943
  displayLegend: chart.legendPosition === "top",
28676
28944
  }),
28677
28945
  };
28678
- const xAxis = {
28679
- ticks: {
28680
- padding: 5,
28681
- color: fontColor,
28682
- },
28683
- title: getChartAxisTitleRuntime(chart.axesDesign?.x),
28684
- };
28946
+ const definition = chart.getDefinition();
28947
+ const stacked = "stacked" in chart && chart.stacked;
28685
28948
  config.options.scales = {
28686
- x: xAxis,
28687
- };
28688
- const yAxis = {
28689
- beginAtZero: true, // the origin of the y axis is always zero
28690
- ticks: {
28691
- color: fontColor,
28692
- callback: formatTickValue(options),
28693
- },
28949
+ x: getChartAxis(definition, "bottom", "labels", { locale }),
28950
+ y: getChartAxis(definition, "left", "values", { locale, stacked, format: leftAxisFormat }),
28951
+ y1: getChartAxis(definition, "right", "values", { locale, stacked, format: rightAxisFormat }),
28694
28952
  };
28695
- const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
28696
- if (useLeftAxis) {
28697
- config.options.scales.y = {
28698
- ...yAxis,
28699
- position: "left",
28700
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
28701
- };
28702
- }
28703
- if (useRightAxis) {
28704
- config.options.scales.y1 = {
28705
- ...yAxis,
28706
- position: "right",
28707
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
28708
- };
28709
- }
28710
- if ("stacked" in chart && chart.stacked) {
28711
- if (useLeftAxis) {
28712
- // @ts-ignore chart.js type is broken
28713
- config.options.scales.y.stacked = true;
28714
- }
28715
- if (useRightAxis) {
28716
- // @ts-ignore chart.js type is broken
28717
- config.options.scales.y1.stacked = true;
28718
- }
28719
- }
28953
+ config.options.scales = removeFalsyAttributes(config.options.scales);
28720
28954
  config.options.plugins.chartShowValuesPlugin = {
28721
28955
  showValues: chart.showValues,
28722
28956
  background: chart.background,
28723
- callback: formatTickValue(options),
28957
+ callback: formatChartDatasetValue(axisFormats, locale),
28724
28958
  };
28725
28959
  if (chart.dataSetsHaveTitle &&
28726
28960
  dataSetsValues[0] &&
@@ -28739,7 +28973,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28739
28973
  else if (axisType === "linear") {
28740
28974
  config.options.scales.x.type = "linear";
28741
28975
  config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });
28742
- config.options.plugins.tooltip.callbacks.title = () => "";
28743
28976
  config.options.plugins.tooltip.callbacks.label = (tooltipItem) => {
28744
28977
  const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];
28745
28978
  let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];
@@ -28747,7 +28980,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
28747
28980
  label = toNumber(label, locale);
28748
28981
  }
28749
28982
  const formattedX = formatValue(label, { locale, format: labelFormat });
28750
- const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });
28983
+ const formattedY = formatValue(dataSetPoint, { locale, format: leftAxisFormat });
28751
28984
  const dataSetTitle = tooltipItem.dataset.label;
28752
28985
  return formattedX
28753
28986
  ? `${dataSetTitle}: (${formattedX}, ${formattedY})`
@@ -28757,7 +28990,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28757
28990
  const areaChart = "fillArea" in chart ? chart.fillArea : false;
28758
28991
  const stackedChart = "stacked" in chart ? chart.stacked : false;
28759
28992
  const cumulative = "cumulative" in chart ? chart.cumulative : false;
28760
- const definition = chart.getDefinition();
28761
28993
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28762
28994
  for (let [index, { label, data }] of dataSetsValues.entries()) {
28763
28995
  const color = colors.next();
@@ -28795,12 +29027,9 @@ function createLineOrScatterChartRuntime(chart, getters) {
28795
29027
  const trendDatasets = [];
28796
29028
  for (const [index, dataset] of config.data.datasets.entries()) {
28797
29029
  if (definition.dataSets?.[index]?.label) {
28798
- const label = definition.dataSets[index].label;
28799
- dataset.label = label;
28800
- }
28801
- if (definition.dataSets?.[index]?.yAxisId) {
28802
- dataset["yAxisID"] = definition.dataSets[index].yAxisId;
29030
+ dataset.label = definition.dataSets[index].label;
28803
29031
  }
29032
+ dataset["yAxisID"] = definition.dataSets[index].yAxisId || "y";
28804
29033
  const trend = definition.dataSets?.[index].trend;
28805
29034
  if (!trend?.display) {
28806
29035
  continue;
@@ -28817,7 +29046,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
28817
29046
  * set so that the second axis points match the classical x axis
28818
29047
  */
28819
29048
  config.options.scales[TREND_LINE_XAXIS_ID] = {
28820
- ...xAxis,
29049
+ ...config.options.scales.x,
28821
29050
  type: "category",
28822
29051
  labels: range(0, maxLength).map((x) => x.toString()),
28823
29052
  offset: false,
@@ -28827,22 +29056,15 @@ function createLineOrScatterChartRuntime(chart, getters) {
28827
29056
  * distinguish the originals and trendLine datasets after
28828
29057
  */
28829
29058
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28830
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28831
- config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28832
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28833
- // @ts-expect-error
28834
- return originalTooltipTitle?.(tooltipItems);
28835
- }
28836
- return "";
28837
- };
28838
29059
  }
29060
+ config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29061
+ const displayTooltipTitle = axisType !== "linear" &&
29062
+ tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
29063
+ return displayTooltipTitle ? undefined : "";
29064
+ };
28839
29065
  return {
28840
29066
  chartJsConfig: config,
28841
29067
  background: chart.background || BACKGROUND_CHART_COLOR,
28842
- dataSetsValues,
28843
- labelValues,
28844
- dataSetFormat,
28845
- labelFormat,
28846
29068
  };
28847
29069
  }
28848
29070
 
@@ -28900,6 +29122,7 @@ class ComboChart extends AbstractChart {
28900
29122
  ranges.push({
28901
29123
  ...this.dataSetDesign?.[i],
28902
29124
  dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
29125
+ type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
28903
29126
  });
28904
29127
  }
28905
29128
  return {
@@ -28945,9 +29168,13 @@ class ComboChart extends AbstractChart {
28945
29168
  return new ComboChart(definition, this.sheetId, this.getters);
28946
29169
  }
28947
29170
  static getDefinitionFromContextCreation(context) {
29171
+ const dataSets = (context.range ?? []).map((ds, index) => ({
29172
+ ...ds,
29173
+ type: index ? "line" : "bar",
29174
+ }));
28948
29175
  return {
28949
29176
  background: context.background,
28950
- dataSets: context.range ?? [],
29177
+ dataSets,
28951
29178
  dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
28952
29179
  aggregated: context.aggregated,
28953
29180
  legendPosition: context.legendPosition ?? "top",
@@ -28970,10 +29197,8 @@ class ComboChart extends AbstractChart {
28970
29197
  }
28971
29198
  }
28972
29199
  function createComboChartRuntime(chart, getters) {
28973
- const mainDataSetFormat = chart.dataSets.length
28974
- ? getChartDatasetFormat(getters, [chart.dataSets[0]])
28975
- : undefined;
28976
- const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets.slice(1));
29200
+ const mainDataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
29201
+ const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28977
29202
  const locale = getters.getLocale();
28978
29203
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
28979
29204
  let labels = labelValues.formattedValues;
@@ -28987,12 +29212,11 @@ function createComboChartRuntime(chart, getters) {
28987
29212
  if (chart.aggregated) {
28988
29213
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28989
29214
  }
28990
- const localeFormat = { format: mainDataSetFormat, locale };
28991
29215
  const fontColor = chartFontColor(chart.background);
28992
- const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
29216
+ const axisFormats = { y: mainDataSetFormat, y1: lineDataSetsFormat };
29217
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, { locale, axisFormats });
28993
29218
  const legend = {
28994
29219
  labels: { color: fontColor },
28995
- reverse: true,
28996
29220
  };
28997
29221
  if (chart.legendPosition === "none") {
28998
29222
  legend.display = false;
@@ -29007,52 +29231,17 @@ function createComboChartRuntime(chart, getters) {
29007
29231
  displayLegend: chart.legendPosition === "top",
29008
29232
  }),
29009
29233
  };
29234
+ const definition = chart.getDefinition();
29010
29235
  config.options.scales = {
29011
- x: {
29012
- ticks: {
29013
- padding: 5,
29014
- color: fontColor,
29015
- },
29016
- title: getChartAxisTitleRuntime(chart.axesDesign?.x),
29017
- },
29236
+ x: getChartAxis(definition, "bottom", "labels", { locale }),
29237
+ y: getChartAxis(definition, "left", "values", { locale, format: mainDataSetFormat }),
29238
+ y1: getChartAxis(definition, "right", "values", { locale, format: lineDataSetsFormat }),
29018
29239
  };
29019
- const leftVerticalAxis = {
29020
- beginAtZero: true, // the origin of the y axis is always zero
29021
- ticks: {
29022
- color: fontColor,
29023
- callback: formatTickValue({ format: mainDataSetFormat, locale }),
29024
- },
29025
- };
29026
- const rightVerticalAxis = {
29027
- beginAtZero: true, // the origin of the y axis is always zero
29028
- ticks: {
29029
- color: fontColor,
29030
- callback: formatTickValue({ format: lineDataSetsFormat, locale }),
29031
- },
29032
- };
29033
- const definition = chart.getDefinition();
29034
- const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);
29035
- if (useLeftAxis) {
29036
- config.options.scales.y = {
29037
- ...leftVerticalAxis,
29038
- position: "left",
29039
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
29040
- };
29041
- }
29042
- if (useRightAxis) {
29043
- config.options.scales.y1 = {
29044
- ...rightVerticalAxis,
29045
- position: "right",
29046
- grid: {
29047
- display: false,
29048
- },
29049
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
29050
- };
29051
- }
29240
+ config.options.scales = removeFalsyAttributes(config.options.scales);
29052
29241
  config.options.plugins.chartShowValuesPlugin = {
29053
29242
  showValues: chart.showValues,
29054
29243
  background: chart.background,
29055
- callback: formatTickValue({ format: mainDataSetFormat, locale }),
29244
+ callback: formatChartDatasetValue(axisFormats, locale),
29056
29245
  };
29057
29246
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
29058
29247
  let maxLength = 0;
@@ -29060,14 +29249,15 @@ function createComboChartRuntime(chart, getters) {
29060
29249
  for (let [index, { label, data }] of dataSetsValues.entries()) {
29061
29250
  const design = definition.dataSets[index];
29062
29251
  const color = colors.next();
29252
+ const type = design?.type ?? "line";
29063
29253
  const dataset = {
29064
29254
  label: design?.label ?? label,
29065
29255
  data,
29066
29256
  borderColor: color,
29067
29257
  backgroundColor: color,
29068
29258
  yAxisID: design?.yAxisId ?? "y",
29069
- type: index === 0 ? "bar" : "line",
29070
- order: -index,
29259
+ type,
29260
+ order: type === "bar" ? dataSetsValues.length + index : index,
29071
29261
  };
29072
29262
  config.data.datasets.push(dataset);
29073
29263
  const trend = definition.dataSets?.[index].trend;
@@ -29094,13 +29284,10 @@ function createComboChartRuntime(chart, getters) {
29094
29284
  * distinguish the originals and trendLine datasets after
29095
29285
  */
29096
29286
  trendDatasets.forEach((x) => config.data.datasets.push(x));
29097
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
29098
29287
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29099
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
29100
- // @ts-expect-error
29101
- return originalTooltipTitle?.(tooltipItems);
29102
- }
29103
- return "";
29288
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
29289
+ ? undefined
29290
+ : "";
29104
29291
  };
29105
29292
  }
29106
29293
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -29680,7 +29867,7 @@ function createPieChartRuntime(chart, getters) {
29680
29867
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
29681
29868
  }
29682
29869
  ({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));
29683
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
29870
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
29684
29871
  const locale = getters.getLocale();
29685
29872
  const config = getPieConfiguration(chart, labels, { format: dataSetFormat, locale });
29686
29873
  const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
@@ -29835,7 +30022,199 @@ function createPyramidChartRuntime(chart, getters) {
29835
30022
  return tooltipLabelCallback(tooltipItem);
29836
30023
  };
29837
30024
  const callback = config.options.plugins.chartShowValuesPlugin.callback;
29838
- config.options.plugins.chartShowValuesPlugin.callback = (x) => callback(Math.abs(x));
30025
+ config.options.plugins.chartShowValuesPlugin.callback = (x, axisId) => callback(Math.abs(x), axisId);
30026
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
30027
+ }
30028
+
30029
+ class RadarChart extends AbstractChart {
30030
+ dataSets;
30031
+ labelRange;
30032
+ background;
30033
+ legendPosition;
30034
+ stacked;
30035
+ aggregated;
30036
+ type = "radar";
30037
+ dataSetsHaveTitle;
30038
+ dataSetDesign;
30039
+ fillArea;
30040
+ constructor(definition, sheetId, getters) {
30041
+ super(definition, sheetId, getters);
30042
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
30043
+ this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
30044
+ this.background = definition.background;
30045
+ this.legendPosition = definition.legendPosition;
30046
+ this.stacked = definition.stacked;
30047
+ this.aggregated = definition.aggregated;
30048
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
30049
+ this.dataSetDesign = definition.dataSets;
30050
+ this.fillArea = definition.fillArea;
30051
+ }
30052
+ static transformDefinition(definition, executed) {
30053
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
30054
+ }
30055
+ static validateChartDefinition(validator, definition) {
30056
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
30057
+ }
30058
+ static getDefinitionFromContextCreation(context) {
30059
+ return {
30060
+ background: context.background,
30061
+ dataSets: context.range ?? [],
30062
+ dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
30063
+ stacked: context.stacked ?? false,
30064
+ aggregated: context.aggregated ?? false,
30065
+ legendPosition: context.legendPosition ?? "top",
30066
+ title: context.title || { text: "" },
30067
+ type: "radar",
30068
+ labelRange: context.auxiliaryRange || undefined,
30069
+ fillArea: context.fillArea ?? false,
30070
+ };
30071
+ }
30072
+ getContextCreation() {
30073
+ const range = [];
30074
+ for (const [i, dataSet] of this.dataSets.entries()) {
30075
+ range.push({
30076
+ ...this.dataSetDesign?.[i],
30077
+ dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
30078
+ });
30079
+ }
30080
+ return {
30081
+ ...this,
30082
+ range,
30083
+ auxiliaryRange: this.labelRange
30084
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
30085
+ : undefined,
30086
+ };
30087
+ }
30088
+ copyForSheetId(sheetId) {
30089
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
30090
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
30091
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
30092
+ return new RadarChart(definition, sheetId, this.getters);
30093
+ }
30094
+ copyInSheetId(sheetId) {
30095
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
30096
+ return new RadarChart(definition, sheetId, this.getters);
30097
+ }
30098
+ getDefinition() {
30099
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
30100
+ }
30101
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
30102
+ const ranges = [];
30103
+ for (const [i, dataSet] of dataSets.entries()) {
30104
+ ranges.push({
30105
+ ...this.dataSetDesign?.[i],
30106
+ dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
30107
+ });
30108
+ }
30109
+ return {
30110
+ type: "radar",
30111
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
30112
+ background: this.background,
30113
+ dataSets: ranges,
30114
+ legendPosition: this.legendPosition,
30115
+ labelRange: labelRange
30116
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
30117
+ : undefined,
30118
+ title: this.title,
30119
+ stacked: this.stacked,
30120
+ aggregated: this.aggregated,
30121
+ fillArea: this.fillArea,
30122
+ };
30123
+ }
30124
+ getDefinitionForExcel() {
30125
+ if (this.aggregated) {
30126
+ return undefined;
30127
+ }
30128
+ const dataSets = this.dataSets
30129
+ .map((ds) => toExcelDataset(this.getters, ds))
30130
+ .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
30131
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
30132
+ const definition = this.getDefinition();
30133
+ return {
30134
+ ...definition,
30135
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
30136
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
30137
+ dataSets,
30138
+ labelRange,
30139
+ };
30140
+ }
30141
+ updateRanges(applyChange) {
30142
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
30143
+ if (!isStale) {
30144
+ return this;
30145
+ }
30146
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
30147
+ return new RadarChart(definition, this.sheetId, this.getters);
30148
+ }
30149
+ }
30150
+ function createRadarChartRuntime(chart, getters) {
30151
+ const definition = chart.getDefinition();
30152
+ const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
30153
+ let labels = labelValues.formattedValues;
30154
+ let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
30155
+ if (chart.dataSetsHaveTitle &&
30156
+ dataSetsValues[0] &&
30157
+ labels.length > dataSetsValues[0].data.length) {
30158
+ labels.shift();
30159
+ }
30160
+ ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
30161
+ if (chart.aggregated) {
30162
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
30163
+ }
30164
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
30165
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
30166
+ const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat };
30167
+ const locale = getters.getLocale();
30168
+ const fontColor = chartFontColor(chart.background);
30169
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, {
30170
+ axisFormats,
30171
+ locale,
30172
+ });
30173
+ const legend = {
30174
+ labels: { color: fontColor },
30175
+ };
30176
+ if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
30177
+ legend.display = false;
30178
+ }
30179
+ else {
30180
+ legend.position = chart.legendPosition;
30181
+ }
30182
+ const fill = definition.fillArea ?? false;
30183
+ if (!fill) {
30184
+ legend.labels["boxHeight"] = 0;
30185
+ }
30186
+ config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
30187
+ config.options.plugins.tooltip = {
30188
+ ...config.options.plugins?.tooltip,
30189
+ callbacks: {
30190
+ label: function (tooltipItem) {
30191
+ const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
30192
+ const yLabel = tooltipItem.parsed.r;
30193
+ return xLabel ? `${xLabel}: ${yLabel}` : yLabel.toString();
30194
+ },
30195
+ },
30196
+ };
30197
+ config.options.layout = {
30198
+ padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
30199
+ };
30200
+ const colorGenerator = getChartColorsGenerator(definition, dataSetsValues.length);
30201
+ for (let i = 0; i < dataSetsValues.length; i++) {
30202
+ let { label, data } = dataSetsValues[i];
30203
+ if (definition.dataSets?.[i]?.label) {
30204
+ label = definition.dataSets[i].label;
30205
+ }
30206
+ const borderColor = colorGenerator.next();
30207
+ const dataset = {
30208
+ label,
30209
+ data,
30210
+ borderColor,
30211
+ };
30212
+ if (fill) {
30213
+ dataset.backgroundColor = setColorAlpha(borderColor, 0.3);
30214
+ dataset["fill"] = true;
30215
+ }
30216
+ config.data.datasets.push(dataset);
30217
+ }
29839
30218
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
29840
30219
  }
29841
30220
 
@@ -30226,7 +30605,8 @@ function createWaterfallChartRuntime(chart, getters) {
30226
30605
  if (chart.showSubTotals) {
30227
30606
  labels.push(_t("Subtotal"));
30228
30607
  }
30229
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
30608
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left") ||
30609
+ getChartDatasetFormat(getters, chart.dataSets, "right");
30230
30610
  const locale = getters.getLocale();
30231
30611
  const dataSeriesLabels = dataSetsValues.map((dataSet) => dataSet.label);
30232
30612
  const config = getWaterfallConfiguration(chart, labels, dataSeriesLabels, {
@@ -30360,6 +30740,15 @@ chartRegistry.add("pyramid", {
30360
30740
  getChartDefinitionFromContextCreation: PyramidChart.getDefinitionFromContextCreation,
30361
30741
  sequence: 80,
30362
30742
  });
30743
+ chartRegistry.add("radar", {
30744
+ match: (type) => type === "radar",
30745
+ createChart: (definition, sheetId, getters) => new RadarChart(definition, sheetId, getters),
30746
+ getChartRuntime: createRadarChartRuntime,
30747
+ validateChartDefinition: RadarChart.validateChartDefinition,
30748
+ transformDefinition: RadarChart.transformDefinition,
30749
+ getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation,
30750
+ sequence: 80,
30751
+ });
30363
30752
  const chartComponentRegistry = new Registry();
30364
30753
  chartComponentRegistry.add("line", ChartJsComponent);
30365
30754
  chartComponentRegistry.add("bar", ChartJsComponent);
@@ -30370,6 +30759,7 @@ chartComponentRegistry.add("scatter", ChartJsComponent);
30370
30759
  chartComponentRegistry.add("scorecard", ScorecardChart);
30371
30760
  chartComponentRegistry.add("waterfall", ChartJsComponent);
30372
30761
  chartComponentRegistry.add("pyramid", ChartJsComponent);
30762
+ chartComponentRegistry.add("radar", ChartJsComponent);
30373
30763
  const chartCategories = {
30374
30764
  line: _t("Line"),
30375
30765
  column: _t("Column"),
@@ -30511,6 +30901,24 @@ chartSubtypeRegistry
30511
30901
  chartType: "pyramid",
30512
30902
  category: "misc",
30513
30903
  preview: "o-spreadsheet-ChartPreview.POPULATION_PYRAMID_CHART",
30904
+ })
30905
+ .add("radar", {
30906
+ matcher: (definition) => definition.type === "radar" && !definition.fillArea,
30907
+ displayName: _t("Radar"),
30908
+ chartSubtype: "radar",
30909
+ chartType: "radar",
30910
+ subtypeDefinition: { fillArea: false },
30911
+ category: "misc",
30912
+ preview: "o-spreadsheet-ChartPreview.RADAR_CHART",
30913
+ })
30914
+ .add("filled_radar", {
30915
+ matcher: (definition) => definition.type === "radar" && !!definition.fillArea,
30916
+ displayName: _t("Filled Radar"),
30917
+ chartType: "radar",
30918
+ chartSubtype: "filled_radar",
30919
+ subtypeDefinition: { fillArea: true },
30920
+ category: "misc",
30921
+ preview: "o-spreadsheet-ChartPreview.FILLED_RADAR_CHART",
30514
30922
  });
30515
30923
 
30516
30924
  /**
@@ -30578,11 +30986,10 @@ function centerFigurePosition(getters, size) {
30578
30986
  const rect = getters.getVisibleRect(getters.getActiveMainViewport());
30579
30987
  const scrollableViewportWidth = Math.min(rect.width, dim.width - offsetCorrectionX);
30580
30988
  const scrollableViewportHeight = Math.min(rect.height, dim.height - offsetCorrectionY);
30581
- const position = {
30989
+ return {
30582
30990
  x: offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2),
30583
30991
  y: offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2),
30584
30992
  }; // Position at the center of the scrollable viewport
30585
- return position;
30586
30993
  }
30587
30994
  function getMaxFigureSize(getters, figureSize) {
30588
30995
  const size = deepCopy(figureSize);
@@ -31155,14 +31562,13 @@ class PopoverPositionContext {
31155
31562
  const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
31156
31563
  const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
31157
31564
  verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset;
31158
- const cssProperties = {
31565
+ return {
31159
31566
  top: this.getTopCoordinate(actualHeight, shouldRenderAtBottom) -
31160
31567
  this.spreadsheetOffset.y -
31161
31568
  verticalOffset +
31162
31569
  "px",
31163
31570
  left: this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + "px",
31164
31571
  };
31165
- return cssProperties;
31166
31572
  }
31167
31573
  getCurrentPosition(elDims) {
31168
31574
  const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
@@ -32390,7 +32796,7 @@ function getSmartChartDefinition(zone, getters) {
32390
32796
  * Create a table on the selected zone, with UI warnings to the user if the creation fails.
32391
32797
  * If a single cell is selected, expand the selection to non-empty adjacent cells to create a table.
32392
32798
  */
32393
- function interactiveCreateTable(env, sheetId, tableConfig) {
32799
+ function interactiveCreateTable(env, sheetId, tableConfig = DEFAULT_TABLE_CONFIG) {
32394
32800
  let target = env.model.getters.getSelectedZones();
32395
32801
  let isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
32396
32802
  if (target.length === 1 && !isDynamic && getZoneArea(target[0]) === 1) {
@@ -33449,7 +33855,7 @@ function getColumnsNumber(env) {
33449
33855
  }
33450
33856
 
33451
33857
  const pivotProperties = {
33452
- name: _t("Edit Pivot"),
33858
+ name: _t("See pivot properties"),
33453
33859
  execute(env) {
33454
33860
  const position = env.model.getters.getActivePosition();
33455
33861
  const pivotId = env.model.getters.getPivotIdFromPosition(position);
@@ -33603,8 +34009,7 @@ cellMenuRegistry
33603
34009
  })
33604
34010
  .add("pivot_properties", {
33605
34011
  ...pivotProperties,
33606
- sequence: 160,
33607
- separator: true,
34012
+ sequence: 170,
33608
34013
  });
33609
34014
 
33610
34015
  const sortRange = {
@@ -35296,6 +35701,7 @@ const CHECK_SVG = /*xml*/ `
35296
35701
  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35297
35702
  </svg>
35298
35703
  `;
35704
+ const CHECKBOX_WIDTH = 14;
35299
35705
  css /* scss */ `
35300
35706
  label.o-checkbox {
35301
35707
  input {
@@ -35303,8 +35709,8 @@ css /* scss */ `
35303
35709
  -webkit-appearance: none;
35304
35710
  -moz-appearance: none;
35305
35711
  border-radius: 0;
35306
- width: 14px;
35307
- height: 14px;
35712
+ width: ${CHECKBOX_WIDTH}px;
35713
+ height: ${CHECKBOX_WIDTH}px;
35308
35714
  vertical-align: top;
35309
35715
  box-sizing: border-box;
35310
35716
  outline: none;
@@ -35350,6 +35756,7 @@ class Section extends owl.Component {
35350
35756
  static template = "o_spreadsheet.Section";
35351
35757
  static props = {
35352
35758
  class: { type: String, optional: true },
35759
+ title: { type: String, optional: true },
35353
35760
  slots: Object,
35354
35761
  };
35355
35762
  }
@@ -36079,17 +36486,11 @@ class BarConfigPanel extends GenericChartConfigPanel {
36079
36486
  stacked,
36080
36487
  });
36081
36488
  }
36082
- onUpdateAggregated(aggregated) {
36083
- this.props.updateChart(this.props.figureId, {
36084
- aggregated,
36085
- });
36086
- }
36087
36489
  }
36088
36490
 
36089
36491
  css /* scss */ `
36090
36492
  .o_side_panel_collapsible_title {
36091
36493
  font-size: 16px;
36092
- font-weight: bold;
36093
36494
  cursor: pointer;
36094
36495
  padding: 6px 0px 6px 6px !important;
36095
36496
 
@@ -36126,49 +36527,40 @@ class SidePanelCollapsible extends owl.Component {
36126
36527
  static template = "o-spreadsheet-SidePanelCollapsible";
36127
36528
  static props = {
36128
36529
  slots: Object,
36530
+ title: { type: String, optional: true },
36129
36531
  collapsedAtInit: { type: Boolean, optional: true },
36130
36532
  class: { type: String, optional: true },
36131
36533
  };
36132
36534
  currentId = (CURRENT_COLLAPSIBLE_ID++).toString();
36133
36535
  }
36134
36536
 
36135
- const CIRCLE_SVG = /*xml*/ `
36136
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'>
36137
- <circle r="2" fill="#FFF"/>
36138
- </svg>
36139
- `;
36140
36537
  css /* scss */ `
36141
- .o-radio {
36142
- input {
36143
- appearance: none;
36144
- -webkit-appearance: none;
36145
- -moz-appearance: none;
36146
- width: 14px;
36147
- height: 14px;
36148
- border: 1px solid ${GRAY_300};
36149
- box-sizing: border-box;
36150
- outline: none;
36151
- border-radius: 8px;
36152
-
36153
- &:checked {
36154
- background: url("data:image/svg+xml,${encodeURIComponent(CIRCLE_SVG)}");
36155
- background-color: ${ACTION_COLOR};
36538
+ .o-badge-selection {
36539
+ gap: 1px;
36540
+ button.o-button {
36541
+ border-radius: 0;
36542
+ &.selected {
36543
+ color: ${GRAY_900};
36156
36544
  border-color: ${ACTION_COLOR};
36545
+ background: ${BADGE_SELECTED_COLOR};
36546
+ font-weight: 600;
36547
+ }
36548
+
36549
+ &:first-child {
36550
+ border-radius: 4px 0 0 4px;
36551
+ }
36552
+ &:last-child {
36553
+ border-radius: 0 4px 4px 0;
36157
36554
  }
36158
36555
  }
36159
36556
  }
36160
36557
  `;
36161
- class RadioSelection extends owl.Component {
36162
- static template = "o-spreadsheet.RadioSelection";
36558
+ class BadgeSelection extends owl.Component {
36559
+ static template = "o-spreadsheet.BadgeSelection";
36163
36560
  static props = {
36164
36561
  choices: Array,
36165
36562
  onChange: Function,
36166
- selectedValue: { optional: false },
36167
- name: String,
36168
- direction: { type: String, optional: true },
36169
- };
36170
- static defaultProps = {
36171
- direction: "horizontal",
36563
+ selectedValue: String,
36172
36564
  };
36173
36565
  }
36174
36566
 
@@ -36713,86 +37105,6 @@ class ColorPickerWidget extends owl.Component {
36713
37105
  }
36714
37106
  }
36715
37107
 
36716
- const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
36717
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
36718
- <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
36719
- </svg>
36720
- `;
36721
- css /* scss */ `
36722
- .o-round-color-picker-button {
36723
- width: 18px;
36724
- height: 18px;
36725
- cursor: pointer;
36726
- border: 1px solid ${GRAY_300};
36727
- background-position: 1px 1px;
36728
- background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
36729
- }
36730
- `;
36731
- class RoundColorPicker extends owl.Component {
36732
- static template = "o-spreadsheet.RoundColorPicker";
36733
- static components = { ColorPickerWidget, Section, ColorPicker };
36734
- static props = {
36735
- currentColor: { type: String, optional: true },
36736
- title: { type: String, optional: true },
36737
- onColorPicked: Function,
36738
- };
36739
- colorPickerButtonRef = owl.useRef("colorPickerButton");
36740
- state;
36741
- setup() {
36742
- this.state = owl.useState({ pickerOpened: false });
36743
- owl.useExternalListener(window, "click", this.closePicker);
36744
- }
36745
- closePicker() {
36746
- this.state.pickerOpened = false;
36747
- }
36748
- togglePicker() {
36749
- this.state.pickerOpened = !this.state.pickerOpened;
36750
- }
36751
- onColorPicked(color) {
36752
- this.props.onColorPicked(color);
36753
- this.state.pickerOpened = false;
36754
- }
36755
- get colorPickerAnchorRect() {
36756
- const button = this.colorPickerButtonRef.el;
36757
- return getBoundingRectAsPOJO(button);
36758
- }
36759
- get buttonStyle() {
36760
- return cssPropertiesToCss({
36761
- background: this.props.currentColor,
36762
- });
36763
- }
36764
- }
36765
-
36766
- css /* scss */ `
36767
- .o-badge-selection {
36768
- gap: 1px;
36769
- button.o-button {
36770
- border-radius: 0;
36771
- &.selected {
36772
- color: ${GRAY_900};
36773
- border-color: ${ACTION_COLOR};
36774
- background: ${BADGE_SELECTED_COLOR};
36775
- font-weight: 600;
36776
- }
36777
-
36778
- &:first-child {
36779
- border-radius: 4px 0 0 4px;
36780
- }
36781
- &:last-child {
36782
- border-radius: 0 4px 4px 0;
36783
- }
36784
- }
36785
- }
36786
- `;
36787
- class BadgeSelection extends owl.Component {
36788
- static template = "o-spreadsheet.BadgeSelection";
36789
- static props = {
36790
- choices: Array,
36791
- onChange: Function,
36792
- selectedValue: String,
36793
- };
36794
- }
36795
-
36796
37108
  css /* scss */ `
36797
37109
  .o-chart-title-designer {
36798
37110
  > span {
@@ -36947,8 +37259,7 @@ class AxisDesignEditor extends owl.Component {
36947
37259
  this.props.updateChart(this.props.figureId, { axesDesign });
36948
37260
  }
36949
37261
  updateAxisEditor(ev) {
36950
- const axis = ev.target.value;
36951
- this.state.currentAxis = axis;
37262
+ this.state.currentAxis = ev.target.value;
36952
37263
  }
36953
37264
  getAxisTitle() {
36954
37265
  const axesDesign = this.props.definition.axesDesign ?? {};
@@ -36967,6 +37278,56 @@ class AxisDesignEditor extends owl.Component {
36967
37278
  }
36968
37279
  }
36969
37280
 
37281
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
37282
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
37283
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
37284
+ </svg>
37285
+ `;
37286
+ css /* scss */ `
37287
+ .o-round-color-picker-button {
37288
+ width: 18px;
37289
+ height: 18px;
37290
+ cursor: pointer;
37291
+ border: 1px solid ${GRAY_300};
37292
+ background-position: 1px 1px;
37293
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
37294
+ }
37295
+ `;
37296
+ class RoundColorPicker extends owl.Component {
37297
+ static template = "o-spreadsheet.RoundColorPicker";
37298
+ static components = { Section, ColorPicker };
37299
+ static props = {
37300
+ currentColor: { type: String, optional: true },
37301
+ title: { type: String, optional: true },
37302
+ onColorPicked: Function,
37303
+ };
37304
+ colorPickerButtonRef = owl.useRef("colorPickerButton");
37305
+ state;
37306
+ setup() {
37307
+ this.state = owl.useState({ pickerOpened: false });
37308
+ owl.useExternalListener(window, "click", this.closePicker);
37309
+ }
37310
+ closePicker() {
37311
+ this.state.pickerOpened = false;
37312
+ }
37313
+ togglePicker() {
37314
+ this.state.pickerOpened = !this.state.pickerOpened;
37315
+ }
37316
+ onColorPicked(color) {
37317
+ this.props.onColorPicked(color);
37318
+ this.state.pickerOpened = false;
37319
+ }
37320
+ get colorPickerAnchorRect() {
37321
+ const button = this.colorPickerButtonRef.el;
37322
+ return getBoundingRectAsPOJO(button);
37323
+ }
37324
+ get buttonStyle() {
37325
+ return cssPropertiesToCss({
37326
+ background: this.props.currentColor,
37327
+ });
37328
+ }
37329
+ }
37330
+
36970
37331
  class GeneralDesignEditor extends owl.Component {
36971
37332
  static template = "o-spreadsheet-GeneralDesignEditor";
36972
37333
  static components = {
@@ -37031,58 +37392,75 @@ class GeneralDesignEditor extends owl.Component {
37031
37392
  }
37032
37393
  }
37033
37394
 
37034
- class ChartWithAxisDesignPanel extends owl.Component {
37035
- static template = "o-spreadsheet-ChartWithAxisDesignPanel";
37395
+ const CIRCLE_SVG = /*xml*/ `
37396
+ <svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'>
37397
+ <circle r="2" fill="#FFF"/>
37398
+ </svg>
37399
+ `;
37400
+ css /* scss */ `
37401
+ .o-radio {
37402
+ input {
37403
+ appearance: none;
37404
+ -webkit-appearance: none;
37405
+ -moz-appearance: none;
37406
+ width: 14px;
37407
+ height: 14px;
37408
+ border: 1px solid ${GRAY_300};
37409
+ box-sizing: border-box;
37410
+ outline: none;
37411
+ border-radius: 8px;
37412
+
37413
+ &:checked {
37414
+ background: url("data:image/svg+xml,${encodeURIComponent(CIRCLE_SVG)}");
37415
+ background-color: ${ACTION_COLOR};
37416
+ border-color: ${ACTION_COLOR};
37417
+ }
37418
+ }
37419
+ }
37420
+ `;
37421
+ class RadioSelection extends owl.Component {
37422
+ static template = "o-spreadsheet.RadioSelection";
37423
+ static props = {
37424
+ choices: Array,
37425
+ onChange: Function,
37426
+ selectedValue: { optional: false },
37427
+ name: String,
37428
+ direction: { type: String, optional: true },
37429
+ };
37430
+ static defaultProps = {
37431
+ direction: "horizontal",
37432
+ };
37433
+ }
37434
+
37435
+ class SeriesDesignEditor extends owl.Component {
37436
+ static template = "o-spreadsheet-SeriesDesignEditor";
37036
37437
  static components = {
37037
- GeneralDesignEditor,
37038
37438
  SidePanelCollapsible,
37039
37439
  Section,
37040
- AxisDesignEditor,
37041
37440
  RoundColorPicker,
37042
- Checkbox,
37043
- RadioSelection,
37044
37441
  };
37045
37442
  static props = {
37046
37443
  figureId: String,
37047
37444
  definition: Object,
37048
- canUpdateChart: Function,
37049
37445
  updateChart: Function,
37446
+ canUpdateChart: Function,
37447
+ slots: { type: Object, optional: true },
37050
37448
  };
37051
- axisChoices = CHART_AXIS_CHOICES;
37052
37449
  state = owl.useState({ index: 0 });
37053
- get axesList() {
37054
- const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);
37055
- let axes = [{ id: "x", name: _t("Horizontal axis") }];
37056
- if (useLeftAxis) {
37057
- axes.push({ id: "y", name: useRightAxis ? _t("Left axis") : _t("Vertical axis") });
37058
- }
37059
- if (useRightAxis) {
37060
- axes.push({ id: "y1", name: useLeftAxis ? _t("Right axis") : _t("Vertical axis") });
37061
- }
37062
- return axes;
37063
- }
37064
- updateLegendPosition(ev) {
37065
- this.props.updateChart(this.props.figureId, {
37066
- legendPosition: ev.target.value,
37067
- });
37068
- }
37069
37450
  getDataSeries() {
37070
- return this.props.definition.dataSets.map((d, i) => d.label ?? `${ChartTerms.Series} ${i + 1}`);
37451
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);
37452
+ if (!runtime || !("chartJsConfig" in runtime)) {
37453
+ return [];
37454
+ }
37455
+ return runtime.chartJsConfig.data.datasets.map((d) => d.label);
37071
37456
  }
37072
37457
  updateSerieEditor(ev) {
37073
- const chartId = this.props.figureId;
37074
- const selectedIndex = ev.target.selectedIndex;
37075
- const runtime = this.env.model.getters.getChartRuntime(chartId);
37076
- if (!runtime) {
37077
- return;
37078
- }
37079
- this.state.index = selectedIndex;
37458
+ this.state.index = ev.target.selectedIndex;
37080
37459
  }
37081
37460
  updateDataSeriesColor(color) {
37082
- const dataSets = [...this.props.definition.dataSets];
37083
- if (!dataSets?.[this.state.index]) {
37461
+ const dataSets = this.props.definition.dataSets;
37462
+ if (!dataSets?.[this.state.index])
37084
37463
  return;
37085
- }
37086
37464
  dataSets[this.state.index] = {
37087
37465
  ...dataSets[this.state.index],
37088
37466
  backgroundColor: color,
@@ -37091,71 +37469,87 @@ class ChartWithAxisDesignPanel extends owl.Component {
37091
37469
  }
37092
37470
  getDataSerieColor() {
37093
37471
  const dataSets = this.props.definition.dataSets;
37094
- if (!dataSets?.[this.state.index]) {
37472
+ if (!dataSets?.[this.state.index])
37095
37473
  return "";
37096
- }
37097
37474
  const color = dataSets[this.state.index].backgroundColor;
37098
- return color ? toHex(color) : getNthColor(this.state.index, getColorsPalette(dataSets.length));
37475
+ return color
37476
+ ? toHex(color)
37477
+ : getNthColor(this.state.index, getColorsPalette(this.props.definition.dataSets.length));
37099
37478
  }
37100
- updateDataSeriesAxis(axis) {
37101
- const dataSets = [...this.props.definition.dataSets];
37102
- if (!dataSets?.[this.state.index]) {
37479
+ updateDataSeriesLabel(ev) {
37480
+ const label = ev.target.value;
37481
+ const dataSets = this.props.definition.dataSets;
37482
+ if (!dataSets?.[this.state.index])
37103
37483
  return;
37104
- }
37105
37484
  dataSets[this.state.index] = {
37106
37485
  ...dataSets[this.state.index],
37107
- yAxisId: axis === "left" ? "y" : "y1",
37486
+ label,
37108
37487
  };
37109
37488
  this.props.updateChart(this.props.figureId, { dataSets });
37110
37489
  }
37111
- getDataSerieAxis() {
37490
+ getDataSerieLabel() {
37112
37491
  const dataSets = this.props.definition.dataSets;
37113
- if (!dataSets?.[this.state.index]) {
37114
- return "left";
37115
- }
37116
- return dataSets[this.state.index].yAxisId === "y1" ? "right" : "left";
37117
- }
37118
- get canHaveTwoVerticalAxis() {
37119
- return "horizontal" in this.props.definition ? !this.props.definition.horizontal : true;
37492
+ return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
37120
37493
  }
37121
- updateDataSeriesLabel(ev) {
37122
- const label = ev.target.value;
37494
+ }
37495
+
37496
+ class SeriesWithAxisDesignEditor extends owl.Component {
37497
+ static template = "o-spreadsheet-SeriesWithAxisDesignEditor";
37498
+ static components = {
37499
+ SeriesDesignEditor,
37500
+ Checkbox,
37501
+ RadioSelection,
37502
+ Section,
37503
+ RoundColorPicker,
37504
+ };
37505
+ static props = {
37506
+ figureId: String,
37507
+ definition: Object,
37508
+ canUpdateChart: Function,
37509
+ updateChart: Function,
37510
+ slots: { type: Object, optional: true },
37511
+ };
37512
+ axisChoices = CHART_AXIS_CHOICES;
37513
+ updateDataSeriesAxis(index, axis) {
37123
37514
  const dataSets = [...this.props.definition.dataSets];
37124
- if (!dataSets?.[this.state.index]) {
37515
+ if (!dataSets?.[index]) {
37125
37516
  return;
37126
37517
  }
37127
- dataSets[this.state.index] = {
37128
- ...dataSets[this.state.index],
37129
- label,
37518
+ dataSets[index] = {
37519
+ ...dataSets[index],
37520
+ yAxisId: axis === "left" ? "y" : "y1",
37130
37521
  };
37131
37522
  this.props.updateChart(this.props.figureId, { dataSets });
37132
37523
  }
37133
- getDataSerieLabel() {
37524
+ getDataSerieAxis(index) {
37134
37525
  const dataSets = this.props.definition.dataSets;
37135
- return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
37526
+ if (!dataSets?.[index]) {
37527
+ return "left";
37528
+ }
37529
+ return dataSets[index].yAxisId === "y1" ? "right" : "left";
37136
37530
  }
37137
- updateShowValues(showValues) {
37138
- this.props.updateChart(this.props.figureId, { showValues });
37531
+ get canHaveTwoVerticalAxis() {
37532
+ return !("horizontal" in this.props.definition && this.props.definition.horizontal);
37139
37533
  }
37140
- toggleDataTrend(display) {
37534
+ toggleDataTrend(index, display) {
37141
37535
  const dataSets = [...this.props.definition.dataSets];
37142
- if (!dataSets?.[this.state.index]) {
37536
+ if (!dataSets?.[index]) {
37143
37537
  return;
37144
37538
  }
37145
- dataSets[this.state.index] = {
37146
- ...dataSets[this.state.index],
37539
+ dataSets[index] = {
37540
+ ...dataSets[index],
37147
37541
  trend: {
37148
37542
  type: "polynomial",
37149
37543
  order: 1,
37150
- ...dataSets[this.state.index].trend,
37544
+ ...dataSets[index].trend,
37151
37545
  display,
37152
37546
  },
37153
37547
  };
37154
37548
  this.props.updateChart(this.props.figureId, { dataSets });
37155
37549
  }
37156
- getTrendLineConfiguration() {
37550
+ getTrendLineConfiguration(index) {
37157
37551
  const dataSets = this.props.definition.dataSets;
37158
- return dataSets?.[this.state.index]?.trend;
37552
+ return dataSets?.[index]?.trend;
37159
37553
  }
37160
37554
  getTrendType(config) {
37161
37555
  if (!config) {
@@ -37163,7 +37557,7 @@ class ChartWithAxisDesignPanel extends owl.Component {
37163
37557
  }
37164
37558
  return config.type === "polynomial" && config.order === 1 ? "linear" : config.type;
37165
37559
  }
37166
- onChangeTrendType(ev) {
37560
+ onChangeTrendType(index, ev) {
37167
37561
  const type = ev.target.value;
37168
37562
  let config;
37169
37563
  switch (type) {
@@ -37176,37 +37570,59 @@ class ChartWithAxisDesignPanel extends owl.Component {
37176
37570
  break;
37177
37571
  case "exponential":
37178
37572
  case "logarithmic":
37573
+ case "trailingMovingAverage":
37179
37574
  config = { type };
37180
37575
  break;
37181
37576
  default:
37182
37577
  return;
37183
37578
  }
37184
- this.updateTrendLineValue(config);
37579
+ this.updateTrendLineValue(index, config);
37185
37580
  }
37186
- onChangePolynomialDegree(ev) {
37581
+ onChangePolynomialDegree(index, ev) {
37187
37582
  const element = ev.target;
37188
37583
  const order = parseInt(element.value || "1");
37189
37584
  if (order < 2) {
37190
- element.value = `${this.getTrendLineConfiguration()?.order ?? 2}`;
37585
+ element.value = `${this.getTrendLineConfiguration(index)?.order ?? 2}`;
37191
37586
  return;
37192
37587
  }
37193
- this.updateTrendLineValue({ order });
37588
+ this.updateTrendLineValue(index, { order });
37589
+ }
37590
+ get defaultWindowSize() {
37591
+ return DEFAULT_WINDOW_SIZE;
37592
+ }
37593
+ onChangeMovingAverageWindow(index, ev) {
37594
+ const element = ev.target;
37595
+ let window = parseInt(element.value) || DEFAULT_WINDOW_SIZE;
37596
+ if (window <= 1) {
37597
+ window = DEFAULT_WINDOW_SIZE;
37598
+ }
37599
+ this.updateTrendLineValue(index, { window });
37194
37600
  }
37195
- getTrendLineColor() {
37196
- return this.getTrendLineConfiguration()?.color ?? setColorAlpha(this.getDataSerieColor(), 0.5);
37601
+ getDataSerieColor(index) {
37602
+ const dataSets = this.props.definition.dataSets;
37603
+ if (!dataSets?.[index])
37604
+ return "";
37605
+ const color = dataSets[index].backgroundColor;
37606
+ return color
37607
+ ? toHex(color)
37608
+ : getNthColor(index, getColorsPalette(this.props.definition.dataSets.length));
37609
+ }
37610
+ getTrendLineColor(index) {
37611
+ return (this.getTrendLineConfiguration(index)?.color ??
37612
+ setColorAlpha(this.getDataSerieColor(index), 0.5));
37197
37613
  }
37198
- updateTrendLineColor(color) {
37199
- this.updateTrendLineValue({ color });
37614
+ updateTrendLineColor(index, color) {
37615
+ this.updateTrendLineValue(index, { color });
37200
37616
  }
37201
- updateTrendLineValue(config) {
37617
+ updateTrendLineValue(index, config) {
37202
37618
  const dataSets = [...this.props.definition.dataSets];
37203
- if (!dataSets?.[this.state.index]) {
37619
+ if (!dataSets?.[index]) {
37204
37620
  return;
37205
37621
  }
37206
- dataSets[this.state.index] = {
37207
- ...dataSets[this.state.index],
37622
+ dataSets[index] = {
37623
+ ...dataSets[index],
37208
37624
  trend: {
37209
- ...dataSets[this.state.index].trend,
37625
+ ...dataSets[index].trend,
37210
37626
  ...config,
37211
37627
  },
37212
37628
  };
@@ -37214,6 +37630,70 @@ class ChartWithAxisDesignPanel extends owl.Component {
37214
37630
  }
37215
37631
  }
37216
37632
 
37633
+ class ChartWithAxisDesignPanel extends owl.Component {
37634
+ static template = "o-spreadsheet-ChartWithAxisDesignPanel";
37635
+ static components = {
37636
+ GeneralDesignEditor,
37637
+ SidePanelCollapsible,
37638
+ Section,
37639
+ AxisDesignEditor,
37640
+ Checkbox,
37641
+ SeriesWithAxisDesignEditor,
37642
+ };
37643
+ static props = {
37644
+ figureId: String,
37645
+ definition: Object,
37646
+ canUpdateChart: Function,
37647
+ updateChart: Function,
37648
+ };
37649
+ get axesList() {
37650
+ const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);
37651
+ let axes = [{ id: "x", name: _t("Horizontal axis") }];
37652
+ if (useLeftAxis) {
37653
+ axes.push({ id: "y", name: useRightAxis ? _t("Left axis") : _t("Vertical axis") });
37654
+ }
37655
+ if (useRightAxis) {
37656
+ axes.push({ id: "y1", name: useLeftAxis ? _t("Right axis") : _t("Vertical axis") });
37657
+ }
37658
+ return axes;
37659
+ }
37660
+ updateLegendPosition(ev) {
37661
+ this.props.updateChart(this.props.figureId, {
37662
+ legendPosition: ev.target.value,
37663
+ });
37664
+ }
37665
+ }
37666
+
37667
+ class ComboChartDesignPanel extends ChartWithAxisDesignPanel {
37668
+ static template = "o-spreadsheet-ComboChartDesignPanel";
37669
+ static components = {
37670
+ ...ChartWithAxisDesignPanel.components,
37671
+ RadioSelection,
37672
+ };
37673
+ seriesTypeChoices = [
37674
+ { value: "bar", label: _t("Bar") },
37675
+ { value: "line", label: _t("Line") },
37676
+ ];
37677
+ updateDataSeriesType(index, type) {
37678
+ const dataSets = [...this.props.definition.dataSets];
37679
+ if (!dataSets?.[index]) {
37680
+ return;
37681
+ }
37682
+ dataSets[index] = {
37683
+ ...dataSets[index],
37684
+ type,
37685
+ };
37686
+ this.props.updateChart(this.props.figureId, { dataSets });
37687
+ }
37688
+ getDataSeriesType(index) {
37689
+ const dataSets = this.props.definition.dataSets;
37690
+ if (!dataSets?.[index]) {
37691
+ return "bar";
37692
+ }
37693
+ return dataSets[index].type ?? "line";
37694
+ }
37695
+ }
37696
+
37217
37697
  class GaugeChartConfigPanel extends owl.Component {
37218
37698
  static template = "o-spreadsheet-GaugeChartConfigPanel";
37219
37699
  static components = { ChartErrorSection, ChartDataSeries };
@@ -37393,11 +37873,6 @@ class LineConfigPanel extends GenericChartConfigPanel {
37393
37873
  stacked,
37394
37874
  });
37395
37875
  }
37396
- onUpdateAggregated(aggregated) {
37397
- this.props.updateChart(this.props.figureId, {
37398
- aggregated,
37399
- });
37400
- }
37401
37876
  onUpdateCumulative(cumulative) {
37402
37877
  this.props.updateChart(this.props.figureId, {
37403
37878
  cumulative,
@@ -37425,6 +37900,27 @@ class PieChartDesignPanel extends owl.Component {
37425
37900
  }
37426
37901
  }
37427
37902
 
37903
+ class RadarChartDesignPanel extends owl.Component {
37904
+ static template = "o-spreadsheet-RadarChartDesignPanel";
37905
+ static components = {
37906
+ GeneralDesignEditor,
37907
+ SeriesDesignEditor,
37908
+ Section,
37909
+ Checkbox,
37910
+ };
37911
+ static props = {
37912
+ figureId: String,
37913
+ definition: Object,
37914
+ canUpdateChart: Function,
37915
+ updateChart: Function,
37916
+ };
37917
+ updateLegendPosition(ev) {
37918
+ this.props.updateChart(this.props.figureId, {
37919
+ legendPosition: ev.target.value,
37920
+ });
37921
+ }
37922
+ }
37923
+
37428
37924
  class ScatterConfigPanel extends GenericChartConfigPanel {
37429
37925
  static template = "o-spreadsheet-ScatterConfigPanel";
37430
37926
  get canTreatLabelsAsText() {
@@ -37619,9 +38115,6 @@ class WaterfallChartDesignPanel extends owl.Component {
37619
38115
  verticalAxisPosition: value,
37620
38116
  });
37621
38117
  }
37622
- updateShowValues(showValues) {
37623
- this.props.updateChart(this.props.figureId, { showValues });
37624
- }
37625
38118
  }
37626
38119
 
37627
38120
  const chartSidePanelComponentRegistry = new Registry();
@@ -37640,7 +38133,7 @@ chartSidePanelComponentRegistry
37640
38133
  })
37641
38134
  .add("combo", {
37642
38135
  configuration: GenericChartConfigPanel,
37643
- design: ChartWithAxisDesignPanel,
38136
+ design: ComboChartDesignPanel,
37644
38137
  })
37645
38138
  .add("pie", {
37646
38139
  configuration: GenericChartConfigPanel,
@@ -37661,6 +38154,10 @@ chartSidePanelComponentRegistry
37661
38154
  .add("pyramid", {
37662
38155
  configuration: GenericChartConfigPanel,
37663
38156
  design: ChartWithAxisDesignPanel,
38157
+ })
38158
+ .add("radar", {
38159
+ configuration: GenericChartConfigPanel,
38160
+ design: RadarChartDesignPanel,
37664
38161
  });
37665
38162
 
37666
38163
  css /* scss */ `
@@ -37926,6 +38423,7 @@ class AbstractComposerStore extends SpreadsheetStore {
37926
38423
  "stopComposerRangeSelection",
37927
38424
  "cancelEdition",
37928
38425
  "cycleReferences",
38426
+ "toggleEditionMode",
37929
38427
  "changeComposerCursorSelection",
37930
38428
  "replaceComposerCursorSelection",
37931
38429
  ];
@@ -38081,6 +38579,42 @@ class AbstractComposerStore extends SpreadsheetStore {
38081
38579
  }
38082
38580
  this.setCurrentContent(updated.content, updated.selection);
38083
38581
  }
38582
+ toggleEditionMode() {
38583
+ if (this.editionMode === "inactive")
38584
+ return;
38585
+ const start = Math.min(this.selectionStart, this.selectionEnd);
38586
+ const end = Math.max(this.selectionStart, this.selectionEnd);
38587
+ const refToken = [...this.currentTokens]
38588
+ .reverse()
38589
+ .find((tk) => tk.end >= start && end >= tk.start && tk.type === "REFERENCE");
38590
+ if (this.editionMode === "editing" && refToken) {
38591
+ const currentSheetId = this.getters.getActiveSheetId();
38592
+ const { sheetName, xc } = splitReference(refToken.value);
38593
+ const sheetId = this.getters.getSheetIdByName(sheetName);
38594
+ if (sheetId && sheetId !== currentSheetId) {
38595
+ this.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: currentSheetId, sheetIdTo: sheetId });
38596
+ }
38597
+ // move cursor to the right part of the token
38598
+ this.selectionStart = this.selectionEnd = refToken.end;
38599
+ const zone = this.getters.getRangeFromSheetXC(this.sheetId, xc).zone;
38600
+ this.captureSelection(zone);
38601
+ this.editionMode = "selecting";
38602
+ }
38603
+ else {
38604
+ this.editionMode = "editing";
38605
+ }
38606
+ }
38607
+ captureSelection(zone, col, row) {
38608
+ this.model.selection.capture(this, {
38609
+ cell: { col: col || zone.left, row: row || zone.right },
38610
+ zone,
38611
+ }, {
38612
+ handleEvent: this.handleEvent.bind(this),
38613
+ release: () => {
38614
+ this._stopEdition();
38615
+ },
38616
+ });
38617
+ }
38084
38618
  isSelectionValid(length, start, end) {
38085
38619
  return start >= 0 && start <= length && end >= 0 && end <= length;
38086
38620
  }
@@ -38119,12 +38653,7 @@ class AbstractComposerStore extends SpreadsheetStore {
38119
38653
  this.setContent(str || this.initialContent, selection);
38120
38654
  this.colorIndexByRange = {};
38121
38655
  const zone = positionToZone({ col: this.col, row: this.row });
38122
- this.model.selection.capture(this, { cell: { col: this.col, row: this.row }, zone }, {
38123
- handleEvent: this.handleEvent.bind(this),
38124
- release: () => {
38125
- this._stopEdition();
38126
- },
38127
- });
38656
+ this.captureSelection(zone, col, row);
38128
38657
  }
38129
38658
  _stopEdition() {
38130
38659
  if (this.editionMode !== "inactive") {
@@ -38831,13 +39360,6 @@ class DOMDndHelper {
38831
39360
  return;
38832
39361
  this.edgeScrollIntervalId = window.setInterval(() => {
38833
39362
  const offset = direction * 3;
38834
- let newPosition = this.currentMousePosition + offset;
38835
- if (newPosition < Math.min(this.container.start, this.minPosition)) {
38836
- newPosition = Math.min(this.container.start, this.minPosition);
38837
- }
38838
- else if (newPosition > Math.max(this.container.end, this.maxPosition)) {
38839
- newPosition = Math.max(this.container.end, this.maxPosition);
38840
- }
38841
39363
  this.container.scroll += offset;
38842
39364
  }, 5);
38843
39365
  }
@@ -39014,7 +39536,6 @@ css /* scss */ `
39014
39536
  width: 142px;
39015
39537
  .o-cf-preview-description-rule {
39016
39538
  margin-bottom: 4px;
39017
- font-weight: 600;
39018
39539
  max-height: 2.8em;
39019
39540
  line-height: 1.4em;
39020
39541
  }
@@ -39486,7 +40007,7 @@ class ConditionalFormattingEditor extends owl.Component {
39486
40007
  setColorScaleColor(target, color) {
39487
40008
  const point = this.state.rules.colorScale[target];
39488
40009
  if (point) {
39489
- point.color = Number.parseInt(color.substr(1), 16);
40010
+ point.color = Number.parseInt(color.slice(1), 16);
39490
40011
  }
39491
40012
  this.closeMenus();
39492
40013
  }
@@ -39597,7 +40118,7 @@ class ConditionalFormattingEditor extends owl.Component {
39597
40118
  return [this.state.rules.dataBar.rangeValues || ""];
39598
40119
  }
39599
40120
  updateDataBarColor(color) {
39600
- this.state.rules.dataBar.color = Number.parseInt(color.substr(1), 16);
40121
+ this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
39601
40122
  }
39602
40123
  onDataBarRangeUpdate(ranges) {
39603
40124
  this.state.rules.dataBar.rangeValues = ranges[0];
@@ -41010,10 +41531,11 @@ class FindAndReplaceStore extends SpreadsheetStore {
41010
41531
  activeSheetMatches = [];
41011
41532
  specificRangeMatches = [];
41012
41533
  currentSearchRegex = null;
41013
- isSearchDirty = false;
41014
41534
  initialShowFormulaState;
41015
41535
  preserveSelectedMatchIndex = false;
41016
41536
  irreplaceableMatchCount = 0;
41537
+ isSearchDirty = false;
41538
+ shouldFinalizeUpdateSelection = false;
41017
41539
  notificationStore = this.get(NotificationStore);
41018
41540
  // fixme: why do we make selectedMatchIndex on top of a selected
41019
41541
  // property in the matches?
@@ -41059,10 +41581,13 @@ class FindAndReplaceStore extends SpreadsheetStore {
41059
41581
  this.updateSearchOptions({ searchFormulas: showFormula });
41060
41582
  }
41061
41583
  selectPreviousMatch() {
41062
- this.selectNextCell(Direction.previous);
41584
+ this.selectNextCell(Direction.previous, {
41585
+ jumpToMatchSheet: true,
41586
+ updateSelection: true,
41587
+ });
41063
41588
  }
41064
41589
  selectNextMatch() {
41065
- this.selectNextCell(Direction.next);
41590
+ this.selectNextCell(Direction.next, { jumpToMatchSheet: true, updateSelection: true });
41066
41591
  }
41067
41592
  handle(cmd) {
41068
41593
  switch (cmd.type) {
@@ -41079,8 +41604,11 @@ class FindAndReplaceStore extends SpreadsheetStore {
41079
41604
  case "ADD_COLUMNS_ROWS":
41080
41605
  case "EVALUATE_CELLS":
41081
41606
  case "UPDATE_CELL":
41607
+ this.isSearchDirty = true;
41608
+ break;
41082
41609
  case "ACTIVATE_SHEET":
41083
41610
  this.isSearchDirty = true;
41611
+ this.shouldFinalizeUpdateSelection = true;
41084
41612
  break;
41085
41613
  case "REPLACE_SEARCH":
41086
41614
  for (const match of cmd.matches) {
@@ -41095,7 +41623,11 @@ class FindAndReplaceStore extends SpreadsheetStore {
41095
41623
  }
41096
41624
  finalize() {
41097
41625
  if (this.isSearchDirty) {
41098
- this.refreshSearch(false);
41626
+ this.refreshSearch({
41627
+ jumpToMatchSheet: false,
41628
+ updateSelection: this.shouldFinalizeUpdateSelection,
41629
+ });
41630
+ this.shouldFinalizeUpdateSelection = false;
41099
41631
  this.isSearchDirty = false;
41100
41632
  }
41101
41633
  }
@@ -41122,17 +41654,17 @@ class FindAndReplaceStore extends SpreadsheetStore {
41122
41654
  }
41123
41655
  this.toSearch = toSearch;
41124
41656
  this.currentSearchRegex = getSearchRegex(this.toSearch, this.searchOptions);
41125
- this.refreshSearch();
41657
+ this.refreshSearch({ jumpToMatchSheet: true, updateSelection: true });
41126
41658
  }
41127
41659
  /**
41128
41660
  * refresh the matches according to the current search options
41129
41661
  */
41130
- refreshSearch(jumpToMatchSheet = true) {
41662
+ refreshSearch(options) {
41131
41663
  if (!this.preserveSelectedMatchIndex) {
41132
41664
  this.selectedMatchIndex = null;
41133
41665
  }
41134
41666
  this.findMatches();
41135
- this.selectNextCell(Direction.current, jumpToMatchSheet);
41667
+ this.selectNextCell(Direction.current, options);
41136
41668
  }
41137
41669
  getSheetsInSearchOrder() {
41138
41670
  switch (this.searchOptions.searchScope) {
@@ -41202,7 +41734,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
41202
41734
  * It is also used to keep coherence between the selected searchMatch
41203
41735
  * and selectedMatchIndex.
41204
41736
  */
41205
- selectNextCell(indexChange, jumpToMatchSheet = true) {
41737
+ selectNextCell(indexChange, options) {
41206
41738
  const matches = this.searchMatches;
41207
41739
  if (!matches.length) {
41208
41740
  this.selectedMatchIndex = null;
@@ -41228,7 +41760,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
41228
41760
  this.selectedMatchIndex = nextIndex;
41229
41761
  const selectedMatch = matches[nextIndex];
41230
41762
  // Switch to the sheet where the match is located
41231
- if (jumpToMatchSheet && this.getters.getActiveSheetId() !== selectedMatch.sheetId) {
41763
+ if (options.jumpToMatchSheet && this.getters.getActiveSheetId() !== selectedMatch.sheetId) {
41232
41764
  // We set `preserveSelectedMatchIndex` to true to avoid resetting the selected search
41233
41765
  // index in the `refreshSearch` function when a new sheet is activated. The reason being
41234
41766
  // that, when we automatically go back to previous sheet while performing a search, the
@@ -41244,7 +41776,9 @@ class FindAndReplaceStore extends SpreadsheetStore {
41244
41776
  }
41245
41777
  // we want grid selection to capture the selection stream
41246
41778
  this.model.selection.getBackToDefault();
41247
- this.model.selection.selectCell(selectedMatch.col, selectedMatch.row);
41779
+ if (options.updateSelection) {
41780
+ this.model.selection.selectCell(selectedMatch.col, selectedMatch.row);
41781
+ }
41248
41782
  }
41249
41783
  /**
41250
41784
  * Replace the value of the currently selected match
@@ -41259,7 +41793,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
41259
41793
  matches: [this.searchMatches[this.selectedMatchIndex]],
41260
41794
  searchOptions: this.searchOptions,
41261
41795
  });
41262
- this.selectNextCell(Direction.next);
41796
+ this.selectNextCell(Direction.next, { jumpToMatchSheet: true, updateSelection: true });
41263
41797
  }
41264
41798
  /**
41265
41799
  * Apply the replace function to all the matches one time.
@@ -41441,6 +41975,14 @@ class FindAndReplacePanel extends owl.Component {
41441
41975
  owl.onMounted(() => this.searchInput.el?.focus());
41442
41976
  owl.onWillUnmount(() => this.updateSearchContent.stopDebounce());
41443
41977
  this.updateSearchContent = debounce(this.store.updateSearchContent, 200);
41978
+ owl.useExternalListener(window, "keydown", (ev) => {
41979
+ const code = keyboardEventToShortcutString(ev);
41980
+ if (code === "Ctrl+F" || code === "Ctrl+H") {
41981
+ this.searchInput.el?.focus();
41982
+ ev.preventDefault();
41983
+ ev.stopPropagation();
41984
+ }
41985
+ }, { capture: true });
41444
41986
  }
41445
41987
  onFocusSearch() {
41446
41988
  this.updateDataRange();
@@ -42016,7 +42558,6 @@ function createMeasureAutoComplete(pivot, forComputedMeasure) {
42016
42558
  sequence: 0,
42017
42559
  autoSelectFirstProposal: true,
42018
42560
  getProposals(tokenAtCursor) {
42019
- // return []
42020
42561
  const measureProposals = pivot.measures
42021
42562
  .filter((m) => m !== forComputedMeasure)
42022
42563
  .map((measure) => {
@@ -42819,8 +43360,9 @@ const EMPTY_PIVOT_CELL = { type: "EMPTY" };
42819
43360
  * This function converts a list of data entry into a spreadsheet pivot table.
42820
43361
  */
42821
43362
  function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
43363
+ const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42822
43364
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
42823
- computeWidthOfColumnsNodes(columnsTree, definition.measures.length);
43365
+ computeWidthOfColumnsNodes(columnsTree, measureIds.length);
42824
43366
  const cols = columnsTreeToColumns(columnsTree, definition);
42825
43367
  const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);
42826
43368
  // Add the total row
@@ -42829,7 +43371,6 @@ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
42829
43371
  values: [],
42830
43372
  indent: 0,
42831
43373
  });
42832
- const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42833
43374
  const fieldsType = {};
42834
43375
  for (const columns of definition.columns) {
42835
43376
  fieldsType[columns.fieldName] = columns.type;
@@ -43590,20 +44131,22 @@ pivotRegistry.add("SPREADSHEET", {
43590
44131
  onIterationEndEvaluation: (pivot) => pivot.markAsDirtyForEvaluation(),
43591
44132
  dateGranularities: [...dateGranularities],
43592
44133
  datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
43593
- isMeasureCandidate: (field) => !["date", "boolean"].includes(field.type),
44134
+ isMeasureCandidate: (field) => !["datetime", "boolean"].includes(field.type),
43594
44135
  isGroupable: () => true,
43595
44136
  });
43596
44137
 
43597
44138
  class PivotSidePanelStore extends SpreadsheetStore {
43598
44139
  pivotId;
43599
44140
  mutators = ["reset", "deferUpdates", "applyUpdate", "discardPendingUpdate", "update"];
43600
- updatesAreDeferred = false;
44141
+ updatesAreDeferred;
43601
44142
  draft = null;
43602
44143
  notification = this.get(NotificationStore);
43603
44144
  alreadyNotified = false;
43604
44145
  constructor(get, pivotId) {
43605
44146
  super(get);
43606
44147
  this.pivotId = pivotId;
44148
+ this.updatesAreDeferred =
44149
+ this.getters.getPivotCoreDefinition(this.pivotId).deferUpdates ?? false;
43607
44150
  }
43608
44151
  handle(cmd) {
43609
44152
  switch (cmd.type) {
@@ -43691,10 +44234,14 @@ class PivotSidePanelStore extends SpreadsheetStore {
43691
44234
  this.draft = null;
43692
44235
  }
43693
44236
  deferUpdates(shouldDefer) {
43694
- this.updatesAreDeferred = shouldDefer;
43695
44237
  if (shouldDefer === false && this.draft) {
44238
+ this.draft.deferUpdates = false;
43696
44239
  this.applyUpdate();
43697
44240
  }
44241
+ else {
44242
+ this.update({ deferUpdates: shouldDefer });
44243
+ }
44244
+ this.updatesAreDeferred = shouldDefer;
43698
44245
  }
43699
44246
  applyUpdate() {
43700
44247
  if (this.draft) {
@@ -43952,7 +44499,7 @@ class RemoveDuplicatesPanel extends owl.Component {
43952
44499
  return colLabel;
43953
44500
  }
43954
44501
  get isEveryColumnSelected() {
43955
- return Object.values(this.state.columns).every((value) => value === true);
44502
+ return Object.values(this.state.columns).every((value) => value);
43956
44503
  }
43957
44504
  get errorMessages() {
43958
44505
  const cancelledReasons = this.env.model.canDispatch("REMOVE_DUPLICATES", {
@@ -44052,8 +44599,7 @@ class SettingsPanel extends owl.Component {
44052
44599
  const currentLocale = this.currentLocale;
44053
44600
  const localeInLoadedLocales = this.loadedLocales.find((l) => l.code === currentLocale.code);
44054
44601
  if (!localeInLoadedLocales) {
44055
- const locales = [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
44056
- return locales;
44602
+ return [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
44057
44603
  }
44058
44604
  else if (!deepEquals(currentLocale, localeInLoadedLocales)) {
44059
44605
  const index = this.loadedLocales.indexOf(localeInLoadedLocales);
@@ -45947,10 +46493,9 @@ class GridComposer extends owl.Component {
45947
46493
  });
45948
46494
  }
45949
46495
  get focus() {
45950
- const focus = this.composerFocusStore.activeComposer === this.composerInterface
46496
+ return this.composerFocusStore.activeComposer === this.composerInterface
45951
46497
  ? this.composerFocusStore.focusMode
45952
46498
  : "inactive";
45953
- return focus;
45954
46499
  }
45955
46500
  get composerProps() {
45956
46501
  const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
@@ -46119,13 +46664,10 @@ class GridCellIcon extends owl.Component {
46119
46664
  }
46120
46665
  }
46121
46666
 
46122
- const CHECKBOX_WIDTH = 15;
46123
46667
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
46124
46668
  css /* scss */ `
46125
46669
  .o-dv-checkbox {
46126
46670
  box-sizing: border-box !important;
46127
- width: ${CHECKBOX_WIDTH}px;
46128
- height: ${CHECKBOX_WIDTH}px;
46129
46671
  accent-color: #808080;
46130
46672
  margin: ${MARGIN}px;
46131
46673
  /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
@@ -46134,13 +46676,15 @@ css /* scss */ `
46134
46676
  `;
46135
46677
  class DataValidationCheckbox extends owl.Component {
46136
46678
  static template = "o-spreadsheet-DataValidationCheckbox";
46679
+ static components = {
46680
+ Checkbox,
46681
+ };
46137
46682
  static props = {
46138
46683
  cellPosition: Object,
46139
46684
  };
46140
- onCheckboxChange(ev) {
46141
- const newValue = ev.target.checked;
46685
+ onCheckboxChange(value) {
46142
46686
  const { sheetId, col, row } = this.props.cellPosition;
46143
- const cellContent = newValue ? "TRUE" : "FALSE";
46687
+ const cellContent = value ? "TRUE" : "FALSE";
46144
46688
  this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
46145
46689
  }
46146
46690
  get checkBoxValue() {
@@ -46954,7 +47498,12 @@ class GridAddRowsFooter extends owl.Component {
46954
47498
  class PaintFormatStore extends SpreadsheetStore {
46955
47499
  mutators = ["activate", "cancel", "pasteFormat"];
46956
47500
  highlightStore = this.get(HighlightStore);
46957
- cellClipboardHandler = new CellClipboardHandler(this.getters, this.model.dispatch);
47501
+ clipboardHandlers = [
47502
+ new CellClipboardHandler(this.getters, this.model.dispatch),
47503
+ new BorderClipboardHandler(this.getters, this.model.dispatch),
47504
+ new TableClipboardHandler(this.getters, this.model.dispatch),
47505
+ new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
47506
+ ];
46958
47507
  status = "inactive";
46959
47508
  copiedData;
46960
47509
  constructor(get) {
@@ -46964,6 +47513,13 @@ class PaintFormatStore extends SpreadsheetStore {
46964
47513
  this.highlightStore.unRegister(this);
46965
47514
  });
46966
47515
  }
47516
+ handle(cmd) {
47517
+ switch (cmd.type) {
47518
+ case "PAINT_FORMAT":
47519
+ this.paintFormat(cmd.sheetId, cmd.target);
47520
+ break;
47521
+ }
47522
+ }
46967
47523
  activate(args) {
46968
47524
  this.copiedData = this.copyFormats();
46969
47525
  this.status = args.persistent ? "persistent" : "oneOff";
@@ -46973,16 +47529,7 @@ class PaintFormatStore extends SpreadsheetStore {
46973
47529
  this.copiedData = undefined;
46974
47530
  }
46975
47531
  pasteFormat(target) {
46976
- if (this.copiedData) {
46977
- const sheetId = this.getters.getActiveSheetId();
46978
- this.cellClipboardHandler.paste({ zones: target, sheetId }, this.copiedData, {
46979
- isCutOperation: false,
46980
- pasteOption: "onlyFormat",
46981
- });
46982
- }
46983
- if (this.status === "oneOff") {
46984
- this.cancel();
46985
- }
47532
+ this.model.dispatch("PAINT_FORMAT", { target, sheetId: this.getters.getActiveSheetId() });
46986
47533
  }
46987
47534
  get isActive() {
46988
47535
  return this.status !== "inactive";
@@ -46990,7 +47537,24 @@ class PaintFormatStore extends SpreadsheetStore {
46990
47537
  copyFormats() {
46991
47538
  const sheetId = this.getters.getActiveSheetId();
46992
47539
  const zones = this.getters.getSelectedZones();
46993
- return this.cellClipboardHandler.copy(getClipboardDataPositions(sheetId, zones));
47540
+ const copiedData = {};
47541
+ for (const handler of this.clipboardHandlers) {
47542
+ Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones)));
47543
+ }
47544
+ return copiedData;
47545
+ }
47546
+ paintFormat(sheetId, target) {
47547
+ if (this.copiedData) {
47548
+ for (const handler of this.clipboardHandlers) {
47549
+ handler.paste({ zones: target, sheetId }, this.copiedData, {
47550
+ isCutOperation: false,
47551
+ pasteOption: "onlyFormat",
47552
+ });
47553
+ }
47554
+ }
47555
+ if (this.status === "oneOff") {
47556
+ this.cancel();
47557
+ }
46994
47558
  }
46995
47559
  get highlights() {
46996
47560
  const data = this.copiedData;
@@ -47368,7 +47932,7 @@ class AbstractResizer extends owl.Component {
47368
47932
  if (index < 0) {
47369
47933
  return;
47370
47934
  }
47371
- if (this.state.waitingForMove === true) {
47935
+ if (this.state.waitingForMove) {
47372
47936
  if (!this.env.model.getters.isGridSelectionActive()) {
47373
47937
  this._selectElement(index, false);
47374
47938
  }
@@ -48927,7 +49491,7 @@ class SidePanelStore extends SpreadsheetStore {
48927
49491
  }
48928
49492
  open(componentTag, panelProps = {}) {
48929
49493
  const state = this.computeState(componentTag, panelProps);
48930
- if (state.isOpen === false) {
49494
+ if (!state.isOpen) {
48931
49495
  return;
48932
49496
  }
48933
49497
  if (this.isOpen && componentTag !== this.componentTag) {
@@ -49266,6 +49830,8 @@ class Grid extends owl.Component {
49266
49830
  },
49267
49831
  "Ctrl+D": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ABOVE"),
49268
49832
  "Ctrl+R": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ON_LEFT"),
49833
+ "Ctrl+H": () => this.sidePanel.open("FindAndReplace", {}),
49834
+ "Ctrl+F": () => this.sidePanel.open("FindAndReplace", {}),
49269
49835
  "Ctrl+Shift+E": () => this.setHorizontalAlign("center"),
49270
49836
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
49271
49837
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
@@ -49673,31 +50239,6 @@ class Grid extends owl.Component {
49673
50239
  }
49674
50240
  }
49675
50241
 
49676
- /** @odoo-module */
49677
- class EditableName extends owl.Component {
49678
- static template = "o-spreadsheet-EditableName";
49679
- static props = {
49680
- name: String,
49681
- displayName: String,
49682
- onChanged: Function,
49683
- };
49684
- state;
49685
- setup() {
49686
- this.state = owl.useState({
49687
- isEditing: false,
49688
- name: "",
49689
- });
49690
- }
49691
- rename() {
49692
- this.state.isEditing = true;
49693
- this.state.name = this.props.name;
49694
- }
49695
- save() {
49696
- this.props.onChanged(this.state.name.trim());
49697
- this.state.isEditing = false;
49698
- }
49699
- }
49700
-
49701
50242
  /**
49702
50243
  * BasePlugin
49703
50244
  *
@@ -49798,12 +50339,10 @@ class BasePlugin {
49798
50339
  */
49799
50340
  class CorePlugin extends BasePlugin {
49800
50341
  getters;
49801
- uuidGenerator;
49802
- constructor({ getters, stateObserver, range, dispatch, canDispatch, uuidGenerator, }) {
50342
+ constructor({ getters, stateObserver, range, dispatch, canDispatch }) {
49803
50343
  super(stateObserver, dispatch, canDispatch);
49804
50344
  range.addRangeProvider(this.adaptRanges.bind(this));
49805
50345
  this.getters = getters;
49806
- this.uuidGenerator = uuidGenerator;
49807
50346
  }
49808
50347
  // ---------------------------------------------------------------------------
49809
50348
  // Import/Export
@@ -53617,7 +54156,7 @@ class SheetPlugin extends CorePlugin {
53617
54156
  this.sheetIdsMapName[sheet.name] = sheet.id;
53618
54157
  }
53619
54158
  for (let sheetData of data.sheets) {
53620
- const name = sheetData.name || _t("Sheet") + (Object.keys(this.sheets).length + 1);
54159
+ const name = sheetData.name || "Sheet" + (Object.keys(this.sheets).length + 1);
53621
54160
  const { colNumber, rowNumber } = this.getImportedSheetSize(sheetData);
53622
54161
  const sheet = {
53623
54162
  id: sheetData.id,
@@ -54301,6 +54840,7 @@ class SheetPlugin extends CorePlugin {
54301
54840
  }
54302
54841
  }
54303
54842
 
54843
+ let nextTableId = 1;
54304
54844
  class TablePlugin extends CorePlugin {
54305
54845
  static getters = ["getCoreTable", "getCoreTables", "getCoreTableMatchingTopLeft"];
54306
54846
  tables = {};
@@ -54368,7 +54908,7 @@ class TablePlugin extends CorePlugin {
54368
54908
  const union = this.getters.getRangesUnion(ranges);
54369
54909
  const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
54370
54910
  this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
54371
- const id = this.uuidGenerator.uuidv4();
54911
+ const id = `${nextTableId++}`;
54372
54912
  const config = cmd.config || DEFAULT_TABLE_CONFIG;
54373
54913
  const newTable = cmd.tableType === "dynamic"
54374
54914
  ? this.createDynamicTable(id, union, config)
@@ -54521,7 +55061,7 @@ class TablePlugin extends CorePlugin {
54521
55061
  filters = [];
54522
55062
  for (const i of range(zone.left, zone.right + 1)) {
54523
55063
  const filterZone = { ...zone, left: i, right: i };
54524
- const uid = this.uuidGenerator.uuidv4();
55064
+ const uid = `${nextTableId++}`;
54525
55065
  filters.push(this.createFilterFromZone(uid, tableRange.sheetId, filterZone, config));
54526
55066
  }
54527
55067
  }
@@ -54586,7 +55126,7 @@ class TablePlugin extends CorePlugin {
54586
55126
  ? table.filters.find((f) => f.col === i)
54587
55127
  : undefined;
54588
55128
  const filterZone = { ...tableZone, left: i, right: i };
54589
- const filterId = oldFilter?.id || this.uuidGenerator.uuidv4();
55129
+ const filterId = oldFilter?.id || `${nextTableId++}`;
54590
55130
  filters.push(this.createFilterFromZone(filterId, tableRange.sheetId, filterZone, config));
54591
55131
  }
54592
55132
  }
@@ -54687,7 +55227,7 @@ class TablePlugin extends CorePlugin {
54687
55227
  if (filters.length < zoneToDimension(tableZone).numberOfCols) {
54688
55228
  for (let col = tableZone.left; col <= tableZone.right; col++) {
54689
55229
  if (!filters.find((filter) => filter.col === col)) {
54690
- const uid = this.uuidGenerator.uuidv4();
55230
+ const uid = `${nextTableId++}`;
54691
55231
  const filterZone = { ...tableZone, left: col, right: col };
54692
55232
  filters.push(this.createFilterFromZone(uid, sheetId, filterZone, table.config));
54693
55233
  }
@@ -54703,7 +55243,7 @@ class TablePlugin extends CorePlugin {
54703
55243
  import(data) {
54704
55244
  for (const sheet of data.sheets) {
54705
55245
  for (const tableData of sheet.tables || []) {
54706
- const uuid = this.uuidGenerator.uuidv4();
55246
+ const uuid = `${nextTableId++}`;
54707
55247
  const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;
54708
55248
  const range = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
54709
55249
  const tableType = tableData.type || "static";
@@ -55235,7 +55775,7 @@ class PivotCorePlugin extends CorePlugin {
55235
55775
  case "DUPLICATE_PIVOT": {
55236
55776
  const { pivotId, newPivotId } = cmd;
55237
55777
  const pivot = deepCopy(this.getPivotCore(pivotId).definition);
55238
- pivot.name = _t("%s (copy)", pivot.name);
55778
+ pivot.name = cmd.duplicatedPivotName ?? pivot.name + " (copy)";
55239
55779
  this.addPivot(newPivotId, pivot);
55240
55780
  break;
55241
55781
  }
@@ -55275,7 +55815,7 @@ class PivotCorePlugin extends CorePlugin {
55275
55815
  return `(#${formulaId}) ${this.getPivotName(pivotId)}`;
55276
55816
  }
55277
55817
  getPivotName(pivotId) {
55278
- return _t(this.getPivotCore(pivotId).definition.name);
55818
+ return this.getPivotCore(pivotId).definition.name;
55279
55819
  }
55280
55820
  /**
55281
55821
  * Returns the pivot core definition of the pivot with the given id.
@@ -56917,7 +57457,7 @@ class Evaluator {
56917
57457
  cellsToCompute.addMany(arrayFormulasPositions);
56918
57458
  cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
56919
57459
  this.evaluate(cellsToCompute);
56920
- console.info("evaluate Cells", performance.now() - start, "ms");
57460
+ console.debug("evaluate Cells", performance.now() - start, "ms");
56921
57461
  }
56922
57462
  getArrayFormulasImpactedByChangesOf(positions) {
56923
57463
  const impactedPositions = this.createEmptyPositionSet();
@@ -56961,7 +57501,7 @@ class Evaluator {
56961
57501
  const start = performance.now();
56962
57502
  this.evaluatedCells = new PositionMap();
56963
57503
  this.evaluate(this.getAllCells());
56964
- console.info("evaluate all cells", performance.now() - start, "ms");
57504
+ console.debug("evaluate all cells", performance.now() - start, "ms");
56965
57505
  }
56966
57506
  evaluateFormulaResult(sheetId, formulaString) {
56967
57507
  const compiledFormula = compile(formulaString);
@@ -57967,8 +58507,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
57967
58507
  .map((cell) => cell.value);
57968
58508
  switch (threshold.type) {
57969
58509
  case "value":
57970
- const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
57971
- return result;
58510
+ return functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
57972
58511
  case "number":
57973
58512
  return Number(threshold.value);
57974
58513
  case "percentage":
@@ -59052,7 +59591,7 @@ function withPivotPresentationLayer (PivotClass) {
59052
59591
  const ranking = {};
59053
59592
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59054
59593
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59055
- let pivotCells = this.getPivotValueCells();
59594
+ let pivotCells = this.getPivotValueCells(measure.id);
59056
59595
  if (mainDimension === "column") {
59057
59596
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59058
59597
  // This means that we need to transpose back the ranking at the end
@@ -59096,7 +59635,7 @@ function withPivotPresentationLayer (PivotClass) {
59096
59635
  const cellsRunningTotals = {};
59097
59636
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59098
59637
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59099
- let pivotCells = this.getPivotValueCells();
59638
+ let pivotCells = this.getPivotValueCells(measure.id);
59100
59639
  if (mainDimension === "column") {
59101
59640
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59102
59641
  // This means that we need to transpose back the totals at the end
@@ -59172,13 +59711,12 @@ function withPivotPresentationLayer (PivotClass) {
59172
59711
  throw new NotAvailableError();
59173
59712
  }
59174
59713
  const comparedValue = this._getPivotCellValueAndFormat(measure.id, comparedDomain);
59175
- const comparedValueNumber = this.strictMeasureValueToNumber(comparedValue);
59176
- return comparedValueNumber;
59714
+ return this.strictMeasureValueToNumber(comparedValue);
59177
59715
  }
59178
- getPivotValueCells() {
59716
+ getPivotValueCells(measureId) {
59179
59717
  return this.getTableStructure()
59180
59718
  .getPivotCells()
59181
- .map((col) => col.filter((cell) => cell.type === "VALUE"))
59719
+ .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
59182
59720
  .filter((col) => col.length > 0);
59183
59721
  }
59184
59722
  measureValueToNumber(result) {
@@ -60707,7 +61245,7 @@ class Session extends EventBus {
60707
61245
  this.onMessageReceived(message);
60708
61246
  }
60709
61247
  this.isReplayingInitialRevisions = false;
60710
- console.info("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
61248
+ console.debug("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
60711
61249
  }
60712
61250
  /**
60713
61251
  * Notify the server that the user client left the collaborative session
@@ -60879,7 +61417,6 @@ class Session extends EventBus {
60879
61417
  if (this.waitingAck) {
60880
61418
  return;
60881
61419
  }
60882
- this.waitingAck = true;
60883
61420
  this.sendPendingMessage();
60884
61421
  }
60885
61422
  /**
@@ -60916,6 +61453,7 @@ class Session extends EventBus {
60916
61453
  throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.
60917
61454
  ${JSON.stringify(message)}`);
60918
61455
  }
61456
+ this.waitingAck = true;
60919
61457
  this.transportService.sendMessage({
60920
61458
  ...message,
60921
61459
  serverRevisionId: this.serverRevisionId,
@@ -60946,6 +61484,7 @@ class Session extends EventBus {
60946
61484
  case "REMOTE_REVISION":
60947
61485
  case "REVISION_REDONE":
60948
61486
  case "REVISION_UNDONE":
61487
+ case "SNAPSHOT_CREATED":
60949
61488
  return this.processedRevisions.has(message.nextRevisionId);
60950
61489
  default:
60951
61490
  return false;
@@ -61074,8 +61613,7 @@ class CollaborativePlugin extends UIPlugin {
61074
61613
  }
61075
61614
  const color = client.color;
61076
61615
  /* Cell background */
61077
- const cellBackgroundColor = `${color}10`;
61078
- ctx.fillStyle = cellBackgroundColor;
61616
+ ctx.fillStyle = `${color}10`;
61079
61617
  ctx.lineWidth = 4 * thinLineWidth;
61080
61618
  ctx.strokeStyle = color;
61081
61619
  ctx.globalCompositeOperation = "multiply";
@@ -61442,8 +61980,7 @@ class HeaderVisibilityUIPlugin extends UIPlugin {
61442
61980
  exportForExcel(data) {
61443
61981
  for (const sheetData of data.sheets) {
61444
61982
  for (const [row, rowData] of Object.entries(sheetData.rows)) {
61445
- const isHidden = this.isRowHidden(sheetData.id, Number(row));
61446
- rowData.isHidden = isHidden;
61983
+ rowData.isHidden = this.isRowHidden(sheetData.id, Number(row));
61447
61984
  }
61448
61985
  }
61449
61986
  }
@@ -61503,12 +62040,13 @@ class InsertPivotPlugin extends UIPlugin {
61503
62040
  this.dispatch("DUPLICATE_PIVOT", {
61504
62041
  pivotId,
61505
62042
  newPivotId,
62043
+ duplicatedPivotName: _t("%s (copy)", this.getters.getPivotCoreDefinition(pivotId).name),
61506
62044
  });
61507
62045
  const activeSheetId = this.getters.getActiveSheetId();
61508
62046
  const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;
61509
62047
  const formulaId = this.getters.getPivotFormulaId(newPivotId);
61510
62048
  const newPivotName = this.getters.getPivotName(newPivotId);
61511
- this.dispatch("CREATE_SHEET", {
62049
+ const result = this.dispatch("CREATE_SHEET", {
61512
62050
  sheetId: newSheetId,
61513
62051
  name: this.getPivotDuplicateSheetName(_t("%(newPivotName)s (Pivot #%(formulaId)s)", {
61514
62052
  newPivotName,
@@ -61516,20 +62054,19 @@ class InsertPivotPlugin extends UIPlugin {
61516
62054
  })),
61517
62055
  position,
61518
62056
  });
61519
- this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61520
- this.dispatch("UPDATE_CELL", {
61521
- sheetId: newSheetId,
61522
- col: 0,
61523
- row: 0,
61524
- content: `=PIVOT(${formulaId})`,
61525
- });
62057
+ if (result.isSuccessful) {
62058
+ this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
62059
+ const pivot = this.getters.getPivot(pivotId);
62060
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
62061
+ }
61526
62062
  }
61527
62063
  getPivotDuplicateSheetName(pivotName) {
61528
62064
  let i = 1;
61529
62065
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
61530
- let name = pivotName;
62066
+ const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
62067
+ let name = sanitizedName;
61531
62068
  while (names.includes(name)) {
61532
- name = `${pivotName} (${i})`;
62069
+ name = `${sanitizedName} (${i})`;
61533
62070
  i++;
61534
62071
  }
61535
62072
  return name;
@@ -61968,7 +62505,6 @@ class SheetUIPlugin extends UIPlugin {
61968
62505
  if (!isEqual(zone, newZone)) {
61969
62506
  hasExpanded = true;
61970
62507
  zone = newZone;
61971
- continue;
61972
62508
  }
61973
62509
  } while (hasExpanded);
61974
62510
  return zone;
@@ -63061,7 +63597,7 @@ class ClipboardPlugin extends UIPlugin {
63061
63597
  case "ADD_COLUMNS_ROWS": {
63062
63598
  this.status = "invisible";
63063
63599
  // If we add a col/row inside or before the cut area, we invalidate the clipboard
63064
- if (this._isCutOperation !== true || cmd.sheetId !== this.copiedData?.sheetId) {
63600
+ if (!this._isCutOperation || cmd.sheetId !== this.copiedData?.sheetId) {
63065
63601
  return;
63066
63602
  }
63067
63603
  const isClipboardDirty = this.isColRowDirtyingClipboard(cmd.position === "before" ? cmd.base : cmd.base + 1, cmd.dimension);
@@ -63073,7 +63609,7 @@ class ClipboardPlugin extends UIPlugin {
63073
63609
  case "REMOVE_COLUMNS_ROWS": {
63074
63610
  this.status = "invisible";
63075
63611
  // If we remove a col/row inside or before the cut area, we invalidate the clipboard
63076
- if (this._isCutOperation !== true || cmd.sheetId !== this.copiedData?.sheetId) {
63612
+ if (!this._isCutOperation || cmd.sheetId !== this.copiedData?.sheetId) {
63077
63613
  return;
63078
63614
  }
63079
63615
  for (let el of cmd.elements) {
@@ -63095,7 +63631,7 @@ class ClipboardPlugin extends UIPlugin {
63095
63631
  break;
63096
63632
  }
63097
63633
  case "DELETE_SHEET":
63098
- if (this._isCutOperation !== true) {
63634
+ if (!this._isCutOperation) {
63099
63635
  return;
63100
63636
  }
63101
63637
  if (this.originSheetId === cmd.sheetId) {
@@ -63988,8 +64524,7 @@ class GridSelectionPlugin extends UIPlugin {
63988
64524
  this.setSelectionMixin({ zone, cell: { col, row } }, [zone]);
63989
64525
  }
63990
64526
  setActiveSheet(id) {
63991
- const sheet = this.getters.getSheet(id);
63992
- this.activeSheet = sheet;
64527
+ this.activeSheet = this.getters.getSheet(id);
63993
64528
  }
63994
64529
  activateNextSheet(direction) {
63995
64530
  const sheetIds = this.getters.getSheetIds();
@@ -64683,9 +65218,6 @@ class SheetViewPlugin extends UIPlugin {
64683
65218
  case "UNFREEZE_COLUMNS_ROWS":
64684
65219
  this.resetViewports(this.getters.getActiveSheetId());
64685
65220
  break;
64686
- case "DELETE_SHEET":
64687
- this.sheetsWithDirtyViewports.delete(cmd.sheetId);
64688
- break;
64689
65221
  case "SCROLL_TO_CELL":
64690
65222
  this.refreshViewport(this.getters.getActiveSheetId(), { col: cmd.col, row: cmd.row });
64691
65223
  break;
@@ -66637,10 +67169,9 @@ css /* scss */ `
66637
67169
  user-select: none;
66638
67170
  color: ${TEXT_BODY};
66639
67171
 
66640
- .o-heading-3 {
67172
+ .o-sidePanelTitle {
66641
67173
  line-height: 20px;
66642
67174
  font-size: 16px;
66643
- font-weight: 600;
66644
67175
  }
66645
67176
 
66646
67177
  .o-sidePanelHeader {
@@ -66725,6 +67256,10 @@ css /* scss */ `
66725
67256
  }
66726
67257
  }
66727
67258
  }
67259
+
67260
+ .o-fw-bold {
67261
+ font-weight: 500;
67262
+ }
66728
67263
  `;
66729
67264
  class SidePanel extends owl.Component {
66730
67265
  static template = "o-spreadsheet-SidePanel";
@@ -67556,8 +68091,7 @@ class WebClipboardWrapper {
67556
68091
  for (const item of clipboardItems) {
67557
68092
  for (const type of item.types) {
67558
68093
  const blob = await item.getType(type);
67559
- const text = await blob.text();
67560
- clipboardContent[type] = text;
68094
+ clipboardContent[type] = await blob.text();
67561
68095
  }
67562
68096
  }
67563
68097
  return { status: "ok", content: clipboardContent };
@@ -67869,7 +68403,6 @@ class Spreadsheet extends owl.Component {
67869
68403
  spreadsheetRef = owl.useRef("spreadsheet");
67870
68404
  spreadsheetRect = useSpreadsheetRect();
67871
68405
  _focusGrid;
67872
- keyDownMapping;
67873
68406
  isViewportTooSmall = false;
67874
68407
  notificationStore;
67875
68408
  composerFocusStore;
@@ -67893,10 +68426,6 @@ class Spreadsheet extends owl.Component {
67893
68426
  this.notificationStore = useStore(NotificationStore);
67894
68427
  this.composerFocusStore = useStore(ComposerFocusStore);
67895
68428
  this.sidePanel = useStore(SidePanelStore);
67896
- this.keyDownMapping = {
67897
- "CTRL+H": () => this.sidePanel.toggle("FindAndReplace", {}),
67898
- "CTRL+F": () => this.sidePanel.toggle("FindAndReplace", {}),
67899
- };
67900
68429
  const fileStore = this.model.config.external.fileStore;
67901
68430
  owl.useSubEnv({
67902
68431
  model: this.model,
@@ -67996,20 +68525,6 @@ class Spreadsheet extends owl.Component {
67996
68525
  }
67997
68526
  this._focusGrid();
67998
68527
  }
67999
- onKeydown(ev) {
68000
- let keyDownString = "";
68001
- if (isCtrlKey(ev)) {
68002
- keyDownString += "CTRL+";
68003
- }
68004
- keyDownString += ev.key.toUpperCase();
68005
- let handler = this.keyDownMapping[keyDownString];
68006
- if (handler) {
68007
- ev.preventDefault();
68008
- ev.stopPropagation();
68009
- handler();
68010
- return;
68011
- }
68012
- }
68013
68528
  get gridHeight() {
68014
68529
  const { height } = this.env.model.getters.getSheetViewDimension();
68015
68530
  return height;
@@ -69424,10 +69939,10 @@ class SelectionStreamProcessorImpl {
69424
69939
  getNextCellPosition(currentPosition, dimension, direction) {
69425
69940
  const dimOfInterest = dimension === "cols" ? "col" : "row";
69426
69941
  const startingPosition = { ...currentPosition };
69427
- const nextCoord = dimension === "cols"
69428
- ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)
69429
- : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);
69430
- startingPosition[dimOfInterest] = nextCoord;
69942
+ startingPosition[dimOfInterest] =
69943
+ dimension === "cols"
69944
+ ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)
69945
+ : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);
69431
69946
  return { col: startingPosition.col, row: startingPosition.row };
69432
69947
  }
69433
69948
  getPosition() {
@@ -69551,6 +70066,8 @@ function createChart(chart, chartSheetIndex, data) {
69551
70066
  case "pie":
69552
70067
  plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });
69553
70068
  break;
70069
+ case "radar":
70070
+ plot = addRadarChart(chart.data);
69554
70071
  }
69555
70072
  let position = "t";
69556
70073
  switch (chart.data.legendPosition) {
@@ -70009,6 +70526,53 @@ function addScatterChart(chart) {
70009
70526
  `
70010
70527
  : ""}`;
70011
70528
  }
70529
+ function addRadarChart(chart) {
70530
+ const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? "");
70531
+ const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);
70532
+ const dataSetsNodes = [];
70533
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
70534
+ const color = toXlsxHexColor(colors.next());
70535
+ const dataShapeProperty = shapeProperty({
70536
+ line: {
70537
+ width: 2.5,
70538
+ style: "solid",
70539
+ color,
70540
+ },
70541
+ });
70542
+ const dataSetNode = escapeXml /*xml*/ `
70543
+ <c:ser>
70544
+ <c:idx val="${dsIndex}"/>
70545
+ <c:order val="${dsIndex}"/>
70546
+ <c:smooth val="0"/>
70547
+ <c:marker>
70548
+ <c:symbol val="circle" />
70549
+ <c:size val="5"/>
70550
+ ${shapeProperty({ backgroundColor: color, line: { color } })}
70551
+ </c:marker>
70552
+ ${extractDataSetLabel(dataset.label)}
70553
+ ${dataShapeProperty}
70554
+ ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
70555
+ <c:val> <!-- x-coordinate values -->
70556
+ ${numberRef(dataset.range)}
70557
+ </c:val>
70558
+ </c:ser>
70559
+ `;
70560
+ dataSetsNodes.push(dataSetNode);
70561
+ }
70562
+ return escapeXml /*xml*/ `
70563
+ ${escapeXml /*xml*/ `
70564
+ <c:radarChart>
70565
+ <c:radarStyle val="marker"/>
70566
+ <c:varyColors val="0"/>
70567
+ ${joinXmlNodes(dataSetsNodes)}
70568
+ <c:axId val="${catAxId}" />
70569
+ <c:axId val="${valAxId}" />
70570
+ </c:radarChart>
70571
+ ${addAx("b", "c:catAx", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}
70572
+ ${addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}
70573
+ `}
70574
+ `;
70575
+ }
70012
70576
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
70013
70577
  const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
70014
70578
  const colors = new ColorGenerator(maxLength);
@@ -70882,6 +71446,15 @@ function addTableColumns(table, sheetData) {
70882
71446
  ["id", i + 1], // id cannot be 0
70883
71447
  ["name", colName],
70884
71448
  ];
71449
+ if (table.config.totalRow) {
71450
+ // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag
71451
+ // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.
71452
+ const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);
71453
+ const colTotalContent = sheetData.cells[colTotalXc]?.content;
71454
+ if (colTotalContent?.startsWith("=")) {
71455
+ colAttributes.push(["totalsRowFunction", "custom"]);
71456
+ }
71457
+ }
70885
71458
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
70886
71459
  }
70887
71460
  return escapeXml /*xml*/ `
@@ -70976,8 +71549,9 @@ function addRows(construct, data, sheet) {
70976
71549
  }
70977
71550
  else if (cell.content && cell.content !== "") {
70978
71551
  const isTableHeader = isCellTableHeader(c, r, sheet);
71552
+ const isTableTotal = isCellTableTotal(c, r, sheet);
70979
71553
  const isPlainText = !!(cell.format && isTextFormat(data.formats[cell.format]));
70980
- ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isPlainText));
71554
+ ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));
70981
71555
  }
70982
71556
  attributes.push(...additionalAttrs);
70983
71557
  // prettier-ignore
@@ -71011,6 +71585,16 @@ function isCellTableHeader(col, row, sheet) {
71011
71585
  return isInside(col, row, headerZone);
71012
71586
  });
71013
71587
  }
71588
+ function isCellTableTotal(col, row, sheet) {
71589
+ return sheet.tables.some((table) => {
71590
+ if (!table.config.totalRow) {
71591
+ return false;
71592
+ }
71593
+ const zone = toZone(table.range);
71594
+ const totalZone = { ...zone, top: zone.bottom };
71595
+ return isInside(col, row, totalZone);
71596
+ });
71597
+ }
71014
71598
  function addHyperlinks(construct, data, sheetIndex) {
71015
71599
  const sheet = data.sheets[sheetIndex];
71016
71600
  const cells = sheet.cells;
@@ -71096,25 +71680,23 @@ function addSheetViews(sheet) {
71096
71680
  ["showGridLines", sheet.areGridLinesVisible ? 1 : 0],
71097
71681
  ["workbookViewId", 0],
71098
71682
  ];
71099
- let sheetView = escapeXml /*xml*/ `
71683
+ return escapeXml /*xml*/ `
71100
71684
  <sheetViews>
71101
71685
  <sheetView ${formatAttributes(sheetViewAttrs)}>
71102
71686
  ${splitPanes}
71103
71687
  </sheetView>
71104
71688
  </sheetViews>
71105
71689
  `;
71106
- return sheetView;
71107
71690
  }
71108
71691
  function addSheetProperties(sheet) {
71109
71692
  if (!sheet.color) {
71110
71693
  return "";
71111
71694
  }
71112
- let sheetView = escapeXml /*xml*/ `
71695
+ return escapeXml /*xml*/ `
71113
71696
  <sheetPr>
71114
71697
  <tabColor ${formatAttributes([["rgb", toXlsxHexColor(sheet.color)]])} />
71115
71698
  </sheetPr>
71116
71699
  `;
71117
- return sheetView;
71118
71700
  }
71119
71701
 
71120
71702
  /**
@@ -71427,9 +72009,7 @@ var Status;
71427
72009
  })(Status || (Status = {}));
71428
72010
  class Model extends EventBus {
71429
72011
  corePlugins = [];
71430
- featurePlugins = [];
71431
72012
  statefulUIPlugins = [];
71432
- coreViewsPlugins = [];
71433
72013
  range;
71434
72014
  session;
71435
72015
  /**
@@ -71474,7 +72054,7 @@ class Model extends EventBus {
71474
72054
  coreHandlers = [];
71475
72055
  constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = false) {
71476
72056
  const start = performance.now();
71477
- console.group("Model creation");
72057
+ console.debug("##### Model creation #####");
71478
72058
  super();
71479
72059
  setDefaultTranslationMethod();
71480
72060
  stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);
@@ -71501,7 +72081,6 @@ class Model extends EventBus {
71501
72081
  isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
71502
72082
  isDashboard: () => this.config.mode === "dashboard",
71503
72083
  };
71504
- this.uuidGenerator.setIsFastStrategy(true);
71505
72084
  // Initiate stream processor
71506
72085
  this.selection = new SelectionStreamProcessorImpl(this.getters);
71507
72086
  this.coreHandlers.push(this.range);
@@ -71516,7 +72095,6 @@ class Model extends EventBus {
71516
72095
  this.session.loadInitialMessages(stateUpdateMessages);
71517
72096
  for (let Plugin of coreViewsPluginRegistry.getAll()) {
71518
72097
  const plugin = this.setupUiPlugin(Plugin);
71519
- this.coreViewsPlugins.push(plugin);
71520
72098
  this.handlers.push(plugin);
71521
72099
  this.uiHandlers.push(plugin);
71522
72100
  this.coreHandlers.push(plugin);
@@ -71529,11 +72107,9 @@ class Model extends EventBus {
71529
72107
  }
71530
72108
  for (let Plugin of featurePluginRegistry.getAll()) {
71531
72109
  const plugin = this.setupUiPlugin(Plugin);
71532
- this.featurePlugins.push(plugin);
71533
72110
  this.handlers.push(plugin);
71534
72111
  this.uiHandlers.push(plugin);
71535
72112
  }
71536
- this.uuidGenerator.setIsFastStrategy(false);
71537
72113
  // starting plugins
71538
72114
  this.dispatch("START");
71539
72115
  // Model should be the last permanent subscriber in the list since he should render
@@ -71547,16 +72123,16 @@ class Model extends EventBus {
71547
72123
  this.joinSession();
71548
72124
  if (config.snapshotRequested) {
71549
72125
  const startSnapshot = performance.now();
71550
- console.info("Snapshot requested");
72126
+ console.debug("Snapshot requested");
71551
72127
  this.session.snapshot(this.exportData());
71552
72128
  this.garbageCollectExternalResources();
71553
- console.info("Snapshot taken in", performance.now() - startSnapshot, "ms");
72129
+ console.debug("Snapshot taken in", performance.now() - startSnapshot, "ms");
71554
72130
  }
71555
72131
  // mark all models as "raw", so they will not be turned into reactive objects
71556
72132
  // by owl, since we do not rely on reactivity
71557
72133
  owl.markRaw(this);
71558
- console.info("Model created in", performance.now() - start, "ms");
71559
- console.groupEnd();
72134
+ console.debug("Model created in", performance.now() - start, "ms");
72135
+ console.debug("######");
71560
72136
  }
71561
72137
  joinSession() {
71562
72138
  this.session.join(this.config.client);
@@ -71615,7 +72191,7 @@ class Model extends EventBus {
71615
72191
  this.finalize();
71616
72192
  }
71617
72193
  setupSession(revisionId) {
71618
- const session = new Session(buildRevisionLog({
72194
+ return new Session(buildRevisionLog({
71619
72195
  initialRevisionId: revisionId,
71620
72196
  recordChanges: this.state.recordChanges.bind(this.state),
71621
72197
  dispatch: (command) => {
@@ -71628,7 +72204,6 @@ class Model extends EventBus {
71628
72204
  this.isReplayingCommand = false;
71629
72205
  },
71630
72206
  }), this.config.transportService, revisionId);
71631
- return session;
71632
72207
  }
71633
72208
  setupSessionEvents() {
71634
72209
  this.session.on("remote-revision-received", this, this.onRemoteRevisionReceived);
@@ -71681,7 +72256,6 @@ class Model extends EventBus {
71681
72256
  range: this.range,
71682
72257
  dispatch: this.dispatchFromCorePlugin,
71683
72258
  canDispatch: this.canDispatch,
71684
- uuidGenerator: this.uuidGenerator,
71685
72259
  custom: this.config.custom,
71686
72260
  external: this.config.external,
71687
72261
  };
@@ -71722,8 +72296,7 @@ class Model extends EventBus {
71722
72296
  return results;
71723
72297
  }
71724
72298
  checkDispatchAllowedLocalCommand(command) {
71725
- const results = this.uiHandlers.map((handler) => handler.allowDispatch(command));
71726
- return results;
72299
+ return this.uiHandlers.map((handler) => handler.allowDispatch(command));
71727
72300
  }
71728
72301
  finalize() {
71729
72302
  this.status = 3 /* Status.Finalizing */;
@@ -71780,7 +72353,7 @@ class Model extends EventBus {
71780
72353
  this.finalize();
71781
72354
  const time = performance.now() - start;
71782
72355
  if (time > 5) {
71783
- console.info(type, time, "ms");
72356
+ console.debug(type, time, "ms");
71784
72357
  }
71785
72358
  });
71786
72359
  this.session.save(command, commands, changes);
@@ -72083,7 +72656,6 @@ const components = {
72083
72656
  PivotDimensionOrder,
72084
72657
  PivotDimension,
72085
72658
  PivotLayoutConfigurator,
72086
- EditableName,
72087
72659
  PivotDeferUpdate,
72088
72660
  PivotTitleSection,
72089
72661
  CogWheelMenu,
@@ -72175,6 +72747,6 @@ exports.tokenColors = tokenColors;
72175
72747
  exports.tokenize = tokenize;
72176
72748
 
72177
72749
 
72178
- __info__.version = "18.1.0-alpha.0";
72179
- __info__.date = "2024-09-25T13:17:49.636Z";
72180
- __info__.hash = "288f0b7";
72750
+ __info__.version = "18.1.0-alpha.2";
72751
+ __info__.date = "2024-10-24T08:53:21.828Z";
72752
+ __info__.hash = "2a01250";