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