@odoo/o-spreadsheet 18.2.5 → 18.2.7

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.5
6
- * @date 2025-03-26T12:47:44.113Z
7
- * @hash 4675edd
5
+ * @version 18.2.7
6
+ * @date 2025-04-14T17:19:31.011Z
7
+ * @hash e187958
8
8
  */
9
9
 
10
10
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
@@ -804,8 +804,7 @@ function removeFalsyAttributes(obj) {
804
804
  *
805
805
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
806
806
  */
807
- const whiteSpaceSpecialCharacters = [
808
- " ",
807
+ const specialWhiteSpaceSpecialCharacters = [
809
808
  "\t",
810
809
  "\f",
811
810
  "\v",
@@ -820,7 +819,7 @@ const whiteSpaceSpecialCharacters = [
820
819
  String.fromCharCode(parseInt("3000", 16)),
821
820
  String.fromCharCode(parseInt("feff", 16)),
822
821
  ];
823
- const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|"), "g");
822
+ const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
824
823
  const newLineRegexp = /(\r\n|\r)/g;
825
824
  /**
826
825
  * Replace all different newlines characters by \n
@@ -3574,6 +3573,7 @@ const coreTypes = new Set([
3574
3573
  "CLEAR_FORMATTING",
3575
3574
  "SET_BORDER",
3576
3575
  "SET_ZONE_BORDERS",
3576
+ "SET_BORDERS_ON_TARGET",
3577
3577
  /** CHART */
3578
3578
  "CREATE_CHART",
3579
3579
  "UPDATE_CHART",
@@ -6723,6 +6723,7 @@ class AbstractCellClipboardHandler extends ClipboardHandler {
6723
6723
  }
6724
6724
 
6725
6725
  class BorderClipboardHandler extends AbstractCellClipboardHandler {
6726
+ queuedBordersToAdd = {};
6726
6727
  copy(data) {
6727
6728
  const sheetId = data.sheetId;
6728
6729
  if (data.zones.length === 0) {
@@ -6753,6 +6754,7 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
6753
6754
  const { left, top } = zones[0];
6754
6755
  this.pasteZone(sheetId, left, top, content.borders);
6755
6756
  }
6757
+ this.executeQueuedChanges(sheetId);
6756
6758
  }
6757
6759
  pasteZone(sheetId, col, row, borders) {
6758
6760
  for (const [r, rowBorders] of borders.entries()) {
@@ -6771,7 +6773,20 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
6771
6773
  ...targetBorders,
6772
6774
  ...originBorders,
6773
6775
  };
6774
- this.dispatch("SET_BORDER", { ...target, border });
6776
+ const borderKey = JSON.stringify(border);
6777
+ if (!this.queuedBordersToAdd[borderKey]) {
6778
+ this.queuedBordersToAdd[borderKey] = [];
6779
+ }
6780
+ this.queuedBordersToAdd[borderKey].push(positionToZone(target));
6781
+ }
6782
+ executeQueuedChanges(pasteSheetTarget) {
6783
+ for (const borderKey in this.queuedBordersToAdd) {
6784
+ const zones = this.queuedBordersToAdd[borderKey];
6785
+ const border = JSON.parse(borderKey);
6786
+ const target = recomputeZones(zones, []);
6787
+ this.dispatch("SET_BORDERS_ON_TARGET", { sheetId: pasteSheetTarget, target, border });
6788
+ }
6789
+ this.queuedBordersToAdd = {};
6775
6790
  }
6776
6791
  }
6777
6792
 
@@ -6797,8 +6812,12 @@ function tokenize(str, locale = DEFAULT_LOCALE) {
6797
6812
  str = replaceNewLines(str);
6798
6813
  const chars = new TokenizingChars(str);
6799
6814
  const result = [];
6815
+ const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
6816
+ ? tokenizeSpecialCharacterSpace
6817
+ : tokenizeSimpleSpace;
6800
6818
  while (!chars.isOver()) {
6801
- let token = tokenizeSpace(chars) ||
6819
+ let token = tokenizeNewLine(chars) ||
6820
+ tokenizeSpace(chars) ||
6802
6821
  tokenizeArgsSeparator(chars, locale) ||
6803
6822
  tokenizeParenthesis(chars) ||
6804
6823
  tokenizeOperator(chars) ||
@@ -6932,17 +6951,19 @@ function tokenizeSymbol(chars) {
6932
6951
  }
6933
6952
  return null;
6934
6953
  }
6935
- function tokenizeSpace(chars) {
6936
- let length = 0;
6937
- while (chars.current === NEWLINE) {
6938
- length++;
6939
- chars.shift();
6954
+ function tokenizeSpecialCharacterSpace(chars) {
6955
+ let spaces = "";
6956
+ while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
6957
+ spaces += chars.shift();
6940
6958
  }
6941
- if (length) {
6942
- return { type: "SPACE", value: NEWLINE.repeat(length) };
6959
+ if (spaces) {
6960
+ return { type: "SPACE", value: spaces };
6943
6961
  }
6962
+ return null;
6963
+ }
6964
+ function tokenizeSimpleSpace(chars) {
6944
6965
  let spaces = "";
6945
- while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6966
+ while (chars.current === " ") {
6946
6967
  spaces += chars.shift();
6947
6968
  }
6948
6969
  if (spaces) {
@@ -6950,6 +6971,17 @@ function tokenizeSpace(chars) {
6950
6971
  }
6951
6972
  return null;
6952
6973
  }
6974
+ function tokenizeNewLine(chars) {
6975
+ let length = 0;
6976
+ while (chars.current === NEWLINE) {
6977
+ length++;
6978
+ chars.shift();
6979
+ }
6980
+ if (length) {
6981
+ return { type: "SPACE", value: NEWLINE.repeat(length) };
6982
+ }
6983
+ return null;
6984
+ }
6953
6985
  function tokenizeInvalidRange(chars) {
6954
6986
  if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
6955
6987
  chars.advanceBy(CellErrorType.InvalidReference.length);
@@ -8694,12 +8726,13 @@ class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
8694
8726
  }
8695
8727
  pasteCf(origin, target, isCutOperation) {
8696
8728
  if (origin?.rules && origin.rules.length > 0) {
8729
+ const originZone = positionToZone(origin.position);
8697
8730
  const zone = positionToZone(target);
8698
8731
  for (const rule of origin.rules) {
8699
8732
  const toRemoveZones = [];
8700
8733
  if (isCutOperation) {
8701
8734
  //remove from current rule
8702
- toRemoveZones.push(positionToZone(origin.position));
8735
+ toRemoveZones.push(originZone);
8703
8736
  }
8704
8737
  if (origin.position.sheetId === target.sheetId) {
8705
8738
  this.adaptCFRules(origin.position.sheetId, rule, [zone], toRemoveZones);
@@ -8813,6 +8846,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8813
8846
  pasteDataValidation(origin, target, isCutOperation) {
8814
8847
  if (origin) {
8815
8848
  const zone = positionToZone(target);
8849
+ const originZone = positionToZone(origin.position);
8816
8850
  const rule = origin.rule;
8817
8851
  if (!rule) {
8818
8852
  const targetRule = this.getters.getValidationRuleForCell(target);
@@ -8824,7 +8858,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8824
8858
  }
8825
8859
  const toRemoveZone = [];
8826
8860
  if (isCutOperation) {
8827
- toRemoveZone.push(positionToZone(origin.position));
8861
+ toRemoveZone.push(originZone);
8828
8862
  }
8829
8863
  if (origin.position.sheetId === target.sheetId) {
8830
8864
  const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule, false);
@@ -8884,7 +8918,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8884
8918
  continue;
8885
8919
  }
8886
8920
  this.dispatch("ADD_DATA_VALIDATION_RULE", {
8887
- rule: dv,
8921
+ rule: { id: dv.id, criterion: dv.criterion, isBlocking: dv.isBlocking },
8888
8922
  ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
8889
8923
  sheetId,
8890
8924
  });
@@ -9576,6 +9610,159 @@ class ComposerFocusStore extends SpreadsheetStore {
9576
9610
  }
9577
9611
  }
9578
9612
 
9613
+ /**
9614
+ * This file is largely inspired by owl 1.
9615
+ * `css` tag has been removed from owl 2 without workaround to manage css.
9616
+ * So, the solution was to import the behavior of owl 1 directly in our
9617
+ * codebase, with one difference: the css is added to the sheet as soon as the
9618
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
9619
+ * created for the first time.
9620
+ */
9621
+ const STYLESHEETS = {};
9622
+ let nextId = 0;
9623
+ /**
9624
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
9625
+ * an inline stylesheet with just the following code:
9626
+ * ```js
9627
+ * css`.component-a { color: red; }`;
9628
+ * ```
9629
+ */
9630
+ function css(strings, ...args) {
9631
+ const name = `__sheet__${nextId++}`;
9632
+ const value = String.raw(strings, ...args);
9633
+ registerSheet(name, value);
9634
+ activateSheet(name);
9635
+ return name;
9636
+ }
9637
+ function processSheet(str) {
9638
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9639
+ const selectorStack = [];
9640
+ const parts = [];
9641
+ let rules = [];
9642
+ function generateSelector(stackIndex, parentSelector) {
9643
+ const parts = [];
9644
+ for (const selector of selectorStack[stackIndex]) {
9645
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
9646
+ if (part.includes("&")) {
9647
+ part = selector.replace(/&/g, parentSelector || "");
9648
+ }
9649
+ if (stackIndex < selectorStack.length - 1) {
9650
+ part = generateSelector(stackIndex + 1, part);
9651
+ }
9652
+ parts.push(part);
9653
+ }
9654
+ return parts.join(", ");
9655
+ }
9656
+ function generateRules() {
9657
+ if (rules.length) {
9658
+ parts.push(generateSelector(0) + " {");
9659
+ parts.push(...rules);
9660
+ parts.push("}");
9661
+ rules = [];
9662
+ }
9663
+ }
9664
+ while (tokens.length) {
9665
+ let token = tokens.shift();
9666
+ if (token === "}") {
9667
+ generateRules();
9668
+ selectorStack.pop();
9669
+ }
9670
+ else {
9671
+ if (tokens[0] === "{") {
9672
+ generateRules();
9673
+ selectorStack.push(token.split(/\s*,\s*/));
9674
+ tokens.shift();
9675
+ }
9676
+ if (tokens[0] === ";") {
9677
+ rules.push(" " + token + ";");
9678
+ }
9679
+ }
9680
+ }
9681
+ return parts.join("\n");
9682
+ }
9683
+ function registerSheet(id, css) {
9684
+ const sheet = document.createElement("style");
9685
+ sheet.textContent = processSheet(css);
9686
+ STYLESHEETS[id] = sheet;
9687
+ }
9688
+ function activateSheet(id) {
9689
+ const sheet = STYLESHEETS[id];
9690
+ sheet.setAttribute("component", id);
9691
+ document.head.appendChild(sheet);
9692
+ }
9693
+ function getTextDecoration({ strikethrough, underline, }) {
9694
+ if (!strikethrough && !underline) {
9695
+ return "none";
9696
+ }
9697
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9698
+ }
9699
+ /**
9700
+ * Convert the cell style to CSS properties.
9701
+ */
9702
+ function cellStyleToCss(style) {
9703
+ const attributes = cellTextStyleToCss(style);
9704
+ if (!style)
9705
+ return attributes;
9706
+ if (style.fillColor) {
9707
+ attributes["background"] = style.fillColor;
9708
+ }
9709
+ return attributes;
9710
+ }
9711
+ /**
9712
+ * Convert the cell text style to CSS properties.
9713
+ */
9714
+ function cellTextStyleToCss(style) {
9715
+ const attributes = {};
9716
+ if (!style)
9717
+ return attributes;
9718
+ if (style.bold) {
9719
+ attributes["font-weight"] = "bold";
9720
+ }
9721
+ if (style.italic) {
9722
+ attributes["font-style"] = "italic";
9723
+ }
9724
+ if (style.strikethrough || style.underline) {
9725
+ let decoration = style.strikethrough ? "line-through" : "";
9726
+ decoration = style.underline ? decoration + " underline" : decoration;
9727
+ attributes["text-decoration"] = decoration;
9728
+ }
9729
+ if (style.textColor) {
9730
+ attributes["color"] = style.textColor;
9731
+ }
9732
+ return attributes;
9733
+ }
9734
+ /**
9735
+ * Transform CSS properties into a CSS string.
9736
+ */
9737
+ function cssPropertiesToCss(attributes) {
9738
+ let styleStr = "";
9739
+ for (const attName in attributes) {
9740
+ if (!attributes[attName]) {
9741
+ continue;
9742
+ }
9743
+ styleStr += `${attName}:${attributes[attName]}; `;
9744
+ }
9745
+ return styleStr;
9746
+ }
9747
+ function getElementMargins(el) {
9748
+ const style = window.getComputedStyle(el);
9749
+ return {
9750
+ top: parseInt(style.marginTop, 10) || 0,
9751
+ bottom: parseInt(style.marginBottom, 10) || 0,
9752
+ left: parseInt(style.marginLeft, 10) || 0,
9753
+ right: parseInt(style.marginRight, 10) || 0,
9754
+ };
9755
+ }
9756
+
9757
+ const chartJsExtensionRegistry = new Registry();
9758
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
9759
+ function getChartJSConstructor() {
9760
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
9761
+ window.Chart.register(...chartJsExtensionRegistry.getAll());
9762
+ }
9763
+ return window.Chart;
9764
+ }
9765
+
9579
9766
  const TREND_LINE_XAXIS_ID = "x1";
9580
9767
  const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
9581
9768
  /**
@@ -10124,341 +10311,79 @@ function getNextNonEmptyBar(bars, startIndex) {
10124
10311
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10125
10312
  }
10126
10313
 
10127
- const GAUGE_PADDING_SIDE = 30;
10128
- const GAUGE_PADDING_TOP = 10;
10129
- const GAUGE_PADDING_BOTTOM = 20;
10130
- const GAUGE_LABELS_FONT_SIZE = 12;
10131
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10132
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10133
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10134
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
10135
- function drawGaugeChart(canvas, runtime) {
10136
- const canvasBoundingRect = canvas.getBoundingClientRect();
10137
- canvas.width = canvasBoundingRect.width;
10138
- canvas.height = canvasBoundingRect.height;
10139
- const ctx = canvas.getContext("2d");
10140
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10141
- drawBackground(ctx, config);
10142
- drawGauge(ctx, config);
10143
- drawInflectionValues(ctx, config);
10144
- drawLabels(ctx, config);
10145
- drawTitle(ctx, config);
10146
- }
10147
- function drawGauge(ctx, config) {
10148
- ctx.save();
10149
- const gauge = config.gauge;
10150
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10151
- const arcCenterY = gauge.rect.y + gauge.rect.height;
10152
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10153
- if (arcRadius < 0) {
10154
- return;
10155
- }
10156
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10157
- // Gauge background
10158
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10159
- ctx.beginPath();
10160
- ctx.lineWidth = gauge.arcWidth;
10161
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10162
- ctx.stroke();
10163
- // Gauge value
10164
- ctx.strokeStyle = gauge.color;
10165
- ctx.beginPath();
10166
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10167
- ctx.stroke();
10168
- ctx.restore();
10169
- }
10170
- function drawBackground(ctx, config) {
10171
- ctx.save();
10172
- ctx.fillStyle = config.backgroundColor;
10173
- ctx.fillRect(0, 0, config.width, config.height);
10174
- ctx.restore();
10175
- }
10176
- function drawLabels(ctx, config) {
10177
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10178
- ctx.save();
10179
- ctx.textAlign = "center";
10180
- ctx.fillStyle = label.color;
10181
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10182
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10183
- ctx.restore();
10184
- }
10185
- }
10186
- function drawInflectionValues(ctx, config) {
10187
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10188
- for (const inflectionValue of config.inflectionValues) {
10189
- ctx.save();
10190
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10191
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10192
- ctx.lineWidth = 2;
10193
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10194
- ctx.beginPath();
10195
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
10196
- ctx.lineTo(0, -height - 3);
10197
- ctx.stroke();
10198
- ctx.textAlign = "center";
10199
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10200
- ctx.fillStyle = inflectionValue.color;
10201
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10202
- ctx.fillText(inflectionValue.label, 0, textY);
10203
- ctx.restore();
10204
- }
10205
- }
10206
- function drawTitle(ctx, config) {
10207
- ctx.save();
10208
- const title = config.title;
10209
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10210
- ctx.textBaseline = "middle";
10211
- ctx.fillStyle = title.color;
10212
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10213
- ctx.restore();
10214
- }
10215
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10216
- const maxValue = runtime.maxValue;
10217
- const minValue = runtime.minValue;
10218
- const gaugeValue = runtime.gaugeValue;
10219
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10220
- const gaugeArcWidth = gaugeRect.width / 6;
10221
- const gaugePercentage = gaugeValue
10222
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10223
- : 0;
10224
- const gaugeValuePosition = {
10225
- x: boundingRect.width / 2,
10226
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10227
- };
10228
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10229
- // Scale down the font size if the gaugeRect is too small
10230
- if (gaugeRect.height < 300) {
10231
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10232
- }
10233
- // Scale down the font size if the text is too long
10234
- const maxTextWidth = gaugeRect.width / 2;
10235
- const gaugeLabel = gaugeValue?.label || "-";
10236
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10237
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10314
+ css /* scss */ `
10315
+ .o-spreadsheet {
10316
+ .o-chart-custom-tooltip {
10317
+ font-size: 12px;
10318
+ background-color: #fff;
10319
+ z-index: ${ComponentsImportance.FigureTooltip};
10238
10320
  }
10239
- const minLabelPosition = {
10240
- x: gaugeRect.x + gaugeArcWidth / 2,
10241
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10242
- };
10243
- const maxLabelPosition = {
10244
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10245
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10321
+ }
10322
+ `;
10323
+ chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
10324
+ chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
10325
+ class ChartJsComponent extends Component {
10326
+ static template = "o-spreadsheet-ChartJsComponent";
10327
+ static props = {
10328
+ figure: Object,
10246
10329
  };
10247
- const textColor = chartMutedFontColor(runtime.background);
10248
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10249
- let x = 0, titleWidth = 0, titleHeight = 0;
10250
- if (runtime.title.text) {
10251
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10252
- }
10253
- switch (runtime.title.align) {
10254
- case "right":
10255
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
10256
- break;
10257
- case "center":
10258
- x = (boundingRect.width - titleWidth) / 2;
10259
- break;
10260
- case "left":
10261
- default:
10262
- x = CHART_PADDING$1;
10263
- break;
10330
+ canvas = useRef("graphContainer");
10331
+ chart;
10332
+ currentRuntime;
10333
+ get background() {
10334
+ return this.chartRuntime.background;
10264
10335
  }
10265
- return {
10266
- width: boundingRect.width,
10267
- height: boundingRect.height,
10268
- title: {
10269
- label: runtime.title.text ?? "",
10270
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10271
- textPosition: {
10272
- x,
10273
- y: CHART_PADDING_TOP + titleHeight / 2,
10274
- },
10275
- color: runtime.title.color ?? textColor,
10276
- bold: runtime.title.bold,
10277
- italic: runtime.title.italic,
10278
- },
10279
- backgroundColor: runtime.background,
10280
- gauge: {
10281
- rect: gaugeRect,
10282
- arcWidth: gaugeArcWidth,
10283
- percentage: clip(gaugePercentage, 0, 1),
10284
- color: getGaugeColor(runtime),
10285
- },
10286
- inflectionValues,
10287
- gaugeValue: {
10288
- label: gaugeLabel,
10289
- textPosition: gaugeValuePosition,
10290
- fontSize: gaugeValueFontSize,
10291
- color: textColor,
10292
- },
10293
- minLabel: {
10294
- label: runtime.minValue.label,
10295
- textPosition: minLabelPosition,
10296
- fontSize: GAUGE_LABELS_FONT_SIZE,
10297
- color: textColor,
10298
- },
10299
- maxLabel: {
10300
- label: runtime.maxValue.label,
10301
- textPosition: maxLabelPosition,
10302
- fontSize: GAUGE_LABELS_FONT_SIZE,
10303
- color: textColor,
10304
- },
10305
- };
10306
- }
10307
- /**
10308
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10309
- * space for the title and labels.
10310
- */
10311
- function getGaugeRect(boundingRect, title) {
10312
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10313
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10314
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10315
- let gaugeWidth;
10316
- let gaugeHeight;
10317
- if (drawWidth > 2 * drawHeight) {
10318
- gaugeWidth = 2 * drawHeight;
10319
- gaugeHeight = drawHeight;
10336
+ get canvasStyle() {
10337
+ return `background-color: ${this.background}`;
10320
10338
  }
10321
- else {
10322
- gaugeWidth = drawWidth;
10323
- gaugeHeight = drawWidth / 2;
10339
+ get chartRuntime() {
10340
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10341
+ if (!("chartJsConfig" in runtime)) {
10342
+ throw new Error("Unsupported chart runtime");
10343
+ }
10344
+ return runtime;
10324
10345
  }
10325
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10326
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10327
- return {
10328
- x: gaugeX,
10329
- y: gaugeY,
10330
- width: gaugeWidth,
10331
- height: gaugeHeight,
10332
- };
10333
- }
10334
- /**
10335
- * 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).
10336
- *
10337
- * Also compute an offset for the text so that it doesn't overlap with other text.
10338
- */
10339
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10340
- const maxValue = runtime.maxValue;
10341
- const minValue = runtime.minValue;
10342
- const gaugeCircleCenter = {
10343
- x: gaugeRect.x + gaugeRect.width / 2,
10344
- y: gaugeRect.y + gaugeRect.height,
10345
- };
10346
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10347
- const inflectionValues = [];
10348
- const inflectionValuesTextRects = [];
10349
- for (const inflectionValue of runtime.inflectionValues) {
10350
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10351
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10352
- const angle = Math.PI - Math.PI * percentage;
10353
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10354
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10355
- gaugeCircleCenter.x, // center of the gauge circle
10356
- gaugeCircleCenter.y, // center of the gauge circle
10357
- labelWidth + 2, // width of the text + some margin
10358
- GAUGE_LABELS_FONT_SIZE // height of the text
10359
- );
10360
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10361
- ? GAUGE_LABELS_FONT_SIZE
10362
- : 0;
10363
- inflectionValuesTextRects.push(textRect);
10364
- inflectionValues.push({
10365
- rotation: angle,
10366
- label: inflectionValue.label,
10367
- fontSize: GAUGE_LABELS_FONT_SIZE,
10368
- color: textColor,
10369
- offset,
10346
+ setup() {
10347
+ onMounted(() => {
10348
+ const runtime = this.chartRuntime;
10349
+ this.currentRuntime = runtime;
10350
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
10351
+ this.createChart(deepCopy(runtime.chartJsConfig));
10352
+ });
10353
+ onWillUnmount(() => this.chart?.destroy());
10354
+ useEffect(() => {
10355
+ const runtime = this.chartRuntime;
10356
+ if (runtime !== this.currentRuntime) {
10357
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10358
+ this.chart?.destroy();
10359
+ this.createChart(deepCopy(runtime.chartJsConfig));
10360
+ }
10361
+ else {
10362
+ this.updateChartJs(deepCopy(runtime.chartJsConfig));
10363
+ }
10364
+ this.currentRuntime = runtime;
10365
+ }
10370
10366
  });
10371
10367
  }
10372
- return inflectionValues;
10373
- }
10374
- function getGaugeColor(runtime) {
10375
- const gaugeValue = runtime.gaugeValue?.value;
10376
- if (gaugeValue === undefined) {
10377
- return GAUGE_BACKGROUND_COLOR;
10378
- }
10379
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
10380
- const inflectionValue = runtime.inflectionValues[i];
10381
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10382
- return runtime.colors[i];
10383
- }
10384
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10385
- return runtime.colors[i];
10386
- }
10387
- }
10388
- return runtime.colors.at(-1);
10389
- }
10390
- function getSegmentsOfRectangle(rectangle) {
10391
- return [
10392
- { start: rectangle.topLeft, end: rectangle.topRight },
10393
- { start: rectangle.topRight, end: rectangle.bottomRight },
10394
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10395
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
10396
- ];
10397
- }
10398
- /**
10399
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10400
- * is not handled.
10401
- */
10402
- function doSegmentIntersect(segment1, segment2) {
10403
- const A = segment1.start;
10404
- const B = segment1.end;
10405
- const C = segment2.start;
10406
- const D = segment2.end;
10407
- /**
10408
- * Line segment intersection algorithm
10409
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10410
- */
10411
- function ccw(a, b, c) {
10412
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10368
+ createChart(chartData) {
10369
+ const canvas = this.canvas.el;
10370
+ const ctx = canvas.getContext("2d");
10371
+ const Chart = getChartJSConstructor();
10372
+ this.chart = new Chart(ctx, chartData);
10413
10373
  }
10414
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10415
- }
10416
- function doRectanglesIntersect(rect1, rect2) {
10417
- const segments1 = getSegmentsOfRectangle(rect1);
10418
- const segments2 = getSegmentsOfRectangle(rect2);
10419
- for (const segment1 of segments1) {
10420
- for (const segment2 of segments2) {
10421
- if (doSegmentIntersect(segment1, segment2)) {
10422
- return true;
10374
+ updateChartJs(chartData) {
10375
+ if (chartData.data && chartData.data.datasets) {
10376
+ this.chart.data = chartData.data;
10377
+ if (chartData.options?.plugins?.title) {
10378
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
10423
10379
  }
10424
10380
  }
10381
+ else {
10382
+ this.chart.data.datasets = [];
10383
+ }
10384
+ this.chart.config.options = chartData.options;
10385
+ this.chart.update();
10425
10386
  }
10426
- return false;
10427
- }
10428
- /**
10429
- * Get the rectangle that is tangent to a circle at a given angle.
10430
- *
10431
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10432
- */
10433
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10434
- const cos = Math.cos(angle);
10435
- const sin = Math.sin(angle);
10436
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10437
- const x = cos * radius;
10438
- const y = sin * radius;
10439
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10440
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10441
- const y2 = cos * (rectWidth / 2);
10442
- const bottomRight = {
10443
- x: x + x2 + circleCenterX,
10444
- y: circleCenterY - (y - y2),
10445
- };
10446
- const bottomLeft = {
10447
- x: x - x2 + circleCenterX,
10448
- y: circleCenterY - (y + y2),
10449
- };
10450
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10451
- const xp = cos * (radius + rectHeight);
10452
- const yp = sin * (radius + rectHeight);
10453
- const topLeft = {
10454
- x: xp - x2 + circleCenterX,
10455
- y: circleCenterY - (yp + y2),
10456
- };
10457
- const topRight = {
10458
- x: xp + x2 + circleCenterX,
10459
- y: circleCenterY - (yp - y2),
10460
- };
10461
- return { bottomLeft, bottomRight, topRight, topLeft };
10462
10387
  }
10463
10388
 
10464
10389
  /**
@@ -11040,299 +10965,6 @@ class ScorecardChartConfigBuilder {
11040
10965
  }
11041
10966
  }
11042
10967
 
11043
- const CHART_COMMON_OPTIONS = {
11044
- // https://www.chartjs.org/docs/latest/general/responsive.html
11045
- responsive: true, // will resize when its container is resized
11046
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11047
- elements: {
11048
- line: {
11049
- fill: false, // do not fill the area under line charts
11050
- },
11051
- point: {
11052
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11053
- },
11054
- },
11055
- animation: false,
11056
- };
11057
- function chartToImage(runtime, figure, type) {
11058
- // wrap the canvas in a div with a fixed size because chart.js would
11059
- // fill the whole page otherwise
11060
- const div = document.createElement("div");
11061
- div.style.width = `${figure.width}px`;
11062
- div.style.height = `${figure.height}px`;
11063
- const canvas = document.createElement("canvas");
11064
- div.append(canvas);
11065
- canvas.setAttribute("width", figure.width.toString());
11066
- canvas.setAttribute("height", figure.height.toString());
11067
- // we have to add the canvas to the DOM otherwise it won't be rendered
11068
- document.body.append(div);
11069
- if ("chartJsConfig" in runtime) {
11070
- const config = deepCopy(runtime.chartJsConfig);
11071
- config.plugins = [backgroundColorChartJSPlugin];
11072
- const Chart = getChartJSConstructor();
11073
- const chart = new Chart(canvas, config);
11074
- const imgContent = chart.toBase64Image();
11075
- chart.destroy();
11076
- div.remove();
11077
- return imgContent;
11078
- }
11079
- else if (type === "scorecard") {
11080
- const design = getScorecardConfiguration(figure, runtime);
11081
- drawScoreChart(design, canvas);
11082
- const imgContent = canvas.toDataURL();
11083
- div.remove();
11084
- return imgContent;
11085
- }
11086
- else if (type === "gauge") {
11087
- drawGaugeChart(canvas, runtime);
11088
- const imgContent = canvas.toDataURL();
11089
- div.remove();
11090
- return imgContent;
11091
- }
11092
- return undefined;
11093
- }
11094
- /**
11095
- * Custom chart.js plugin to set the background color of the canvas
11096
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11097
- */
11098
- const backgroundColorChartJSPlugin = {
11099
- id: "customCanvasBackgroundColor",
11100
- beforeDraw: (chart) => {
11101
- const { ctx } = chart;
11102
- ctx.save();
11103
- ctx.globalCompositeOperation = "destination-over";
11104
- ctx.fillStyle = "#ffffff";
11105
- ctx.fillRect(0, 0, chart.width, chart.height);
11106
- ctx.restore();
11107
- },
11108
- };
11109
- /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11110
- function getChartJSConstructor() {
11111
- if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11112
- window.Chart.register(chartShowValuesPlugin);
11113
- window.Chart.register(waterfallLinesPlugin);
11114
- }
11115
- return window.Chart;
11116
- }
11117
-
11118
- /**
11119
- * This file is largely inspired by owl 1.
11120
- * `css` tag has been removed from owl 2 without workaround to manage css.
11121
- * So, the solution was to import the behavior of owl 1 directly in our
11122
- * codebase, with one difference: the css is added to the sheet as soon as the
11123
- * css tag is executed. In owl 1, the css was added as soon as a Component was
11124
- * created for the first time.
11125
- */
11126
- const STYLESHEETS = {};
11127
- let nextId = 0;
11128
- /**
11129
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
11130
- * an inline stylesheet with just the following code:
11131
- * ```js
11132
- * css`.component-a { color: red; }`;
11133
- * ```
11134
- */
11135
- function css(strings, ...args) {
11136
- const name = `__sheet__${nextId++}`;
11137
- const value = String.raw(strings, ...args);
11138
- registerSheet(name, value);
11139
- activateSheet(name);
11140
- return name;
11141
- }
11142
- function processSheet(str) {
11143
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11144
- const selectorStack = [];
11145
- const parts = [];
11146
- let rules = [];
11147
- function generateSelector(stackIndex, parentSelector) {
11148
- const parts = [];
11149
- for (const selector of selectorStack[stackIndex]) {
11150
- let part = (parentSelector && parentSelector + " " + selector) || selector;
11151
- if (part.includes("&")) {
11152
- part = selector.replace(/&/g, parentSelector || "");
11153
- }
11154
- if (stackIndex < selectorStack.length - 1) {
11155
- part = generateSelector(stackIndex + 1, part);
11156
- }
11157
- parts.push(part);
11158
- }
11159
- return parts.join(", ");
11160
- }
11161
- function generateRules() {
11162
- if (rules.length) {
11163
- parts.push(generateSelector(0) + " {");
11164
- parts.push(...rules);
11165
- parts.push("}");
11166
- rules = [];
11167
- }
11168
- }
11169
- while (tokens.length) {
11170
- let token = tokens.shift();
11171
- if (token === "}") {
11172
- generateRules();
11173
- selectorStack.pop();
11174
- }
11175
- else {
11176
- if (tokens[0] === "{") {
11177
- generateRules();
11178
- selectorStack.push(token.split(/\s*,\s*/));
11179
- tokens.shift();
11180
- }
11181
- if (tokens[0] === ";") {
11182
- rules.push(" " + token + ";");
11183
- }
11184
- }
11185
- }
11186
- return parts.join("\n");
11187
- }
11188
- function registerSheet(id, css) {
11189
- const sheet = document.createElement("style");
11190
- sheet.textContent = processSheet(css);
11191
- STYLESHEETS[id] = sheet;
11192
- }
11193
- function activateSheet(id) {
11194
- const sheet = STYLESHEETS[id];
11195
- sheet.setAttribute("component", id);
11196
- document.head.appendChild(sheet);
11197
- }
11198
- function getTextDecoration({ strikethrough, underline, }) {
11199
- if (!strikethrough && !underline) {
11200
- return "none";
11201
- }
11202
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11203
- }
11204
- /**
11205
- * Convert the cell style to CSS properties.
11206
- */
11207
- function cellStyleToCss(style) {
11208
- const attributes = cellTextStyleToCss(style);
11209
- if (!style)
11210
- return attributes;
11211
- if (style.fillColor) {
11212
- attributes["background"] = style.fillColor;
11213
- }
11214
- return attributes;
11215
- }
11216
- /**
11217
- * Convert the cell text style to CSS properties.
11218
- */
11219
- function cellTextStyleToCss(style) {
11220
- const attributes = {};
11221
- if (!style)
11222
- return attributes;
11223
- if (style.bold) {
11224
- attributes["font-weight"] = "bold";
11225
- }
11226
- if (style.italic) {
11227
- attributes["font-style"] = "italic";
11228
- }
11229
- if (style.strikethrough || style.underline) {
11230
- let decoration = style.strikethrough ? "line-through" : "";
11231
- decoration = style.underline ? decoration + " underline" : decoration;
11232
- attributes["text-decoration"] = decoration;
11233
- }
11234
- if (style.textColor) {
11235
- attributes["color"] = style.textColor;
11236
- }
11237
- return attributes;
11238
- }
11239
- /**
11240
- * Transform CSS properties into a CSS string.
11241
- */
11242
- function cssPropertiesToCss(attributes) {
11243
- let styleStr = "";
11244
- for (const attName in attributes) {
11245
- if (!attributes[attName]) {
11246
- continue;
11247
- }
11248
- styleStr += `${attName}:${attributes[attName]}; `;
11249
- }
11250
- return styleStr;
11251
- }
11252
- function getElementMargins(el) {
11253
- const style = window.getComputedStyle(el);
11254
- return {
11255
- top: parseInt(style.marginTop, 10) || 0,
11256
- bottom: parseInt(style.marginBottom, 10) || 0,
11257
- left: parseInt(style.marginLeft, 10) || 0,
11258
- right: parseInt(style.marginRight, 10) || 0,
11259
- };
11260
- }
11261
-
11262
- css /* scss */ `
11263
- .o-spreadsheet {
11264
- .o-chart-custom-tooltip {
11265
- font-size: 12px;
11266
- background-color: #fff;
11267
- z-index: ${ComponentsImportance.FigureTooltip};
11268
- }
11269
- }
11270
- `;
11271
- class ChartJsComponent extends Component {
11272
- static template = "o-spreadsheet-ChartJsComponent";
11273
- static props = {
11274
- figure: Object,
11275
- };
11276
- canvas = useRef("graphContainer");
11277
- chart;
11278
- currentRuntime;
11279
- get background() {
11280
- return this.chartRuntime.background;
11281
- }
11282
- get canvasStyle() {
11283
- return `background-color: ${this.background}`;
11284
- }
11285
- get chartRuntime() {
11286
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11287
- if (!("chartJsConfig" in runtime)) {
11288
- throw new Error("Unsupported chart runtime");
11289
- }
11290
- return runtime;
11291
- }
11292
- setup() {
11293
- onMounted(() => {
11294
- const runtime = this.chartRuntime;
11295
- this.currentRuntime = runtime;
11296
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
11297
- this.createChart(deepCopy(runtime.chartJsConfig));
11298
- });
11299
- onWillUnmount(() => this.chart?.destroy());
11300
- useEffect(() => {
11301
- const runtime = this.chartRuntime;
11302
- if (runtime !== this.currentRuntime) {
11303
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11304
- this.chart?.destroy();
11305
- this.createChart(deepCopy(runtime.chartJsConfig));
11306
- }
11307
- else {
11308
- this.updateChartJs(deepCopy(runtime));
11309
- }
11310
- this.currentRuntime = runtime;
11311
- }
11312
- });
11313
- }
11314
- createChart(chartData) {
11315
- const canvas = this.canvas.el;
11316
- const ctx = canvas.getContext("2d");
11317
- const Chart = getChartJSConstructor();
11318
- this.chart = new Chart(ctx, chartData);
11319
- }
11320
- updateChartJs(chartRuntime) {
11321
- const chartData = chartRuntime.chartJsConfig;
11322
- if (chartData.data && chartData.data.datasets) {
11323
- this.chart.data = chartData.data;
11324
- if (chartData.options?.plugins?.title) {
11325
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
11326
- }
11327
- }
11328
- else {
11329
- this.chart.data.datasets = [];
11330
- }
11331
- this.chart.config.options = chartData.options;
11332
- this.chart.update();
11333
- }
11334
- }
11335
-
11336
10968
  class ScorecardChart extends Component {
11337
10969
  static template = "o-spreadsheet-ScorecardChart";
11338
10970
  static props = {
@@ -22930,6 +22562,343 @@ function getDateIntervals(dates) {
22930
22562
 
22931
22563
  const cellPopoverRegistry = new Registry();
22932
22564
 
22565
+ const GAUGE_PADDING_SIDE = 30;
22566
+ const GAUGE_PADDING_TOP = 10;
22567
+ const GAUGE_PADDING_BOTTOM = 20;
22568
+ const GAUGE_LABELS_FONT_SIZE = 12;
22569
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22570
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22571
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22572
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
22573
+ function drawGaugeChart(canvas, runtime) {
22574
+ const canvasBoundingRect = canvas.getBoundingClientRect();
22575
+ canvas.width = canvasBoundingRect.width;
22576
+ canvas.height = canvasBoundingRect.height;
22577
+ const ctx = canvas.getContext("2d");
22578
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22579
+ drawBackground(ctx, config);
22580
+ drawGauge(ctx, config);
22581
+ drawInflectionValues(ctx, config);
22582
+ drawLabels(ctx, config);
22583
+ drawTitle(ctx, config);
22584
+ }
22585
+ function drawGauge(ctx, config) {
22586
+ ctx.save();
22587
+ const gauge = config.gauge;
22588
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22589
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
22590
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22591
+ if (arcRadius < 0) {
22592
+ return;
22593
+ }
22594
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22595
+ // Gauge background
22596
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22597
+ ctx.beginPath();
22598
+ ctx.lineWidth = gauge.arcWidth;
22599
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22600
+ ctx.stroke();
22601
+ // Gauge value
22602
+ ctx.strokeStyle = gauge.color;
22603
+ ctx.beginPath();
22604
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22605
+ ctx.stroke();
22606
+ ctx.restore();
22607
+ }
22608
+ function drawBackground(ctx, config) {
22609
+ ctx.save();
22610
+ ctx.fillStyle = config.backgroundColor;
22611
+ ctx.fillRect(0, 0, config.width, config.height);
22612
+ ctx.restore();
22613
+ }
22614
+ function drawLabels(ctx, config) {
22615
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22616
+ ctx.save();
22617
+ ctx.textAlign = "center";
22618
+ ctx.fillStyle = label.color;
22619
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22620
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22621
+ ctx.restore();
22622
+ }
22623
+ }
22624
+ function drawInflectionValues(ctx, config) {
22625
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22626
+ for (const inflectionValue of config.inflectionValues) {
22627
+ ctx.save();
22628
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22629
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22630
+ ctx.lineWidth = 2;
22631
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22632
+ ctx.beginPath();
22633
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
22634
+ ctx.lineTo(0, -height - 3);
22635
+ ctx.stroke();
22636
+ ctx.textAlign = "center";
22637
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22638
+ ctx.fillStyle = inflectionValue.color;
22639
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22640
+ ctx.fillText(inflectionValue.label, 0, textY);
22641
+ ctx.restore();
22642
+ }
22643
+ }
22644
+ function drawTitle(ctx, config) {
22645
+ ctx.save();
22646
+ const title = config.title;
22647
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22648
+ ctx.textBaseline = "middle";
22649
+ ctx.fillStyle = title.color;
22650
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22651
+ ctx.restore();
22652
+ }
22653
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22654
+ const maxValue = runtime.maxValue;
22655
+ const minValue = runtime.minValue;
22656
+ const gaugeValue = runtime.gaugeValue;
22657
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22658
+ const gaugeArcWidth = gaugeRect.width / 6;
22659
+ const gaugePercentage = gaugeValue
22660
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22661
+ : 0;
22662
+ const gaugeValuePosition = {
22663
+ x: boundingRect.width / 2,
22664
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22665
+ };
22666
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22667
+ // Scale down the font size if the gaugeRect is too small
22668
+ if (gaugeRect.height < 300) {
22669
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22670
+ }
22671
+ // Scale down the font size if the text is too long
22672
+ const maxTextWidth = gaugeRect.width / 2;
22673
+ const gaugeLabel = gaugeValue?.label || "-";
22674
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22675
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22676
+ }
22677
+ const minLabelPosition = {
22678
+ x: gaugeRect.x + gaugeArcWidth / 2,
22679
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22680
+ };
22681
+ const maxLabelPosition = {
22682
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22683
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22684
+ };
22685
+ const textColor = chartMutedFontColor(runtime.background);
22686
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22687
+ let x = 0, titleWidth = 0, titleHeight = 0;
22688
+ if (runtime.title.text) {
22689
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22690
+ }
22691
+ switch (runtime.title.align) {
22692
+ case "right":
22693
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
22694
+ break;
22695
+ case "center":
22696
+ x = (boundingRect.width - titleWidth) / 2;
22697
+ break;
22698
+ case "left":
22699
+ default:
22700
+ x = CHART_PADDING$1;
22701
+ break;
22702
+ }
22703
+ return {
22704
+ width: boundingRect.width,
22705
+ height: boundingRect.height,
22706
+ title: {
22707
+ label: runtime.title.text ?? "",
22708
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22709
+ textPosition: {
22710
+ x,
22711
+ y: CHART_PADDING_TOP + titleHeight / 2,
22712
+ },
22713
+ color: runtime.title.color ?? textColor,
22714
+ bold: runtime.title.bold,
22715
+ italic: runtime.title.italic,
22716
+ },
22717
+ backgroundColor: runtime.background,
22718
+ gauge: {
22719
+ rect: gaugeRect,
22720
+ arcWidth: gaugeArcWidth,
22721
+ percentage: clip(gaugePercentage, 0, 1),
22722
+ color: getGaugeColor(runtime),
22723
+ },
22724
+ inflectionValues,
22725
+ gaugeValue: {
22726
+ label: gaugeLabel,
22727
+ textPosition: gaugeValuePosition,
22728
+ fontSize: gaugeValueFontSize,
22729
+ color: textColor,
22730
+ },
22731
+ minLabel: {
22732
+ label: runtime.minValue.label,
22733
+ textPosition: minLabelPosition,
22734
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22735
+ color: textColor,
22736
+ },
22737
+ maxLabel: {
22738
+ label: runtime.maxValue.label,
22739
+ textPosition: maxLabelPosition,
22740
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22741
+ color: textColor,
22742
+ },
22743
+ };
22744
+ }
22745
+ /**
22746
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22747
+ * space for the title and labels.
22748
+ */
22749
+ function getGaugeRect(boundingRect, title) {
22750
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22751
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22752
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22753
+ let gaugeWidth;
22754
+ let gaugeHeight;
22755
+ if (drawWidth > 2 * drawHeight) {
22756
+ gaugeWidth = 2 * drawHeight;
22757
+ gaugeHeight = drawHeight;
22758
+ }
22759
+ else {
22760
+ gaugeWidth = drawWidth;
22761
+ gaugeHeight = drawWidth / 2;
22762
+ }
22763
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22764
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22765
+ return {
22766
+ x: gaugeX,
22767
+ y: gaugeY,
22768
+ width: gaugeWidth,
22769
+ height: gaugeHeight,
22770
+ };
22771
+ }
22772
+ /**
22773
+ * 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).
22774
+ *
22775
+ * Also compute an offset for the text so that it doesn't overlap with other text.
22776
+ */
22777
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22778
+ const maxValue = runtime.maxValue;
22779
+ const minValue = runtime.minValue;
22780
+ const gaugeCircleCenter = {
22781
+ x: gaugeRect.x + gaugeRect.width / 2,
22782
+ y: gaugeRect.y + gaugeRect.height,
22783
+ };
22784
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22785
+ const inflectionValues = [];
22786
+ const inflectionValuesTextRects = [];
22787
+ for (const inflectionValue of runtime.inflectionValues) {
22788
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22789
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22790
+ const angle = Math.PI - Math.PI * percentage;
22791
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22792
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22793
+ gaugeCircleCenter.x, // center of the gauge circle
22794
+ gaugeCircleCenter.y, // center of the gauge circle
22795
+ labelWidth + 2, // width of the text + some margin
22796
+ GAUGE_LABELS_FONT_SIZE // height of the text
22797
+ );
22798
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22799
+ ? GAUGE_LABELS_FONT_SIZE
22800
+ : 0;
22801
+ inflectionValuesTextRects.push(textRect);
22802
+ inflectionValues.push({
22803
+ rotation: angle,
22804
+ label: inflectionValue.label,
22805
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22806
+ color: textColor,
22807
+ offset,
22808
+ });
22809
+ }
22810
+ return inflectionValues;
22811
+ }
22812
+ function getGaugeColor(runtime) {
22813
+ const gaugeValue = runtime.gaugeValue?.value;
22814
+ if (gaugeValue === undefined) {
22815
+ return GAUGE_BACKGROUND_COLOR;
22816
+ }
22817
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
22818
+ const inflectionValue = runtime.inflectionValues[i];
22819
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22820
+ return runtime.colors[i];
22821
+ }
22822
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22823
+ return runtime.colors[i];
22824
+ }
22825
+ }
22826
+ return runtime.colors.at(-1);
22827
+ }
22828
+ function getSegmentsOfRectangle(rectangle) {
22829
+ return [
22830
+ { start: rectangle.topLeft, end: rectangle.topRight },
22831
+ { start: rectangle.topRight, end: rectangle.bottomRight },
22832
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22833
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
22834
+ ];
22835
+ }
22836
+ /**
22837
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22838
+ * is not handled.
22839
+ */
22840
+ function doSegmentIntersect(segment1, segment2) {
22841
+ const A = segment1.start;
22842
+ const B = segment1.end;
22843
+ const C = segment2.start;
22844
+ const D = segment2.end;
22845
+ /**
22846
+ * Line segment intersection algorithm
22847
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22848
+ */
22849
+ function ccw(a, b, c) {
22850
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22851
+ }
22852
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22853
+ }
22854
+ function doRectanglesIntersect(rect1, rect2) {
22855
+ const segments1 = getSegmentsOfRectangle(rect1);
22856
+ const segments2 = getSegmentsOfRectangle(rect2);
22857
+ for (const segment1 of segments1) {
22858
+ for (const segment2 of segments2) {
22859
+ if (doSegmentIntersect(segment1, segment2)) {
22860
+ return true;
22861
+ }
22862
+ }
22863
+ }
22864
+ return false;
22865
+ }
22866
+ /**
22867
+ * Get the rectangle that is tangent to a circle at a given angle.
22868
+ *
22869
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22870
+ */
22871
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22872
+ const cos = Math.cos(angle);
22873
+ const sin = Math.sin(angle);
22874
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22875
+ const x = cos * radius;
22876
+ const y = sin * radius;
22877
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22878
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22879
+ const y2 = cos * (rectWidth / 2);
22880
+ const bottomRight = {
22881
+ x: x + x2 + circleCenterX,
22882
+ y: circleCenterY - (y - y2),
22883
+ };
22884
+ const bottomLeft = {
22885
+ x: x - x2 + circleCenterX,
22886
+ y: circleCenterY - (y + y2),
22887
+ };
22888
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22889
+ const xp = cos * (radius + rectHeight);
22890
+ const yp = sin * (radius + rectHeight);
22891
+ const topLeft = {
22892
+ x: xp - x2 + circleCenterX,
22893
+ y: circleCenterY - (yp + y2),
22894
+ };
22895
+ const topRight = {
22896
+ x: xp + x2 + circleCenterX,
22897
+ y: circleCenterY - (yp - y2),
22898
+ };
22899
+ return { bottomLeft, bottomRight, topRight, topLeft };
22900
+ }
22901
+
22933
22902
  class GaugeChartComponent extends Component {
22934
22903
  static template = "o-spreadsheet-GaugeChartComponent";
22935
22904
  canvas = useRef("chartContainer");
@@ -22962,6 +22931,73 @@ function toXlsxHexColor(color) {
22962
22931
  return color;
22963
22932
  }
22964
22933
 
22934
+ const CHART_COMMON_OPTIONS = {
22935
+ // https://www.chartjs.org/docs/latest/general/responsive.html
22936
+ responsive: true, // will resize when its container is resized
22937
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22938
+ elements: {
22939
+ line: {
22940
+ fill: false, // do not fill the area under line charts
22941
+ },
22942
+ point: {
22943
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22944
+ },
22945
+ },
22946
+ animation: false,
22947
+ };
22948
+ function chartToImage(runtime, figure, type) {
22949
+ // wrap the canvas in a div with a fixed size because chart.js would
22950
+ // fill the whole page otherwise
22951
+ const div = document.createElement("div");
22952
+ div.style.width = `${figure.width}px`;
22953
+ div.style.height = `${figure.height}px`;
22954
+ const canvas = document.createElement("canvas");
22955
+ div.append(canvas);
22956
+ canvas.setAttribute("width", figure.width.toString());
22957
+ canvas.setAttribute("height", figure.height.toString());
22958
+ // we have to add the canvas to the DOM otherwise it won't be rendered
22959
+ document.body.append(div);
22960
+ if ("chartJsConfig" in runtime) {
22961
+ const config = deepCopy(runtime.chartJsConfig);
22962
+ config.plugins = [backgroundColorChartJSPlugin];
22963
+ const Chart = getChartJSConstructor();
22964
+ const chart = new Chart(canvas, config);
22965
+ const imgContent = chart.toBase64Image();
22966
+ chart.destroy();
22967
+ div.remove();
22968
+ return imgContent;
22969
+ }
22970
+ else if (type === "scorecard") {
22971
+ const design = getScorecardConfiguration(figure, runtime);
22972
+ drawScoreChart(design, canvas);
22973
+ const imgContent = canvas.toDataURL();
22974
+ div.remove();
22975
+ return imgContent;
22976
+ }
22977
+ else if (type === "gauge") {
22978
+ drawGaugeChart(canvas, runtime);
22979
+ const imgContent = canvas.toDataURL();
22980
+ div.remove();
22981
+ return imgContent;
22982
+ }
22983
+ return undefined;
22984
+ }
22985
+ /**
22986
+ * Custom chart.js plugin to set the background color of the canvas
22987
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22988
+ */
22989
+ const backgroundColorChartJSPlugin = {
22990
+ id: "customCanvasBackgroundColor",
22991
+ beforeDraw: (chart) => {
22992
+ const { ctx } = chart;
22993
+ ctx.save();
22994
+ ctx.globalCompositeOperation = "destination-over";
22995
+ ctx.fillStyle = "#ffffff";
22996
+ ctx.fillRect(0, 0, chart.width, chart.height);
22997
+ ctx.restore();
22998
+ },
22999
+ };
23000
+
22965
23001
  /**
22966
23002
  * Represent a raw XML string
22967
23003
  */
@@ -34232,7 +34268,6 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34232
34268
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34233
34269
  formatChartDatasetValue: formatChartDatasetValue,
34234
34270
  formatTickValue: formatTickValue,
34235
- getChartJSConstructor: getChartJSConstructor,
34236
34271
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34237
34272
  getDefinedAxis: getDefinedAxis,
34238
34273
  getPieColors: getPieColors,
@@ -45080,7 +45115,8 @@ css /* scss */ `
45080
45115
  &.pivot-dimension-invalid {
45081
45116
  background-color: #ffdddd;
45082
45117
  border-color: red !important;
45083
- select {
45118
+ select,
45119
+ input {
45084
45120
  background-color: #ffdddd;
45085
45121
  }
45086
45122
  }
@@ -46941,7 +46977,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46941
46977
  this.notification.notifyUser({
46942
46978
  type: "info",
46943
46979
  text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
46944
- sticky: false,
46980
+ sticky: true,
46945
46981
  });
46946
46982
  }
46947
46983
  }
@@ -53293,6 +53329,15 @@ class BordersPlugin extends CorePlugin {
53293
53329
  case "SET_BORDER":
53294
53330
  this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);
53295
53331
  break;
53332
+ case "SET_BORDERS_ON_TARGET":
53333
+ for (const zone of cmd.target) {
53334
+ for (let row = zone.top; row <= zone.bottom; row++) {
53335
+ for (let col = zone.left; col <= zone.right; col++) {
53336
+ this.setBorder(cmd.sheetId, col, row, cmd.border);
53337
+ }
53338
+ }
53339
+ }
53340
+ break;
53296
53341
  case "SET_ZONE_BORDERS":
53297
53342
  if (cmd.border) {
53298
53343
  const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
@@ -63055,25 +63100,6 @@ class AutofillPlugin extends UIPlugin {
63055
63100
  case "AUTOFILL_AUTO":
63056
63101
  this.autofillAuto();
63057
63102
  break;
63058
- case "AUTOFILL_CELL":
63059
- this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
63060
- const sheetId = this.getters.getActiveSheetId();
63061
- this.dispatch("UPDATE_CELL", {
63062
- sheetId,
63063
- col: cmd.col,
63064
- row: cmd.row,
63065
- style: cmd.style || null,
63066
- content: cmd.content || "",
63067
- format: cmd.format || "",
63068
- });
63069
- this.dispatch("SET_BORDER", {
63070
- sheetId,
63071
- col: cmd.col,
63072
- row: cmd.row,
63073
- border: cmd.border,
63074
- });
63075
- this.autofillCF(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
63076
- this.autofillDV(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
63077
63103
  }
63078
63104
  }
63079
63105
  // ---------------------------------------------------------------------------
@@ -63097,6 +63123,7 @@ class AutofillPlugin extends UIPlugin {
63097
63123
  }
63098
63124
  const source = this.getters.getSelectedZone();
63099
63125
  const target = this.autofillZone;
63126
+ const autofillCellsData = [];
63100
63127
  switch (this.direction) {
63101
63128
  case "down" /* DIRECTION.DOWN */:
63102
63129
  for (let col = source.left; col <= source.right; col++) {
@@ -63106,7 +63133,7 @@ class AutofillPlugin extends UIPlugin {
63106
63133
  }
63107
63134
  const generator = this.createGenerator(xcs);
63108
63135
  for (let row = target.top; row <= target.bottom; row++) {
63109
- this.computeNewCell(generator, col, row, apply);
63136
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63110
63137
  }
63111
63138
  }
63112
63139
  break;
@@ -63118,7 +63145,7 @@ class AutofillPlugin extends UIPlugin {
63118
63145
  }
63119
63146
  const generator = this.createGenerator(xcs);
63120
63147
  for (let row = target.bottom; row >= target.top; row--) {
63121
- this.computeNewCell(generator, col, row, apply);
63148
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63122
63149
  }
63123
63150
  }
63124
63151
  break;
@@ -63130,7 +63157,7 @@ class AutofillPlugin extends UIPlugin {
63130
63157
  }
63131
63158
  const generator = this.createGenerator(xcs);
63132
63159
  for (let col = target.right; col >= target.left; col--) {
63133
- this.computeNewCell(generator, col, row, apply);
63160
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63134
63161
  }
63135
63162
  }
63136
63163
  break;
@@ -63142,12 +63169,26 @@ class AutofillPlugin extends UIPlugin {
63142
63169
  }
63143
63170
  const generator = this.createGenerator(xcs);
63144
63171
  for (let col = target.left; col <= target.right; col++) {
63145
- this.computeNewCell(generator, col, row, apply);
63172
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63146
63173
  }
63147
63174
  }
63148
63175
  break;
63149
63176
  }
63150
63177
  if (apply) {
63178
+ const bordersZones = {};
63179
+ const cfNewRanges = {};
63180
+ const dvNewZones = {};
63181
+ const sheetId = this.getters.getActiveSheetId();
63182
+ for (const data of autofillCellsData) {
63183
+ this.collectBordersData(data, bordersZones);
63184
+ this.autofillMerge(sheetId, data);
63185
+ this.autofillCell(sheetId, data);
63186
+ this.collectConditionalFormatsData(sheetId, data, cfNewRanges);
63187
+ this.collectDataValidationsData(sheetId, data, dvNewZones);
63188
+ }
63189
+ this.autofillBorders(sheetId, bordersZones);
63190
+ this.autofillConditionalFormats(sheetId, cfNewRanges);
63191
+ this.autofillDataValidations(sheetId, dvNewZones);
63151
63192
  this.autofillZone = undefined;
63152
63193
  this.selection.resizeAnchorZone(this.direction, this.steps);
63153
63194
  this.lastCellSelected = {};
@@ -63156,6 +63197,95 @@ class AutofillPlugin extends UIPlugin {
63156
63197
  this.tooltip = undefined;
63157
63198
  }
63158
63199
  }
63200
+ collectBordersData(data, bordersPositions) {
63201
+ const key = JSON.stringify(data.border);
63202
+ if (!(key in bordersPositions)) {
63203
+ bordersPositions[key] = [];
63204
+ }
63205
+ bordersPositions[key].push(positionToZone({ col: data.col, row: data.row }));
63206
+ }
63207
+ collectConditionalFormatsData(sheetId, data, cfNewRanges) {
63208
+ const { originCol, originRow, col, row } = data;
63209
+ const cfsAtOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
63210
+ const xc = toXC(col, row);
63211
+ for (const cf of cfsAtOrigin) {
63212
+ if (!(cf.id in cfNewRanges)) {
63213
+ cfNewRanges[cf.id] = [];
63214
+ }
63215
+ cfNewRanges[cf.id].push(xc);
63216
+ }
63217
+ }
63218
+ collectDataValidationsData(sheetId, data, dvNewZones) {
63219
+ const { originCol, originRow, col, row } = data;
63220
+ const cellPosition = { sheetId, col: originCol, row: originRow };
63221
+ const dvsAtOrigin = this.getters.getValidationRuleForCell(cellPosition);
63222
+ if (!dvsAtOrigin) {
63223
+ return;
63224
+ }
63225
+ if (!(dvsAtOrigin.id in dvNewZones)) {
63226
+ dvNewZones[dvsAtOrigin.id] = [];
63227
+ }
63228
+ dvNewZones[dvsAtOrigin.id].push(positionToZone({ col, row }));
63229
+ }
63230
+ autofillCell(sheetId, data) {
63231
+ this.dispatch("UPDATE_CELL", {
63232
+ sheetId,
63233
+ col: data.col,
63234
+ row: data.row,
63235
+ content: data.content || "",
63236
+ style: data.style || null,
63237
+ format: data.format || "",
63238
+ });
63239
+ // Still usefull in odoo ATM to autofill field sync
63240
+ this.dispatch("AUTOFILL_CELL", data);
63241
+ }
63242
+ autofillBorders(sheetId, bordersPositions) {
63243
+ for (const stringifiedBorder in bordersPositions) {
63244
+ const border = stringifiedBorder === "undefined" ? undefined : JSON.parse(stringifiedBorder);
63245
+ this.dispatch("SET_BORDERS_ON_TARGET", {
63246
+ sheetId,
63247
+ border,
63248
+ target: recomputeZones(bordersPositions[stringifiedBorder]),
63249
+ });
63250
+ }
63251
+ }
63252
+ autofillConditionalFormats(sheetId, cfNewRanges) {
63253
+ for (const cfId in cfNewRanges) {
63254
+ const changes = cfNewRanges[cfId];
63255
+ const cf = this.getters.getConditionalFormats(sheetId).find((cf) => cf.id === cfId);
63256
+ if (!cf) {
63257
+ continue;
63258
+ }
63259
+ const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, changes.map(toZone), []);
63260
+ if (newCfRanges) {
63261
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
63262
+ cf: {
63263
+ id: cf.id,
63264
+ rule: cf.rule,
63265
+ stopIfTrue: cf.stopIfTrue,
63266
+ },
63267
+ ranges: newCfRanges,
63268
+ sheetId,
63269
+ });
63270
+ }
63271
+ }
63272
+ }
63273
+ autofillDataValidations(sheetId, dvNewZones) {
63274
+ for (const dvId in dvNewZones) {
63275
+ const changes = dvNewZones[dvId];
63276
+ const dvOrigin = this.getters.getDataValidationRule(sheetId, dvId);
63277
+ if (!dvOrigin) {
63278
+ continue;
63279
+ }
63280
+ const dvRangesXcs = dvOrigin.ranges.map((range) => range.zone);
63281
+ const newDvRanges = recomputeZones(dvRangesXcs.concat(changes), []);
63282
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
63283
+ rule: dvOrigin,
63284
+ ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
63285
+ sheetId,
63286
+ });
63287
+ }
63288
+ }
63159
63289
  /**
63160
63290
  * Select a cell which becomes the last cell of the autofillZone
63161
63291
  */
@@ -63234,22 +63364,20 @@ class AutofillPlugin extends UIPlugin {
63234
63364
  /**
63235
63365
  * Generate the next cell
63236
63366
  */
63237
- computeNewCell(generator, col, row, apply) {
63367
+ computeNewCell(generator, col, row) {
63238
63368
  const { cellData, tooltip, origin } = generator.next();
63239
63369
  const { content, style, border, format } = cellData;
63240
63370
  this.tooltip = tooltip;
63241
- if (apply) {
63242
- this.dispatch("AUTOFILL_CELL", {
63243
- originCol: origin.col,
63244
- originRow: origin.row,
63245
- col,
63246
- row,
63247
- content,
63248
- style,
63249
- border,
63250
- format,
63251
- });
63252
- }
63371
+ return {
63372
+ originCol: origin.col,
63373
+ originRow: origin.row,
63374
+ col,
63375
+ row,
63376
+ content,
63377
+ style,
63378
+ border,
63379
+ format,
63380
+ };
63253
63381
  }
63254
63382
  /**
63255
63383
  * Get the rule associated to the current cell
@@ -63317,8 +63445,8 @@ class AutofillPlugin extends UIPlugin {
63317
63445
  ? position[first].value
63318
63446
  : position[second].value;
63319
63447
  }
63320
- autoFillMerge(originCol, originRow, col, row) {
63321
- const sheetId = this.getters.getActiveSheetId();
63448
+ autofillMerge(sheetId, data) {
63449
+ const { originCol, originRow, col, row } = data;
63322
63450
  const position = { sheetId, col, row };
63323
63451
  const originPosition = { sheetId, col: originCol, row: originRow };
63324
63452
  if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {
@@ -63345,35 +63473,6 @@ class AutofillPlugin extends UIPlugin {
63345
63473
  });
63346
63474
  }
63347
63475
  }
63348
- autofillCF(originCol, originRow, col, row) {
63349
- const sheetId = this.getters.getActiveSheetId();
63350
- const cfOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
63351
- for (const cf of cfOrigin) {
63352
- const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, [positionToZone({ col, row })], []);
63353
- if (newCfRanges) {
63354
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
63355
- cf: deepCopy(cf),
63356
- ranges: newCfRanges,
63357
- sheetId,
63358
- });
63359
- }
63360
- }
63361
- }
63362
- autofillDV(originCol, originRow, col, row) {
63363
- const sheetId = this.getters.getActiveSheetId();
63364
- const cellPosition = { sheetId, col: originCol, row: originRow };
63365
- const dvOrigin = this.getters.getValidationRuleForCell(cellPosition);
63366
- if (!dvOrigin) {
63367
- return;
63368
- }
63369
- const dvRangesZones = dvOrigin.ranges.map((range) => range.zone);
63370
- const newDvRanges = recomputeZones(dvRangesZones.concat(positionToZone({ col, row })), []);
63371
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
63372
- rule: dvOrigin,
63373
- ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
63374
- sheetId,
63375
- });
63376
- }
63377
63476
  // ---------------------------------------------------------------------------
63378
63477
  // Grid rendering
63379
63478
  // ---------------------------------------------------------------------------
@@ -75888,6 +75987,7 @@ const registries = {
75888
75987
  supportedPivotPositionalFormulaRegistry,
75889
75988
  pivotToFunctionValueRegistry,
75890
75989
  migrationStepRegistry,
75990
+ chartJsExtensionRegistry,
75891
75991
  };
75892
75992
  const helpers = {
75893
75993
  arg,
@@ -76043,6 +76143,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
76043
76143
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, CoreViewPlugin, DispatchResult, EvaluationError, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, chartHelpers, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
76044
76144
 
76045
76145
 
76046
- __info__.version = "18.2.5";
76047
- __info__.date = "2025-03-26T12:47:44.113Z";
76048
- __info__.hash = "4675edd";
76146
+ __info__.version = "18.2.7";
76147
+ __info__.date = "2025-04-14T17:19:31.011Z";
76148
+ __info__.hash = "e187958";