@odoo/o-spreadsheet 16.5.0-alpha.10 → 16.5.0-alpha.11

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 16.5.0-alpha.10
6
- * @date 2023-10-10T07:31:20.876Z
7
- * @hash 4dc340f
5
+ * @version 16.5.0-alpha.11
6
+ * @date 2023-10-19T14:26:01.790Z
7
+ * @hash 4b42059
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -4500,6 +4500,69 @@
4500
4500
  function toLowerCase(str) {
4501
4501
  return str ? str.toLowerCase() : "";
4502
4502
  }
4503
+ /**
4504
+ * Extract the fontSize from a context font string
4505
+ * @param font The (context) font string to parse
4506
+ * @returns The fontSize in pixels
4507
+ */
4508
+ const pxRegex = /([0-9\.]*)px/;
4509
+ function getContextFontSize(font) {
4510
+ return Number(font.match(pxRegex)?.[1]);
4511
+ }
4512
+ function drawDecoratedText(context, text, position, underline = false, strikethrough = false, strokeWidth = getContextFontSize(context.font) / 10 //This value is defined to get a good looking stroke
4513
+ ) {
4514
+ context.fillText(text, position.x, position.y);
4515
+ if (!underline && !strikethrough) {
4516
+ return;
4517
+ }
4518
+ const measure = context.measureText(text);
4519
+ const textWidth = measure.width;
4520
+ const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
4521
+ const boxHeight = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
4522
+ let { x, y } = position;
4523
+ let strikeY = y, underlineY = y;
4524
+ switch (context.textAlign) {
4525
+ case "center":
4526
+ x -= textWidth / 2;
4527
+ break;
4528
+ case "right":
4529
+ x -= textWidth;
4530
+ break;
4531
+ }
4532
+ switch (context.textBaseline) {
4533
+ case "top":
4534
+ underlineY += boxHeight - 2 * strokeWidth;
4535
+ strikeY += boxHeight - textHeight;
4536
+ break;
4537
+ case "middle":
4538
+ underlineY += boxHeight / 2 - strokeWidth;
4539
+ break;
4540
+ case "alphabetic":
4541
+ underlineY += 2 * strokeWidth;
4542
+ strikeY -= textHeight / 2 - strokeWidth / 2;
4543
+ break;
4544
+ case "bottom":
4545
+ underlineY = y;
4546
+ strikeY -= textHeight / 2 - strokeWidth / 2;
4547
+ break;
4548
+ }
4549
+ if (underline) {
4550
+ context.lineWidth = strokeWidth;
4551
+ context.strokeStyle = context.fillStyle;
4552
+ context.beginPath();
4553
+ context.moveTo(x, underlineY);
4554
+ context.lineTo(x + textWidth, underlineY);
4555
+ context.stroke();
4556
+ }
4557
+ if (strikethrough) {
4558
+ context.lineWidth = strokeWidth;
4559
+ context.strokeStyle = context.fillStyle;
4560
+ context.beginPath();
4561
+ context.moveTo(x, strikeY);
4562
+ context.lineTo(x + textWidth, strikeY);
4563
+ context.stroke();
4564
+ }
4565
+ }
4503
4566
 
4504
4567
  /*
4505
4568
  * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
@@ -4563,6 +4626,29 @@
4563
4626
  };
4564
4627
  }
4565
4628
 
4629
+ function transformZone(zone, executed) {
4630
+ if (executed.type === "REMOVE_COLUMNS_ROWS") {
4631
+ return reduceZoneOnDeletion(zone, executed.dimension === "COL" ? "left" : "top", executed.elements);
4632
+ }
4633
+ if (executed.type === "ADD_COLUMNS_ROWS") {
4634
+ return expandZoneOnInsertion(zone, executed.dimension === "COL" ? "left" : "top", executed.base, executed.position, executed.quantity);
4635
+ }
4636
+ return { ...zone };
4637
+ }
4638
+ function transformRangeData(range, executed) {
4639
+ const deletedSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
4640
+ if ("sheetId" in executed && range._sheetId !== executed.sheetId) {
4641
+ return range;
4642
+ }
4643
+ else {
4644
+ const newZone = transformZone(range._zone, executed);
4645
+ if (newZone && deletedSheet !== range._sheetId) {
4646
+ return { ...range, _zone: newZone };
4647
+ }
4648
+ }
4649
+ return undefined;
4650
+ }
4651
+
4566
4652
  class ChartJsComponent extends owl.Component {
4567
4653
  static template = "o-spreadsheet-ChartJsComponent";
4568
4654
  canvas = owl.useRef("graphContainer");
@@ -4574,7 +4660,7 @@
4574
4660
  return `background-color: ${this.background}`;
4575
4661
  }
4576
4662
  get chartRuntime() {
4577
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
4663
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);
4578
4664
  if (!("chartJsConfig" in runtime)) {
4579
4665
  throw new Error("Unsupported chart runtime");
4580
4666
  }
@@ -4585,15 +4671,7 @@
4585
4671
  const runtime = this.chartRuntime;
4586
4672
  this.createChart(runtime.chartJsConfig);
4587
4673
  });
4588
- let previousRuntime = this.chartRuntime;
4589
- owl.onPatched(() => {
4590
- const chartRuntime = this.chartRuntime;
4591
- if (deepEquals(previousRuntime, chartRuntime)) {
4592
- return;
4593
- }
4594
- this.updateChartJs(chartRuntime);
4595
- previousRuntime = chartRuntime;
4596
- });
4674
+ owl.useEffect(() => this.updateChartJs(this.chartRuntime), () => [this.chartRuntime]);
4597
4675
  }
4598
4676
  createChart(chartData) {
4599
4677
  const canvas = this.canvas.el;
@@ -4626,149 +4704,594 @@
4626
4704
  }
4627
4705
  }
4628
4706
  ChartJsComponent.props = {
4629
- figure: Object,
4707
+ figureId: String,
4630
4708
  };
4631
4709
 
4632
4710
  /**
4633
- * This file is largely inspired by owl 1.
4634
- * `css` tag has been removed from owl 2 without workaround to manage css.
4635
- * So, the solution was to import the behavior of owl 1 directly in our
4636
- * codebase, with one difference: the css is added to the sheet as soon as the
4637
- * css tag is executed. In owl 1, the css was added as soon as a Component was
4638
- * created for the first time.
4711
+ * AbstractChart is the class from which every Chart should inherit.
4712
+ * The role of this class is to maintain the state of each chart.
4639
4713
  */
4640
- const STYLESHEETS = {};
4641
- let nextId = 0;
4714
+ class AbstractChart {
4715
+ sheetId;
4716
+ title;
4717
+ getters;
4718
+ constructor(definition, sheetId, getters) {
4719
+ this.title = definition.title;
4720
+ this.sheetId = sheetId;
4721
+ this.getters = getters;
4722
+ }
4723
+ /**
4724
+ * Validate the chart definition given as arguments. This function will be
4725
+ * called from allowDispatch function
4726
+ */
4727
+ static validateChartDefinition(validator, definition) {
4728
+ throw new Error("This method should be implemented by sub class");
4729
+ }
4730
+ /**
4731
+ * Get a new chart definition transformed with the executed command. This
4732
+ * functions will be called during operational transform process
4733
+ */
4734
+ static transformDefinition(definition, executed) {
4735
+ throw new Error("This method should be implemented by sub class");
4736
+ }
4737
+ /**
4738
+ * Get an empty definition based on the given context
4739
+ */
4740
+ static getDefinitionFromContextCreation(context) {
4741
+ throw new Error("This method should be implemented by sub class");
4742
+ }
4743
+ }
4744
+
4642
4745
  /**
4643
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
4644
- * an inline stylesheet with just the following code:
4645
- * ```js
4646
- * css`.component-a { color: red; }`;
4647
- * ```
4746
+ * This file contains helpers that are common to different charts (mainly
4747
+ * line, bar and pie charts)
4648
4748
  */
