@odoo/o-spreadsheet 18.2.1 → 18.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
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.2.1
6
- * @date 2025-02-25T06:03:13.262Z
7
- * @hash 3b4b5c9
5
+ * @version 18.2.3
6
+ * @date 2025-03-12T15:32:36.274Z
7
+ * @hash 81b0e08
8
8
  */
9
9
 
10
10
  'use strict';
@@ -425,7 +425,6 @@ function escapeRegExp(str) {
425
425
  * Sparse arrays remain sparse.
426
426
  */
427
427
  function deepCopy(obj) {
428
- const result = Array.isArray(obj) ? [] : {};
429
428
  switch (typeof obj) {
430
429
  case "object": {
431
430
  if (obj === null) {
@@ -437,8 +436,18 @@ function deepCopy(obj) {
437
436
  else if (!(isPlainObject(obj) || obj instanceof Array)) {
438
437
  throw new Error("Unsupported type: only objects and arrays are supported");
439
438
  }
440
- for (const key in obj) {
441
- result[key] = deepCopy(obj[key]);
439
+ const result = Array.isArray(obj) ? new Array(obj.length) : {};
440
+ if (Array.isArray(obj)) {
441
+ for (let i = 0, len = obj.length; i < len; i++) {
442
+ if (i in obj) {
443
+ result[i] = deepCopy(obj[i]);
444
+ }
445
+ }
446
+ }
447
+ else {
448
+ for (const key in obj) {
449
+ result[key] = deepCopy(obj[key]);
450
+ }
442
451
  }
443
452
  return result;
444
453
  }
@@ -2701,21 +2710,30 @@ function mergeContiguousZones(zones) {
2701
2710
  return mergedZones;
2702
2711
  }
2703
2712
 
2713
+ const globalReverseLookup$1 = new WeakMap();
2714
+ const globalIdCounter = new WeakMap();
2704
2715
  /**
2705
2716
  * Get the id of the given item (its key in the given dictionary).
2706
2717
  * If the given item does not exist in the dictionary, it creates one with a new id.
2707
2718
  */
2708
2719
  function getItemId(item, itemsDic) {
2709
- for (const key in itemsDic) {
2710
- if (deepEquals(itemsDic[key], item)) {
2711
- return parseInt(key, 10);
2712
- }
2720
+ if (!globalReverseLookup$1.has(itemsDic)) {
2721
+ globalReverseLookup$1.set(itemsDic, new Map());
2722
+ globalIdCounter.set(itemsDic, 0);
2723
+ }
2724
+ const reverseLookup = globalReverseLookup$1.get(itemsDic);
2725
+ const canonical = getCanonicalRepresentation(item);
2726
+ if (reverseLookup.has(canonical)) {
2727
+ const id = reverseLookup.get(canonical);
2728
+ itemsDic[id] = item;
2729
+ return id;
2713
2730
  }
2714
2731
  // Generate new Id if the item didn't exist in the dictionary
2715
- const ids = Object.keys(itemsDic);
2716
- const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
2717
- itemsDic[maxId + 1] = item;
2718
- return maxId + 1;
2732
+ const newId = globalIdCounter.get(itemsDic) + 1;
2733
+ reverseLookup.set(canonical, newId);
2734
+ globalIdCounter.set(itemsDic, newId);
2735
+ itemsDic[newId] = item;
2736
+ return newId;
2719
2737
  }
2720
2738
  function groupItemIdsByZones(positionsByItemId) {
2721
2739
  const result = {};
@@ -2739,6 +2757,33 @@ function* iterateItemIdsPositions(sheetId, itemIdsByZones) {
2739
2757
  }
2740
2758
  }
2741
2759
  }
2760
+ function getCanonicalRepresentation(item) {
2761
+ if (item === null)
2762
+ return "null";
2763
+ if (item === undefined)
2764
+ return "undefined";
2765
+ if (typeof item !== "object")
2766
+ return String(item);
2767
+ if (Array.isArray(item)) {
2768
+ const len = item.length;
2769
+ let result = "[";
2770
+ for (let i = 0; i < len; i++) {
2771
+ if (i > 0)
2772
+ result += ",";
2773
+ result += getCanonicalRepresentation(item[i]);
2774
+ }
2775
+ return result + "]";
2776
+ }
2777
+ const keys = Object.keys(item).sort();
2778
+ let repr = "{";
2779
+ for (const key of keys) {
2780
+ if (item[key] !== undefined) {
2781
+ repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
2782
+ }
2783
+ }
2784
+ repr += "}";
2785
+ return repr;
2786
+ }
2742
2787
 
2743
2788
  // -----------------------------------------------------------------------------
2744
2789
  // Date Type
@@ -6102,8 +6147,9 @@ function spreadRange(getters, dataSets) {
6102
6147
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6103
6148
  if (zone.right) {
6104
6149
  for (let j = zone.left; j <= zone.right; ++j) {
6150
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6105
6151
  postProcessedRanges.push({
6106
- ...dataSet,
6152
+ ...datasetOptions,
6107
6153
  dataRange: `${sheetPrefix}${zoneToXc({
6108
6154
  left: j,
6109
6155
  right: j,
@@ -6115,8 +6161,9 @@ function spreadRange(getters, dataSets) {
6115
6161
  }
6116
6162
  else {
6117
6163
  for (let j = zone.top; j <= zone.bottom; ++j) {
6164
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6118
6165
  postProcessedRanges.push({
6119
- ...dataSet,
6166
+ ...datasetOptions,
6120
6167
  dataRange: `${sheetPrefix}${zoneToXc({
6121
6168
  left: zone.left,
6122
6169
  right: zone.right,
@@ -8289,7 +8336,8 @@ function isSortedColumnValid(sortedColumn, pivot) {
8289
8336
  const possibleValues = pivot
8290
8337
  .getPossibleFieldValues(columns[i])
8291
8338
  .map((v) => v.value);
8292
- if (!possibleValues.includes(sortedColumn.domain[i].value)) {
8339
+ if (!possibleValues.includes(sortedColumn.domain[i].value) &&
8340
+ !(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
8293
8341
  return false;
8294
8342
  }
8295
8343
  }
@@ -9534,150 +9582,6 @@ class ComposerFocusStore extends SpreadsheetStore {
9534
9582
  }
9535
9583
  }
9536
9584
 
9537
- /**
9538
- * This file is largely inspired by owl 1.
9539
- * `css` tag has been removed from owl 2 without workaround to manage css.
9540
- * So, the solution was to import the behavior of owl 1 directly in our
9541
- * codebase, with one difference: the css is added to the sheet as soon as the
9542
- * css tag is executed. In owl 1, the css was added as soon as a Component was
9543
- * created for the first time.
9544
- */
9545
- const STYLESHEETS = {};
9546
- let nextId = 0;
9547
- /**
9548
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
9549
- * an inline stylesheet with just the following code:
9550
- * ```js
9551
- * css`.component-a { color: red; }`;
9552
- * ```
9553
- */
9554
- function css(strings, ...args) {
9555
- const name = `__sheet__${nextId++}`;
9556
- const value = String.raw(strings, ...args);
9557
- registerSheet(name, value);
9558
- activateSheet(name);
9559
- return name;
9560
- }
9561
- function processSheet(str) {
9562
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9563
- const selectorStack = [];
9564
- const parts = [];
9565
- let rules = [];
9566
- function generateSelector(stackIndex, parentSelector) {
9567
- const parts = [];
9568
- for (const selector of selectorStack[stackIndex]) {
9569
- let part = (parentSelector && parentSelector + " " + selector) || selector;
9570
- if (part.includes("&")) {
9571
- part = selector.replace(/&/g, parentSelector || "");
9572
- }
9573
- if (stackIndex < selectorStack.length - 1) {
9574
- part = generateSelector(stackIndex + 1, part);
9575
- }
9576
- parts.push(part);
9577
- }
9578
- return parts.join(", ");
9579
- }
9580
- function generateRules() {
9581
- if (rules.length) {
9582
- parts.push(generateSelector(0) + " {");
9583
- parts.push(...rules);
9584
- parts.push("}");
9585
- rules = [];
9586
- }
9587
- }
9588
- while (tokens.length) {
9589
- let token = tokens.shift();
9590
- if (token === "}") {
9591
- generateRules();
9592
- selectorStack.pop();
9593
- }
9594
- else {
9595
- if (tokens[0] === "{") {
9596
- generateRules();
9597
- selectorStack.push(token.split(/\s*,\s*/));
9598
- tokens.shift();
9599
- }
9600
- if (tokens[0] === ";") {
9601
- rules.push(" " + token + ";");
9602
- }
9603
- }
9604
- }
9605
- return parts.join("\n");
9606
- }
9607
- function registerSheet(id, css) {
9608
- const sheet = document.createElement("style");
9609
- sheet.textContent = processSheet(css);
9610
- STYLESHEETS[id] = sheet;
9611
- }
9612
- function activateSheet(id) {
9613
- const sheet = STYLESHEETS[id];
9614
- sheet.setAttribute("component", id);
9615
- document.head.appendChild(sheet);
9616
- }
9617
- function getTextDecoration({ strikethrough, underline, }) {
9618
- if (!strikethrough && !underline) {
9619
- return "none";
9620
- }
9621
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9622
- }
9623
- /**
9624
- * Convert the cell style to CSS properties.
9625
- */
9626
- function cellStyleToCss(style) {
9627
- const attributes = cellTextStyleToCss(style);
9628
- if (!style)
9629
- return attributes;
9630
- if (style.fillColor) {
9631
- attributes["background"] = style.fillColor;
9632
- }
9633
- return attributes;
9634
- }
9635
- /**
9636
- * Convert the cell text style to CSS properties.
9637
- */
9638
- function cellTextStyleToCss(style) {
9639
- const attributes = {};
9640
- if (!style)
9641
- return attributes;
9642
- if (style.bold) {
9643
- attributes["font-weight"] = "bold";
9644
- }
9645
- if (style.italic) {
9646
- attributes["font-style"] = "italic";
9647
- }
9648
- if (style.strikethrough || style.underline) {
9649
- let decoration = style.strikethrough ? "line-through" : "";
9650
- decoration = style.underline ? decoration + " underline" : decoration;
9651
- attributes["text-decoration"] = decoration;
9652
- }
9653
- if (style.textColor) {
9654
- attributes["color"] = style.textColor;
9655
- }
9656
- return attributes;
9657
- }
9658
- /**
9659
- * Transform CSS properties into a CSS string.
9660
- */
9661
- function cssPropertiesToCss(attributes) {
9662
- let styleStr = "";
9663
- for (const attName in attributes) {
9664
- if (!attributes[attName]) {
9665
- continue;
9666
- }
9667
- styleStr += `${attName}:${attributes[attName]}; `;
9668
- }
9669
- return styleStr;
9670
- }
9671
- function getElementMargins(el) {
9672
- const style = window.getComputedStyle(el);
9673
- return {
9674
- top: parseInt(style.marginTop, 10) || 0,
9675
- bottom: parseInt(style.marginBottom, 10) || 0,
9676
- left: parseInt(style.marginLeft, 10) || 0,
9677
- right: parseInt(style.marginRight, 10) || 0,
9678
- };
9679
- }
9680
-
9681
9585
  const TREND_LINE_XAXIS_ID = "x1";
9682
9586
  /**
9683
9587
  * This file contains helpers that are common to different charts (mainly
@@ -10222,79 +10126,341 @@ function getNextNonEmptyBar(bars, startIndex) {
10222
10126
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10223
10127
  }
10224
10128
 
10225
- window.Chart?.register(waterfallLinesPlugin);
10226
- window.Chart?.register(chartShowValuesPlugin);
10227
- css /* scss */ `
10228
- .o-spreadsheet {
10229
- .o-chart-custom-tooltip {
10230
- font-size: 12px;
10231
- background-color: #fff;
10232
- z-index: ${ComponentsImportance.FigureTooltip};
10129
+ const GAUGE_PADDING_SIDE = 30;
10130
+ const GAUGE_PADDING_TOP = 10;
10131
+ const GAUGE_PADDING_BOTTOM = 20;
10132
+ const GAUGE_LABELS_FONT_SIZE = 12;
10133
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10134
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10135
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10136
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10137
+ function drawGaugeChart(canvas, runtime) {
10138
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10139
+ canvas.width = canvasBoundingRect.width;
10140
+ canvas.height = canvasBoundingRect.height;
10141
+ const ctx = canvas.getContext("2d");
10142
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10143
+ drawBackground(ctx, config);
10144
+ drawGauge(ctx, config);
10145
+ drawInflectionValues(ctx, config);
10146
+ drawLabels(ctx, config);
10147
+ drawTitle(ctx, config);
10148
+ }
10149
+ function drawGauge(ctx, config) {
10150
+ ctx.save();
10151
+ const gauge = config.gauge;
10152
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10153
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10154
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10155
+ if (arcRadius < 0) {
10156
+ return;
10233
10157
  }
10234
- }
10235
- `;
10236
- class ChartJsComponent extends owl.Component {
10237
- static template = "o-spreadsheet-ChartJsComponent";
10238
- static props = {
10239
- figure: Object,
10158
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10159
+ // Gauge background
10160
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10161
+ ctx.beginPath();
10162
+ ctx.lineWidth = gauge.arcWidth;
10163
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10164
+ ctx.stroke();
10165
+ // Gauge value
10166
+ ctx.strokeStyle = gauge.color;
10167
+ ctx.beginPath();
10168
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10169
+ ctx.stroke();
10170
+ ctx.restore();
10171
+ }
10172
+ function drawBackground(ctx, config) {
10173
+ ctx.save();
10174
+ ctx.fillStyle = config.backgroundColor;
10175
+ ctx.fillRect(0, 0, config.width, config.height);
10176
+ ctx.restore();
10177
+ }
10178
+ function drawLabels(ctx, config) {
10179
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10180
+ ctx.save();
10181
+ ctx.textAlign = "center";
10182
+ ctx.fillStyle = label.color;
10183
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10184
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10185
+ ctx.restore();
10186
+ }
10187
+ }
10188
+ function drawInflectionValues(ctx, config) {
10189
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10190
+ for (const inflectionValue of config.inflectionValues) {
10191
+ ctx.save();
10192
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10193
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10194
+ ctx.lineWidth = 2;
10195
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10196
+ ctx.beginPath();
10197
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10198
+ ctx.lineTo(0, -height - 3);
10199
+ ctx.stroke();
10200
+ ctx.textAlign = "center";
10201
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10202
+ ctx.fillStyle = inflectionValue.color;
10203
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10204
+ ctx.fillText(inflectionValue.label, 0, textY);
10205
+ ctx.restore();
10206
+ }
10207
+ }
10208
+ function drawTitle(ctx, config) {
10209
+ ctx.save();
10210
+ const title = config.title;
10211
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10212
+ ctx.textBaseline = "middle";
10213
+ ctx.fillStyle = title.color;
10214
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10215
+ ctx.restore();
10216
+ }
10217
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10218
+ const maxValue = runtime.maxValue;
10219
+ const minValue = runtime.minValue;
10220
+ const gaugeValue = runtime.gaugeValue;
10221
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10222
+ const gaugeArcWidth = gaugeRect.width / 6;
10223
+ const gaugePercentage = gaugeValue
10224
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10225
+ : 0;
10226
+ const gaugeValuePosition = {
10227
+ x: boundingRect.width / 2,
10228
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10240
10229
  };
10241
- canvas = owl.useRef("graphContainer");
10242
- chart;
10243
- currentRuntime;
10244
- get background() {
10245
- return this.chartRuntime.background;
10230
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10231
+ // Scale down the font size if the gaugeRect is too small
10232
+ if (gaugeRect.height < 300) {
10233
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10246
10234
  }
10247
- get canvasStyle() {
10248
- return `background-color: ${this.background}`;
10235
+ // Scale down the font size if the text is too long
10236
+ const maxTextWidth = gaugeRect.width / 2;
10237
+ const gaugeLabel = gaugeValue?.label || "-";
10238
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10239
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10249
10240
  }
10250
- get chartRuntime() {
10251
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10252
- if (!("chartJsConfig" in runtime)) {
10253
- throw new Error("Unsupported chart runtime");
10254
- }
10255
- return runtime;
10241
+ const minLabelPosition = {
10242
+ x: gaugeRect.x + gaugeArcWidth / 2,
10243
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10244
+ };
10245
+ const maxLabelPosition = {
10246
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10247
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10248
+ };
10249
+ const textColor = chartMutedFontColor(runtime.background);
10250
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10251
+ let x = 0, titleWidth = 0, titleHeight = 0;
10252
+ if (runtime.title.text) {
10253
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10256
10254
  }
10257
- setup() {
10258
- owl.onMounted(() => {
10259
- const runtime = this.chartRuntime;
10260
- this.currentRuntime = runtime;
10261
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10262
- this.createChart(deepCopy(runtime.chartJsConfig));
10263
- });
10264
- owl.onWillUnmount(() => this.chart?.destroy());
10265
- owl.useEffect(() => {
10266
- const runtime = this.chartRuntime;
10267
- if (runtime !== this.currentRuntime) {
10268
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10269
- this.chart?.destroy();
10270
- this.createChart(deepCopy(runtime.chartJsConfig));
10271
- }
10272
- else {
10273
- this.updateChartJs(deepCopy(runtime));
10274
- }
10275
- this.currentRuntime = runtime;
10276
- }
10255
+ switch (runtime.title.align) {
10256
+ case "right":
10257
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10258
+ break;
10259
+ case "center":
10260
+ x = (boundingRect.width - titleWidth) / 2;
10261
+ break;
10262
+ case "left":
10263
+ default:
10264
+ x = CHART_PADDING$1;
10265
+ break;
10266
+ }
10267
+ return {
10268
+ width: boundingRect.width,
10269
+ height: boundingRect.height,
10270
+ title: {
10271
+ label: runtime.title.text ?? "",
10272
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10273
+ textPosition: {
10274
+ x,
10275
+ y: CHART_PADDING_TOP + titleHeight / 2,
10276
+ },
10277
+ color: runtime.title.color ?? textColor,
10278
+ bold: runtime.title.bold,
10279
+ italic: runtime.title.italic,
10280
+ },
10281
+ backgroundColor: runtime.background,
10282
+ gauge: {
10283
+ rect: gaugeRect,
10284
+ arcWidth: gaugeArcWidth,
10285
+ percentage: clip(gaugePercentage, 0, 1),
10286
+ color: getGaugeColor(runtime),
10287
+ },
10288
+ inflectionValues,
10289
+ gaugeValue: {
10290
+ label: gaugeLabel,
10291
+ textPosition: gaugeValuePosition,
10292
+ fontSize: gaugeValueFontSize,
10293
+ color: textColor,
10294
+ },
10295
+ minLabel: {
10296
+ label: runtime.minValue.label,
10297
+ textPosition: minLabelPosition,
10298
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10299
+ color: textColor,
10300
+ },
10301
+ maxLabel: {
10302
+ label: runtime.maxValue.label,
10303
+ textPosition: maxLabelPosition,
10304
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10305
+ color: textColor,
10306
+ },
10307
+ };
10308
+ }
10309
+ /**
10310
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10311
+ * space for the title and labels.
10312
+ */
10313
+ function getGaugeRect(boundingRect, title) {
10314
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10315
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10316
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10317
+ let gaugeWidth;
10318
+ let gaugeHeight;
10319
+ if (drawWidth > 2 * drawHeight) {
10320
+ gaugeWidth = 2 * drawHeight;
10321
+ gaugeHeight = drawHeight;
10322
+ }
10323
+ else {
10324
+ gaugeWidth = drawWidth;
10325
+ gaugeHeight = drawWidth / 2;
10326
+ }
10327
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10328
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10329
+ return {
10330
+ x: gaugeX,
10331
+ y: gaugeY,
10332
+ width: gaugeWidth,
10333
+ height: gaugeHeight,
10334
+ };
10335
+ }
10336
+ /**
10337
+ * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
10338
+ *
10339
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10340
+ */
10341
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10342
+ const maxValue = runtime.maxValue;
10343
+ const minValue = runtime.minValue;
10344
+ const gaugeCircleCenter = {
10345
+ x: gaugeRect.x + gaugeRect.width / 2,
10346
+ y: gaugeRect.y + gaugeRect.height,
10347
+ };
10348
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10349
+ const inflectionValues = [];
10350
+ const inflectionValuesTextRects = [];
10351
+ for (const inflectionValue of runtime.inflectionValues) {
10352
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10353
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10354
+ const angle = Math.PI - Math.PI * percentage;
10355
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10356
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10357
+ gaugeCircleCenter.x, // center of the gauge circle
10358
+ gaugeCircleCenter.y, // center of the gauge circle
10359
+ labelWidth + 2, // width of the text + some margin
10360
+ GAUGE_LABELS_FONT_SIZE // height of the text
10361
+ );
10362
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10363
+ ? GAUGE_LABELS_FONT_SIZE
10364
+ : 0;
10365
+ inflectionValuesTextRects.push(textRect);
10366
+ inflectionValues.push({
10367
+ rotation: angle,
10368
+ label: inflectionValue.label,
10369
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10370
+ color: textColor,
10371
+ offset,
10277
10372
  });
10278
10373
  }
10279
- createChart(chartData) {
10280
- const canvas = this.canvas.el;
10281
- const ctx = canvas.getContext("2d");
10282
- this.chart = new window.Chart(ctx, chartData);
10374
+ return inflectionValues;
10375
+ }
10376
+ function getGaugeColor(runtime) {
10377
+ const gaugeValue = runtime.gaugeValue?.value;
10378
+ if (gaugeValue === undefined) {
10379
+ return GAUGE_BACKGROUND_COLOR;
10283
10380
  }
10284
- updateChartJs(chartRuntime) {
10285
- const chartData = chartRuntime.chartJsConfig;
10286
- if (chartData.data && chartData.data.datasets) {
10287
- this.chart.data = chartData.data;
10288
- if (chartData.options?.plugins?.title) {
10289
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10290
- }
10381
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10382
+ const inflectionValue = runtime.inflectionValues[i];
10383
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10384
+ return runtime.colors[i];
10291
10385
  }
10292
- else {
10293
- this.chart.data.datasets = [];
10386
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10387
+ return runtime.colors[i];
10294
10388
  }
10295
- this.chart.config.options = chartData.options;
10296
- this.chart.update();
10297
10389
  }
10390
+ return runtime.colors.at(-1);
10391
+ }
10392
+ function getSegmentsOfRectangle(rectangle) {
10393
+ return [
10394
+ { start: rectangle.topLeft, end: rectangle.topRight },
10395
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10396
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10397
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10398
+ ];
10399
+ }
10400
+ /**
10401
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10402
+ * is not handled.
10403
+ */
10404
+ function doSegmentIntersect(segment1, segment2) {
10405
+ const A = segment1.start;
10406
+ const B = segment1.end;
10407
+ const C = segment2.start;
10408
+ const D = segment2.end;
10409
+ /**
10410
+ * Line segment intersection algorithm
10411
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10412
+ */
10413
+ function ccw(a, b, c) {
10414
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10415
+ }
10416
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10417
+ }
10418
+ function doRectanglesIntersect(rect1, rect2) {
10419
+ const segments1 = getSegmentsOfRectangle(rect1);
10420
+ const segments2 = getSegmentsOfRectangle(rect2);
10421
+ for (const segment1 of segments1) {
10422
+ for (const segment2 of segments2) {
10423
+ if (doSegmentIntersect(segment1, segment2)) {
10424
+ return true;
10425
+ }
10426
+ }
10427
+ }
10428
+ return false;
10429
+ }
10430
+ /**
10431
+ * Get the rectangle that is tangent to a circle at a given angle.
10432
+ *
10433
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10434
+ */
10435
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10436
+ const cos = Math.cos(angle);
10437
+ const sin = Math.sin(angle);
10438
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10439
+ const x = cos * radius;
10440
+ const y = sin * radius;
10441
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10442
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10443
+ const y2 = cos * (rectWidth / 2);
10444
+ const bottomRight = {
10445
+ x: x + x2 + circleCenterX,
10446
+ y: circleCenterY - (y - y2),
10447
+ };
10448
+ const bottomLeft = {
10449
+ x: x - x2 + circleCenterX,
10450
+ y: circleCenterY - (y + y2),
10451
+ };
10452
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10453
+ const xp = cos * (radius + rectHeight);
10454
+ const yp = sin * (radius + rectHeight);
10455
+ const topLeft = {
10456
+ x: xp - x2 + circleCenterX,
10457
+ y: circleCenterY - (yp + y2),
10458
+ };
10459
+ const topRight = {
10460
+ x: xp + x2 + circleCenterX,
10461
+ y: circleCenterY - (yp - y2),
10462
+ };
10463
+ return { bottomLeft, bottomRight, topRight, topLeft };
10298
10464
  }
10299
10465
 
10300
10466
  /**
@@ -10876,6 +11042,299 @@ class ScorecardChartConfigBuilder {
10876
11042
  }
10877
11043
  }
10878
11044
 
11045
+ const CHART_COMMON_OPTIONS = {
11046
+ // https://www.chartjs.org/docs/latest/general/responsive.html
11047
+ responsive: true, // will resize when its container is resized
11048
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11049
+ elements: {
11050
+ line: {
11051
+ fill: false, // do not fill the area under line charts
11052
+ },
11053
+ point: {
11054
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11055
+ },
11056
+ },
11057
+ animation: false,
11058
+ };
11059
+ function chartToImage(runtime, figure, type) {
11060
+ // wrap the canvas in a div with a fixed size because chart.js would
11061
+ // fill the whole page otherwise
11062
+ const div = document.createElement("div");
11063
+ div.style.width = `${figure.width}px`;
11064
+ div.style.height = `${figure.height}px`;
11065
+ const canvas = document.createElement("canvas");
11066
+ div.append(canvas);
11067
+ canvas.setAttribute("width", figure.width.toString());
11068
+ canvas.setAttribute("height", figure.height.toString());
11069
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11070
+ document.body.append(div);
11071
+ if ("chartJsConfig" in runtime) {
11072
+ const config = deepCopy(runtime.chartJsConfig);
11073
+ config.plugins = [backgroundColorChartJSPlugin];
11074
+ const Chart = getChartJSConstructor();
11075
+ const chart = new Chart(canvas, config);
11076
+ const imgContent = chart.toBase64Image();
11077
+ chart.destroy();
11078
+ div.remove();
11079
+ return imgContent;
11080
+ }
11081
+ else if (type === "scorecard") {
11082
+ const design = getScorecardConfiguration(figure, runtime);
11083
+ drawScoreChart(design, canvas);
11084
+ const imgContent = canvas.toDataURL();
11085
+ div.remove();
11086
+ return imgContent;
11087
+ }
11088
+ else if (type === "gauge") {
11089
+ drawGaugeChart(canvas, runtime);
11090
+ const imgContent = canvas.toDataURL();
11091
+ div.remove();
11092
+ return imgContent;
11093
+ }
11094
+ return undefined;
11095
+ }
11096
+ /**
11097
+ * Custom chart.js plugin to set the background color of the canvas
11098
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11099
+ */
11100
+ const backgroundColorChartJSPlugin = {
11101
+ id: "customCanvasBackgroundColor",
11102
+ beforeDraw: (chart) => {
11103
+ const { ctx } = chart;
11104
+ ctx.save();
11105
+ ctx.globalCompositeOperation = "destination-over";
11106
+ ctx.fillStyle = "#ffffff";
11107
+ ctx.fillRect(0, 0, chart.width, chart.height);
11108
+ ctx.restore();
11109
+ },
11110
+ };
11111
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11112
+ function getChartJSConstructor() {
11113
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11114
+ window.Chart.register(chartShowValuesPlugin);
11115
+ window.Chart.register(waterfallLinesPlugin);
11116
+ }
11117
+ return window.Chart;
11118
+ }
11119
+
11120
+ /**
11121
+ * This file is largely inspired by owl 1.
11122
+ * `css` tag has been removed from owl 2 without workaround to manage css.
11123
+ * So, the solution was to import the behavior of owl 1 directly in our
11124
+ * codebase, with one difference: the css is added to the sheet as soon as the
11125
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
11126
+ * created for the first time.
11127
+ */
11128
+ const STYLESHEETS = {};
11129
+ let nextId = 0;
11130
+ /**
11131
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
11132
+ * an inline stylesheet with just the following code:
11133
+ * ```js
11134
+ * css`.component-a { color: red; }`;
11135
+ * ```
11136
+ */
11137
+ function css(strings, ...args) {
11138
+ const name = `__sheet__${nextId++}`;
11139
+ const value = String.raw(strings, ...args);
11140
+ registerSheet(name, value);
11141
+ activateSheet(name);
11142
+ return name;
11143
+ }
11144
+ function processSheet(str) {
11145
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11146
+ const selectorStack = [];
11147
+ const parts = [];
11148
+ let rules = [];
11149
+ function generateSelector(stackIndex, parentSelector) {
11150
+ const parts = [];
11151
+ for (const selector of selectorStack[stackIndex]) {
11152
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
11153
+ if (part.includes("&")) {
11154
+ part = selector.replace(/&/g, parentSelector || "");
11155
+ }
11156
+ if (stackIndex < selectorStack.length - 1) {
11157
+ part = generateSelector(stackIndex + 1, part);
11158
+ }
11159
+ parts.push(part);
11160
+ }
11161
+ return parts.join(", ");
11162
+ }
11163
+ function generateRules() {
11164
+ if (rules.length) {
11165
+ parts.push(generateSelector(0) + " {");
11166
+ parts.push(...rules);
11167
+ parts.push("}");
11168
+ rules = [];
11169
+ }
11170
+ }
11171
+ while (tokens.length) {
11172
+ let token = tokens.shift();
11173
+ if (token === "}") {
11174
+ generateRules();
11175
+ selectorStack.pop();
11176
+ }
11177
+ else {
11178
+ if (tokens[0] === "{") {
11179
+ generateRules();
11180
+ selectorStack.push(token.split(/\s*,\s*/));
11181
+ tokens.shift();
11182
+ }
11183
+ if (tokens[0] === ";") {
11184
+ rules.push(" " + token + ";");
11185
+ }
11186
+ }
11187
+ }
11188
+ return parts.join("\n");
11189
+ }
11190
+ function registerSheet(id, css) {
11191
+ const sheet = document.createElement("style");
11192
+ sheet.textContent = processSheet(css);
11193
+ STYLESHEETS[id] = sheet;
11194
+ }
11195
+ function activateSheet(id) {
11196
+ const sheet = STYLESHEETS[id];
11197
+ sheet.setAttribute("component", id);
11198
+ document.head.appendChild(sheet);
11199
+ }
11200
+ function getTextDecoration({ strikethrough, underline, }) {
11201
+ if (!strikethrough && !underline) {
11202
+ return "none";
11203
+ }
11204
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11205
+ }
11206
+ /**
11207
+ * Convert the cell style to CSS properties.
11208
+ */
11209
+ function cellStyleToCss(style) {
11210
+ const attributes = cellTextStyleToCss(style);
11211
+ if (!style)
11212
+ return attributes;
11213
+ if (style.fillColor) {
11214
+ attributes["background"] = style.fillColor;
11215
+ }
11216
+ return attributes;
11217
+ }
11218
+ /**
11219
+ * Convert the cell text style to CSS properties.
11220
+ */
11221
+ function cellTextStyleToCss(style) {
11222
+ const attributes = {};
11223
+ if (!style)
11224
+ return attributes;
11225
+ if (style.bold) {
11226
+ attributes["font-weight"] = "bold";
11227
+ }
11228
+ if (style.italic) {
11229
+ attributes["font-style"] = "italic";
11230
+ }
11231
+ if (style.strikethrough || style.underline) {
11232
+ let decoration = style.strikethrough ? "line-through" : "";
11233
+ decoration = style.underline ? decoration + " underline" : decoration;
11234
+ attributes["text-decoration"] = decoration;
11235
+ }
11236
+ if (style.textColor) {
11237
+ attributes["color"] = style.textColor;
11238
+ }
11239
+ return attributes;
11240
+ }
11241
+ /**
11242
+ * Transform CSS properties into a CSS string.
11243
+ */
11244
+ function cssPropertiesToCss(attributes) {
11245
+ let styleStr = "";
11246
+ for (const attName in attributes) {
11247
+ if (!attributes[attName]) {
11248
+ continue;
11249
+ }
11250
+ styleStr += `${attName}:${attributes[attName]}; `;
11251
+ }
11252
+ return styleStr;
11253
+ }
11254
+ function getElementMargins(el) {
11255
+ const style = window.getComputedStyle(el);
11256
+ return {
11257
+ top: parseInt(style.marginTop, 10) || 0,
11258
+ bottom: parseInt(style.marginBottom, 10) || 0,
11259
+ left: parseInt(style.marginLeft, 10) || 0,
11260
+ right: parseInt(style.marginRight, 10) || 0,
11261
+ };
11262
+ }
11263
+
11264
+ css /* scss */ `
11265
+ .o-spreadsheet {
11266
+ .o-chart-custom-tooltip {
11267
+ font-size: 12px;
11268
+ background-color: #fff;
11269
+ z-index: ${ComponentsImportance.FigureTooltip};
11270
+ }
11271
+ }
11272
+ `;
11273
+ class ChartJsComponent extends owl.Component {
11274
+ static template = "o-spreadsheet-ChartJsComponent";
11275
+ static props = {
11276
+ figure: Object,
11277
+ };
11278
+ canvas = owl.useRef("graphContainer");
11279
+ chart;
11280
+ currentRuntime;
11281
+ get background() {
11282
+ return this.chartRuntime.background;
11283
+ }
11284
+ get canvasStyle() {
11285
+ return `background-color: ${this.background}`;
11286
+ }
11287
+ get chartRuntime() {
11288
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11289
+ if (!("chartJsConfig" in runtime)) {
11290
+ throw new Error("Unsupported chart runtime");
11291
+ }
11292
+ return runtime;
11293
+ }
11294
+ setup() {
11295
+ owl.onMounted(() => {
11296
+ const runtime = this.chartRuntime;
11297
+ this.currentRuntime = runtime;
11298
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11299
+ this.createChart(deepCopy(runtime.chartJsConfig));
11300
+ });
11301
+ owl.onWillUnmount(() => this.chart?.destroy());
11302
+ owl.useEffect(() => {
11303
+ const runtime = this.chartRuntime;
11304
+ if (runtime !== this.currentRuntime) {
11305
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11306
+ this.chart?.destroy();
11307
+ this.createChart(deepCopy(runtime.chartJsConfig));
11308
+ }
11309
+ else {
11310
+ this.updateChartJs(deepCopy(runtime));
11311
+ }
11312
+ this.currentRuntime = runtime;
11313
+ }
11314
+ });
11315
+ }
11316
+ createChart(chartData) {
11317
+ const canvas = this.canvas.el;
11318
+ const ctx = canvas.getContext("2d");
11319
+ const Chart = getChartJSConstructor();
11320
+ this.chart = new Chart(ctx, chartData);
11321
+ }
11322
+ updateChartJs(chartRuntime) {
11323
+ const chartData = chartRuntime.chartJsConfig;
11324
+ if (chartData.data && chartData.data.datasets) {
11325
+ this.chart.data = chartData.data;
11326
+ if (chartData.options?.plugins?.title) {
11327
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11328
+ }
11329
+ }
11330
+ else {
11331
+ this.chart.data.datasets = [];
11332
+ }
11333
+ this.chart.config.options = chartData.options;
11334
+ this.chart.update();
11335
+ }
11336
+ }
11337
+
10879
11338
  class ScorecardChart extends owl.Component {
10880
11339
  static template = "o-spreadsheet-ScorecardChart";
10881
11340
  static props = {
@@ -10907,6 +11366,7 @@ class ScorecardChart extends owl.Component {
10907
11366
  const autoCompleteProviders = new Registry();
10908
11367
 
10909
11368
  autoCompleteProviders.add("dataValidation", {
11369
+ displayAllOnInitialContent: true,
10910
11370
  getProposals(tokenAtCursor, content) {
10911
11371
  if (content.startsWith("=")) {
10912
11372
  return [];
@@ -21203,6 +21663,15 @@ class AbstractComposerStore extends SpreadsheetStore {
21203
21663
  const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
21204
21664
  // remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
21205
21665
  const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
21666
+ if (this._currentContent === this.initialContent &&
21667
+ provider.displayAllOnInitialContent &&
21668
+ proposals?.length) {
21669
+ return {
21670
+ proposals,
21671
+ selectProposal: provider.selectProposal,
21672
+ autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
21673
+ };
21674
+ }
21206
21675
  if (exactMatch && this._currentContent !== this.initialContent) {
21207
21676
  // this means the user has chosen a proposal
21208
21677
  return;
@@ -22203,586 +22672,255 @@ const alphaNumericValueRegExp = /^(.*\D+)(\d+)$/;
22203
22672
  /**
22204
22673
  * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
22205
22674
  * Return the one which contains the given cell
22206
- */
22207
- function getGroup(cell, cells, filter) {
22208
- let group = [];
22209
- let found = false;
22210
- for (let x of cells) {
22211
- if (x === cell) {
22212
- found = true;
22213
- }
22214
- const cellValue = x === undefined || x.isFormula
22215
- ? undefined
22216
- : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22217
- if (cellValue && filter(cellValue)) {
22218
- group.push(cellValue);
22219
- }
22220
- else {
22221
- if (found) {
22222
- return group;
22223
- }
22224
- group = [];
22225
- }
22226
- }
22227
- return group;
22228
- }
22229
- /**
22230
- * Get the average steps between numbers
22231
- */
22232
- function getAverageIncrement(group) {
22233
- const averages = [];
22234
- let last = group[0];
22235
- for (let i = 1; i < group.length; i++) {
22236
- const current = group[i];
22237
- averages.push(current - last);
22238
- last = current;
22239
- }
22240
- return averages.reduce((a, b) => a + b, 0) / averages.length;
22241
- }
22242
- /**
22243
- * Get the step for a group
22244
- */
22245
- function calculateIncrementBasedOnGroup(group) {
22246
- let increment = 1;
22247
- if (group.length >= 2) {
22248
- increment = getAverageIncrement(group) * group.length;
22249
- }
22250
- return increment;
22251
- }
22252
- /**
22253
- * Iterates on a list of date intervals.
22254
- * if every interval is the same, return the interval
22255
- * Otherwise return undefined
22256
- *
22257
- */
22258
- function getEqualInterval(intervals) {
22259
- if (intervals.length < 2) {
22260
- return intervals[0] || { years: 0, months: 0, days: 0 };
22261
- }
22262
- const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22263
- interval.months === intervals[0].months &&
22264
- interval.days === intervals[0].days);
22265
- return equal ? intervals[0] : undefined;
22266
- }
22267
- /**
22268
- * Based on a group of dates, calculate the increment that should be applied
22269
- * to the next date.
22270
- *
22271
- * This will compute the date difference in calendar terms (years, months, days)
22272
- * In order to make abstraction of leap years and months with different number of days.
22273
- *
22274
- * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22275
- * In case of equidistant dates, we either have in that order:
22276
- * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22277
- * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22278
- * - equidistant dates but not the same interval, in which case we return increment of the same interval
22279
- *
22280
- * */
22281
- function calculateDateIncrementBasedOnGroup(group) {
22282
- if (group.length < 2) {
22283
- return 1;
22284
- }
22285
- const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22286
- const datesIntervals = getDateIntervals(jsDates);
22287
- const datesEquidistantInterval = getEqualInterval(datesIntervals);
22288
- if (datesEquidistantInterval === undefined) {
22289
- // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22290
- return undefined;
22291
- }
22292
- // The dates are apart by an exact interval of years, months or days
22293
- // but not a combination of them
22294
- const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22295
- const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22296
- if (!exactDateInterval || isSameDay) {
22297
- const timeIntervals = jsDates
22298
- .map((date, index) => {
22299
- if (index === 0) {
22300
- return 0;
22301
- }
22302
- const previous = jsDates[index - 1];
22303
- return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22304
- })
22305
- .slice(1);
22306
- const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22307
- if (equidistantDates) {
22308
- return group.length * (group[1] - group[0]);
22309
- }
22310
- }
22311
- return {
22312
- years: datesEquidistantInterval.years * group.length,
22313
- months: datesEquidistantInterval.months * group.length,
22314
- days: datesEquidistantInterval.days * group.length,
22315
- };
22316
- }
22317
- autofillRulesRegistry
22318
- .add("simple_value_copy", {
22319
- condition: (cell, cells) => {
22320
- return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
22321
- },
22322
- generateRule: () => {
22323
- return { type: "COPY_MODIFIER" };
22324
- },
22325
- sequence: 10,
22326
- })
22327
- .add("increment_alphanumeric_value", {
22328
- condition: (cell) => !cell.isFormula &&
22329
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22330
- alphaNumericValueRegExp.test(cell.content),
22331
- generateRule: (cell, cells) => {
22332
- const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22333
- const prefix = cell.content.match(stringPrefixRegExp)[0];
22334
- const numberPostfixLength = cell.content.length - prefix.length;
22335
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
22336
- alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22337
- .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22338
- .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22339
- const increment = calculateIncrementBasedOnGroup(group);
22340
- return {
22341
- type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22342
- prefix,
22343
- current: numberPostfix,
22344
- increment,
22345
- numberPostfixLength,
22346
- };
22347
- },
22348
- sequence: 15,
22349
- })
22350
- .add("copy_text", {
22351
- condition: (cell) => !cell.isFormula &&
22352
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
22353
- generateRule: () => {
22354
- return { type: "COPY_MODIFIER" };
22355
- },
22356
- sequence: 20,
22357
- })
22358
- .add("update_formula", {
22359
- condition: (cell) => cell.isFormula,
22360
- generateRule: (_, cells) => {
22361
- return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22362
- },
22363
- sequence: 30,
22364
- })
22365
- .add("increment_dates", {
22366
- condition: (cell, cells) => {
22367
- return (!cell.isFormula &&
22368
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22369
- !!cell.format &&
22370
- isDateTimeFormat(cell.format));
22371
- },
22372
- generateRule: (cell, cells) => {
22373
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22374
- !!evaluatedCell.format &&
22375
- isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22376
- const increment = calculateDateIncrementBasedOnGroup(group);
22377
- if (increment === undefined) {
22378
- return { type: "COPY_MODIFIER" };
22379
- }
22380
- /** requires to detect the current date (requires to be an integer value with the right format)
22381
- * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22382
- */
22383
- const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22384
- if (typeof increment === "object") {
22385
- return {
22386
- type: "DATE_INCREMENT_MODIFIER",
22387
- increment,
22388
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22389
- };
22390
- }
22391
- return {
22392
- type: "INCREMENT_MODIFIER",
22393
- increment,
22394
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22395
- };
22396
- },
22397
- sequence: 25,
22398
- })
22399
- .add("increment_number", {
22400
- condition: (cell) => !cell.isFormula &&
22401
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22402
- generateRule: (cell, cells) => {
22403
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22404
- !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22405
- const increment = calculateIncrementBasedOnGroup(group);
22406
- const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22407
- return {
22408
- type: "INCREMENT_MODIFIER",
22409
- increment,
22410
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22411
- };
22412
- },
22413
- sequence: 40,
22414
- });
22415
- /**
22416
- * Returns the date intervals between consecutive dates of an array
22417
- * in the format of { years: number, months: number, days: number }
22418
- *
22419
- * The split is necessary to make abstraction of leap years and
22420
- * months with different number of days.
22421
- *
22422
- * @param dates
22423
- */
22424
- function getDateIntervals(dates) {
22425
- if (dates.length < 2) {
22426
- return [{ years: 0, months: 0, days: 0 }];
22427
- }
22428
- const res = dates.map((date, index) => {
22429
- if (index === 0) {
22430
- return { years: 0, months: 0, days: 0 };
22431
- }
22432
- const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22433
- const years = getTimeDifferenceInWholeYears(previous, date);
22434
- const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22435
- previous.setFullYear(previous.getFullYear() + years);
22436
- previous.setMonth(previous.getMonth() + months);
22437
- const days = getTimeDifferenceInWholeDays(previous, date);
22438
- return {
22439
- years,
22440
- months,
22441
- days,
22442
- };
22443
- });
22444
- return res.slice(1);
22445
- }
22446
-
22447
- const cellPopoverRegistry = new Registry();
22448
-
22449
- const GAUGE_PADDING_SIDE = 30;
22450
- const GAUGE_PADDING_TOP = 10;
22451
- const GAUGE_PADDING_BOTTOM = 20;
22452
- const GAUGE_LABELS_FONT_SIZE = 12;
22453
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22454
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22455
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22456
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22457
- function drawGaugeChart(canvas, runtime) {
22458
- const canvasBoundingRect = canvas.getBoundingClientRect();
22459
- canvas.width = canvasBoundingRect.width;
22460
- canvas.height = canvasBoundingRect.height;
22461
- const ctx = canvas.getContext("2d");
22462
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22463
- drawBackground(ctx, config);
22464
- drawGauge(ctx, config);
22465
- drawInflectionValues(ctx, config);
22466
- drawLabels(ctx, config);
22467
- drawTitle(ctx, config);
22468
- }
22469
- function drawGauge(ctx, config) {
22470
- ctx.save();
22471
- const gauge = config.gauge;
22472
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22473
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22474
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22475
- if (arcRadius < 0) {
22476
- return;
22477
- }
22478
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22479
- // Gauge background
22480
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22481
- ctx.beginPath();
22482
- ctx.lineWidth = gauge.arcWidth;
22483
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22484
- ctx.stroke();
22485
- // Gauge value
22486
- ctx.strokeStyle = gauge.color;
22487
- ctx.beginPath();
22488
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22489
- ctx.stroke();
22490
- ctx.restore();
22491
- }
22492
- function drawBackground(ctx, config) {
22493
- ctx.save();
22494
- ctx.fillStyle = config.backgroundColor;
22495
- ctx.fillRect(0, 0, config.width, config.height);
22496
- ctx.restore();
22497
- }
22498
- function drawLabels(ctx, config) {
22499
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22500
- ctx.save();
22501
- ctx.textAlign = "center";
22502
- ctx.fillStyle = label.color;
22503
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22504
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22505
- ctx.restore();
22506
- }
22507
- }
22508
- function drawInflectionValues(ctx, config) {
22509
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22510
- for (const inflectionValue of config.inflectionValues) {
22511
- ctx.save();
22512
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22513
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22514
- ctx.lineWidth = 2;
22515
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22516
- ctx.beginPath();
22517
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22518
- ctx.lineTo(0, -height - 3);
22519
- ctx.stroke();
22520
- ctx.textAlign = "center";
22521
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22522
- ctx.fillStyle = inflectionValue.color;
22523
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22524
- ctx.fillText(inflectionValue.label, 0, textY);
22525
- ctx.restore();
22526
- }
22527
- }
22528
- function drawTitle(ctx, config) {
22529
- ctx.save();
22530
- const title = config.title;
22531
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22532
- ctx.textBaseline = "middle";
22533
- ctx.fillStyle = title.color;
22534
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22535
- ctx.restore();
22536
- }
22537
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22538
- const maxValue = runtime.maxValue;
22539
- const minValue = runtime.minValue;
22540
- const gaugeValue = runtime.gaugeValue;
22541
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22542
- const gaugeArcWidth = gaugeRect.width / 6;
22543
- const gaugePercentage = gaugeValue
22544
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22545
- : 0;
22546
- const gaugeValuePosition = {
22547
- x: boundingRect.width / 2,
22548
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22549
- };
22550
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22551
- // Scale down the font size if the gaugeRect is too small
22552
- if (gaugeRect.height < 300) {
22553
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22554
- }
22555
- // Scale down the font size if the text is too long
22556
- const maxTextWidth = gaugeRect.width / 2;
22557
- const gaugeLabel = gaugeValue?.label || "-";
22558
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22559
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22560
- }
22561
- const minLabelPosition = {
22562
- x: gaugeRect.x + gaugeArcWidth / 2,
22563
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22564
- };
22565
- const maxLabelPosition = {
22566
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22567
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22568
- };
22569
- const textColor = chartMutedFontColor(runtime.background);
22570
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22571
- let x = 0, titleWidth = 0, titleHeight = 0;
22572
- if (runtime.title.text) {
22573
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22574
- }
22575
- switch (runtime.title.align) {
22576
- case "right":
22577
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22578
- break;
22579
- case "center":
22580
- x = (boundingRect.width - titleWidth) / 2;
22581
- break;
22582
- case "left":
22583
- default:
22584
- x = CHART_PADDING$1;
22585
- break;
22586
- }
22587
- return {
22588
- width: boundingRect.width,
22589
- height: boundingRect.height,
22590
- title: {
22591
- label: runtime.title.text ?? "",
22592
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22593
- textPosition: {
22594
- x,
22595
- y: CHART_PADDING_TOP + titleHeight / 2,
22596
- },
22597
- color: runtime.title.color ?? textColor,
22598
- bold: runtime.title.bold,
22599
- italic: runtime.title.italic,
22600
- },
22601
- backgroundColor: runtime.background,
22602
- gauge: {
22603
- rect: gaugeRect,
22604
- arcWidth: gaugeArcWidth,
22605
- percentage: clip(gaugePercentage, 0, 1),
22606
- color: getGaugeColor(runtime),
22607
- },
22608
- inflectionValues,
22609
- gaugeValue: {
22610
- label: gaugeLabel,
22611
- textPosition: gaugeValuePosition,
22612
- fontSize: gaugeValueFontSize,
22613
- color: textColor,
22614
- },
22615
- minLabel: {
22616
- label: runtime.minValue.label,
22617
- textPosition: minLabelPosition,
22618
- fontSize: GAUGE_LABELS_FONT_SIZE,
22619
- color: textColor,
22620
- },
22621
- maxLabel: {
22622
- label: runtime.maxValue.label,
22623
- textPosition: maxLabelPosition,
22624
- fontSize: GAUGE_LABELS_FONT_SIZE,
22625
- color: textColor,
22626
- },
22627
- };
22628
- }
22629
- /**
22630
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22631
- * space for the title and labels.
22632
- */
22633
- function getGaugeRect(boundingRect, title) {
22634
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22635
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22636
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22637
- let gaugeWidth;
22638
- let gaugeHeight;
22639
- if (drawWidth > 2 * drawHeight) {
22640
- gaugeWidth = 2 * drawHeight;
22641
- gaugeHeight = drawHeight;
22642
- }
22643
- else {
22644
- gaugeWidth = drawWidth;
22645
- gaugeHeight = drawWidth / 2;
22646
- }
22647
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22648
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22649
- return {
22650
- x: gaugeX,
22651
- y: gaugeY,
22652
- width: gaugeWidth,
22653
- height: gaugeHeight,
22654
- };
22655
- }
22656
- /**
22657
- * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
22658
- *
22659
- * Also compute an offset for the text so that it doesn't overlap with other text.
22660
- */
22661
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22662
- const maxValue = runtime.maxValue;
22663
- const minValue = runtime.minValue;
22664
- const gaugeCircleCenter = {
22665
- x: gaugeRect.x + gaugeRect.width / 2,
22666
- y: gaugeRect.y + gaugeRect.height,
22667
- };
22668
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22669
- const inflectionValues = [];
22670
- const inflectionValuesTextRects = [];
22671
- for (const inflectionValue of runtime.inflectionValues) {
22672
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22673
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22674
- const angle = Math.PI - Math.PI * percentage;
22675
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22676
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22677
- gaugeCircleCenter.x, // center of the gauge circle
22678
- gaugeCircleCenter.y, // center of the gauge circle
22679
- labelWidth + 2, // width of the text + some margin
22680
- GAUGE_LABELS_FONT_SIZE // height of the text
22681
- );
22682
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22683
- ? GAUGE_LABELS_FONT_SIZE
22684
- : 0;
22685
- inflectionValuesTextRects.push(textRect);
22686
- inflectionValues.push({
22687
- rotation: angle,
22688
- label: inflectionValue.label,
22689
- fontSize: GAUGE_LABELS_FONT_SIZE,
22690
- color: textColor,
22691
- offset,
22692
- });
22693
- }
22694
- return inflectionValues;
22695
- }
22696
- function getGaugeColor(runtime) {
22697
- const gaugeValue = runtime.gaugeValue?.value;
22698
- if (gaugeValue === undefined) {
22699
- return GAUGE_BACKGROUND_COLOR;
22700
- }
22701
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22702
- const inflectionValue = runtime.inflectionValues[i];
22703
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22704
- return runtime.colors[i];
22675
+ */
22676
+ function getGroup(cell, cells, filter) {
22677
+ let group = [];
22678
+ let found = false;
22679
+ for (let x of cells) {
22680
+ if (x === cell) {
22681
+ found = true;
22705
22682
  }
22706
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22707
- return runtime.colors[i];
22683
+ const cellValue = x === undefined || x.isFormula
22684
+ ? undefined
22685
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22686
+ if (cellValue && filter(cellValue)) {
22687
+ group.push(cellValue);
22688
+ }
22689
+ else {
22690
+ if (found) {
22691
+ return group;
22692
+ }
22693
+ group = [];
22708
22694
  }
22709
22695
  }
22710
- return runtime.colors.at(-1);
22696
+ return group;
22711
22697
  }
22712
- function getSegmentsOfRectangle(rectangle) {
22713
- return [
22714
- { start: rectangle.topLeft, end: rectangle.topRight },
22715
- { start: rectangle.topRight, end: rectangle.bottomRight },
22716
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22717
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22718
- ];
22698
+ /**
22699
+ * Get the average steps between numbers
22700
+ */
22701
+ function getAverageIncrement(group) {
22702
+ const averages = [];
22703
+ let last = group[0];
22704
+ for (let i = 1; i < group.length; i++) {
22705
+ const current = group[i];
22706
+ averages.push(current - last);
22707
+ last = current;
22708
+ }
22709
+ return averages.reduce((a, b) => a + b, 0) / averages.length;
22719
22710
  }
22720
22711
  /**
22721
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22722
- * is not handled.
22712
+ * Get the step for a group
22723
22713
  */
22724
- function doSegmentIntersect(segment1, segment2) {
22725
- const A = segment1.start;
22726
- const B = segment1.end;
22727
- const C = segment2.start;
22728
- const D = segment2.end;
22729
- /**
22730
- * Line segment intersection algorithm
22731
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22732
- */
22733
- function ccw(a, b, c) {
22734
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22714
+ function calculateIncrementBasedOnGroup(group) {
22715
+ let increment = 1;
22716
+ if (group.length >= 2) {
22717
+ increment = getAverageIncrement(group) * group.length;
22735
22718
  }
22736
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22719
+ return increment;
22737
22720
  }
22738
- function doRectanglesIntersect(rect1, rect2) {
22739
- const segments1 = getSegmentsOfRectangle(rect1);
22740
- const segments2 = getSegmentsOfRectangle(rect2);
22741
- for (const segment1 of segments1) {
22742
- for (const segment2 of segments2) {
22743
- if (doSegmentIntersect(segment1, segment2)) {
22744
- return true;
22721
+ /**
22722
+ * Iterates on a list of date intervals.
22723
+ * if every interval is the same, return the interval
22724
+ * Otherwise return undefined
22725
+ *
22726
+ */
22727
+ function getEqualInterval(intervals) {
22728
+ if (intervals.length < 2) {
22729
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22730
+ }
22731
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22732
+ interval.months === intervals[0].months &&
22733
+ interval.days === intervals[0].days);
22734
+ return equal ? intervals[0] : undefined;
22735
+ }
22736
+ /**
22737
+ * Based on a group of dates, calculate the increment that should be applied
22738
+ * to the next date.
22739
+ *
22740
+ * This will compute the date difference in calendar terms (years, months, days)
22741
+ * In order to make abstraction of leap years and months with different number of days.
22742
+ *
22743
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22744
+ * In case of equidistant dates, we either have in that order:
22745
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22746
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22747
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22748
+ *
22749
+ * */
22750
+ function calculateDateIncrementBasedOnGroup(group) {
22751
+ if (group.length < 2) {
22752
+ return 1;
22753
+ }
22754
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22755
+ const datesIntervals = getDateIntervals(jsDates);
22756
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22757
+ if (datesEquidistantInterval === undefined) {
22758
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22759
+ return undefined;
22760
+ }
22761
+ // The dates are apart by an exact interval of years, months or days
22762
+ // but not a combination of them
22763
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22764
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22765
+ if (!exactDateInterval || isSameDay) {
22766
+ const timeIntervals = jsDates
22767
+ .map((date, index) => {
22768
+ if (index === 0) {
22769
+ return 0;
22745
22770
  }
22771
+ const previous = jsDates[index - 1];
22772
+ return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22773
+ })
22774
+ .slice(1);
22775
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22776
+ if (equidistantDates) {
22777
+ return group.length * (group[1] - group[0]);
22746
22778
  }
22747
22779
  }
22748
- return false;
22780
+ return {
22781
+ years: datesEquidistantInterval.years * group.length,
22782
+ months: datesEquidistantInterval.months * group.length,
22783
+ days: datesEquidistantInterval.days * group.length,
22784
+ };
22749
22785
  }
22786
+ autofillRulesRegistry
22787
+ .add("simple_value_copy", {
22788
+ condition: (cell, cells) => {
22789
+ return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
22790
+ },
22791
+ generateRule: () => {
22792
+ return { type: "COPY_MODIFIER" };
22793
+ },
22794
+ sequence: 10,
22795
+ })
22796
+ .add("increment_alphanumeric_value", {
22797
+ condition: (cell) => !cell.isFormula &&
22798
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22799
+ alphaNumericValueRegExp.test(cell.content),
22800
+ generateRule: (cell, cells, direction) => {
22801
+ const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22802
+ const prefix = cell.content.match(stringPrefixRegExp)[0];
22803
+ const numberPostfixLength = cell.content.length - prefix.length;
22804
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
22805
+ alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22806
+ .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22807
+ .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22808
+ let increment = calculateIncrementBasedOnGroup(group);
22809
+ if (["up", "left"].includes(direction) && group.length === 1) {
22810
+ increment = -increment;
22811
+ }
22812
+ return {
22813
+ type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22814
+ prefix,
22815
+ current: numberPostfix,
22816
+ increment,
22817
+ numberPostfixLength,
22818
+ };
22819
+ },
22820
+ sequence: 15,
22821
+ })
22822
+ .add("copy_text", {
22823
+ condition: (cell) => !cell.isFormula &&
22824
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
22825
+ generateRule: () => {
22826
+ return { type: "COPY_MODIFIER" };
22827
+ },
22828
+ sequence: 20,
22829
+ })
22830
+ .add("update_formula", {
22831
+ condition: (cell) => cell.isFormula,
22832
+ generateRule: (_, cells) => {
22833
+ return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22834
+ },
22835
+ sequence: 30,
22836
+ })
22837
+ .add("increment_dates", {
22838
+ condition: (cell, cells) => {
22839
+ return (!cell.isFormula &&
22840
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22841
+ !!cell.format &&
22842
+ isDateTimeFormat(cell.format));
22843
+ },
22844
+ generateRule: (cell, cells) => {
22845
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22846
+ !!evaluatedCell.format &&
22847
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22848
+ const increment = calculateDateIncrementBasedOnGroup(group);
22849
+ if (increment === undefined) {
22850
+ return { type: "COPY_MODIFIER" };
22851
+ }
22852
+ /** requires to detect the current date (requires to be an integer value with the right format)
22853
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22854
+ */
22855
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22856
+ if (typeof increment === "object") {
22857
+ return {
22858
+ type: "DATE_INCREMENT_MODIFIER",
22859
+ increment,
22860
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22861
+ };
22862
+ }
22863
+ return {
22864
+ type: "INCREMENT_MODIFIER",
22865
+ increment,
22866
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22867
+ };
22868
+ },
22869
+ sequence: 25,
22870
+ })
22871
+ .add("increment_number", {
22872
+ condition: (cell) => !cell.isFormula &&
22873
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22874
+ generateRule: (cell, cells, direction) => {
22875
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22876
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22877
+ let increment = calculateIncrementBasedOnGroup(group);
22878
+ if (["up", "left"].includes(direction) && group.length === 1) {
22879
+ increment = -increment;
22880
+ }
22881
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22882
+ return {
22883
+ type: "INCREMENT_MODIFIER",
22884
+ increment,
22885
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22886
+ };
22887
+ },
22888
+ sequence: 40,
22889
+ });
22750
22890
  /**
22751
- * Get the rectangle that is tangent to a circle at a given angle.
22891
+ * Returns the date intervals between consecutive dates of an array
22892
+ * in the format of { years: number, months: number, days: number }
22752
22893
  *
22753
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22894
+ * The split is necessary to make abstraction of leap years and
22895
+ * months with different number of days.
22896
+ *
22897
+ * @param dates
22754
22898
  */
22755
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22756
- const cos = Math.cos(angle);
22757
- const sin = Math.sin(angle);
22758
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22759
- const x = cos * radius;
22760
- const y = sin * radius;
22761
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22762
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22763
- const y2 = cos * (rectWidth / 2);
22764
- const bottomRight = {
22765
- x: x + x2 + circleCenterX,
22766
- y: circleCenterY - (y - y2),
22767
- };
22768
- const bottomLeft = {
22769
- x: x - x2 + circleCenterX,
22770
- y: circleCenterY - (y + y2),
22771
- };
22772
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22773
- const xp = cos * (radius + rectHeight);
22774
- const yp = sin * (radius + rectHeight);
22775
- const topLeft = {
22776
- x: xp - x2 + circleCenterX,
22777
- y: circleCenterY - (yp + y2),
22778
- };
22779
- const topRight = {
22780
- x: xp + x2 + circleCenterX,
22781
- y: circleCenterY - (yp - y2),
22782
- };
22783
- return { bottomLeft, bottomRight, topRight, topLeft };
22899
+ function getDateIntervals(dates) {
22900
+ if (dates.length < 2) {
22901
+ return [{ years: 0, months: 0, days: 0 }];
22902
+ }
22903
+ const res = dates.map((date, index) => {
22904
+ if (index === 0) {
22905
+ return { years: 0, months: 0, days: 0 };
22906
+ }
22907
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22908
+ const years = getTimeDifferenceInWholeYears(previous, date);
22909
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22910
+ previous.setFullYear(previous.getFullYear() + years);
22911
+ previous.setMonth(previous.getMonth() + months);
22912
+ const days = getTimeDifferenceInWholeDays(previous, date);
22913
+ return {
22914
+ years,
22915
+ months,
22916
+ days,
22917
+ };
22918
+ });
22919
+ return res.slice(1);
22784
22920
  }
22785
22921
 
22922
+ const cellPopoverRegistry = new Registry();
22923
+
22786
22924
  class GaugeChartComponent extends owl.Component {
22787
22925
  static template = "o-spreadsheet-GaugeChartComponent";
22788
22926
  canvas = owl.useRef("chartContainer");
@@ -22815,72 +22953,6 @@ function toXlsxHexColor(color) {
22815
22953
  return color;
22816
22954
  }
22817
22955
 
22818
- const CHART_COMMON_OPTIONS = {
22819
- // https://www.chartjs.org/docs/latest/general/responsive.html
22820
- responsive: true, // will resize when its container is resized
22821
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22822
- elements: {
22823
- line: {
22824
- fill: false, // do not fill the area under line charts
22825
- },
22826
- point: {
22827
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22828
- },
22829
- },
22830
- animation: false,
22831
- };
22832
- function chartToImage(runtime, figure, type) {
22833
- // wrap the canvas in a div with a fixed size because chart.js would
22834
- // fill the whole page otherwise
22835
- const div = document.createElement("div");
22836
- div.style.width = `${figure.width}px`;
22837
- div.style.height = `${figure.height}px`;
22838
- const canvas = document.createElement("canvas");
22839
- div.append(canvas);
22840
- canvas.setAttribute("width", figure.width.toString());
22841
- canvas.setAttribute("height", figure.height.toString());
22842
- // we have to add the canvas to the DOM otherwise it won't be rendered
22843
- document.body.append(div);
22844
- if ("chartJsConfig" in runtime) {
22845
- const config = deepCopy(runtime.chartJsConfig);
22846
- config.plugins = [backgroundColorChartJSPlugin];
22847
- const chart = new window.Chart(canvas, config);
22848
- const imgContent = chart.toBase64Image();
22849
- chart.destroy();
22850
- div.remove();
22851
- return imgContent;
22852
- }
22853
- else if (type === "scorecard") {
22854
- const design = getScorecardConfiguration(figure, runtime);
22855
- drawScoreChart(design, canvas);
22856
- const imgContent = canvas.toDataURL();
22857
- div.remove();
22858
- return imgContent;
22859
- }
22860
- else if (type === "gauge") {
22861
- drawGaugeChart(canvas, runtime);
22862
- const imgContent = canvas.toDataURL();
22863
- div.remove();
22864
- return imgContent;
22865
- }
22866
- return undefined;
22867
- }
22868
- /**
22869
- * Custom chart.js plugin to set the background color of the canvas
22870
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22871
- */
22872
- const backgroundColorChartJSPlugin = {
22873
- id: "customCanvasBackgroundColor",
22874
- beforeDraw: (chart) => {
22875
- const { ctx } = chart;
22876
- ctx.save();
22877
- ctx.globalCompositeOperation = "destination-over";
22878
- ctx.fillStyle = "#ffffff";
22879
- ctx.fillRect(0, 0, chart.width, chart.height);
22880
- ctx.restore();
22881
- },
22882
- };
22883
-
22884
22956
  /**
22885
22957
  * Represent a raw XML string
22886
22958
  */
@@ -22946,6 +23018,7 @@ const CONTENT_TYPES = {
22946
23018
  macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
22947
23019
  excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
22948
23020
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
23021
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22949
23022
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22950
23023
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22951
23024
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22958,6 +23031,7 @@ const CONTENT_TYPES = {
22958
23031
  const XLSX_RELATION_TYPE = {
22959
23032
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22960
23033
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
23034
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22961
23035
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22962
23036
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22963
23037
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22967,6 +23041,7 @@ const XLSX_RELATION_TYPE = {
22967
23041
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22968
23042
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22969
23043
  };
23044
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22970
23045
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22971
23046
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22972
23047
  /**
@@ -24342,16 +24417,25 @@ function addRelsToFile(relsFiles, path, rel) {
24342
24417
  }
24343
24418
  return id;
24344
24419
  }
24420
+ const globalReverseLookup = new WeakMap();
24345
24421
  function pushElement(property, propertyList) {
24346
- let len = propertyList.length;
24347
- const operator = typeof property === "object" ? deepEquals : (a, b) => a === b;
24348
- for (let i = 0; i < len; i++) {
24349
- if (operator(property, propertyList[i])) {
24350
- return i;
24422
+ let reverseLookup = globalReverseLookup.get(propertyList);
24423
+ if (!reverseLookup) {
24424
+ reverseLookup = new Map();
24425
+ for (let i = 0; i < propertyList.length; i++) {
24426
+ const canonical = getCanonicalRepresentation(propertyList[i]);
24427
+ reverseLookup.set(canonical, i);
24351
24428
  }
24429
+ globalReverseLookup.set(propertyList, reverseLookup);
24430
+ }
24431
+ const canonical = getCanonicalRepresentation(property);
24432
+ if (reverseLookup.has(canonical)) {
24433
+ return reverseLookup.get(canonical);
24352
24434
  }
24353
- propertyList[propertyList.length] = property;
24354
- return propertyList.length - 1;
24435
+ const maxId = propertyList.length;
24436
+ propertyList.push(property);
24437
+ reverseLookup.set(canonical, maxId);
24438
+ return maxId;
24355
24439
  }
24356
24440
  const chartIds = [];
24357
24441
  /**
@@ -25388,29 +25472,34 @@ function convertPivotTableConfig(pivotTable) {
25388
25472
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25389
25473
  */
25390
25474
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25391
- for (let sheet of convertedSheets) {
25392
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25475
+ for (let tableSheet of convertedSheets) {
25476
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25393
25477
  for (let table of tables) {
25394
25478
  const tabRef = table.name + "[";
25395
- for (let position of positions(toZone(table.ref))) {
25396
- const xc = toXC(position.col, position.row);
25397
- let cellContent = sheet.cells[xc];
25398
- if (cellContent?.startsWith("=")) {
25399
- let refIndex;
25400
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25401
- let reference = cellContent.slice(refIndex + tabRef.length);
25402
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25403
- let endIndex = reference.indexOf("]");
25404
- if (reference.startsWith(`[`)) {
25405
- endIndex = reference.indexOf("]", endIndex + 1);
25406
- endIndex = reference.indexOf("]", endIndex + 1);
25479
+ for (let sheet of convertedSheets) {
25480
+ for (let xc in sheet.cells) {
25481
+ const cell = sheet.cells[xc];
25482
+ let cellContent = sheet.cells[xc];
25483
+ if (cell && cellContent && cellContent.startsWith("=")) {
25484
+ let refIndex;
25485
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25486
+ let endIndex = refIndex + tabRef.length;
25487
+ let openBrackets = 1;
25488
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25489
+ if (cellContent[endIndex] === "[") {
25490
+ openBrackets++;
25491
+ }
25492
+ else if (cellContent[endIndex] === "]") {
25493
+ openBrackets--;
25494
+ }
25495
+ endIndex++;
25496
+ }
25497
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25498
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25499
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25500
+ cellContent =
25501
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25407
25502
  }
25408
- reference = reference.slice(0, endIndex);
25409
- const convertedRef = convertTableReference(reference, table, xc);
25410
- cellContent =
25411
- cellContent.slice(0, refIndex) +
25412
- convertedRef +
25413
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25414
25503
  }
25415
25504
  sheet.cells[xc] = cellContent;
25416
25505
  }
@@ -25419,11 +25508,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25419
25508
  }
25420
25509
  }
25421
25510
  /**
25422
- * Convert table-specific references in formulas into standard references.
25511
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25512
+ * and of keywords determining the rows of the table to reference.
25423
25513
  *
25424
25514
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25425
25515
  * - tableName[colName] : reference to the whole column "colName"
25516
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25426
25517
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25518
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25519
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25520
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25521
+ *
25427
25522
  *
25428
25523
  * The available keywords are :
25429
25524
  * - #All : all the column (including totals)
@@ -25431,58 +25526,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25431
25526
  * - #Headers : only the header of the column
25432
25527
  * - #Totals : only the totals of the column
25433
25528
  * - #This Row : only the element in the same row as the cell
25529
+ *
25530
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25434
25531
  */
25435
- function convertTableReference(expr, table, cellXc) {
25436
- const refElements = expr.split(",");
25532
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25533
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25534
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25535
+ const parts = expr.split(",").map((part) => part.trim());
25437
25536
  const tableZone = toZone(table.ref);
25438
- const refZone = { ...tableZone };
25439
- let isReferencedZoneValid = true;
25440
- // Single column reference
25441
- if (refElements.length === 1) {
25442
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25443
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25444
- if (table.headerRowCount) {
25445
- refZone.top += table.headerRowCount;
25446
- }
25447
- if (table.totalsRowCount) {
25448
- refZone.bottom -= 1;
25537
+ const colIndexes = [];
25538
+ const rowIndexes = [];
25539
+ const foundKeywords = [];
25540
+ for (const part of parts) {
25541
+ if (removeBrackets(part).startsWith("#")) {
25542
+ const keyWord = removeBrackets(part);
25543
+ foundKeywords.push(keyWord);
25544
+ switch (keyWord) {
25545
+ case "#All":
25546
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25547
+ break;
25548
+ case "#Data":
25549
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25550
+ const bottom = table.totalsRowCount
25551
+ ? tableZone.bottom - table.totalsRowCount
25552
+ : tableZone.bottom;
25553
+ rowIndexes.push(top, bottom);
25554
+ break;
25555
+ case "#This Row":
25556
+ rowIndexes.push(toCartesian(cellXc).row);
25557
+ break;
25558
+ case "#Headers":
25559
+ if (!table.headerRowCount) {
25560
+ return CellErrorType.InvalidReference;
25561
+ }
25562
+ rowIndexes.push(tableZone.top);
25563
+ break;
25564
+ case "#Totals":
25565
+ if (!table.totalsRowCount) {
25566
+ return CellErrorType.InvalidReference;
25567
+ }
25568
+ rowIndexes.push(tableZone.bottom);
25569
+ break;
25570
+ }
25449
25571
  }
25450
- }
25451
- // Other references
25452
- else {
25453
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25454
- case "#All":
25455
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25456
- refZone.bottom = tableZone.bottom;
25457
- break;
25458
- case "#Data":
25459
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25460
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25461
- break;
25462
- case "#This Row":
25463
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25464
- break;
25465
- case "#Headers":
25466
- refZone.top = refZone.bottom = tableZone.top;
25467
- if (!table.headerRowCount) {
25468
- isReferencedZoneValid = false;
25469
- }
25470
- break;
25471
- case "#Totals":
25472
- refZone.top = refZone.bottom = tableZone.bottom;
25473
- if (!table.totalsRowCount) {
25474
- isReferencedZoneValid = false;
25572
+ else {
25573
+ const columns = part
25574
+ .split(":")
25575
+ .map((part) => part.trim())
25576
+ .map(removeBrackets);
25577
+ if (colIndexes.length) {
25578
+ return CellErrorType.InvalidReference;
25579
+ }
25580
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25581
+ if (colRelativeIndex === -1) {
25582
+ return CellErrorType.InvalidReference;
25583
+ }
25584
+ colIndexes.push(colRelativeIndex + tableZone.left);
25585
+ if (columns[1]) {
25586
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25587
+ if (colRelativeIndex2 === -1) {
25588
+ return CellErrorType.InvalidReference;
25475
25589
  }
25476
- break;
25590
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25591
+ }
25477
25592
  }
25478
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25479
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25480
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25481
25593
  }
25482
- if (!isReferencedZoneValid) {
25594
+ if (!areKeywordsCompatible(foundKeywords)) {
25483
25595
  return CellErrorType.InvalidReference;
25484
25596
  }
25485
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25597
+ if (rowIndexes.length === 0) {
25598
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25599
+ const bottom = table.totalsRowCount
25600
+ ? tableZone.bottom - table.totalsRowCount
25601
+ : tableZone.bottom;
25602
+ rowIndexes.push(top, bottom);
25603
+ }
25604
+ if (colIndexes.length === 0) {
25605
+ colIndexes.push(tableZone.left, tableZone.right);
25606
+ }
25607
+ const refZone = {
25608
+ top: Math.min(...rowIndexes),
25609
+ left: Math.min(...colIndexes),
25610
+ bottom: Math.max(...rowIndexes),
25611
+ right: Math.max(...colIndexes),
25612
+ };
25613
+ return sheetPrefix + zoneToXc(refZone);
25614
+ }
25615
+ function removeBrackets(str) {
25616
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25617
+ }
25618
+ function areKeywordsCompatible(keywords) {
25619
+ if (keywords.length < 2) {
25620
+ return true;
25621
+ }
25622
+ else if (keywords.length > 2) {
25623
+ return false;
25624
+ }
25625
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25626
+ return true;
25627
+ }
25628
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25629
+ return true;
25630
+ }
25631
+ return false;
25486
25632
  }
25487
25633
 
25488
25634
  // -------------------------------------
@@ -26136,7 +26282,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26136
26282
  title: { text: chartTitle },
26137
26283
  type: CHART_TYPE_CONVERSION_MAP[chartType],
26138
26284
  dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
26139
- labelRange: this.extractChildTextContent(rootChartElement, `c:ser ${chartType === "scatterChart" ? "c:numRef" : "c:cat"} c:f`),
26285
+ labelRange: this.extractLabelRange(chartType, rootChartElement),
26140
26286
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
26141
26287
  default: "ffffff",
26142
26288
  }).asString(),
@@ -26148,6 +26294,13 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26148
26294
  };
26149
26295
  })[0];
26150
26296
  }
26297
+ extractLabelRange(chartType, rootChartElement) {
26298
+ if (chartType === "scatterChart") {
26299
+ return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
26300
+ this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
26301
+ }
26302
+ return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
26303
+ }
26151
26304
  extractComboChart(chartElement) {
26152
26305
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
26153
26306
  const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
@@ -28650,11 +28803,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
28650
28803
  }
28651
28804
  let missingTimeAdapterAlreadyWarned = false;
28652
28805
  function isLuxonTimeAdapterInstalled() {
28653
- if (!window.Chart) {
28806
+ const Chart = getChartJSConstructor();
28807
+ if (!Chart) {
28654
28808
  return false;
28655
28809
  }
28656
28810
  // @ts-ignore
28657
- const adapter = new window.Chart._adapters._date({});
28811
+ const adapter = new Chart._adapters._date({});
28658
28812
  const isInstalled = adapter._id === "luxon";
28659
28813
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28660
28814
  missingTimeAdapterAlreadyWarned = true;
@@ -32508,10 +32662,6 @@ class Popover extends owl.Component {
32508
32662
  this.currentDisplayValue = newDisplay;
32509
32663
  if (!anchor)
32510
32664
  return;
32511
- el.style.top = "";
32512
- el.style.left = "";
32513
- el.style["max-height"] = "";
32514
- el.style["max-width"] = "";
32515
32665
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32516
32666
  let elDims = {
32517
32667
  width: el.getBoundingClientRect().width,
@@ -34063,6 +34213,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34063
34213
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34064
34214
  formatChartDatasetValue: formatChartDatasetValue,
34065
34215
  formatTickValue: formatTickValue,
34216
+ getChartJSConstructor: getChartJSConstructor,
34066
34217
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34067
34218
  getDefinedAxis: getDefinedAxis,
34068
34219
  getPieColors: getPieColors,
@@ -36322,6 +36473,7 @@ const irregularityMap = {
36322
36473
  fingerprintStore.enable();
36323
36474
  }
36324
36475
  },
36476
+ isReadonlyAllowed: true,
36325
36477
  icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
36326
36478
  };
36327
36479
  const viewFormulas = {
@@ -37863,6 +38015,11 @@ class SelectionInputStore extends SpreadsheetStore {
37863
38015
  }
37864
38016
  updateColors(colors) {
37865
38017
  this.colors = colors;
38018
+ const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
38019
+ this.ranges = this.ranges.map((range) => ({
38020
+ ...range,
38021
+ color: colorGenerator.next(),
38022
+ }));
37866
38023
  }
37867
38024
  confirm() {
37868
38025
  for (const range of this.selectionInputs) {
@@ -37897,12 +38054,11 @@ class SelectionInputStore extends SpreadsheetStore {
37897
38054
  * e.g. ["A1", "Sheet2!B3", "E12"]
37898
38055
  */
37899
38056
  get selectionInputs() {
37900
- const generator = new ColorGenerator(this.ranges.length, this.colors);
37901
38057
  return this.ranges.map((input, index) => Object.assign({}, input, {
37902
38058
  color: this.hasMainFocus &&
37903
38059
  this.focusedRangeIndex !== null &&
37904
38060
  this.getters.isRangeValid(input.xc)
37905
- ? generator.next()
38061
+ ? input.color
37906
38062
  : null,
37907
38063
  isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
37908
38064
  isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
@@ -38172,10 +38328,10 @@ class SelectionInput extends owl.Component {
38172
38328
  if (originalIndex === finalIndex) {
38173
38329
  return;
38174
38330
  }
38175
- const draggedItems = [...draggableIds];
38176
- draggedItems.splice(originalIndex, 1);
38177
- draggedItems.splice(finalIndex, 0, rangeId);
38178
- this.props.onSelectionReordered?.(this.store.selectionInputs.map((range) => draggedItems.indexOf(range.id)));
38331
+ const indexes = range(0, draggableIds.length);
38332
+ indexes.splice(originalIndex, 1);
38333
+ indexes.splice(finalIndex, 0, originalIndex);
38334
+ this.props.onSelectionReordered?.(indexes);
38179
38335
  this.props.onSelectionConfirmed?.();
38180
38336
  this.store.confirm();
38181
38337
  },
@@ -38453,6 +38609,9 @@ class GenericChartConfigPanel extends owl.Component {
38453
38609
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
38454
38610
  dataSets: this.dataSets,
38455
38611
  });
38612
+ if (this.state.datasetDispatchResult.isSuccessful) {
38613
+ this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
38614
+ }
38456
38615
  }
38457
38616
  getDataSeriesRanges() {
38458
38617
  return this.dataSets;
@@ -39869,8 +40028,16 @@ class ContentEditableHelper {
39869
40028
  }
39870
40029
  let startNode = this.findChildAtCharacterIndex(start);
39871
40030
  let endNode = this.findChildAtCharacterIndex(end);
39872
- range.setStart(startNode.node, startNode.offset);
39873
- range.setEnd(endNode.node, endNode.offset);
40031
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
40032
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
40033
+ if (start <= end) {
40034
+ range.setStart(startNode.node, startNode.offset);
40035
+ range.setEnd(endNode.node, endNode.offset);
40036
+ }
40037
+ else {
40038
+ range.setStart(endNode.node, endNode.offset);
40039
+ range.setEnd(startNode.node, startNode.offset);
40040
+ }
39874
40041
  }
39875
40042
  }
39876
40043
  /**
@@ -40172,8 +40339,7 @@ css /* scss */ `
40172
40339
  }
40173
40340
 
40174
40341
  .o-composer-assistant {
40175
- position: absolute;
40176
- margin: 1px 4px;
40342
+ margin-top: 1px;
40177
40343
 
40178
40344
  .o-semi-bold {
40179
40345
  /* FIXME: to remove in favor of Bootstrap
@@ -40224,10 +40390,11 @@ class Composer extends owl.Component {
40224
40390
  });
40225
40391
  compositionActive = false;
40226
40392
  spreadsheetRect = useSpreadsheetRect();
40227
- get assistantStyle() {
40393
+ get assistantStyleProperties() {
40228
40394
  const composerRect = this.composerRef.el.getBoundingClientRect();
40229
40395
  const assistantStyle = {};
40230
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40396
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40397
+ assistantStyle["min-width"] = `${minWidth}px`;
40231
40398
  const proposals = this.autoCompleteState.provider?.proposals;
40232
40399
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40233
40400
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40251,13 +40418,29 @@ class Composer extends owl.Component {
40251
40418
  }
40252
40419
  }
40253
40420
  else {
40254
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40421
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40255
40422
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40256
40423
  this.spreadsheetRect.width) {
40257
40424
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40258
40425
  }
40259
40426
  }
40260
- return cssPropertiesToCss(assistantStyle);
40427
+ return assistantStyle;
40428
+ }
40429
+ get assistantStyle() {
40430
+ const allProperties = this.assistantStyleProperties;
40431
+ return cssPropertiesToCss({
40432
+ "max-height": allProperties["max-height"],
40433
+ width: allProperties["width"],
40434
+ "min-width": allProperties["min-width"],
40435
+ });
40436
+ }
40437
+ get assistantContainerStyle() {
40438
+ const allProperties = this.assistantStyleProperties;
40439
+ return cssPropertiesToCss({
40440
+ top: allProperties["top"],
40441
+ right: allProperties["right"],
40442
+ transform: allProperties["transform"],
40443
+ });
40261
40444
  }
40262
40445
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40263
40446
  shouldProcessInputEvents = false;
@@ -46731,9 +46914,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46731
46914
  pivot: this.draft,
46732
46915
  });
46733
46916
  this.draft = null;
46734
- if (!this.alreadyNotified &&
46735
- !this.isDynamicPivotInViewport() &&
46736
- this.isStaticPivotInViewport()) {
46917
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46737
46918
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46738
46919
  const pivotExample = `=PIVOT(${formulaId})`;
46739
46920
  this.alreadyNotified = true;
@@ -46789,26 +46970,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
46789
46970
  this.applyUpdate();
46790
46971
  }
46791
46972
  }
46792
- isDynamicPivotInViewport() {
46793
- for (const position of this.getters.getVisibleCellPositions()) {
46794
- const isDynamicPivot = this.getters.isSpillPivotFormula(position);
46795
- if (isDynamicPivot) {
46796
- return true;
46797
- }
46798
- }
46799
- return false;
46800
- }
46801
- isStaticPivotInViewport() {
46973
+ /**
46974
+ * @returns true if the updated pivot is visible in the viewport only as a
46975
+ * static pivot and not as a dynamic pivot
46976
+ */
46977
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46978
+ let staticPivotCount = 0;
46979
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46802
46980
  for (const position of this.getters.getVisibleCellPositions()) {
46803
46981
  const cell = this.getters.getCell(position);
46804
46982
  if (cell?.isFormula) {
46805
46983
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46806
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46807
- return true;
46984
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46985
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46986
+ if (pivotFunction.functionName === "PIVOT") {
46987
+ // if we have at least one dynamic pivot visible inserted the viewport
46988
+ // we return false
46989
+ return false;
46990
+ }
46991
+ else {
46992
+ staticPivotCount++;
46993
+ }
46808
46994
  }
46809
46995
  }
46810
46996
  }
46811
- return false;
46997
+ // we return true if there are only static pivots visible inserted the viewport,
46998
+ // otherwise false
46999
+ return staticPivotCount > 0;
46812
47000
  }
46813
47001
  addDefaultDateTimeGranularity(fields, definition) {
46814
47002
  const { columns, rows } = definition;
@@ -50347,6 +50535,71 @@ class GridPopover extends owl.Component {
50347
50535
  }
50348
50536
  }
50349
50537
 
50538
+ class UnhideRowHeaders extends owl.Component {
50539
+ static template = "o-spreadsheet-UnhideRowHeaders";
50540
+ static props = {
50541
+ headersGroups: Array,
50542
+ headerRange: Object,
50543
+ offset: { type: Number, optional: true },
50544
+ };
50545
+ static defaultProps = { offset: 0 };
50546
+ get sheetId() {
50547
+ return this.env.model.getters.getActiveSheetId();
50548
+ }
50549
+ getUnhidePreviousButtonStyle(hiddenIndex) {
50550
+ const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
50551
+ const y = rect.y + rect.height - HEADER_HEIGHT;
50552
+ return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
50553
+ }
50554
+ getUnhideNextButtonStyle(hiddenIndex) {
50555
+ const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
50556
+ const y = rect.y - HEADER_HEIGHT;
50557
+ return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
50558
+ }
50559
+ unhide(hiddenElements) {
50560
+ this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50561
+ sheetId: this.sheetId,
50562
+ dimension: "ROW",
50563
+ elements: hiddenElements,
50564
+ });
50565
+ }
50566
+ isVisible(header) {
50567
+ return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
50568
+ }
50569
+ }
50570
+ class UnhideColumnHeaders extends owl.Component {
50571
+ static template = "o-spreadsheet-UnhideColumnHeaders";
50572
+ static props = {
50573
+ headersGroups: Array,
50574
+ headerRange: Object,
50575
+ offset: { type: Number, optional: true },
50576
+ };
50577
+ static defaultProps = { offset: 0 };
50578
+ get sheetId() {
50579
+ return this.env.model.getters.getActiveSheetId();
50580
+ }
50581
+ getUnhidePreviousButtonStyle(hiddenIndex) {
50582
+ const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
50583
+ const x = rect.x + rect.width - HEADER_WIDTH;
50584
+ return cssPropertiesToCss({ left: x - this.props.offset + "px" });
50585
+ }
50586
+ getUnhideNextButtonStyle(hiddenIndex) {
50587
+ const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
50588
+ const x = rect.x - HEADER_WIDTH;
50589
+ return cssPropertiesToCss({ left: x - this.props.offset + "px" });
50590
+ }
50591
+ unhide(hiddenElements) {
50592
+ this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50593
+ sheetId: this.sheetId,
50594
+ dimension: "COL",
50595
+ elements: hiddenElements,
50596
+ });
50597
+ }
50598
+ isVisible(header) {
50599
+ return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
50600
+ }
50601
+ }
50602
+
50350
50603
  class AbstractResizer extends owl.Component {
50351
50604
  static props = {
50352
50605
  onOpenContextMenu: Function,
@@ -50565,6 +50818,7 @@ css /* scss */ `
50565
50818
  left: ${HEADER_WIDTH}px;
50566
50819
  right: 0;
50567
50820
  height: ${HEADER_HEIGHT}px;
50821
+ width: calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}px);
50568
50822
  &.o-dragging {
50569
50823
  cursor: grabbing;
50570
50824
  }
@@ -50614,6 +50868,7 @@ class ColResizer extends AbstractResizer {
50614
50868
  onOpenContextMenu: Function,
50615
50869
  };
50616
50870
  static template = "o-spreadsheet-ColResizer";
50871
+ static components = { UnhideColumnHeaders };
50617
50872
  colResizerRef;
50618
50873
  setup() {
50619
50874
  super.setup();
@@ -50622,6 +50877,9 @@ class ColResizer extends AbstractResizer {
50622
50877
  this.MAX_SIZE_MARGIN = 90;
50623
50878
  this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
50624
50879
  }
50880
+ get sheetId() {
50881
+ return this.env.model.getters.getActiveSheetId();
50882
+ }
50625
50883
  _getEvOffset(ev) {
50626
50884
  return ev.offsetX;
50627
50885
  }
@@ -50644,10 +50902,10 @@ class ColResizer extends AbstractResizer {
50644
50902
  return this.env.model.getters.getEdgeScrollCol(position, position, position);
50645
50903
  }
50646
50904
  _getDimensionsInViewport(index) {
50647
- return this.env.model.getters.getColDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
50905
+ return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
50648
50906
  }
50649
50907
  _getElementSize(index) {
50650
- return this.env.model.getters.getColSize(this.env.model.getters.getActiveSheetId(), index);
50908
+ return this.env.model.getters.getColSize(this.sheetId, index);
50651
50909
  }
50652
50910
  _getMaxSize() {
50653
50911
  return this.colResizerRef.el.clientWidth;
@@ -50658,7 +50916,7 @@ class ColResizer extends AbstractResizer {
50658
50916
  const cols = this.env.model.getters.getActiveCols();
50659
50917
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50660
50918
  dimension: "COL",
50661
- sheetId: this.env.model.getters.getActiveSheetId(),
50919
+ sheetId: this.sheetId,
50662
50920
  elements: cols.has(index) ? [...cols] : [index],
50663
50921
  size,
50664
50922
  });
@@ -50671,7 +50929,7 @@ class ColResizer extends AbstractResizer {
50671
50929
  elements.push(colIndex);
50672
50930
  }
50673
50931
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50674
- sheetId: this.env.model.getters.getActiveSheetId(),
50932
+ sheetId: this.sheetId,
50675
50933
  dimension: "COL",
50676
50934
  base: this.state.base,
50677
50935
  elements,
@@ -50690,7 +50948,7 @@ class ColResizer extends AbstractResizer {
50690
50948
  _fitElementSize(index) {
50691
50949
  const cols = this.env.model.getters.getActiveCols();
50692
50950
  this.env.model.dispatch("AUTORESIZE_COLUMNS", {
50693
- sheetId: this.env.model.getters.getActiveSheetId(),
50951
+ sheetId: this.sheetId,
50694
50952
  cols: cols.has(index) ? [...cols] : [index],
50695
50953
  });
50696
50954
  }
@@ -50701,7 +50959,7 @@ class ColResizer extends AbstractResizer {
50701
50959
  return this.env.model.getters.getActiveCols();
50702
50960
  }
50703
50961
  _getPreviousVisibleElement(index) {
50704
- const sheetId = this.env.model.getters.getActiveSheetId();
50962
+ const sheetId = this.sheetId;
50705
50963
  let row;
50706
50964
  for (row = index - 1; row >= 0; row--) {
50707
50965
  if (!this.env.model.getters.isColHidden(sheetId, row)) {
@@ -50712,13 +50970,38 @@ class ColResizer extends AbstractResizer {
50712
50970
  }
50713
50971
  unhide(hiddenElements) {
50714
50972
  this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50715
- sheetId: this.env.model.getters.getActiveSheetId(),
50973
+ sheetId: this.sheetId,
50716
50974
  elements: hiddenElements,
50717
50975
  dimension: "COL",
50718
50976
  });
50719
50977
  }
50720
- getUnhideButtonStyle(hiddenIndex) {
50721
- return cssPropertiesToCss({ left: this._getDimensionsInViewport(hiddenIndex).start + "px" });
50978
+ get mainUnhideHeadersProps() {
50979
+ const { left, right } = this.env.model.getters.getActiveMainViewport();
50980
+ const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
50981
+ const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
50982
+ const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
50983
+ return {
50984
+ headersGroups: hiddenGroups.slice(index),
50985
+ offset: this.env.model.getters.getMainViewportCoordinates().x,
50986
+ headerRange: { start: left, end: right },
50987
+ };
50988
+ }
50989
+ get frozenUnhideHeadersProps() {
50990
+ const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
50991
+ const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
50992
+ const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
50993
+ return {
50994
+ headersGroups: hiddenGroups.slice(0, index + 1),
50995
+ headerRange: { start: 0, end: xSplit - 1 },
50996
+ };
50997
+ }
50998
+ get frozenContainerStyle() {
50999
+ return cssPropertiesToCss({
51000
+ width: this.env.model.getters.getMainViewportCoordinates().x + "px",
51001
+ });
51002
+ }
51003
+ get hasFrozenPane() {
51004
+ return this.env.model.getters.getPaneDivisions(this.sheetId).xSplit > 0;
50722
51005
  }
50723
51006
  }
50724
51007
  css /* scss */ `
@@ -50728,7 +51011,7 @@ css /* scss */ `
50728
51011
  left: 0;
50729
51012
  right: 0;
50730
51013
  width: ${HEADER_WIDTH}px;
50731
- height: 100%;
51014
+ height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
50732
51015
  &.o-dragging {
50733
51016
  cursor: grabbing;
50734
51017
  }
@@ -50778,6 +51061,7 @@ class RowResizer extends AbstractResizer {
50778
51061
  onOpenContextMenu: Function,
50779
51062
  };
50780
51063
  static template = "o-spreadsheet-RowResizer";
51064
+ static components = { UnhideRowHeaders };
50781
51065
  setup() {
50782
51066
  super.setup();
50783
51067
  this.rowResizerRef = owl.useRef("rowResizer");
@@ -50786,6 +51070,9 @@ class RowResizer extends AbstractResizer {
50786
51070
  this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
50787
51071
  }
50788
51072
  rowResizerRef;
51073
+ get sheetId() {
51074
+ return this.env.model.getters.getActiveSheetId();
51075
+ }
50789
51076
  _getEvOffset(ev) {
50790
51077
  return ev.offsetY;
50791
51078
  }
@@ -50808,10 +51095,10 @@ class RowResizer extends AbstractResizer {
50808
51095
  return this.env.model.getters.getEdgeScrollRow(position, position, position);
50809
51096
  }
50810
51097
  _getDimensionsInViewport(index) {
50811
- return this.env.model.getters.getRowDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
51098
+ return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
50812
51099
  }
50813
51100
  _getElementSize(index) {
50814
- return this.env.model.getters.getRowSize(this.env.model.getters.getActiveSheetId(), index);
51101
+ return this.env.model.getters.getRowSize(this.sheetId, index);
50815
51102
  }
50816
51103
  _getMaxSize() {
50817
51104
  return this.rowResizerRef.el.clientHeight;
@@ -50822,7 +51109,7 @@ class RowResizer extends AbstractResizer {
50822
51109
  const rows = this.env.model.getters.getActiveRows();
50823
51110
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50824
51111
  dimension: "ROW",
50825
- sheetId: this.env.model.getters.getActiveSheetId(),
51112
+ sheetId: this.sheetId,
50826
51113
  elements: rows.has(index) ? [...rows] : [index],
50827
51114
  size,
50828
51115
  });
@@ -50835,7 +51122,7 @@ class RowResizer extends AbstractResizer {
50835
51122
  elements.push(rowIndex);
50836
51123
  }
50837
51124
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50838
- sheetId: this.env.model.getters.getActiveSheetId(),
51125
+ sheetId: this.sheetId,
50839
51126
  dimension: "ROW",
50840
51127
  base: this.state.base,
50841
51128
  elements,
@@ -50854,7 +51141,7 @@ class RowResizer extends AbstractResizer {
50854
51141
  _fitElementSize(index) {
50855
51142
  const rows = this.env.model.getters.getActiveRows();
50856
51143
  this.env.model.dispatch("AUTORESIZE_ROWS", {
50857
- sheetId: this.env.model.getters.getActiveSheetId(),
51144
+ sheetId: this.sheetId,
50858
51145
  rows: rows.has(index) ? [...rows] : [index],
50859
51146
  });
50860
51147
  }
@@ -50865,7 +51152,7 @@ class RowResizer extends AbstractResizer {
50865
51152
  return this.env.model.getters.getActiveRows();
50866
51153
  }
50867
51154
  _getPreviousVisibleElement(index) {
50868
- const sheetId = this.env.model.getters.getActiveSheetId();
51155
+ const sheetId = this.sheetId;
50869
51156
  let row;
50870
51157
  for (row = index - 1; row >= 0; row--) {
50871
51158
  if (!this.env.model.getters.isRowHidden(sheetId, row)) {
@@ -50874,15 +51161,33 @@ class RowResizer extends AbstractResizer {
50874
51161
  }
50875
51162
  return row;
50876
51163
  }
50877
- unhide(hiddenElements) {
50878
- this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50879
- sheetId: this.env.model.getters.getActiveSheetId(),
50880
- dimension: "ROW",
50881
- elements: hiddenElements,
51164
+ get mainUnhideHeadersProps() {
51165
+ const { top, bottom } = this.env.model.getters.getActiveMainViewport();
51166
+ const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
51167
+ const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
51168
+ const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
51169
+ return {
51170
+ headersGroups: hiddenGroups.slice(index),
51171
+ offset: this.env.model.getters.getMainViewportCoordinates().y,
51172
+ headerRange: { start: top, end: bottom },
51173
+ };
51174
+ }
51175
+ get frozenUnhideHeadersProps() {
51176
+ const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
51177
+ const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
51178
+ const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
51179
+ return {
51180
+ headersGroups: hiddenGroups.slice(0, index + 1),
51181
+ headerRange: { start: 0, end: ySplit - 1 },
51182
+ };
51183
+ }
51184
+ get frozenContainerStyle() {
51185
+ return cssPropertiesToCss({
51186
+ height: this.env.model.getters.getMainViewportCoordinates().y + "px",
50882
51187
  });
50883
51188
  }
50884
- getUnhideButtonStyle(hiddenIndex) {
50885
- return cssPropertiesToCss({ top: this._getDimensionsInViewport(hiddenIndex).start + "px" });
51189
+ get hasFrozenPane() {
51190
+ return this.env.model.getters.getPaneDivisions(this.sheetId).ySplit > 0;
50886
51191
  }
50887
51192
  }
50888
51193
  css /* scss */ `
@@ -60662,6 +60967,7 @@ class EvaluationPlugin extends CoreViewPlugin {
60662
60967
  exportForExcel(data) {
60663
60968
  for (const sheet of data.sheets) {
60664
60969
  sheet.cellValues = {};
60970
+ sheet.formulaSpillRanges = {};
60665
60971
  }
60666
60972
  for (const position of this.evaluator.getEvaluatedPositions()) {
60667
60973
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60673,8 +60979,9 @@ class EvaluationPlugin extends CoreViewPlugin {
60673
60979
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60674
60980
  const formulaCell = this.getCorrespondingFormulaCell(position);
60675
60981
  if (formulaCell) {
60982
+ const cell = this.getters.getCell(position);
60676
60983
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60677
- isFormula = isExported;
60984
+ isFormula = isExported && cell?.content === formulaCell.content;
60678
60985
  // If the cell contains a non-exported formula and that is evaluates to
60679
60986
  // nothing* ,we don't export it.
60680
60987
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60697,7 +61004,11 @@ class EvaluationPlugin extends CoreViewPlugin {
60697
61004
  content = !isExported ? newContent : exportedCellData;
60698
61005
  }
60699
61006
  exportedSheetData.cells[xc] = content;
60700
- exportedSheetData.cellValues[xc] = value;
61007
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
61008
+ const spillZone = this.getSpreadZone(position);
61009
+ if (spillZone) {
61010
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
61011
+ }
60701
61012
  }
60702
61013
  }
60703
61014
  /**
@@ -62925,7 +63236,7 @@ class AutofillPlugin extends UIPlugin {
62925
63236
  getRule(cell, cells) {
62926
63237
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62927
63238
  const rule = rules.find((rule) => rule.condition(cell, cells));
62928
- return rule && rule.generateRule(cell, cells);
63239
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62929
63240
  }
62930
63241
  /**
62931
63242
  * Create the generator to be able to autofill the next cells.
@@ -67646,7 +67957,7 @@ class InternalViewport {
67646
67957
  *
67647
67958
  */
67648
67959
  getFullRect(zone) {
67649
- const targetZone = intersection(zone, this);
67960
+ const targetZone = intersection(zone, this.boundaries);
67650
67961
  const scrollDeltaX = this.snapCorrection.x;
67651
67962
  const scrollDeltaY = this.snapCorrection.y;
67652
67963
  if (targetZone) {
@@ -68114,7 +68425,8 @@ class SheetViewPlugin extends UIPlugin {
68114
68425
  ? this.getters.getSheetViewVisibleCols()
68115
68426
  : this.getters.getSheetViewVisibleRows();
68116
68427
  const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
68117
- const endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
68428
+ let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
68429
+ endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
68118
68430
  const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
68119
68431
  let offset = 0;
68120
68432
  for (const i of relevantIndexes) {
@@ -68239,11 +68551,12 @@ class SheetViewPlugin extends UIPlugin {
68239
68551
  * column of the current viewport
68240
68552
  */
68241
68553
  getColDimensionsInViewport(sheetId, col) {
68554
+ const { top } = this.getMainInternalViewport(sheetId);
68242
68555
  const zone = {
68243
68556
  left: col,
68244
68557
  right: col,
68245
- top: 0,
68246
- bottom: this.getters.getNumberRows(sheetId) - 1,
68558
+ top: top,
68559
+ bottom: top,
68247
68560
  };
68248
68561
  const { x, width } = this.getVisibleRect(zone);
68249
68562
  const start = x - this.gridOffsetX;
@@ -68254,9 +68567,10 @@ class SheetViewPlugin extends UIPlugin {
68254
68567
  * of the current viewport
68255
68568
  */
68256
68569
  getRowDimensionsInViewport(sheetId, row) {
68570
+ const { left } = this.getMainInternalViewport(sheetId);
68257
68571
  const zone = {
68258
68572
  left: 0,
68259
- right: this.getters.getNumberCols(sheetId) - 1,
68573
+ right: left,
68260
68574
  top: row,
68261
68575
  bottom: row,
68262
68576
  };
@@ -73407,7 +73721,7 @@ function numberRef(reference) {
73407
73721
  `;
73408
73722
  }
73409
73723
 
73410
- function addFormula(formula, value) {
73724
+ function addFormula(formula, value, formulaSpillRange) {
73411
73725
  if (!formula) {
73412
73726
  return { attrs: [], node: escapeXml `` };
73413
73727
  }
@@ -73415,10 +73729,17 @@ function addFormula(formula, value) {
73415
73729
  if (type === undefined) {
73416
73730
  return { attrs: [], node: escapeXml `` };
73417
73731
  }
73418
- const attrs = [["t", type]];
73732
+ const attrs = [
73733
+ ["cm", "1"],
73734
+ ["t", type],
73735
+ ];
73419
73736
  const XlsxFormula = adaptFormulaToExcel(formula);
73420
73737
  const exportedValue = adaptFormulaValueToExcel(value);
73421
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73738
+ // We treat all formulas as array formulas (a simple formula
73739
+ // is an array formula that spills on only one cell) to avoid
73740
+ // trying to detect spilling sub-formulas which is not a trivial task.
73741
+ let node;
73742
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73422
73743
  return { attrs, node };
73423
73744
  }
73424
73745
  function addContent(content, sharedStrings, forceString = false) {
@@ -74201,7 +74522,7 @@ function addStyles(styles) {
74201
74522
  }
74202
74523
  if (alignAttrs.length > 0) {
74203
74524
  attributes.push(["applyAlignment", "1"]); // for Libre Office
74204
- styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}>${escapeXml /*xml*/ `<alignment ${formatAttributes(alignAttrs)} />`}</xf> `);
74525
+ styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
74205
74526
  }
74206
74527
  else {
74207
74528
  styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
@@ -74369,6 +74690,9 @@ function addColumns(cols) {
74369
74690
  }
74370
74691
  function addRows(construct, data, sheet) {
74371
74692
  const rowNodes = [];
74693
+ const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74694
+ const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74695
+ const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74372
74696
  for (let r = 0; r < sheet.rowNumber; r++) {
74373
74697
  const rowAttrs = [["r", r + 1]];
74374
74698
  const row = sheet.rows[r] || {};
@@ -74384,9 +74708,6 @@ function addRows(construct, data, sheet) {
74384
74708
  if (row.collapsed) {
74385
74709
  rowAttrs.push(["collapsed", 1]);
74386
74710
  }
74387
- const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74388
- const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74389
- const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74390
74711
  const cellNodes = [];
74391
74712
  for (let c = 0; c < sheet.colNumber; c++) {
74392
74713
  const xc = toXC(c, r);
@@ -74408,7 +74729,7 @@ function addRows(construct, data, sheet) {
74408
74729
  let cellNode = escapeXml ``;
74409
74730
  // Either formula or static value inside the cell
74410
74731
  if (content?.startsWith("=") && value !== undefined) {
74411
- const res = addFormula(content, value);
74732
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74412
74733
  if (!res) {
74413
74734
  continue;
74414
74735
  }
@@ -74694,6 +75015,30 @@ function createWorksheets(data, construct) {
74694
75015
  `;
74695
75016
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74696
75017
  }
75018
+ const sheetMetadataXml = escapeXml /*xml*/ `
75019
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
75020
+ <metadataTypes count="1">
75021
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
75022
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
75023
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
75024
+ </metadataTypes>
75025
+ <futureMetadata name="XLDAPR" count="1">
75026
+ <bk>
75027
+ <extLst>
75028
+ <ext uri="{${ARRAY_FORMULA_URI}}">
75029
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
75030
+ </ext>
75031
+ </extLst>
75032
+ </bk>
75033
+ </futureMetadata>
75034
+ <cellMetadata count="1">
75035
+ <bk>
75036
+ <rc t="1" v="0" />
75037
+ </bk>
75038
+ </cellMetadata>
75039
+ </metadata>
75040
+ `;
75041
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74697
75042
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74698
75043
  type: XLSX_RELATION_TYPE.sharedStrings,
74699
75044
  target: "sharedStrings.xml",
@@ -74702,6 +75047,10 @@ function createWorksheets(data, construct) {
74702
75047
  type: XLSX_RELATION_TYPE.styles,
74703
75048
  target: "styles.xml",
74704
75049
  });
75050
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
75051
+ type: XLSX_RELATION_TYPE.metadata,
75052
+ target: "metadata.xml",
75053
+ });
74705
75054
  return files;
74706
75055
  }
74707
75056
  /**
@@ -75663,6 +76012,6 @@ exports.tokenColors = tokenColors;
75663
76012
  exports.tokenize = tokenize;
75664
76013
 
75665
76014
 
75666
- __info__.version = "18.2.1";
75667
- __info__.date = "2025-02-25T06:03:13.262Z";
75668
- __info__.hash = "3b4b5c9";
76015
+ __info__.version = "18.2.3";
76016
+ __info__.date = "2025-03-12T15:32:36.274Z";
76017
+ __info__.hash = "81b0e08";