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