4649
- function css(strings, ...args) {
4650
- const name = `__sheet__${nextId++}`;
4651
- const value = String.raw(strings, ...args);
4652
- registerSheet(name, value);
4653
- activateSheet(name);
4654
- return name;
4655
- }
4656
- function processSheet(str) {
4657
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
4658
- const selectorStack = [];
4659
- const parts = [];
4660
- let rules = [];
4661
- function generateSelector(stackIndex, parentSelector) {
4662
- const parts = [];
4663
- for (const selector of selectorStack[stackIndex]) {
4664
- let part = (parentSelector && parentSelector + " " + selector) || selector;
4665
- if (part.includes("&")) {
4666
- part = selector.replace(/&/g, parentSelector || "");
4667
- }
4668
- if (stackIndex < selectorStack.length - 1) {
4669
- part = generateSelector(stackIndex + 1, part);
4749
+ /**
4750
+ * Adapt ranges of a chart which support DataSet (dataSets and LabelRange).
4751
+ */
4752
+ function updateChartRangesWithDataSets(getters, applyChange, chartDataSets, chartLabelRange) {
4753
+ let isStale = false;
4754
+ const dataSetsWithUndefined = [];
4755
+ for (let index in chartDataSets) {
4756
+ let ds = chartDataSets[index];
4757
+ if (ds.labelCell) {
4758
+ const labelCell = adaptChartRange(ds.labelCell, applyChange);
4759
+ if (ds.labelCell !== labelCell) {
4760
+ isStale = true;
4761
+ ds = {
4762
+ ...ds,
4763
+ labelCell: labelCell,
4764
+ };
4670
4765
  }
4671
- parts.push(part);
4672
4766
  }
4673
- return parts.join(", ");
4674
- }
4675
- function generateRules() {
4676
- if (rules.length) {
4677
- parts.push(generateSelector(0) + " {");
4678
- parts.push(...rules);
4679
- parts.push("}");
4680
- rules = [];
4767
+ const dataRange = adaptChartRange(ds.dataRange, applyChange);
4768
+ if (dataRange === undefined ||
4769
+ getters.getRangeString(dataRange, dataRange.sheetId) === INCORRECT_RANGE_STRING) {
4770
+ isStale = true;
4771
+ ds = undefined;
4772
+ }
4773
+ else if (dataRange !== ds.dataRange) {
4774
+ isStale = true;
4775
+ ds = {
4776
+ ...ds,
4777
+ dataRange,
4778
+ };
4681
4779
  }
4780
+ dataSetsWithUndefined[index] = ds;
4682
4781
  }
4683
- while (tokens.length) {
4684
- let token = tokens.shift();
4685
- if (token === "}") {
4686
- generateRules();
4687
- selectorStack.pop();
4782
+ let labelRange = chartLabelRange;
4783
+ const range = adaptChartRange(labelRange, applyChange);
4784
+ if (range !== labelRange) {
4785
+ isStale = true;
4786
+ labelRange = range;
4787
+ }
4788
+ const dataSets = dataSetsWithUndefined.filter(isDefined$1);
4789
+ return {
4790
+ isStale,
4791
+ dataSets,
4792
+ labelRange,
4793
+ };
4794
+ }
4795
+ /**
4796
+ * Copy the dataSets given. All the ranges which are on sheetIdFrom will target
4797
+ * sheetIdTo.
4798
+ */
4799
+ function copyDataSetsWithNewSheetId(sheetIdFrom, sheetIdTo, dataSets) {
4800
+ return dataSets.map((ds) => {
4801
+ return {
4802
+ dataRange: copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.dataRange),
4803
+ labelCell: ds.labelCell
4804
+ ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.labelCell)
4805
+ : undefined,
4806
+ };
4807
+ });
4808
+ }
4809
+ /**
4810
+ * Copy a range. If the range is on the sheetIdFrom, the range will target
4811
+ * sheetIdTo.
4812
+ */
4813
+ function copyLabelRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {
4814
+ return range ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) : undefined;
4815
+ }
4816
+ /**
4817
+ * Adapt a single range of a chart
4818
+ */
4819
+ function adaptChartRange(range, applyChange) {
4820
+ if (!range) {
4821
+ return undefined;
4822
+ }
4823
+ const change = applyChange(range);
4824
+ switch (change.changeType) {
4825
+ case "NONE":
4826
+ return range;
4827
+ case "REMOVE":
4828
+ return undefined;
4829
+ default:
4830
+ return change.range;
4831
+ }
4832
+ }
4833
+ /**
4834
+ * Create the dataSet objects from xcs
4835
+ */
4836
+ function createDataSets(getters, dataSetsString, sheetId, dataSetsHaveTitle) {
4837
+ const dataSets = [];
4838
+ for (const sheetXC of dataSetsString) {
4839
+ const dataRange = getters.getRangeFromSheetXC(sheetId, sheetXC);
4840
+ const { unboundedZone: zone, sheetId: dataSetSheetId, invalidSheetName, invalidXc } = dataRange;
4841
+ if (invalidSheetName || invalidXc) {
4842
+ continue;
4688
4843
  }
4689
- else {
4690
- if (tokens[0] === "{") {
4691
- generateRules();
4692
- selectorStack.push(token.split(/\s*,\s*/));
4693
- tokens.shift();
4844
+ // It's a rectangle. We treat all columns (arbitrary) as different data series.
4845
+ if (zone.left !== zone.right && zone.top !== zone.bottom) {
4846
+ if (zone.right === undefined) {
4847
+ // Should never happens because of the allowDispatch of charts, but just making sure
4848
+ continue;
4694
4849
  }
4695
- if (tokens[0] === ";") {
4696
- rules.push(" " + token + ";");
4850
+ for (let column = zone.left; column <= zone.right; column++) {
4851
+ const columnZone = {
4852
+ ...zone,
4853
+ left: column,
4854
+ right: column,
4855
+ };
4856
+ dataSets.push(createDataSet(getters, dataSetSheetId, columnZone, dataSetsHaveTitle
4857
+ ? {
4858
+ top: columnZone.top,
4859
+ bottom: columnZone.top,
4860
+ left: columnZone.left,
4861
+ right: columnZone.left,
4862
+ }
4863
+ : undefined));
4697
4864
  }
4698
4865
  }
4866
+ else if (zone.left === zone.right && zone.top === zone.bottom) {
4867
+ // A single cell. If it's only the title, the dataset is not added.
4868
+ if (!dataSetsHaveTitle) {
4869
+ dataSets.push(createDataSet(getters, dataSetSheetId, zone, undefined));
4870
+ }
4871
+ }
4872
+ else {
4873
+ /* 1 row or 1 column */
4874
+ dataSets.push(createDataSet(getters, dataSetSheetId, zone, dataSetsHaveTitle
4875
+ ? {
4876
+ top: zone.top,
4877
+ bottom: zone.top,
4878
+ left: zone.left,
4879
+ right: zone.left,
4880
+ }
4881
+ : undefined));
4882
+ }
4699
4883
  }
4700
- return parts.join("\n");
4884
+ return dataSets;
4701
4885
  }
4702
- function registerSheet(id, css) {
4703
- const sheet = document.createElement("style");
4704
- sheet.textContent = processSheet(css);
4705
- STYLESHEETS[id] = sheet;
4886
+ function createDataSet(getters, sheetId, fullZone, titleZone) {
4887
+ if (fullZone.left !== fullZone.right && fullZone.top !== fullZone.bottom) {
4888
+ throw new Error(`Zone should be a single column or row: ${zoneToXc(fullZone)}`);
4889
+ }
4890
+ if (titleZone) {
4891
+ const dataXC = zoneToXc(fullZone);
4892
+ const labelCellXC = zoneToXc(titleZone);
4893
+ return {
4894
+ labelCell: getters.getRangeFromSheetXC(sheetId, labelCellXC),
4895
+ dataRange: getters.getRangeFromSheetXC(sheetId, dataXC),
4896
+ };
4897
+ }
4898
+ else {
4899
+ return {
4900
+ labelCell: undefined,
4901
+ dataRange: getters.getRangeFromSheetXC(sheetId, zoneToXc(fullZone)),
4902
+ };
4903
+ }
4706
4904
  }
4707
- function activateSheet(id) {
4708
- const sheet = STYLESHEETS[id];
4709
- sheet.setAttribute("component", id);
4710
- document.head.appendChild(sheet);
4905
+ /**
4906
+ * Transform a dataSet to a ExcelDataSet
4907
+ */
4908
+ function toExcelDataset(getters, ds) {
4909
+ const labelZone = ds.labelCell?.zone;
4910
+ let dataZone = ds.dataRange.zone;
4911
+ if (labelZone) {
4912
+ const { numberOfRows, numberOfCols } = zoneToDimension(dataZone);
4913
+ if (numberOfRows === 1) {
4914
+ dataZone = { ...dataZone, left: dataZone.left + 1 };
4915
+ }
4916
+ else if (numberOfCols === 1) {
4917
+ dataZone = { ...dataZone, top: dataZone.top + 1 };
4918
+ }
4919
+ }
4920
+ const dataRange = ds.dataRange.clone({ zone: dataZone });
4921
+ return {
4922
+ label: ds.labelCell ? getters.getRangeString(ds.labelCell, "forceSheetReference") : undefined,
4923
+ range: getters.getRangeString(dataRange, "forceSheetReference"),
4924
+ };
4711
4925
  }
4712
- function getTextDecoration({ strikethrough, underline, }) {
4713
- if (!strikethrough && !underline) {
4714
- return "none";
4926
+ function toExcelLabelRange(getters, labelRange, shouldRemoveFirstLabel) {
4927
+ if (!labelRange)
4928
+ return undefined;
4929
+ let zone = {
4930
+ ...labelRange.zone,
4931
+ };
4932
+ if (shouldRemoveFirstLabel && labelRange.zone.bottom > labelRange.zone.top) {
4933
+ zone.top = zone.top + 1;
4715
4934
  }
4716
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
4935
+ const range = labelRange.clone({ zone });
4936
+ return getters.getRangeString(range, "forceSheetReference");
4717
4937
  }
4718
4938
  /**
4719
- * Convert the cell style to CSS properties.
4939
+ * Transform a chart definition which supports dataSets (dataSets and LabelRange)
4940
+ * with an executed command
4720
4941
  */
4721
- function cellStyleToCss(style) {
4722
- const attributes = cellTextStyleToCss(style);
4723
- if (!style)
4724
- return attributes;
4725
- if (style.fillColor) {
4726
- attributes["background"] = style.fillColor;
4942
+ function transformChartDefinitionWithDataSetsWithZone(definition, executed) {
4943
+ let labelRange;
4944
+ if (definition.labelRange) {
4945
+ const labelZone = transformZone(toUnboundedZone(definition.labelRange), executed);
4946
+ labelRange = labelZone ? zoneToXc(labelZone) : undefined;
4947
+ }
4948
+ const dataSets = definition.dataSets
4949
+ .map(toUnboundedZone)
4950
+ .map((zone) => transformZone(zone, executed))
4951
+ .filter(isDefined$1)
4952
+ .map(zoneToXc);
4953
+ return {
4954
+ ...definition,
4955
+ labelRange,
4956
+ dataSets,
4957
+ };
4958
+ }
4959
+ const GraphColors = [
4960
+ // the same colors as those used in odoo reporting
4961
+ "rgb(31,119,180)",
4962
+ "rgb(255,127,14)",
4963
+ "rgb(174,199,232)",
4964
+ "rgb(255,187,120)",
4965
+ "rgb(44,160,44)",
4966
+ "rgb(152,223,138)",
4967
+ "rgb(214,39,40)",
4968
+ "rgb(255,152,150)",
4969
+ "rgb(148,103,189)",
4970
+ "rgb(197,176,213)",
4971
+ "rgb(140,86,75)",
4972
+ "rgb(196,156,148)",
4973
+ "rgb(227,119,194)",
4974
+ "rgb(247,182,210)",
4975
+ "rgb(127,127,127)",
4976
+ "rgb(199,199,199)",
4977
+ "rgb(188,189,34)",
4978
+ "rgb(219,219,141)",
4979
+ "rgb(23,190,207)",
4980
+ "rgb(158,218,229)",
4981
+ ];
4982
+ class ChartColors {
4983
+ graphColorIndex = 0;
4984
+ next() {
4985
+ return GraphColors[this.graphColorIndex++ % GraphColors.length];
4727
4986
  }
4728
- return attributes;
4729
4987
  }
4730
4988
  /**
4731
- * Convert the cell text style to CSS properties.
4989
+ * Choose a font color based on a background color.
4990
+ * The font is white with a dark background.
4732
4991
  */
4733
- function cellTextStyleToCss(style) {
4734
- const attributes = {};
4735
- if (!style)
4736
- return attributes;
4737
- if (style.bold) {
4738
- attributes["font-weight"] = "bold";
4992
+ function chartFontColor(backgroundColor) {
4993
+ if (!backgroundColor) {
4994
+ return "#000000";
4739
4995
  }
4740
- if (style.italic) {
4741
- attributes["font-style"] = "italic";
4996
+ return relativeLuminance(backgroundColor) < 0.3 ? "#FFFFFF" : "#000000";
4997
+ }
4998
+ function checkDataset(definition) {
4999
+ if (definition.dataSets) {
5000
+ const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range)) !== undefined;
5001
+ if (invalidRanges) {
5002
+ return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
5003
+ }
5004
+ const zones = definition.dataSets.map(toUnboundedZone);
5005
+ if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) {
5006
+ return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
5007
+ }
4742
5008
  }
4743
- if (style.strikethrough || style.underline) {
4744
- let decoration = style.strikethrough ? "line-through" : "";
4745
- decoration = style.underline ? decoration + " underline" : decoration;
4746
- attributes["text-decoration"] = decoration;
5009
+ return "Success" /* CommandResult.Success */;
5010
+ }
5011
+ function checkLabelRange(definition) {
5012
+ if (definition.labelRange) {
5013
+ const invalidLabels = !rangeReference.test(definition.labelRange || "");
5014
+ if (invalidLabels) {
5015
+ return "InvalidLabelRange" /* CommandResult.InvalidLabelRange */;
5016
+ }
4747
5017
  }
4748
- if (style.textColor) {
4749
- attributes["color"] = style.textColor;
5018
+ return "Success" /* CommandResult.Success */;
5019
+ }
5020
+ function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
5021
+ if (!dataSetsHaveTitle)
5022
+ return false;
5023
+ if (!labelRange)
5024
+ return false;
5025
+ if (!dataset)
5026
+ return true;
5027
+ const datasetLength = getZoneArea(dataset.dataRange.zone);
5028
+ const labelLength = getZoneArea(labelRange.zone);
5029
+ if (labelLength < datasetLength) {
5030
+ return false;
4750
5031
  }
4751
- return attributes;
5032
+ return true;
4752
5033
  }
4753
- /**
4754
- * Transform CSS properties into a CSS string.
4755
- */
4756
- function cssPropertiesToCss(attributes) {
4757
- const str = Object.entries(attributes)
4758
- .filter(([attName, attValue]) => attValue !== undefined)
4759
- .map(([attName, attValue]) => `${attName}:${attValue};`)
4760
- .join(" ");
4761
- return str;
5034
+ // ---------------------------------------------------------------------------
5035
+ // Scorecard
5036
+ // ---------------------------------------------------------------------------
5037
+ function getBaselineText(baseline, keyValue, baselineMode, locale) {
5038
+ if (!baseline) {
5039
+ return "";
5040
+ }
5041
+ else if (baselineMode === "text" ||
5042
+ keyValue?.type !== CellValueType.number ||
5043
+ baseline.type !== CellValueType.number) {
5044
+ return baseline.formattedValue;
5045
+ }
5046
+ else {
5047
+ let diff = keyValue.value - baseline.value;
5048
+ if (baselineMode === "percentage" && diff !== 0) {
5049
+ diff = (diff / baseline.value) * 100;
5050
+ }
5051
+ if (baselineMode !== "percentage" && baseline.format) {
5052
+ return formatValue(diff, { format: baseline.format, locale });
5053
+ }
5054
+ const baselineStr = Math.abs(parseFloat(diff.toFixed(2))).toLocaleString();
5055
+ return baselineMode === "percentage" ? baselineStr + "%" : baselineStr;
5056
+ }
4762
5057
  }
4763
- function getElementMargins(el) {
4764
- const style = window.getComputedStyle(el);
4765
- const margins = {
4766
- top: parseInt(style.marginTop, 10) || 0,
4767
- bottom: parseInt(style.marginBottom, 10) || 0,
4768
- left: parseInt(style.marginLeft, 10) || 0,
4769
- right: parseInt(style.marginRight, 10) || 0,
5058
+ function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
5059
+ if (baselineMode === "text" ||
5060
+ baseline?.type !== CellValueType.number ||
5061
+ keyValue?.type !== CellValueType.number) {
5062
+ return undefined;
5063
+ }
5064
+ const diff = keyValue.value - baseline.value;
5065
+ if (diff > 0) {
5066
+ return colorUp;
5067
+ }
5068
+ else if (diff < 0) {
5069
+ return colorDown;
5070
+ }
5071
+ return undefined;
5072
+ }
5073
+ function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
5074
+ if (baselineMode === "text" ||
5075
+ baseline?.type !== CellValueType.number ||
5076
+ keyValue?.type !== CellValueType.number) {
5077
+ return "neutral";
5078
+ }
5079
+ const diff = keyValue.value - baseline.value;
5080
+ if (diff > 0) {
5081
+ return "up";
5082
+ }
5083
+ else if (diff < 0) {
5084
+ return "down";
5085
+ }
5086
+ return "neutral";
5087
+ }
5088
+ function getChartPositionAtCenterOfViewport(getters, chartSize) {
5089
+ const { x, y } = getters.getMainViewportCoordinates();
5090
+ const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
5091
+ const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
5092
+ const position = {
5093
+ x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
5094
+ y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
5095
+ }; // Position at the center of the scrollable viewport
5096
+ return position;
5097
+ }
5098
+
5099
+ function checkKeyValue(definition) {
5100
+ return definition.keyValue && !rangeReference.test(definition.keyValue)
5101
+ ? "InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */
5102
+ : "Success" /* CommandResult.Success */;
5103
+ }
5104
+ function checkBaseline(definition) {
5105
+ return definition.baseline && !rangeReference.test(definition.baseline)
5106
+ ? "InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */
5107
+ : "Success" /* CommandResult.Success */;
5108
+ }
5109
+ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
5110
+ keyValue;
5111
+ baseline;
5112
+ baselineMode;
5113
+ baselineDescr;
5114
+ background;
5115
+ baselineColorUp;
5116
+ baselineColorDown;
5117
+ fontColor;
5118
+ type = "scorecard";
5119
+ constructor(definition, sheetId, getters) {
5120
+ super(definition, sheetId, getters);
5121
+ this.keyValue = createRange(getters, sheetId, definition.keyValue);
5122
+ this.baseline = createRange(getters, sheetId, definition.baseline);
5123
+ this.baselineMode = definition.baselineMode;
5124
+ this.baselineDescr = definition.baselineDescr;
5125
+ this.background = definition.background;
5126
+ this.baselineColorUp = definition.baselineColorUp;
5127
+ this.baselineColorDown = definition.baselineColorDown;
5128
+ }
5129
+ static validateChartDefinition(validator, definition) {
5130
+ return validator.checkValidations(definition, checkKeyValue, checkBaseline);
5131
+ }
5132
+ static getDefinitionFromContextCreation(context) {
5133
+ return {
5134
+ background: context.background,
5135
+ type: "scorecard",
5136
+ keyValue: context.range ? context.range[0] : undefined,
5137
+ title: context.title || "",
5138
+ baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
5139
+ baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
5140
+ baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
5141
+ baseline: context.auxiliaryRange || "",
5142
+ };
5143
+ }
5144
+ static transformDefinition(definition, executed) {
5145
+ let baselineZone;
5146
+ let keyValueZone;
5147
+ if (definition.baseline) {
5148
+ baselineZone = transformZone(toUnboundedZone(definition.baseline), executed);
5149
+ }
5150
+ if (definition.keyValue) {
5151
+ keyValueZone = transformZone(toUnboundedZone(definition.keyValue), executed);
5152
+ }
5153
+ return {
5154
+ ...definition,
5155
+ baseline: baselineZone ? zoneToXc(baselineZone) : undefined,
5156
+ keyValue: keyValueZone ? zoneToXc(keyValueZone) : undefined,
5157
+ };
5158
+ }
5159
+ copyForSheetId(sheetId) {
5160
+ const baseline = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.baseline);
5161
+ const keyValue = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.keyValue);
5162
+ const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue, sheetId);
5163
+ return new ScorecardChart(definition, sheetId, this.getters);
5164
+ }
5165
+ copyInSheetId(sheetId) {
5166
+ const definition = this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue, sheetId);
5167
+ return new ScorecardChart(definition, sheetId, this.getters);
5168
+ }
5169
+ getDefinition() {
5170
+ return this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue);
5171
+ }
5172
+ getContextCreation() {
5173
+ return {
5174
+ background: this.background,
5175
+ title: this.title,
5176
+ range: this.keyValue ? [this.getters.getRangeString(this.keyValue, this.sheetId)] : undefined,
5177
+ auxiliaryRange: this.baseline
5178
+ ? this.getters.getRangeString(this.baseline, this.sheetId)
5179
+ : undefined,
5180
+ };
5181
+ }
5182
+ getDefinitionWithSpecificRanges(baseline, keyValue, targetSheetId) {
5183
+ return {
5184
+ baselineColorDown: this.baselineColorDown,
5185
+ baselineColorUp: this.baselineColorUp,
5186
+ baselineMode: this.baselineMode,
5187
+ title: this.title,
5188
+ type: "scorecard",
5189
+ background: this.background,
5190
+ baseline: baseline
5191
+ ? this.getters.getRangeString(baseline, targetSheetId || this.sheetId)
5192
+ : undefined,
5193
+ baselineDescr: this.baselineDescr,
5194
+ keyValue: keyValue
5195
+ ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
5196
+ : undefined,
5197
+ };
5198
+ }
5199
+ getDefinitionForExcel() {
5200
+ // This kind of graph is not exportable in Excel
5201
+ return undefined;
5202
+ }
5203
+ updateRanges(applyChange) {
5204
+ const baseline = adaptChartRange(this.baseline, applyChange);
5205
+ const keyValue = adaptChartRange(this.keyValue, applyChange);
5206
+ if (this.baseline === baseline && this.keyValue === keyValue) {
5207
+ return this;
5208
+ }
5209
+ const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue);
5210
+ return new ScorecardChart(definition, this.sheetId, this.getters);
5211
+ }
5212
+ };
5213
+ function drawScoreChart(structure, canvas) {
5214
+ const ctx = canvas.getContext("2d");
5215
+ canvas.width = structure.canvas.width;
5216
+ canvas.height = structure.canvas.height;
5217
+ ctx.fillStyle = structure.canvas.backgroundColor;
5218
+ ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);
5219
+ if (structure.title) {
5220
+ ctx.font = structure.title.style.font;
5221
+ ctx.fillStyle = structure.title.style.color;
5222
+ ctx.fillText(structure.title.text, structure.title.position.x, structure.title.position.y);
5223
+ }
5224
+ if (structure.baseline) {
5225
+ ctx.font = structure.baseline.style.font;
5226
+ ctx.fillStyle = structure.baseline.style.color;
5227
+ drawDecoratedText(ctx, structure.baseline.text, structure.baseline.position, structure.baseline.style.underline, structure.baseline.style.strikethrough);
5228
+ }
5229
+ if (structure.baselineArrow) {
5230
+ ctx.font = structure.baselineArrow.style.font;
5231
+ ctx.fillStyle = structure.baselineArrow.style.color;
5232
+ ctx.fillText(structure.baselineArrow.text, structure.baselineArrow.position.x, structure.baselineArrow.position.y);
5233
+ }
5234
+ if (structure.baselineDescr) {
5235
+ ctx.font = structure.baselineDescr.style.font;
5236
+ ctx.fillStyle = structure.baselineDescr.style.color;
5237
+ ctx.fillText(structure.baselineDescr.text, structure.baselineDescr.position.x, structure.baselineDescr.position.y);
5238
+ }
5239
+ if (structure.key) {
5240
+ ctx.font = structure.key.style.font;
5241
+ ctx.fillStyle = structure.key.style.color;
5242
+ drawDecoratedText(ctx, structure.key.text, structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
5243
+ }
5244
+ }
5245
+ function createScorecardChartRuntime(chart, getters) {
5246
+ let keyValue = "";
5247
+ let formattedKeyValue = "";
5248
+ let keyValueCell;
5249
+ if (chart.keyValue) {
5250
+ const keyValuePosition = {
5251
+ sheetId: chart.keyValue.sheetId,
5252
+ col: chart.keyValue.zone.left,
5253
+ row: chart.keyValue.zone.top,
5254
+ };
5255
+ keyValueCell = getters.getEvaluatedCell(keyValuePosition);
5256
+ keyValue = String(keyValueCell.value);
5257
+ formattedKeyValue = keyValueCell.formattedValue;
5258
+ }
5259
+ let baselineCell;
5260
+ const baseline = chart.baseline;
5261
+ if (baseline) {
5262
+ const baselinePosition = {
5263
+ sheetId: chart.baseline.sheetId,
5264
+ col: chart.baseline.zone.left,
5265
+ row: chart.baseline.zone.top,
5266
+ };
5267
+ baselineCell = getters.getEvaluatedCell(baselinePosition);
5268
+ }
5269
+ const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue);
5270
+ const locale = getters.getLocale();
5271
+ return {
5272
+ title: _t(chart.title),
5273
+ keyValue: formattedKeyValue || keyValue,
5274
+ baselineDisplay: getBaselineText(baselineCell, keyValueCell, chart.baselineMode, locale),
5275
+ baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
5276
+ baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
5277
+ baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
5278
+ fontColor: chartFontColor(background),
5279
+ background,
5280
+ baselineStyle: chart.baselineMode !== "percentage" && baseline
5281
+ ? getters.getCellStyle({
5282
+ sheetId: baseline.sheetId,
5283
+ col: baseline.zone.left,
5284
+ row: baseline.zone.top,
5285
+ })
5286
+ : undefined,
5287
+ keyValueStyle: chart.keyValue
5288
+ ? getters.getCellStyle({
5289
+ sheetId: chart.keyValue.sheetId,
5290
+ col: chart.keyValue.zone.left,
5291
+ row: chart.keyValue.zone.top,
5292
+ })
5293
+ : undefined,
4770
5294
  };
4771
- return margins;
4772
5295
  }
4773
5296
 
4774
5297
  /* Sizes of boxes containing the texts, in percentage of the Chart size */
@@ -4784,64 +5307,92 @@
4784
5307
  * Having a line heigh =1em (=font size) don't work, the font will overflow.
4785
5308
  */
4786
5309
  const LINE_HEIGHT = 1.2;
4787
- css /* scss */ `
4788
- div.o-scorecard {
4789
- font-family: ${DEFAULT_FONT};
4790
- user-select: none;
4791
- background-color: white;
4792
- display: flex;
4793
- flex-direction: column;
4794
- box-sizing: border-box;
4795
-
4796
- .o-scorecard-content {
4797
- display: flex;
4798
- flex-direction: column;
4799
- height: 100%;
4800
- justify-content: center;
4801
- text-align: center;
4802
- }
4803
-
4804
- .o-title-text {
4805
- text-align: left;
4806
- height: ${LINE_HEIGHT + "em"};
4807
- line-height: ${LINE_HEIGHT + "em"};
4808
- overflow: hidden;
4809
- white-space: nowrap;
4810
- }
4811
-
4812
- .o-key-text {
4813
- line-height: ${LINE_HEIGHT + "em"};
4814
- height: ${LINE_HEIGHT + "em"};
4815
- overflow: hidden;
4816
- white-space: nowrap;
4817
- }
4818
-
4819
- .o-cf-icon {
4820
- display: inline-block;
4821
- width: 0.65em;
4822
- height: 1em;
4823
- line-height: 1em;
4824
- padding-bottom: 0.07em;
4825
- padding-right: 3px;
4826
- }
4827
-
4828
- .o-baseline-text {
4829
- line-height: ${LINE_HEIGHT + "em"};
4830
- height: ${LINE_HEIGHT + "em"};
4831
- overflow: hidden;
4832
- white-space: nowrap;
4833
-
4834
- .o-baseline-text-description {
4835
- white-space: pre;
4836
- }
4837
- }
5310
+ function formatBaselineDescr(baselineDescr, baseline) {
5311
+ const _baselineDescr = baselineDescr || "";
5312
+ return baseline && _baselineDescr ? " " + _baselineDescr : _baselineDescr;
4838
5313
  }
4839
- `;
4840
- let ScorecardChart$1 = class ScorecardChart extends owl.Component {
4841
- static template = "o-spreadsheet-ScorecardChart";
4842
- ctx = document.createElement("canvas").getContext("2d");
4843
- get runtime() {
4844
- return this.env.model.getters.getChartRuntime(this.props.figure.id);
5314
+ function getDefaultContextFont(fontSize, bold = false, italic = false) {
5315
+ const italicStr = italic ? "italic" : "";
5316
+ const weight = bold ? "bold" : "";
5317
+ return `${italicStr} ${weight} ${fontSize}px ${DEFAULT_FONT}`;
5318
+ }
5319
+ function getScorecardConfiguration({ width, height }, runtime) {
5320
+ const designer = new ScorecardChartConfigBuilder({ width, height }, runtime);
5321
+ return designer.computeDesign();
5322
+ }
5323
+ class ScorecardChartConfigBuilder {
5324
+ runtime;
5325
+ context;
5326
+ width;
5327
+ height;
5328
+ constructor({ width, height }, runtime) {
5329
+ this.runtime = runtime;
5330
+ const canvas = document.createElement("canvas");
5331
+ this.width = canvas.width = width;
5332
+ this.height = canvas.height = height;
5333
+ this.context = canvas.getContext("2d");
5334
+ }
5335
+ computeDesign() {
5336
+ const structure = {
5337
+ canvas: {
5338
+ width: this.width,
5339
+ height: this.height,
5340
+ backgroundColor: this.backgroundColor,
5341
+ },
5342
+ };
5343
+ const style = this.getTextStyles();
5344
+ const { height: titleHeight } = this.getTextDimensions(this.title, style.title.font);
5345
+ if (this.title) {
5346
+ structure.title = {
5347
+ text: this.title,
5348
+ style: style.title,
5349
+ position: {
5350
+ x: this.chartPadding,
5351
+ y: this.chartPadding + titleHeight,
5352
+ },
5353
+ };
5354
+ }
5355
+ const { height: baselineArrowHeight, width: baselineArrowWidth } = this.getTextDimensions(this.baselineArrow, style.baselineArrow.font);
5356
+ const { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
5357
+ const { width: baselineDescrWidth } = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font);
5358
+ structure.baseline = {
5359
+ text: this.baseline,
5360
+ style: style.baselineValue,
5361
+ position: {
5362
+ x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowWidth) / 2,
5363
+ y: this.height - 2 * this.chartPadding,
5364
+ },
5365
+ };
5366
+ structure.baselineArrow = {
5367
+ text: this.baselineArrow,
5368
+ style: style.baselineArrow,
5369
+ position: {
5370
+ x: structure.baseline.position.x - baselineArrowWidth,
5371
+ y: structure.baseline.position.y - (baselineHeight - baselineArrowHeight) / 2,
5372
+ },
5373
+ };
5374
+ if (this.baselineDescr) {
5375
+ structure.baselineDescr = {
5376
+ text: this.baselineDescr,
5377
+ style: style.baselineDescr,
5378
+ position: {
5379
+ x: structure.baseline.position.x + baselineWidth,
5380
+ y: structure.baseline.position.y,
5381
+ },
5382
+ };
5383
+ }
5384
+ const { height: keyHeight, width: keyWidth } = this.getTextDimensions(this.keyValue, style.keyValue.font);
5385
+ if (this.keyValue) {
5386
+ structure.key = {
5387
+ text: this.keyValue,
5388
+ style: style.keyValue,
5389
+ position: {
5390
+ x: (this.width - keyWidth) / 2,
5391
+ y: (this.height - baselineHeight + titleHeight + keyHeight) / 2 - this.chartPadding,
5392
+ },
5393
+ };
5394
+ }
5395
+ return structure;
4845
5396
  }
4846
5397
  get title() {
4847
5398
  return this.runtime.title;
@@ -4853,87 +5404,77 @@
4853
5404
  return this.runtime.baselineDisplay;
4854
5405
  }
4855
5406
  get baselineDescr() {
4856
- const baselineDescr = this.runtime.baselineDescr || "";
4857
- return this.baseline && baselineDescr ? " " + baselineDescr : baselineDescr;
5407
+ return formatBaselineDescr(this.runtime.baselineDescr, this.baseline);
4858
5408
  }
4859
- get baselineArrowDirection() {
4860
- return this.runtime.baselineArrow;
5409
+ get baselineArrow() {
5410
+ switch (this.runtime.baselineArrow) {
5411
+ case "up":
5412
+ return "\u{1F871}";
5413
+ case "down":
5414
+ return "\u{1F873}";
5415
+ }
5416
+ return "";
4861
5417
  }
4862
5418
  get backgroundColor() {
4863
5419
  return this.runtime.background;
4864
5420
  }
4865
- get primaryFontColor() {
4866
- return this.runtime.fontColor;
4867
- }
4868
5421
  get secondaryFontColor() {
4869
5422
  return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
4870
5423
  }
4871
- get figure() {
4872
- return this.props.figure;
4873
- }
4874
- get chartStyle() {
4875
- return cssPropertiesToCss({
4876
- padding: `${this.chartPadding}px`,
4877
- background: this.backgroundColor,
4878
- });
4879
- }
4880
- get chartContentStyle() {
4881
- return cssPropertiesToCss({ height: `${this.getDrawableHeight()}px` });
4882
- }
4883
5424
  get chartPadding() {
4884
- return this.props.figure.width * CHART_PADDING_RATIO;
5425
+ return this.width * CHART_PADDING_RATIO;
4885
5426
  }
4886
- translate(term) {
4887
- return _t(term);
5427
+ getTextDimensions(text, font) {
5428
+ this.context.font = font;
5429
+ const measure = this.context.measureText(text);
5430
+ return {
5431
+ width: measure.width,
5432
+ height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,
5433
+ };
4888
5434
  }
4889
5435
  getTextStyles() {
4890
5436
  // If the widest text overflows horizontally, scale it down, and apply the same scaling factors to all the other fonts.
4891
- const maxLineWidth = this.props.figure.width * (1 - 2 * CHART_PADDING_RATIO);
5437
+ const maxLineWidth = this.width * (1 - 2 * CHART_PADDING_RATIO);
4892
5438
  const widestElement = this.getWidestElement();
4893
5439
  const baseFontSize = widestElement.getElementMaxFontSize(this.getDrawableHeight(), this);
4894
- const fontSizeMatchingWidth = getFontSizeMatchingWidth(maxLineWidth, baseFontSize, (fontSize) => widestElement.getElementWidth(fontSize, this.ctx, this));
5440
+ const fontSizeMatchingWidth = getFontSizeMatchingWidth(maxLineWidth, baseFontSize, (fontSize) => widestElement.getElementWidth(fontSize, this.context, this));
4895
5441
  let scalingFactor = fontSizeMatchingWidth / baseFontSize;
4896
5442
  // Fonts sizes in px
4897
5443
  const keyFontSize = new KeyValueElement(this.runtime.keyValueStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
4898
5444
  const baselineFontSize = new BaselineElement(this.runtime.baselineStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
4899
5445
  return {
4900
- titleStyle: this.getTextStyle({
4901
- fontSize: TITLE_FONT_SIZE,
5446
+ title: {
5447
+ font: getDefaultContextFont(TITLE_FONT_SIZE),
4902
5448
  color: this.secondaryFontColor,
4903
- }),
4904
- keyStyle: this.getTextStyle({
4905
- fontSize: keyFontSize,
4906
- cellStyle: this.runtime.keyValueStyle,
4907
- color: this.primaryFontColor,
4908
- }),
4909
- baselineStyle: this.getTextStyle({
4910
- fontSize: baselineFontSize,
4911
- }),
4912
- baselineValueStyle: this.getTextStyle({
4913
- fontSize: baselineFontSize,
4914
- cellStyle: this.runtime.baselineStyle,
4915
- color: this.runtime.baselineColor || this.secondaryFontColor,
4916
- }),
4917
- baselineDescrStyle: this.getTextStyle({
4918
- fontSize: baselineFontSize * BASELINE_DESCR_FONT_RATIO,
5449
+ },
5450
+ keyValue: {
5451
+ color: this.runtime.keyValueStyle?.textColor || this.runtime.fontColor,
5452
+ font: getDefaultContextFont(keyFontSize, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
5453
+ strikethrough: this.runtime.keyValueStyle?.strikethrough,
5454
+ underline: this.runtime.keyValueStyle?.underline,
5455
+ },
5456
+ baselineValue: {
5457
+ font: getDefaultContextFont(baselineFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
5458
+ strikethrough: this.runtime.baselineStyle?.strikethrough,
5459
+ underline: this.runtime.baselineStyle?.underline,
5460
+ color: this.runtime.baselineStyle?.textColor ||
5461
+ this.runtime.baselineColor ||
5462
+ this.secondaryFontColor,
5463
+ },
5464
+ baselineDescr: {
5465
+ font: getDefaultContextFont(baselineFontSize * BASELINE_DESCR_FONT_RATIO),
4919
5466
  color: this.secondaryFontColor,
4920
- }),
5467
+ },
5468
+ baselineArrow: {
5469
+ font: getDefaultContextFont(0.8 * baselineFontSize),
5470
+ color: this.runtime.baselineColor || this.secondaryFontColor,
5471
+ },
4921
5472
  };
4922
5473
  }
4923
- /** Return an CSS style string corresponding to the given arguments */
4924
- getTextStyle(args) {
4925
- const cssAttributes = cellTextStyleToCss(args.cellStyle);
4926
- cssAttributes["font-size"] = `${args.fontSize}px`;
4927
- cssAttributes["display"] = "inline-block";
4928
- if (!cssAttributes["color"] && args.color) {
4929
- cssAttributes["color"] = args.color;
4930
- }
4931
- return cssPropertiesToCss(cssAttributes);
4932
- }
4933
5474
  /** Get the height of the chart minus all the vertical paddings */
4934
5475
  getDrawableHeight() {
4935
5476
  const verticalPadding = 2 * this.chartPadding;
4936
- let availableHeight = this.props.figure.height - verticalPadding;
5477
+ let availableHeight = this.height - verticalPadding;
4937
5478
  availableHeight -= this.title ? TITLE_FONT_SIZE * LINE_HEIGHT : 0;
4938
5479
  return availableHeight;
4939
5480
  }
@@ -4941,39 +5482,39 @@
4941
5482
  getWidestElement() {
4942
5483
  const baseline = new BaselineElement(this.runtime.baselineStyle);
4943
5484
  const keyValue = new KeyValueElement(this.runtime.keyValueStyle);
4944
- return baseline.getElementWidth(BASELINE_BOX_HEIGHT_RATIO, this.ctx, this) >
4945
- keyValue.getElementWidth(KEY_BOX_HEIGHT_RATIO, this.ctx, this)
5485
+ return baseline.getElementWidth(BASELINE_BOX_HEIGHT_RATIO, this.context, this) >
5486
+ keyValue.getElementWidth(KEY_BOX_HEIGHT_RATIO, this.context, this)
4946
5487
  ? baseline
4947
5488
  : keyValue;
4948
5489
  }
4949
- };
5490
+ }
4950
5491
  class ScorecardScalableElement {
4951
5492
  style;
4952
5493
  constructor(style = {}) {
4953
5494
  this.style = style;
4954
5495
  }
4955
5496
  measureTextWidth(ctx, text, fontSize) {
4956
- const italic = this.style.italic ? "italic" : "";
4957
- const weight = this.style.bold ? "bold" : "";
4958
- ctx.font = `${italic} ${weight} ${fontSize}px ${DEFAULT_FONT}`;
5497
+ ctx.font = getDefaultContextFont(fontSize, this.style.bold, this.style.italic);
4959
5498
  return ctx.measureText(text).width;
4960
5499
  }
4961
5500
  }
4962
5501
  class BaselineElement extends ScorecardScalableElement {
4963
5502
  getElementWidth(fontSize, ctx, chart) {
4964
- if (!chart.runtime)
5503
+ if (!chart.runtime) {
4965
5504
  return 0;
5505
+ }
4966
5506
  const baselineStr = chart.baseline;
4967
5507
  // Put mock text to simulate the width of the up/down arrow
4968
- const largeText = chart.baselineArrowDirection !== "neutral" ? "A " + baselineStr : baselineStr;
5508
+ const largeText = chart.baselineArrow !== "" ? "A " + baselineStr : baselineStr;
4969
5509
  let textWidth = this.measureTextWidth(ctx, largeText, fontSize);
4970
5510
  // Baseline descr font size should be smaller than baseline font size
4971
5511
  textWidth += this.measureTextWidth(ctx, chart.baselineDescr, fontSize * BASELINE_DESCR_FONT_RATIO);
4972
5512
  return textWidth;
4973
5513
  }
4974
5514
  getElementMaxFontSize(availableHeight, chart) {
4975
- if (!chart.runtime)
5515
+ if (!chart.runtime) {
4976
5516
  return 0;
5517
+ }
4977
5518
  const haveBaseline = chart.baseline !== "" || chart.baselineDescr;
4978
5519
  const maxHeight = haveBaseline ? BASELINE_BOX_HEIGHT_RATIO * availableHeight : 0;
4979
5520
  return maxHeight / LINE_HEIGHT;
@@ -4981,21 +5522,43 @@
4981
5522
  }
4982
5523
  class KeyValueElement extends ScorecardScalableElement {
4983
5524
  getElementWidth(fontSize, ctx, chart) {
4984
- if (!chart.runtime)
5525
+ if (!chart.runtime) {
4985
5526
  return 0;
5527
+ }
4986
5528
  const str = chart.keyValue || "";
4987
5529
  return this.measureTextWidth(ctx, str, fontSize);
4988
5530
  }
4989
5531
  getElementMaxFontSize(availableHeight, chart) {
4990
- if (!chart.runtime)
5532
+ if (!chart.runtime) {
4991
5533
  return 0;
5534
+ }
4992
5535
  const haveBaseline = chart.baseline !== "" || chart.baselineDescr;
4993
5536
  const maxHeight = haveBaseline ? KEY_BOX_HEIGHT_RATIO * availableHeight : availableHeight;
4994
5537
  return maxHeight / LINE_HEIGHT;
4995
5538
  }
4996
5539
  }
4997
- ScorecardChart$1.props = {
4998
- figure: Object,
5540
+
5541
+ class ScorecardChart extends owl.Component {
5542
+ static template = "o-spreadsheet-ScorecardChart";
5543
+ canvas = owl.useRef("chartContainer");
5544
+ get runtime() {
5545
+ return this.env.model.getters.getChartRuntime(this.props.figureId);
5546
+ }
5547
+ setup() {
5548
+ owl.useEffect(this.createChart.bind(this), () => {
5549
+ const canvas = this.canvas.el;
5550
+ const rect = canvas.getBoundingClientRect();
5551
+ return [rect.width, rect.height, this.runtime, this.canvas.el];
5552
+ });
5553
+ }
5554
+ createChart() {
5555
+ const canvas = this.canvas.el;
5556
+ const config = getScorecardConfiguration(canvas.getBoundingClientRect(), this.runtime);
5557
+ drawScoreChart(config, canvas);
5558
+ }
5559
+ }
5560
+ ScorecardChart.props = {
5561
+ figureId: String,
4999
5562
  };
5000
5563
 
5001
5564
  /**
@@ -5507,6 +6070,148 @@
5507
6070
  sequence: 40,
5508
6071
  });
5509
6072
 
6073
+ /**
6074
+ * This file is largely inspired by owl 1.
6075
+ * `css` tag has been removed from owl 2 without workaround to manage css.
6076
+ * So, the solution was to import the behavior of owl 1 directly in our
6077
+ * codebase, with one difference: the css is added to the sheet as soon as the
6078
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
6079
+ * created for the first time.
6080
+ */
6081
+ const STYLESHEETS = {};
6082
+ let nextId = 0;
6083
+ /**
6084
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
6085
+ * an inline stylesheet with just the following code:
6086
+ * ```js
6087
+ * css`.component-a { color: red; }`;
6088
+ * ```
6089
+ */
6090
+ function css(strings, ...args) {
6091
+ const name = `__sheet__${nextId++}`;
6092
+ const value = String.raw(strings, ...args);
6093
+ registerSheet(name, value);
6094
+ activateSheet(name);
6095
+ return name;
6096
+ }
6097
+ function processSheet(str) {
6098
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
6099
+ const selectorStack = [];
6100
+ const parts = [];
6101
+ let rules = [];
6102
+ function generateSelector(stackIndex, parentSelector) {
6103
+ const parts = [];
6104
+ for (const selector of selectorStack[stackIndex]) {
6105
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
6106
+ if (part.includes("&")) {
6107
+ part = selector.replace(/&/g, parentSelector || "");
6108
+ }
6109
+ if (stackIndex < selectorStack.length - 1) {
6110
+ part = generateSelector(stackIndex + 1, part);
6111
+ }
6112
+ parts.push(part);
6113
+ }
6114
+ return parts.join(", ");
6115
+ }
6116
+ function generateRules() {
6117
+ if (rules.length) {
6118
+ parts.push(generateSelector(0) + " {");
6119
+ parts.push(...rules);
6120
+ parts.push("}");
6121
+ rules = [];
6122
+ }
6123
+ }
6124
+ while (tokens.length) {
6125
+ let token = tokens.shift();
6126
+ if (token === "}") {
6127
+ generateRules();
6128
+ selectorStack.pop();
6129
+ }
6130
+ else {
6131
+ if (tokens[0] === "{") {
6132
+ generateRules();
6133
+ selectorStack.push(token.split(/\s*,\s*/));
6134
+ tokens.shift();
6135
+ }
6136
+ if (tokens[0] === ";") {
6137
+ rules.push(" " + token + ";");
6138
+ }
6139
+ }
6140
+ }
6141
+ return parts.join("\n");
6142
+ }
6143
+ function registerSheet(id, css) {
6144
+ const sheet = document.createElement("style");
6145
+ sheet.textContent = processSheet(css);
6146
+ STYLESHEETS[id] = sheet;
6147
+ }
6148
+ function activateSheet(id) {
6149
+ const sheet = STYLESHEETS[id];
6150
+ sheet.setAttribute("component", id);
6151
+ document.head.appendChild(sheet);
6152
+ }
6153
+ function getTextDecoration({ strikethrough, underline, }) {
6154
+ if (!strikethrough && !underline) {
6155
+ return "none";
6156
+ }
6157
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
6158
+ }
6159
+ /**
6160
+ * Convert the cell style to CSS properties.
6161
+ */
6162
+ function cellStyleToCss(style) {
6163
+ const attributes = cellTextStyleToCss(style);
6164
+ if (!style)
6165
+ return attributes;
6166
+ if (style.fillColor) {
6167
+ attributes["background"] = style.fillColor;
6168
+ }
6169
+ return attributes;
6170
+ }
6171
+ /**
6172
+ * Convert the cell text style to CSS properties.
6173
+ */
6174
+ function cellTextStyleToCss(style) {
6175
+ const attributes = {};
6176
+ if (!style)
6177
+ return attributes;
6178
+ if (style.bold) {
6179
+ attributes["font-weight"] = "bold";
6180
+ }
6181
+ if (style.italic) {
6182
+ attributes["font-style"] = "italic";
6183
+ }
6184
+ if (style.strikethrough || style.underline) {
6185
+ let decoration = style.strikethrough ? "line-through" : "";
6186
+ decoration = style.underline ? decoration + " underline" : decoration;
6187
+ attributes["text-decoration"] = decoration;
6188
+ }
6189
+ if (style.textColor) {
6190
+ attributes["color"] = style.textColor;
6191
+ }
6192
+ return attributes;
6193
+ }
6194
+ /**
6195
+ * Transform CSS properties into a CSS string.
6196
+ */
6197
+ function cssPropertiesToCss(attributes) {
6198
+ const str = Object.entries(attributes)
6199
+ .filter(([attName, attValue]) => attValue !== undefined)
6200
+ .map(([attName, attValue]) => `${attName}:${attValue};`)
6201
+ .join(" ");
6202
+ return str;
6203
+ }
6204
+ function getElementMargins(el) {
6205
+ const style = window.getComputedStyle(el);
6206
+ const margins = {
6207
+ top: parseInt(style.marginTop, 10) || 0,
6208
+ bottom: parseInt(style.marginBottom, 10) || 0,
6209
+ left: parseInt(style.marginLeft, 10) || 0,
6210
+ right: parseInt(style.marginRight, 10) || 0,
6211
+ };
6212
+ return margins;
6213
+ }
6214
+
5510
6215
  const ERROR_TOOLTIP_MAX_HEIGHT = 80;
5511
6216
  const ERROR_TOOLTIP_WIDTH = 180;
5512
6217
  css /* scss */ `
@@ -6240,6 +6945,10 @@
6240
6945
  cursor: pointer;
6241
6946
  user-select: none;
6242
6947
 
6948
+ .o-menu-item-name {
6949
+ min-width: 40%;
6950
+ }
6951
+
6243
6952
  &.o-menu-root {
6244
6953
  display: flex;
6245
6954
  justify-content: space-between;
@@ -16375,405 +17084,6 @@
16375
17084
  return color;
16376
17085
  }
16377
17086
 
16378
- /**
16379
- * AbstractChart is the class from which every Chart should inherit.
16380
- * The role of this class is to maintain the state of each chart.
16381
- */
16382
- class AbstractChart {
16383
- sheetId;
16384
- title;
16385
- getters;
16386
- constructor(definition, sheetId, getters) {
16387
- this.title = definition.title;
16388
- this.sheetId = sheetId;
16389
- this.getters = getters;
16390
- }
16391
- /**
16392
- * Validate the chart definition given as arguments. This function will be
16393
- * called from allowDispatch function
16394
- */
16395
- static validateChartDefinition(validator, definition) {
16396
- throw new Error("This method should be implemented by sub class");
16397
- }
16398
- /**
16399
- * Get a new chart definition transformed with the executed command. This
16400
- * functions will be called during operational transform process
16401
- */
16402
- static transformDefinition(definition, executed) {
16403
- throw new Error("This method should be implemented by sub class");
16404
- }
16405
- /**
16406
- * Get an empty definition based on the given context
16407
- */
16408
- static getDefinitionFromContextCreation(context) {
16409
- throw new Error("This method should be implemented by sub class");
16410
- }
16411
- }
16412
-
16413
- function transformZone(zone, executed) {
16414
- if (executed.type === "REMOVE_COLUMNS_ROWS") {
16415
- return reduceZoneOnDeletion(zone, executed.dimension === "COL" ? "left" : "top", executed.elements);
16416
- }
16417
- if (executed.type === "ADD_COLUMNS_ROWS") {
16418
- return expandZoneOnInsertion(zone, executed.dimension === "COL" ? "left" : "top", executed.base, executed.position, executed.quantity);
16419
- }
16420
- return { ...zone };
16421
- }
16422
-
16423
- /**
16424
- * This file contains helpers that are common to different charts (mainly
16425
- * line, bar and pie charts)
16426
- */
16427
- /**
16428
- * Adapt ranges of a chart which support DataSet (dataSets and LabelRange).
16429
- */
16430
- function updateChartRangesWithDataSets(getters, applyChange, chartDataSets, chartLabelRange) {
16431
- let isStale = false;
16432
- const dataSetsWithUndefined = [];
16433
- for (let index in chartDataSets) {
16434
- let ds = chartDataSets[index];
16435
- if (ds.labelCell) {
16436
- const labelCell = adaptChartRange(ds.labelCell, applyChange);
16437
- if (ds.labelCell !== labelCell) {
16438
- isStale = true;
16439
- ds = {
16440
- ...ds,
16441
- labelCell: labelCell,
16442
- };
16443
- }
16444
- }
16445
- const dataRange = adaptChartRange(ds.dataRange, applyChange);
16446
- if (dataRange === undefined ||
16447
- getters.getRangeString(dataRange, dataRange.sheetId) === INCORRECT_RANGE_STRING) {
16448
- isStale = true;
16449
- ds = undefined;
16450
- }
16451
- else if (dataRange !== ds.dataRange) {
16452
- isStale = true;
16453
- ds = {
16454
- ...ds,
16455
- dataRange,
16456
- };
16457
- }
16458
- dataSetsWithUndefined[index] = ds;
16459
- }
16460
- let labelRange = chartLabelRange;
16461
- const range = adaptChartRange(labelRange, applyChange);
16462
- if (range !== labelRange) {
16463
- isStale = true;
16464
- labelRange = range;
16465
- }
16466
- const dataSets = dataSetsWithUndefined.filter(isDefined$1);
16467
- return {
16468
- isStale,
16469
- dataSets,
16470
- labelRange,
16471
- };
16472
- }
16473
- /**
16474
- * Copy the dataSets given. All the ranges which are on sheetIdFrom will target
16475
- * sheetIdTo.
16476
- */
16477
- function copyDataSetsWithNewSheetId(sheetIdFrom, sheetIdTo, dataSets) {
16478
- return dataSets.map((ds) => {
16479
- return {
16480
- dataRange: copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.dataRange),
16481
- labelCell: ds.labelCell
16482
- ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.labelCell)
16483
- : undefined,
16484
- };
16485
- });
16486
- }
16487
- /**
16488
- * Copy a range. If the range is on the sheetIdFrom, the range will target
16489
- * sheetIdTo.
16490
- */
16491
- function copyLabelRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {
16492
- return range ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) : undefined;
16493
- }
16494
- /**
16495
- * Adapt a single range of a chart
16496
- */
16497
- function adaptChartRange(range, applyChange) {
16498
- if (!range) {
16499
- return undefined;
16500
- }
16501
- const change = applyChange(range);
16502
- switch (change.changeType) {
16503
- case "NONE":
16504
- return range;
16505
- case "REMOVE":
16506
- return undefined;
16507
- default:
16508
- return change.range;
16509
- }
16510
- }
16511
- /**
16512
- * Create the dataSet objects from xcs
16513
- */
16514
- function createDataSets(getters, dataSetsString, sheetId, dataSetsHaveTitle) {
16515
- const dataSets = [];
16516
- for (const sheetXC of dataSetsString) {
16517
- const dataRange = getters.getRangeFromSheetXC(sheetId, sheetXC);
16518
- const { unboundedZone: zone, sheetId: dataSetSheetId, invalidSheetName, invalidXc } = dataRange;
16519
- if (invalidSheetName || invalidXc) {
16520
- continue;
16521
- }
16522
- // It's a rectangle. We treat all columns (arbitrary) as different data series.
16523
- if (zone.left !== zone.right && zone.top !== zone.bottom) {
16524
- if (zone.right === undefined) {
16525
- // Should never happens because of the allowDispatch of charts, but just making sure
16526
- continue;
16527
- }
16528
- for (let column = zone.left; column <= zone.right; column++) {
16529
- const columnZone = {
16530
- ...zone,
16531
- left: column,
16532
- right: column,
16533
- };
16534
- dataSets.push(createDataSet(getters, dataSetSheetId, columnZone, dataSetsHaveTitle
16535
- ? {
16536
- top: columnZone.top,
16537
- bottom: columnZone.top,
16538
- left: columnZone.left,
16539
- right: columnZone.left,
16540
- }
16541
- : undefined));
16542
- }
16543
- }
16544
- else if (zone.left === zone.right && zone.top === zone.bottom) {
16545
- // A single cell. If it's only the title, the dataset is not added.
16546
- if (!dataSetsHaveTitle) {
16547
- dataSets.push(createDataSet(getters, dataSetSheetId, zone, undefined));
16548
- }
16549
- }
16550
- else {
16551
- /* 1 row or 1 column */
16552
- dataSets.push(createDataSet(getters, dataSetSheetId, zone, dataSetsHaveTitle
16553
- ? {
16554
- top: zone.top,
16555
- bottom: zone.top,
16556
- left: zone.left,
16557
- right: zone.left,
16558
- }
16559
- : undefined));
16560
- }
16561
- }
16562
- return dataSets;
16563
- }
16564
- function createDataSet(getters, sheetId, fullZone, titleZone) {
16565
- if (fullZone.left !== fullZone.right && fullZone.top !== fullZone.bottom) {
16566
- throw new Error(`Zone should be a single column or row: ${zoneToXc(fullZone)}`);
16567
- }
16568
- if (titleZone) {
16569
- const dataXC = zoneToXc(fullZone);
16570
- const labelCellXC = zoneToXc(titleZone);
16571
- return {
16572
- labelCell: getters.getRangeFromSheetXC(sheetId, labelCellXC),
16573
- dataRange: getters.getRangeFromSheetXC(sheetId, dataXC),
16574
- };
16575
- }
16576
- else {
16577
- return {
16578
- labelCell: undefined,
16579
- dataRange: getters.getRangeFromSheetXC(sheetId, zoneToXc(fullZone)),
16580
- };
16581
- }
16582
- }
16583
- /**
16584
- * Transform a dataSet to a ExcelDataSet
16585
- */
16586
- function toExcelDataset(getters, ds) {
16587
- const labelZone = ds.labelCell?.zone;
16588
- let dataZone = ds.dataRange.zone;
16589
- if (labelZone) {
16590
- const { numberOfRows, numberOfCols } = zoneToDimension(dataZone);
16591
- if (numberOfRows === 1) {
16592
- dataZone = { ...dataZone, left: dataZone.left + 1 };
16593
- }
16594
- else if (numberOfCols === 1) {
16595
- dataZone = { ...dataZone, top: dataZone.top + 1 };
16596
- }
16597
- }
16598
- const dataRange = ds.dataRange.clone({ zone: dataZone });
16599
- return {
16600
- label: ds.labelCell ? getters.getRangeString(ds.labelCell, "forceSheetReference") : undefined,
16601
- range: getters.getRangeString(dataRange, "forceSheetReference"),
16602
- };
16603
- }
16604
- function toExcelLabelRange(getters, labelRange, shouldRemoveFirstLabel) {
16605
- if (!labelRange)
16606
- return undefined;
16607
- let zone = {
16608
- ...labelRange.zone,
16609
- };
16610
- if (shouldRemoveFirstLabel && labelRange.zone.bottom > labelRange.zone.top) {
16611
- zone.top = zone.top + 1;
16612
- }
16613
- const range = labelRange.clone({ zone });
16614
- return getters.getRangeString(range, "forceSheetReference");
16615
- }
16616
- /**
16617
- * Transform a chart definition which supports dataSets (dataSets and LabelRange)
16618
- * with an executed command
16619
- */
16620
- function transformChartDefinitionWithDataSetsWithZone(definition, executed) {
16621
- let labelRange;
16622
- if (definition.labelRange) {
16623
- const labelZone = transformZone(toUnboundedZone(definition.labelRange), executed);
16624
- labelRange = labelZone ? zoneToXc(labelZone) : undefined;
16625
- }
16626
- const dataSets = definition.dataSets
16627
- .map(toUnboundedZone)
16628
- .map((zone) => transformZone(zone, executed))
16629
- .filter(isDefined$1)
16630
- .map(zoneToXc);
16631
- return {
16632
- ...definition,
16633
- labelRange,
16634
- dataSets,
16635
- };
16636
- }
16637
- const GraphColors = [
16638
- // the same colors as those used in odoo reporting
16639
- "rgb(31,119,180)",
16640
- "rgb(255,127,14)",
16641
- "rgb(174,199,232)",
16642
- "rgb(255,187,120)",
16643
- "rgb(44,160,44)",
16644
- "rgb(152,223,138)",
16645
- "rgb(214,39,40)",
16646
- "rgb(255,152,150)",
16647
- "rgb(148,103,189)",
16648
- "rgb(197,176,213)",
16649
- "rgb(140,86,75)",
16650
- "rgb(196,156,148)",
16651
- "rgb(227,119,194)",
16652
- "rgb(247,182,210)",
16653
- "rgb(127,127,127)",
16654
- "rgb(199,199,199)",
16655
- "rgb(188,189,34)",
16656
- "rgb(219,219,141)",
16657
- "rgb(23,190,207)",
16658
- "rgb(158,218,229)",
16659
- ];
16660
- class ChartColors {
16661
- graphColorIndex = 0;
16662
- next() {
16663
- return GraphColors[this.graphColorIndex++ % GraphColors.length];
16664
- }
16665
- }
16666
- /**
16667
- * Choose a font color based on a background color.
16668
- * The font is white with a dark background.
16669
- */
16670
- function chartFontColor(backgroundColor) {
16671
- if (!backgroundColor) {
16672
- return "#000000";
16673
- }
16674
- return relativeLuminance(backgroundColor) < 0.3 ? "#FFFFFF" : "#000000";
16675
- }
16676
- function checkDataset(definition) {
16677
- if (definition.dataSets) {
16678
- const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range)) !== undefined;
16679
- if (invalidRanges) {
16680
- return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
16681
- }
16682
- const zones = definition.dataSets.map(toUnboundedZone);
16683
- if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) {
16684
- return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
16685
- }
16686
- }
16687
- return "Success" /* CommandResult.Success */;
16688
- }
16689
- function checkLabelRange(definition) {
16690
- if (definition.labelRange) {
16691
- const invalidLabels = !rangeReference.test(definition.labelRange || "");
16692
- if (invalidLabels) {
16693
- return "InvalidLabelRange" /* CommandResult.InvalidLabelRange */;
16694
- }
16695
- }
16696
- return "Success" /* CommandResult.Success */;
16697
- }
16698
- function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
16699
- if (!dataSetsHaveTitle)
16700
- return false;
16701
- if (!labelRange)
16702
- return false;
16703
- if (!dataset)
16704
- return true;
16705
- const datasetLength = getZoneArea(dataset.dataRange.zone);
16706
- const labelLength = getZoneArea(labelRange.zone);
16707
- if (labelLength < datasetLength) {
16708
- return false;
16709
- }
16710
- return true;
16711
- }
16712
- // ---------------------------------------------------------------------------
16713
- // Scorecard
16714
- // ---------------------------------------------------------------------------
16715
- function getBaselineText(baseline, keyValue, baselineMode, locale) {
16716
- if (!baseline) {
16717
- return "";
16718
- }
16719
- else if (baselineMode === "text" ||
16720
- keyValue?.type !== CellValueType.number ||
16721
- baseline.type !== CellValueType.number) {
16722
- return baseline.formattedValue;
16723
- }
16724
- else {
16725
- let diff = keyValue.value - baseline.value;
16726
- if (baselineMode === "percentage" && diff !== 0) {
16727
- diff = (diff / baseline.value) * 100;
16728
- }
16729
- if (baselineMode !== "percentage" && baseline.format) {
16730
- return formatValue(diff, { format: baseline.format, locale });
16731
- }
16732
- const baselineStr = Math.abs(parseFloat(diff.toFixed(2))).toLocaleString();
16733
- return baselineMode === "percentage" ? baselineStr + "%" : baselineStr;
16734
- }
16735
- }
16736
- function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
16737
- if (baselineMode === "text" ||
16738
- baseline?.type !== CellValueType.number ||
16739
- keyValue?.type !== CellValueType.number) {
16740
- return undefined;
16741
- }
16742
- const diff = keyValue.value - baseline.value;
16743
- if (diff > 0) {
16744
- return colorUp;
16745
- }
16746
- else if (diff < 0) {
16747
- return colorDown;
16748
- }
16749
- return undefined;
16750
- }
16751
- function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
16752
- if (baselineMode === "text" ||
16753
- baseline?.type !== CellValueType.number ||
16754
- keyValue?.type !== CellValueType.number) {
16755
- return "neutral";
16756
- }
16757
- const diff = keyValue.value - baseline.value;
16758
- if (diff > 0) {
16759
- return "up";
16760
- }
16761
- else if (diff < 0) {
16762
- return "down";
16763
- }
16764
- return "neutral";
16765
- }
16766
- function getChartPositionAtCenterOfViewport(getters, chartSize) {
16767
- const { x, y } = getters.getMainViewportCoordinates();
16768
- const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
16769
- const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
16770
- const position = {
16771
- x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
16772
- y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
16773
- }; // Position at the center of the scrollable viewport
16774
- return position;
16775
- }
16776
-
16777
17087
  const PasteInteractiveContent = {
16778
17088
  wrongPasteSelection: _t("This operation is not allowed with multiple selections."),
16779
17089
  willRemoveExistingMerge: _t("This operation is not possible due to a merge. Please remove the merges first than try again."),
@@ -17121,6 +17431,51 @@
17121
17431
  return index - 1;
17122
17432
  }
17123
17433
  }
17434
+ function chartToImage(runtime, figure, type) {
17435
+ // wrap the canvas in a div with a fixed size because chart.js would
17436
+ // fill the whole page otherwise
17437
+ const div = document.createElement("div");
17438
+ div.style.width = `${figure.width}px`;
17439
+ div.style.height = `${figure.height}px`;
17440
+ const canvas = document.createElement("canvas");
17441
+ div.append(canvas);
17442
+ canvas.setAttribute("width", figure.width.toString());
17443
+ canvas.setAttribute("height", figure.height.toString());
17444
+ // we have to add the canvas to the DOM otherwise it won't be rendered
17445
+ document.body.append(div);
17446
+ if ("chartJsConfig" in runtime) {
17447
+ runtime.chartJsConfig.plugins = [backgroundColorChartJSPlugin];
17448
+ // @ts-ignore
17449
+ const chart = new window.Chart(canvas, runtime.chartJsConfig);
17450
+ const imgContent = chart.toBase64Image();
17451
+ chart.destroy();
17452
+ div.remove();
17453
+ return imgContent;
17454
+ }
17455
+ else if (type === "scorecard") {
17456
+ const design = getScorecardConfiguration(figure, runtime);
17457
+ drawScoreChart(design, canvas);
17458
+ const imgContent = canvas.toDataURL();
17459
+ div.remove();
17460
+ return imgContent;
17461
+ }
17462
+ return "";
17463
+ }
17464
+ /**
17465
+ * Custom chart.js plugin to set the background color of the canvas
17466
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
17467
+ */
17468
+ const backgroundColorChartJSPlugin = {
17469
+ id: "customCanvasBackgroundColor",
17470
+ beforeDraw: (chart) => {
17471
+ const { ctx } = chart;
17472
+ ctx.save();
17473
+ ctx.globalCompositeOperation = "destination-over";
17474
+ ctx.fillStyle = "#ffffff";
17475
+ ctx.fillRect(0, 0, chart.width, chart.height);
17476
+ ctx.restore();
17477
+ },
17478
+ };
17124
17479
 
17125
17480
  class BarChart extends AbstractChart {
17126
17481
  dataSets;
@@ -18191,172 +18546,6 @@
18191
18546
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
18192
18547
  }
18193
18548
 
18194
- function checkKeyValue(definition) {
18195
- return definition.keyValue && !rangeReference.test(definition.keyValue)
18196
- ? "InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */
18197
- : "Success" /* CommandResult.Success */;
18198
- }
18199
- function checkBaseline(definition) {
18200
- return definition.baseline && !rangeReference.test(definition.baseline)
18201
- ? "InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */
18202
- : "Success" /* CommandResult.Success */;
18203
- }
18204
- class ScorecardChart extends AbstractChart {
18205
- keyValue;
18206
- baseline;
18207
- baselineMode;
18208
- baselineDescr;
18209
- background;
18210
- baselineColorUp;
18211
- baselineColorDown;
18212
- fontColor;
18213
- type = "scorecard";
18214
- constructor(definition, sheetId, getters) {
18215
- super(definition, sheetId, getters);
18216
- this.keyValue = createRange(getters, sheetId, definition.keyValue);
18217
- this.baseline = createRange(getters, sheetId, definition.baseline);
18218
- this.baselineMode = definition.baselineMode;
18219
- this.baselineDescr = definition.baselineDescr;
18220
- this.background = definition.background;
18221
- this.baselineColorUp = definition.baselineColorUp;
18222
- this.baselineColorDown = definition.baselineColorDown;
18223
- }
18224
- static validateChartDefinition(validator, definition) {
18225
- return validator.checkValidations(definition, checkKeyValue, checkBaseline);
18226
- }
18227
- static getDefinitionFromContextCreation(context) {
18228
- return {
18229
- background: context.background,
18230
- type: "scorecard",
18231
- keyValue: context.range ? context.range[0] : undefined,
18232
- title: context.title || "",
18233
- baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
18234
- baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
18235
- baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
18236
- baseline: context.auxiliaryRange || "",
18237
- };
18238
- }
18239
- static transformDefinition(definition, executed) {
18240
- let baselineZone;
18241
- let keyValueZone;
18242
- if (definition.baseline) {
18243
- baselineZone = transformZone(toUnboundedZone(definition.baseline), executed);
18244
- }
18245
- if (definition.keyValue) {
18246
- keyValueZone = transformZone(toUnboundedZone(definition.keyValue), executed);
18247
- }
18248
- return {
18249
- ...definition,
18250
- baseline: baselineZone ? zoneToXc(baselineZone) : undefined,
18251
- keyValue: keyValueZone ? zoneToXc(keyValueZone) : undefined,
18252
- };
18253
- }
18254
- copyForSheetId(sheetId) {
18255
- const baseline = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.baseline);
18256
- const keyValue = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.keyValue);
18257
- const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue, sheetId);
18258
- return new ScorecardChart(definition, sheetId, this.getters);
18259
- }
18260
- copyInSheetId(sheetId) {
18261
- const definition = this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue, sheetId);
18262
- return new ScorecardChart(definition, sheetId, this.getters);
18263
- }
18264
- getDefinition() {
18265
- return this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue);
18266
- }
18267
- getContextCreation() {
18268
- return {
18269
- background: this.background,
18270
- title: this.title,
18271
- range: this.keyValue ? [this.getters.getRangeString(this.keyValue, this.sheetId)] : undefined,
18272
- auxiliaryRange: this.baseline
18273
- ? this.getters.getRangeString(this.baseline, this.sheetId)
18274
- : undefined,
18275
- };
18276
- }
18277
- getDefinitionWithSpecificRanges(baseline, keyValue, targetSheetId) {
18278
- return {
18279
- baselineColorDown: this.baselineColorDown,
18280
- baselineColorUp: this.baselineColorUp,
18281
- baselineMode: this.baselineMode,
18282
- title: this.title,
18283
- type: "scorecard",
18284
- background: this.background,
18285
- baseline: baseline
18286
- ? this.getters.getRangeString(baseline, targetSheetId || this.sheetId)
18287
- : undefined,
18288
- baselineDescr: this.baselineDescr,
18289
- keyValue: keyValue
18290
- ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
18291
- : undefined,
18292
- };
18293
- }
18294
- getDefinitionForExcel() {
18295
- // This kind of graph is not exportable in Excel
18296
- return undefined;
18297
- }
18298
- updateRanges(applyChange) {
18299
- const baseline = adaptChartRange(this.baseline, applyChange);
18300
- const keyValue = adaptChartRange(this.keyValue, applyChange);
18301
- if (this.baseline === baseline && this.keyValue === keyValue) {
18302
- return this;
18303
- }
18304
- const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue);
18305
- return new ScorecardChart(definition, this.sheetId, this.getters);
18306
- }
18307
- }
18308
- function createScorecardChartRuntime(chart, getters) {
18309
- let keyValue = "";
18310
- let formattedKeyValue = "";
18311
- let keyValueCell;
18312
- if (chart.keyValue) {
18313
- const keyValuePosition = {
18314
- sheetId: chart.keyValue.sheetId,
18315
- col: chart.keyValue.zone.left,
18316
- row: chart.keyValue.zone.top,
18317
- };
18318
- keyValueCell = getters.getEvaluatedCell(keyValuePosition);
18319
- keyValue = String(keyValueCell.value);
18320
- formattedKeyValue = keyValueCell.formattedValue;
18321
- }
18322
- let baselineCell;
18323
- const baseline = chart.baseline;
18324
- if (baseline) {
18325
- const baselinePosition = {
18326
- sheetId: chart.baseline.sheetId,
18327
- col: chart.baseline.zone.left,
18328
- row: chart.baseline.zone.top,
18329
- };
18330
- baselineCell = getters.getEvaluatedCell(baselinePosition);
18331
- }
18332
- const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue);
18333
- const locale = getters.getLocale();
18334
- return {
18335
- title: _t(chart.title),
18336
- keyValue: formattedKeyValue || keyValue,
18337
- baselineDisplay: getBaselineText(baselineCell, keyValueCell, chart.baselineMode, locale),
18338
- baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
18339
- baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
18340
- baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
18341
- fontColor: chartFontColor(background),
18342
- background,
18343
- baselineStyle: chart.baselineMode !== "percentage" && baseline
18344
- ? getters.getCellStyle({
18345
- sheetId: baseline.sheetId,
18346
- col: baseline.zone.left,
18347
- row: baseline.zone.top,
18348
- })
18349
- : undefined,
18350
- keyValueStyle: chart.keyValue
18351
- ? getters.getCellStyle({
18352
- sheetId: chart.keyValue.sheetId,
18353
- col: chart.keyValue.zone.left,
18354
- row: chart.keyValue.zone.top,
18355
- })
18356
- : undefined,
18357
- };
18358
- }
18359
-
18360
18549
  /**
18361
18550
  * This registry is intended to map a cell content (raw string) to
18362
18551
  * an instance of a cell.
@@ -18394,11 +18583,11 @@
18394
18583
  });
18395
18584
  chartRegistry.add("scorecard", {
18396
18585
  match: (type) => type === "scorecard",
18397
- createChart: (definition, sheetId, getters) => new ScorecardChart(definition, sheetId, getters),
18586
+ createChart: (definition, sheetId, getters) => new ScorecardChart$1(definition, sheetId, getters),
18398
18587
  getChartRuntime: createScorecardChartRuntime,
18399
- validateChartDefinition: (validator, definition) => ScorecardChart.validateChartDefinition(validator, definition),
18400
- transformDefinition: (definition, executed) => ScorecardChart.transformDefinition(definition, executed),
18401
- getChartDefinitionFromContextCreation: (context) => ScorecardChart.getDefinitionFromContextCreation(context),
18588
+ validateChartDefinition: (validator, definition) => ScorecardChart$1.validateChartDefinition(validator, definition),
18589
+ transformDefinition: (definition, executed) => ScorecardChart$1.transformDefinition(definition, executed),
18590
+ getChartDefinitionFromContextCreation: (context) => ScorecardChart$1.getDefinitionFromContextCreation(context),
18402
18591
  name: _t("Scorecard"),
18403
18592
  sequence: 40,
18404
18593
  });
@@ -18417,7 +18606,7 @@
18417
18606
  chartComponentRegistry.add("bar", ChartJsComponent);
18418
18607
  chartComponentRegistry.add("pie", ChartJsComponent);
18419
18608
  chartComponentRegistry.add("gauge", ChartJsComponent);
18420
- chartComponentRegistry.add("scorecard", ScorecardChart$1);
18609
+ chartComponentRegistry.add("scorecard", ScorecardChart);
18421
18610
 
18422
18611
  /**
18423
18612
  * Registry intended to support usual currencies. It is mainly used to create
@@ -18439,11 +18628,11 @@
18439
18628
  static template = "o-spreadsheet-ChartFigure";
18440
18629
  static components = {};
18441
18630
  onDoubleClick() {
18442
- this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
18631
+ this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figureId });
18443
18632
  this.env.openSidePanel("ChartPanel");
18444
18633
  }
18445
18634
  get chartType() {
18446
- return this.env.model.getters.getChartType(this.props.figure.id);
18635
+ return this.env.model.getters.getChartType(this.props.figureId);
18447
18636
  }
18448
18637
  get chartComponent() {
18449
18638
  const type = this.chartType;
@@ -18455,7 +18644,7 @@
18455
18644
  }
18456
18645
  }
18457
18646
  ChartFigure.props = {
18458
- figure: Object,
18647
+ figureId: String,
18459
18648
  onFigureDeleted: Function,
18460
18649
  };
18461
18650
 
@@ -18466,14 +18655,14 @@
18466
18655
  // Getters
18467
18656
  // ---------------------------------------------------------------------------
18468
18657
  get figureId() {
18469
- return this.props.figure.id;
18658
+ return this.props.figureId;
18470
18659
  }
18471
18660
  get getImagePath() {
18472
18661
  return this.env.model.getters.getImagePath(this.figureId);
18473
18662
  }
18474
18663
  }
18475
18664
  ImageFigure.props = {
18476
- figure: Object,
18665
+ figureId: String,
18477
18666
  onFigureDeleted: Function,
18478
18667
  };
18479
18668
 
@@ -19159,6 +19348,7 @@
19159
19348
  // Helpers
19160
19349
  //------------------------------------------------------------------------------
19161
19350
  function setFormatter(env, format) {
19351
+ env.model.dispatch("CANCEL_EDITION");
19162
19352
  env.model.dispatch("SET_FORMATTING", {
19163
19353
  sheetId: env.model.getters.getActiveSheetId(),
19164
19354
  target: env.model.getters.getSelectedZones(),
@@ -20270,109 +20460,126 @@
20270
20460
  icon: "o-spreadsheet-Icon.SPLIT_TEXT",
20271
20461
  };
20272
20462
 
20463
+ /**
20464
+ * Create a format action specification for a given format.
20465
+ * The format can be dynamically computed from the environment.
20466
+ */
20467
+ function createFormatActionSpec({ name, format, descriptionValue, }) {
20468
+ const formatCallback = typeof format === "function" ? format : () => format;
20469
+ return {
20470
+ name,
20471
+ description: (env) => formatValue(descriptionValue, {
20472
+ format: formatCallback(env),
20473
+ locale: env.model.getters.getLocale(),
20474
+ }),
20475
+ execute: (env) => setFormatter(env, formatCallback(env)),
20476
+ isActive: (env) => isFormatSelected(env, formatCallback(env)),
20477
+ };
20478
+ }
20273
20479
  const formatNumberAutomatic = {
20274
20480
  name: _t("Automatic"),
20275
20481
  execute: (env) => setFormatter(env, ""),
20276
20482
  isActive: (env) => isAutomaticFormatSelected(env),
20277
20483
  };
20278
- const formatNumberNumber = {
20484
+ const formatNumberNumber = createFormatActionSpec({
20279
20485
  name: _t("Number"),
20280
- description: (env) => formatValue(1000.12, {
20281
- format: "#,##0.00",
20282
- locale: env.model.getters.getLocale(),
20283
- }),
20284
- execute: (env) => setFormatter(env, "#,##0.00"),
20285
- isActive: (env) => isFormatSelected(env, "#,##0.00"),
20286
- };
20486
+ descriptionValue: 1000.12,
20487
+ format: "#,##0.00",
20488
+ });
20287
20489
  const formatPercent = {
20288
20490
  name: _t("Format as percent"),
20289
20491
  execute: FORMAT_PERCENT_ACTION,
20290
20492
  icon: "o-spreadsheet-Icon.PERCENT",
20291
20493
  };
20292
- const formatNumberPercent = {
20494
+ const formatNumberPercent = createFormatActionSpec({
20293
20495
  name: _t("Percent"),
20294
- description: (env) => formatValue(0.1012, {
20295
- format: "0.00%",
20296
- locale: env.model.getters.getLocale(),
20297
- }),
20298
- execute: FORMAT_PERCENT_ACTION,
20299
- isActive: (env) => isFormatSelected(env, "0.00%"),
20300
- };
20301
- const formatNumberCurrency = {
20496
+ descriptionValue: 0.1012,
20497
+ format: "0.00%",
20498
+ });
20499
+ const formatNumberCurrency = createFormatActionSpec({
20302
20500
  name: _t("Currency"),
20303
- description: (env) => formatValue(1000.12, {
20304
- format: env.model.config.defaultCurrencyFormat,
20305
- locale: env.model.getters.getLocale(),
20306
- }),
20307
- execute: (env) => setFormatter(env, env.model.config.defaultCurrencyFormat),
20308
- isActive: (env) => isFormatSelected(env, env.model.config.defaultCurrencyFormat),
20309
- };
20501
+ descriptionValue: 1000.12,
20502
+ format: (env) => env.model.config.defaultCurrencyFormat,
20503
+ });
20310
20504
  const formatNumberCurrencyRounded = {
20311
- name: _t("Currency rounded"),
20312
- description: (env) => formatValue(1000, {
20313
- format: roundFormat(env.model.config.defaultCurrencyFormat),
20314
- locale: env.model.getters.getLocale(),
20505
+ ...createFormatActionSpec({
20506
+ name: _t("Currency rounded"),
20507
+ descriptionValue: 1000,
20508
+ format: (env) => roundFormat(env.model.config.defaultCurrencyFormat),
20315
20509
  }),
20316
- execute: (env) => setFormatter(env, roundFormat(env.model.config.defaultCurrencyFormat)),
20317
- isActive: (env) => isFormatSelected(env, roundFormat(env.model.config.defaultCurrencyFormat)),
20318
20510
  isVisible: (env) => {
20319
20511
  const currencyFormat = env.model.config.defaultCurrencyFormat;
20320
20512
  return currencyFormat !== roundFormat(currencyFormat);
20321
20513
  },
20322
20514
  };
20515
+ const EXAMPLE_DATE = parseLiteral("2023/09/26 10:43:00 PM", DEFAULT_LOCALE);
20323
20516
  const formatCustomCurrency = {
20324
20517
  name: _t("Custom currency"),
20325
20518
  isVisible: (env) => env.loadCurrencies !== undefined,
20326
20519
  execute: (env) => env.openSidePanel("CustomCurrency", {}),
20327
20520
  };
20328
- const formatNumberDate = {
20521
+ const formatNumberDate = createFormatActionSpec({
20329
20522
  name: _t("Date"),
20330
- description: (env) => {
20331
- const locale = env.model.getters.getLocale();
20332
- return formatValue(parseLiteral("9/26/2023", DEFAULT_LOCALE), {
20333
- format: locale.dateFormat,
20334
- locale,
20335
- });
20336
- },
20337
- execute: (env) => setFormatter(env, env.model.getters.getLocale().dateFormat),
20338
- isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().dateFormat),
20339
- };
20340
- const formatNumberTime = {
20523
+ descriptionValue: EXAMPLE_DATE,
20524
+ format: (env) => env.model.getters.getLocale().dateFormat,
20525
+ });
20526
+ const formatNumberTime = createFormatActionSpec({
20341
20527
  name: _t("Time"),
20342
- description: (env) => {
20343
- const locale = env.model.getters.getLocale();
20344
- return formatValue(parseLiteral("9/26/2023 10:43:00 PM", DEFAULT_LOCALE), {
20345
- format: locale.timeFormat,
20346
- locale,
20347
- });
20348
- },
20349
- execute: (env) => setFormatter(env, env.model.getters.getLocale().timeFormat),
20350
- isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().timeFormat),
20351
- };
20352
- const formatNumberDateTime = {
20528
+ descriptionValue: EXAMPLE_DATE,
20529
+ format: (env) => env.model.getters.getLocale().timeFormat,
20530
+ });
20531
+ const formatNumberDateTime = createFormatActionSpec({
20353
20532
  name: _t("Date time"),
20354
- description: (env) => {
20355
- const locale = env.model.getters.getLocale();
20356
- return formatValue(parseLiteral("9/26/2023 22:43:00", DEFAULT_LOCALE), {
20357
- format: locale.dateFormat + " " + locale.timeFormat,
20358
- locale,
20359
- });
20360
- },
20361
- execute: (env) => {
20533
+ descriptionValue: EXAMPLE_DATE,
20534
+ format: (env) => {
20362
20535
  const locale = env.model.getters.getLocale();
20363
- setFormatter(env, locale.dateFormat + " " + locale.timeFormat);
20536
+ return locale.dateFormat + " " + locale.timeFormat;
20364
20537
  },
20365
- isActive: (env) => {
20366
- const locale = env.model.getters.getLocale();
20367
- return isFormatSelected(env, locale.dateFormat + " " + locale.timeFormat);
20368
- },
20369
- };
20370
- const formatNumberDuration = {
20538
+ });
20539
+ const formatNumberDuration = createFormatActionSpec({
20371
20540
  name: _t("Duration"),
20372
- description: "27:51:38",
20373
- execute: (env) => setFormatter(env, "hhhh:mm:ss"),
20374
- isActive: (env) => isFormatSelected(env, "hhhh:mm:ss"),
20375
- };
20541
+ descriptionValue: "27:51:38",
20542
+ format: "hhhh:mm:ss",
20543
+ });
20544
+ const moreFormats = {
20545
+ name: _t("More date formats"),
20546
+ execute: (env) => env.openSidePanel("MoreFormats", {}),
20547
+ };
20548
+ const formatNumberFullDateTime = createFormatActionSpec({
20549
+ name: _t("Full date time"),
20550
+ format: "dddd d mmmm yyyy hh:mm:ss a",
20551
+ descriptionValue: EXAMPLE_DATE,
20552
+ });
20553
+ const formatNumberFullWeekDayAndMonth = createFormatActionSpec({
20554
+ name: _t("Full week day and month"),
20555
+ format: "dddd d mmmm yyyy",
20556
+ descriptionValue: EXAMPLE_DATE,
20557
+ });
20558
+ const formatNumberDayAndFullMonth = createFormatActionSpec({
20559
+ name: _t("Day and full month"),
20560
+ format: "d mmmm yyyy",
20561
+ descriptionValue: EXAMPLE_DATE,
20562
+ });
20563
+ const formatNumberShortWeekDay = createFormatActionSpec({
20564
+ name: _t("Short week day"),
20565
+ format: "ddd d mmm yyyy",
20566
+ descriptionValue: EXAMPLE_DATE,
20567
+ });
20568
+ const formatNumberDayAndShortMonth = createFormatActionSpec({
20569
+ name: _t("Day and short month"),
20570
+ format: "d mmm yyyy",
20571
+ descriptionValue: EXAMPLE_DATE,
20572
+ });
20573
+ const formatNumberFullMonth = createFormatActionSpec({
20574
+ name: _t("Full month"),
20575
+ format: "mmmm yyyy",
20576
+ descriptionValue: EXAMPLE_DATE,
20577
+ });
20578
+ const formatNumberShortMonth = createFormatActionSpec({
20579
+ name: _t("Short month"),
20580
+ format: "mmm yyyy",
20581
+ descriptionValue: EXAMPLE_DATE,
20582
+ });
20376
20583
  const incraseDecimalPlaces = {
20377
20584
  name: _t("Increase decimal places"),
20378
20585
  icon: "o-spreadsheet-Icon.INCREASE_DECIMAL",
@@ -20625,9 +20832,16 @@
20625
20832
  formatNumberCurrencyRounded: formatNumberCurrencyRounded,
20626
20833
  formatNumberDate: formatNumberDate,
20627
20834
  formatNumberDateTime: formatNumberDateTime,
20835
+ formatNumberDayAndFullMonth: formatNumberDayAndFullMonth,
20836
+ formatNumberDayAndShortMonth: formatNumberDayAndShortMonth,
20628
20837
  formatNumberDuration: formatNumberDuration,
20838
+ formatNumberFullDateTime: formatNumberFullDateTime,
20839
+ formatNumberFullMonth: formatNumberFullMonth,
20840
+ formatNumberFullWeekDayAndMonth: formatNumberFullWeekDayAndMonth,
20629
20841
  formatNumberNumber: formatNumberNumber,
20630
20842
  formatNumberPercent: formatNumberPercent,
20843
+ formatNumberShortMonth: formatNumberShortMonth,
20844
+ formatNumberShortWeekDay: formatNumberShortWeekDay,
20631
20845
  formatNumberTime: formatNumberTime,
20632
20846
  formatPercent: formatPercent,
20633
20847
  formatStrikethrough: formatStrikethrough,
@@ -20638,6 +20852,7 @@
20638
20852
  formatWrappingOverflow: formatWrappingOverflow,
20639
20853
  formatWrappingWrap: formatWrappingWrap,
20640
20854
  incraseDecimalPlaces: incraseDecimalPlaces,
20855
+ moreFormats: moreFormats,
20641
20856
  textColor: textColor
20642
20857
  });
20643
20858
 
@@ -21095,6 +21310,10 @@
21095
21310
  ...formatNumberDuration,
21096
21311
  sequence: 100,
21097
21312
  separator: true,
21313
+ })
21314
+ .add("more_formats", {
21315
+ ...moreFormats,
21316
+ sequence: 110,
21098
21317
  });
21099
21318
  const formatNumberMenuItemSpec = {
21100
21319
  name: _t("More formats"),
@@ -22996,7 +23215,7 @@
22996
23215
  });
22997
23216
  }
22998
23217
  canUpdateChart(figureId, updateDefinition) {
22999
- if (figureId !== this.figureId) {
23218
+ if (figureId !== this.figureId || !this.env.model.getters.isChartDefined(figureId)) {
23000
23219
  return;
23001
23220
  }
23002
23221
  const definition = {
@@ -24363,6 +24582,44 @@
24363
24582
  onCloseSidePanel: Function,
24364
24583
  };
24365
24584
 
24585
+ css /* scss */ `
24586
+ .o-more-formats-panel {
24587
+ .format-preview {
24588
+ height: 48px;
24589
+ background-color: white;
24590
+
24591
+ &:hover {
24592
+ background-color: rgba(0, 0, 0, 0.08);
24593
+ }
24594
+ }
24595
+ .check-icon {
24596
+ width: 24px;
24597
+ }
24598
+ }
24599
+ `;
24600
+ const DATE_FORMAT_ACTIONS = createActions([
24601
+ formatNumberFullDateTime,
24602
+ formatNumberFullWeekDayAndMonth,
24603
+ formatNumberDayAndFullMonth,
24604
+ formatNumberShortWeekDay,
24605
+ formatNumberDayAndShortMonth,
24606
+ formatNumberFullMonth,
24607
+ formatNumberShortMonth,
24608
+ formatNumberDate,
24609
+ formatNumberTime,
24610
+ formatNumberDateTime,
24611
+ formatNumberDuration,
24612
+ ]);
24613
+ class MoreFormatsPanel extends owl.Component {
24614
+ static template = "o-spreadsheet-MoreFormatsPanel";
24615
+ get dateFormatsActions() {
24616
+ return DATE_FORMAT_ACTIONS;
24617
+ }
24618
+ }
24619
+ MoreFormatsPanel.props = {
24620
+ onCloseSidePanel: Function,
24621
+ };
24622
+
24366
24623
  css /* scss */ `
24367
24624
  .o-checkbox-selection {
24368
24625
  height: 150px;
@@ -25820,6 +26077,10 @@
25820
26077
  title: _t("Data validation"),
25821
26078
  Body: DataValidationPanel,
25822
26079
  });
26080
+ sidePanelRegistry.add("MoreFormats", {
26081
+ title: _t("More date formats"),
26082
+ Body: MoreFormatsPanel,
26083
+ });
25823
26084
 
25824
26085
  class TopBarComponentRegistry extends Registry {
25825
26086
  mapping = {};
@@ -28765,7 +29026,14 @@
28765
29026
  return;
28766
29027
  }
28767
29028
  if (this.state.waitingForMove === true) {
28768
- this.startMovement(ev);
29029
+ if (!this.env.model.getters.isGridSelectionActive()) {
29030
+ this._selectElement(index, false);
29031
+ }
29032
+ else {
29033
+ // FIXME: Consider reintroducing this feature for all type of selection if we find
29034
+ // a way to have the grid selection follow the other selections evolution
29035
+ this.startMovement(ev);
29036
+ }
28769
29037
  return;
28770
29038
  }
28771
29039
  if (this.env.model.getters.getEditionMode() === "editing") {
@@ -32280,12 +32548,15 @@
32280
32548
  };
32281
32549
  }
32282
32550
  function xmlEscape(str) {
32283
- return String(str)
32551
+ return (String(str)
32284
32552
  .replace(/\&/g, "&amp;")
32285
32553
  .replace(/\</g, "&lt;")
32286
32554
  .replace(/\>/g, "&gt;")
32287
32555
  .replace(/\"/g, "&quot;")
32288
- .replace(/\'/g, "&apos;");
32556
+ .replace(/\'/g, "&apos;")
32557
+ // Delete all ASCII control characters except for TAB (\x09), LF (\x0A) and CR (\x0D)
32558
+ // They are not valid at all in XML 1.0 (even escaped)
32559
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ""));
32289
32560
  }
32290
32561
  function formatAttributes(attrs) {
32291
32562
  return new XMLString(attrs.map(([key, val]) => `${key}="${xmlEscape(val)}"`).join(" "));
@@ -35395,7 +35666,7 @@
35395
35666
  export(data) {
35396
35667
  if (data.sheets) {
35397
35668
  for (let sheet of data.sheets) {
35398
- // TODO This code is false, if two plugins want ot insert figures on the sheet, it will crash !
35669
+ // TODO This code is false, if two plugins want to insert figures on the sheet, it will crash !
35399
35670
  const sheetFigures = this.getters.getFigures(sheet.id);
35400
35671
  const figures = [];
35401
35672
  for (let sheetFigure of sheetFigures) {
@@ -35415,24 +35686,6 @@
35415
35686
  }
35416
35687
  }
35417
35688
  }
35418
- exportForExcel(data) {
35419
- for (let sheet of data.sheets) {
35420
- const sheetFigures = this.getters.getFigures(sheet.id);
35421
- const figures = [];
35422
- for (let figure of sheetFigures) {
35423
- if (figure && figure.tag === "chart") {
35424
- const figureData = this.charts[figure.id]?.getDefinitionForExcel();
35425
- if (figureData) {
35426
- figures.push({
35427
- ...figure,
35428
- data: figureData,
35429
- });
35430
- }
35431
- }
35432
- }
35433
- sheet.charts = figures;
35434
- }
35435
- }
35436
35689
  // ---------------------------------------------------------------------------
35437
35690
  // Private
35438
35691
  // ---------------------------------------------------------------------------
@@ -36954,6 +37207,9 @@
36954
37207
  }
36955
37208
  exportForExcel(data) {
36956
37209
  for (const sheet of data.sheets) {
37210
+ if (!sheet.images) {
37211
+ sheet.images = [];
37212
+ }
36957
37213
  const figures = this.getters.getFigures(sheet.id);
36958
37214
  const images = [];
36959
37215
  for (const figure of figures) {
@@ -36967,7 +37223,7 @@
36967
37223
  }
36968
37224
  }
36969
37225
  }
36970
- sheet.images = images;
37226
+ sheet.images = [...sheet.images, ...images];
36971
37227
  }
36972
37228
  }
36973
37229
  getAllImages() {
@@ -39910,6 +40166,7 @@
39910
40166
  config;
39911
40167
  static getters = [
39912
40168
  "evaluateFormula",
40169
+ "getCorrespondingFormulaCell",
39913
40170
  "getRangeFormattedValues",
39914
40171
  "getRangeValues",
39915
40172
  "getRangeFormats",
@@ -40306,6 +40563,47 @@
40306
40563
  const style = this.getters.getCellComputedStyle({ sheetId, col, row });
40307
40564
  return style.fillColor || BACKGROUND_CHART_COLOR;
40308
40565
  }
40566
+ exportForExcel(data) {
40567
+ for (const sheet of data.sheets) {
40568
+ if (!sheet.images) {
40569
+ sheet.images = [];
40570
+ }
40571
+ const sheetFigures = this.getters.getFigures(sheet.id);
40572
+ const figures = [];
40573
+ for (const figure of sheetFigures) {
40574
+ if (!figure || figure.tag !== "chart") {
40575
+ continue;
40576
+ }
40577
+ const figureId = figure.id;
40578
+ const figureData = this.getters.getChart(figureId)?.getDefinitionForExcel();
40579
+ if (figureData) {
40580
+ figures.push({
40581
+ ...figure,
40582
+ data: figureData,
40583
+ });
40584
+ }
40585
+ else {
40586
+ const chart = this.getters.getChart(figureId);
40587
+ if (!chart) {
40588
+ continue;
40589
+ }
40590
+ const type = this.getters.getChartType(figureId);
40591
+ const runtime = this.getters.getChartRuntime(figureId);
40592
+ const img = chartToImage(runtime, figure, type);
40593
+ sheet.images.push({
40594
+ ...figure,
40595
+ tag: "image",
40596
+ data: {
40597
+ mimetype: "image/png",
40598
+ path: img,
40599
+ size: { width: figure.width, height: figure.height },
40600
+ },
40601
+ });
40602
+ }
40603
+ }
40604
+ sheet.charts = figures;
40605
+ }
40606
+ }
40309
40607
  }
40310
40608
 
40311
40609
  class EvaluationConditionalFormatPlugin extends UIPlugin {
@@ -41922,7 +42220,7 @@
41922
42220
  { match: isZoneDependent, fn: transformZoneDependentCommand },
41923
42221
  { match: isPositionDependent, fn: transformPosition },
41924
42222
  { match: isHeadersDependant, fn: transformHeaders },
41925
- { match: isRangeDependant, fn: transformRangeData },
42223
+ { match: isRangeDependant, fn: transformRangesDependentCommand },
41926
42224
  ];
41927
42225
  /**
41928
42226
  * Get the result of applying the operation transformations on the given command
@@ -42018,23 +42316,13 @@
42018
42316
  }
42019
42317
  return "IGNORE_COMMAND";
42020
42318
  }
42021
- function transformRangeData(toTransform, executed) {
42319
+ function transformRangesDependentCommand(toTransform, executed) {
42022
42320
  if (!("sheetId" in executed)) {
42023
42321
  return toTransform;
42024
42322
  }
42025
- const ranges = [];
42026
- const deletedSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
42027
- for (const range of toTransform.ranges) {
42028
- if (range._sheetId !== executed.sheetId) {
42029
- ranges.push({ ...range, _zone: range._zone });
42030
- }
42031
- else {
42032
- const newZone = transformZone(range._zone, executed);
42033
- if (newZone && deletedSheet !== range._sheetId) {
42034
- ranges.push({ ...range, _zone: newZone });
42035
- }
42036
- }
42037
- }
42323
+ const ranges = toTransform.ranges
42324
+ .map((range) => transformRangeData(range, executed))
42325
+ .filter(isDefined$1);
42038
42326
  if (!ranges.length) {
42039
42327
  return "IGNORE_COMMAND";
42040
42328
  }
@@ -43179,6 +43467,10 @@
43179
43467
  if (!newRangesXC) {
43180
43468
  return;
43181
43469
  }
43470
+ if (newRangesXC.length === 0) {
43471
+ this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
43472
+ return;
43473
+ }
43182
43474
  this.dispatch("ADD_CONDITIONAL_FORMAT", {
43183
43475
  cf: {
43184
43476
  id: cf.id,
@@ -44094,7 +44386,7 @@
44094
44386
  }
44095
44387
  }
44096
44388
  drawTexts(renderingContext) {
44097
- const { ctx, thinLineWidth } = renderingContext;
44389
+ const { ctx } = renderingContext;
44098
44390
  ctx.textBaseline = "top";
44099
44391
  let currentFont;
44100
44392
  for (let box of this.boxes) {
@@ -44139,23 +44431,7 @@
44139
44431
  // use the horizontal and the vertical start points to:
44140
44432
  // fill text / fill strikethrough / fill underline
44141
44433
  for (let brokenLine of box.content.textLines) {
44142
- ctx.fillText(brokenLine, Math.round(x), Math.round(y));
44143
- if (style.strikethrough || style.underline) {
44144
- const lineWidth = computeTextWidth(ctx, brokenLine, style);
44145
- let _x = x;
44146
- if (align === "right") {
44147
- _x -= lineWidth;
44148
- }
44149
- else if (align === "center") {
44150
- _x -= lineWidth / 2;
44151
- }
44152
- if (style.strikethrough) {
44153
- ctx.fillRect(_x, y + textLineHeight / 2, lineWidth, 2.6 * thinLineWidth);
44154
- }
44155
- if (style.underline) {
44156
- ctx.fillRect(_x, y + textLineHeight + 1, lineWidth, 1.3 * thinLineWidth);
44157
- }
44158
- }
44434
+ drawDecoratedText(ctx, brokenLine, { x: Math.round(x), y: Math.round(y) }, style.underline, style.strikethrough);
44159
44435
  y += MIN_CELL_TEXT_MARGIN + textLineHeight;
44160
44436
  }
44161
44437
  if (box.clipRect) {
@@ -46958,7 +47234,15 @@
46958
47234
  return cell?.content || "";
46959
47235
  case CellValueType.number:
46960
47236
  if (format && isDateTimeFormat(format)) {
46961
- return formattedValue;
47237
+ if (parseDateTime(formattedValue, locale) !== null) {
47238
+ // formatted string can be parsed again
47239
+ return formattedValue;
47240
+ }
47241
+ // display a simplified and parsable string otherwise
47242
+ const timeFormat = Number.isInteger(value)
47243
+ ? locale.dateFormat
47244
+ : locale.dateFormat + " " + locale.timeFormat;
47245
+ return formatValue(value, { locale, format: timeFormat });
46962
47246
  }
46963
47247
  return this.numberComposerContent(value, format, locale);
46964
47248
  }
@@ -47513,6 +47797,7 @@
47513
47797
  "isSingleColSelected",
47514
47798
  "getElementsFromSelection",
47515
47799
  "tryGetActiveSheetId",
47800
+ "isGridSelectionActive",
47516
47801
  ];
47517
47802
  gridSelection = {
47518
47803
  anchor: {
@@ -47722,6 +48007,9 @@
47722
48007
  // ---------------------------------------------------------------------------
47723
48008
  // Getters
47724
48009
  // ---------------------------------------------------------------------------
48010
+ isGridSelectionActive() {
48011
+ return this.selection.isListening(this);
48012
+ }
47725
48013
  getActiveSheet() {
47726
48014
  return this.activeSheet;
47727
48015
  }
@@ -52673,6 +52961,9 @@
52673
52961
  },
52674
52962
  });
52675
52963
  }
52964
+ isListening(owner) {
52965
+ return this.stream.isListening(owner);
52966
+ }
52676
52967
  /**
52677
52968
  * Process a new anchor selection event. If the new anchor is inside
52678
52969
  * the sheet boundaries, the event is pushed to the event stream to
@@ -55061,8 +55352,10 @@
55061
55352
  toString,
55062
55353
  toXC,
55063
55354
  toZone,
55355
+ toUnboundedZone,
55064
55356
  toCartesian,
55065
55357
  numberToLetters,
55358
+ lettersToNumber,
55066
55359
  UuidGenerator,
55067
55360
  formatValue,
55068
55361
  createCurrencyFormat,
@@ -55084,6 +55377,7 @@
55084
55377
  genericRepeat,
55085
55378
  createAction,
55086
55379
  createActions,
55380
+ transformRangeData,
55087
55381
  };
55088
55382
  const links = {
55089
55383
  isMarkdownLink,
@@ -55098,7 +55392,7 @@
55098
55392
  ChartJsComponent,
55099
55393
  Grid,
55100
55394
  GridOverlay,
55101
- ScorecardChart: ScorecardChart$1,
55395
+ ScorecardChart,
55102
55396
  LineConfigPanel,
55103
55397
  LineBarPieDesignPanel,
55104
55398
  BarConfigPanel,
@@ -55109,6 +55403,7 @@
55109
55403
  ScorecardChartDesignPanel,
55110
55404
  FigureComponent,
55111
55405
  Menu,
55406
+ SelectionInput,
55112
55407
  };
55113
55408
  const hooks = {
55114
55409
  useDragAndDropListItems,
@@ -55159,9 +55454,9 @@
55159
55454
  exports.tokenize = tokenize;
55160
55455
 
55161
55456
 
55162
- __info__.version = "16.5.0-alpha.10";
55163
- __info__.date = "2023-10-10T07:31:20.876Z";
55164
- __info__.hash = "4dc340f";
55457
+ __info__.version = "16.5.0-alpha.11";
55458
+ __info__.date = "2023-10-19T14:26:01.790Z";
55459
+ __info__.hash = "4b42059";
55165
55460
 
55166
55461
 
55167
55462
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);