@odoo/o-spreadsheet 18.2.0-alpha.6 → 18.2.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.2.0-alpha.6
6
- * @date 2025-02-05T06:50:47.008Z
7
- * @hash dae9ab2
5
+ * @version 18.2.0-alpha.7
6
+ * @date 2025-02-10T09:01:19.353Z
7
+ * @hash 0432f17
8
8
  */
9
9
 
10
10
  'use strict';
@@ -771,9 +771,16 @@ function deepEqualsArray(arr1, arr2) {
771
771
  }
772
772
  return true;
773
773
  }
774
- /** Check if the given array contains all the values of the other array. */
774
+ /**
775
+ * Check if the given array contains all the values of the other array.
776
+ * It makes the assumption that both array do not contain duplicates.
777
+ */
775
778
  function includesAll(arr, values) {
776
- return values.every((value) => arr.includes(value));
779
+ if (arr.length < values.length) {
780
+ return false;
781
+ }
782
+ const set = new Set(arr);
783
+ return values.every((value) => set.has(value));
777
784
  }
778
785
  /**
779
786
  * Return an object with all the keys in the object that have a falsy value removed.
@@ -3647,10 +3654,8 @@ exports.CommandResult = void 0;
3647
3654
  CommandResult["GaugeRangeMinNaN"] = "GaugeRangeMinNaN";
3648
3655
  CommandResult["EmptyGaugeRangeMax"] = "EmptyGaugeRangeMax";
3649
3656
  CommandResult["GaugeRangeMaxNaN"] = "GaugeRangeMaxNaN";
3650
- CommandResult["GaugeRangeMinBiggerThanRangeMax"] = "GaugeRangeMinBiggerThanRangeMax";
3651
3657
  CommandResult["GaugeLowerInflectionPointNaN"] = "GaugeLowerInflectionPointNaN";
3652
3658
  CommandResult["GaugeUpperInflectionPointNaN"] = "GaugeUpperInflectionPointNaN";
3653
- CommandResult["GaugeLowerBiggerThanUpper"] = "GaugeLowerBiggerThanUpper";
3654
3659
  CommandResult["InvalidAutofillSelection"] = "InvalidAutofillSelection";
3655
3660
  CommandResult["MinBiggerThanMax"] = "MinBiggerThanMax";
3656
3661
  CommandResult["LowerBiggerThanUpper"] = "LowerBiggerThanUpper";
@@ -27950,7 +27955,6 @@ const ChartTerms = {
27950
27955
  ["GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */]: _t("The minimum range limit value must be a number"),
27951
27956
  ["EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */]: _t("A maximum range limit value is needed"),
27952
27957
  ["GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */]: _t("The maximum range limit value must be a number"),
27953
- ["GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */]: _t("Minimum range limit must be smaller than maximum range limit"),
27954
27958
  ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
27955
27959
  ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
27956
27960
  },
@@ -29772,6 +29776,8 @@ function getGeoChartTooltip(definition, args) {
29772
29776
  const { locale, axisFormats } = args;
29773
29777
  const format = axisFormats?.y || axisFormats?.y1;
29774
29778
  return {
29779
+ enabled: false,
29780
+ external: customTooltipHandler,
29775
29781
  filter: function (tooltipItem) {
29776
29782
  return tooltipItem.raw.value !== undefined;
29777
29783
  },
@@ -30225,14 +30231,6 @@ function checkInflectionPointsValue(check, batchValidations) {
30225
30231
  return "Success" /* CommandResult.Success */;
30226
30232
  });
30227
30233
  }
30228
- function checkRangeMinBiggerThanRangeMax(definition) {
30229
- if (definition.sectionRule) {
30230
- if (Number(definition.sectionRule.rangeMin) >= Number(definition.sectionRule.rangeMax)) {
30231
- return "GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */;
30232
- }
30233
- }
30234
- return "Success" /* CommandResult.Success */;
30235
- }
30236
30234
  function checkEmpty(value, valueName) {
30237
30235
  if (value === "") {
30238
30236
  switch (valueName) {
@@ -30244,7 +30242,10 @@ function checkEmpty(value, valueName) {
30244
30242
  }
30245
30243
  return "Success" /* CommandResult.Success */;
30246
30244
  }
30247
- function checkNaN(value, valueName) {
30245
+ function checkValueIsNumberOrFormula(value, valueName) {
30246
+ if (value.startsWith("=")) {
30247
+ return "Success" /* CommandResult.Success */;
30248
+ }
30248
30249
  if (isNaN(value)) {
30249
30250
  switch (valueName) {
30250
30251
  case "rangeMin":
@@ -30271,7 +30272,7 @@ class GaugeChart extends AbstractChart {
30271
30272
  this.background = definition.background;
30272
30273
  }
30273
30274
  static validateChartDefinition(validator, definition) {
30274
- return validator.checkValidations(definition, isDataRangeValid, validator.chainValidations(checkRangeLimits(checkEmpty, validator.batchValidations), checkRangeLimits(checkNaN, validator.batchValidations), checkRangeMinBiggerThanRangeMax), validator.chainValidations(checkInflectionPointsValue(checkNaN, validator.batchValidations)));
30275
+ return validator.checkValidations(definition, isDataRangeValid, validator.chainValidations(checkRangeLimits(checkEmpty, validator.batchValidations), checkRangeLimits(checkValueIsNumberOrFormula, validator.batchValidations)), validator.chainValidations(checkInflectionPointsValue(checkValueIsNumberOrFormula, validator.batchValidations)));
30275
30276
  }
30276
30277
  static transformDefinition(definition, executed) {
30277
30278
  let dataRangeZone;
@@ -30312,20 +30313,24 @@ class GaugeChart extends AbstractChart {
30312
30313
  }
30313
30314
  duplicateInDuplicatedSheet(newSheetId) {
30314
30315
  const dataRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.dataRange);
30315
- const definition = this.getDefinitionWithSpecificRanges(dataRange, newSheetId);
30316
+ const adaptFormula = (formula) => this.getters.copyFormulaStringForSheet(this.sheetId, newSheetId, formula, "moveReference");
30317
+ const sectionRule = adaptSectionRuleFormulas(this.sectionRule, adaptFormula);
30318
+ const definition = this.getDefinitionWithSpecificRanges(dataRange, sectionRule, newSheetId);
30316
30319
  return new GaugeChart(definition, newSheetId, this.getters);
30317
30320
  }
30318
30321
  copyInSheetId(sheetId) {
30319
- const definition = this.getDefinitionWithSpecificRanges(this.dataRange, sheetId);
30322
+ const adaptFormula = (formula) => this.getters.copyFormulaStringForSheet(this.sheetId, sheetId, formula, "keepSameReference");
30323
+ const sectionRule = adaptSectionRuleFormulas(this.sectionRule, adaptFormula);
30324
+ const definition = this.getDefinitionWithSpecificRanges(this.dataRange, sectionRule, sheetId);
30320
30325
  return new GaugeChart(definition, sheetId, this.getters);
30321
30326
  }
30322
30327
  getDefinition() {
30323
- return this.getDefinitionWithSpecificRanges(this.dataRange);
30328
+ return this.getDefinitionWithSpecificRanges(this.dataRange, this.sectionRule);
30324
30329
  }
30325
- getDefinitionWithSpecificRanges(dataRange, targetSheetId) {
30330
+ getDefinitionWithSpecificRanges(dataRange, sectionRule, targetSheetId) {
30326
30331
  return {
30327
30332
  background: this.background,
30328
- sectionRule: this.sectionRule,
30333
+ sectionRule: sectionRule,
30329
30334
  title: this.title,
30330
30335
  type: "gauge",
30331
30336
  dataRange: dataRange
@@ -30346,11 +30351,10 @@ class GaugeChart extends AbstractChart {
30346
30351
  };
30347
30352
  }
30348
30353
  updateRanges(applyChange) {
30349
- const range = adaptChartRange(this.dataRange, applyChange);
30350
- if (this.dataRange === range) {
30351
- return this;
30352
- }
30353
- const definition = this.getDefinitionWithSpecificRanges(range);
30354
+ const dataRange = adaptChartRange(this.dataRange, applyChange);
30355
+ const adaptFormula = (formula) => this.getters.adaptFormulaStringDependencies(this.sheetId, formula, applyChange);
30356
+ const sectionRule = adaptSectionRuleFormulas(this.sectionRule, adaptFormula);
30357
+ const definition = this.getDefinitionWithSpecificRanges(dataRange, sectionRule);
30354
30358
  return new GaugeChart(definition, this.sheetId, this.getters);
30355
30359
  }
30356
30360
  }
@@ -30373,12 +30377,18 @@ function createGaugeChartRuntime(chart, getters) {
30373
30377
  format = cell.format;
30374
30378
  }
30375
30379
  }
30376
- const minValue = Number(chart.sectionRule.rangeMin);
30377
- const maxValue = Number(chart.sectionRule.rangeMax);
30380
+ let minValue = getFormulaNumberValue(chart.sheetId, chart.sectionRule.rangeMin, getters);
30381
+ let maxValue = getFormulaNumberValue(chart.sheetId, chart.sectionRule.rangeMax, getters);
30382
+ if (minValue === undefined || maxValue === undefined) {
30383
+ return getInvalidGaugeRuntime(chart, getters);
30384
+ }
30385
+ if (maxValue < minValue) {
30386
+ [minValue, maxValue] = [maxValue, minValue];
30387
+ }
30378
30388
  const lowerPoint = chart.sectionRule.lowerInflectionPoint;
30379
30389
  const upperPoint = chart.sectionRule.upperInflectionPoint;
30380
- const lowerPointValue = getSectionThresholdValue(lowerPoint, minValue, maxValue);
30381
- const upperPointValue = getSectionThresholdValue(upperPoint, minValue, maxValue);
30390
+ const lowerPointValue = getSectionThresholdValue(chart.sheetId, chart.sectionRule.lowerInflectionPoint, minValue, maxValue, getters);
30391
+ const upperPointValue = getSectionThresholdValue(chart.sheetId, chart.sectionRule.upperInflectionPoint, minValue, maxValue, getters);
30382
30392
  const inflectionValues = [];
30383
30393
  const colors = [];
30384
30394
  if (lowerPointValue !== undefined) {
@@ -30426,16 +30436,46 @@ function createGaugeChartRuntime(chart, getters) {
30426
30436
  colors,
30427
30437
  };
30428
30438
  }
30429
- function getSectionThresholdValue(threshold, minValue, maxValue) {
30430
- if (threshold.value === "" || isNaN(Number(threshold.value))) {
30439
+ function getSectionThresholdValue(sheetId, threshold, minValue, maxValue, getters) {
30440
+ const numberValue = getFormulaNumberValue(sheetId, threshold.value, getters);
30441
+ if (numberValue === undefined) {
30431
30442
  return undefined;
30432
30443
  }
30433
- const numberValue = Number(threshold.value);
30434
30444
  const value = threshold.type === "number"
30435
30445
  ? numberValue
30436
30446
  : minValue + ((maxValue - minValue) * numberValue) / 100;
30437
30447
  return clip(value, minValue, maxValue);
30438
30448
  }
30449
+ function getFormulaNumberValue(sheetId, formula, getters) {
30450
+ const value = getters.evaluateFormula(sheetId, formula);
30451
+ return isMatrix(value) ? undefined : tryToNumber(value, getters.getLocale());
30452
+ }
30453
+ function getInvalidGaugeRuntime(chart, getters) {
30454
+ return {
30455
+ background: getters.getStyleOfSingleCellChart(chart.background, chart.dataRange).background,
30456
+ title: chart.title ?? { text: "" },
30457
+ minValue: { value: 0, label: "" },
30458
+ maxValue: { value: 100, label: "" },
30459
+ gaugeValue: { value: 0, label: CellErrorType.GenericError },
30460
+ inflectionValues: [],
30461
+ colors: [],
30462
+ };
30463
+ }
30464
+ function adaptSectionRuleFormulas(sectionRule, adaptCallback) {
30465
+ return {
30466
+ ...sectionRule,
30467
+ rangeMin: adaptCallback(sectionRule.rangeMin),
30468
+ rangeMax: adaptCallback(sectionRule.rangeMax),
30469
+ lowerInflectionPoint: {
30470
+ ...sectionRule.lowerInflectionPoint,
30471
+ value: adaptCallback(sectionRule.lowerInflectionPoint.value),
30472
+ },
30473
+ upperInflectionPoint: {
30474
+ ...sectionRule.upperInflectionPoint,
30475
+ value: adaptCallback(sectionRule.upperInflectionPoint.value),
30476
+ },
30477
+ };
30478
+ }
30439
30479
 
30440
30480
  class GeoChart extends AbstractChart {
30441
30481
  dataSets;
@@ -37323,8 +37363,8 @@ function useDragAndDropListItems() {
37323
37363
  document.body.style.cursor = "move";
37324
37364
  state.draggedItemId = args.draggedItemId;
37325
37365
  const container = direction === "horizontal"
37326
- ? new HorizontalContainer(args.containerEl)
37327
- : new VerticalContainer(args.containerEl);
37366
+ ? new HorizontalContainer(args.scrollableContainerEl)
37367
+ : new VerticalContainer(args.scrollableContainerEl);
37328
37368
  dndHelper = new DOMDndHelper({
37329
37369
  ...args,
37330
37370
  container,
@@ -37335,8 +37375,8 @@ function useDragAndDropListItems() {
37335
37375
  const stopListening = startDnd(dndHelper.onMouseMove.bind(dndHelper), dndHelper.onMouseUp.bind(dndHelper));
37336
37376
  cleanupFns.push(stopListening);
37337
37377
  const onScroll = dndHelper.onScroll.bind(dndHelper);
37338
- args.containerEl.addEventListener("scroll", onScroll);
37339
- cleanupFns.push(() => args.containerEl.removeEventListener("scroll", onScroll));
37378
+ args.scrollableContainerEl.addEventListener("scroll", onScroll);
37379
+ cleanupFns.push(() => args.scrollableContainerEl.removeEventListener("scroll", onScroll));
37340
37380
  cleanupFns.push(dndHelper.destroy.bind(dndHelper));
37341
37381
  };
37342
37382
  owl.onWillUnmount(() => {
@@ -38047,7 +38087,7 @@ class SelectionInput extends owl.Component {
38047
38087
  draggedItemId: rangeId.toString(),
38048
38088
  initialMousePosition: event.clientY,
38049
38089
  items: draggableItems,
38050
- containerEl: this.selectionRef.el,
38090
+ scrollableContainerEl: this.selectionRef.el,
38051
38091
  onDragEnd: (dimensionName, finalIndex) => {
38052
38092
  const originalIndex = draggableIds.findIndex((id) => id === rangeId);
38053
38093
  if (originalIndex === finalIndex) {
@@ -39616,1897 +39656,1954 @@ class GaugeChartConfigPanel extends owl.Component {
39616
39656
  }
39617
39657
  }
39618
39658
 
39619
- css /* scss */ `
39620
- .o-gauge-color-set {
39621
- table {
39622
- table-layout: fixed;
39623
- margin-top: 2%;
39624
- display: table;
39625
- text-align: left;
39626
- font-size: 12px;
39627
- line-height: 18px;
39628
- width: 100%;
39629
- font-size: 12px;
39659
+ class DOMFocusableElementStore {
39660
+ mutators = ["setFocusableElement"];
39661
+ focusableElement = undefined;
39662
+ setFocusableElement(element) {
39663
+ this.focusableElement = element;
39630
39664
  }
39665
+ }
39631
39666
 
39632
- td {
39633
- box-sizing: border-box;
39634
- height: 30px;
39635
- padding: 6px 0;
39636
- }
39637
- th.o-gauge-color-set-colorPicker {
39638
- width: 8%;
39639
- }
39640
- th.o-gauge-color-set-text {
39641
- width: 25%;
39642
- }
39643
- th.o-gauge-color-set-operator {
39644
- width: 10%;
39645
- }
39646
- th.o-gauge-color-set-value {
39647
- width: 22%;
39648
- }
39649
- th.o-gauge-color-set-type {
39650
- width: 30%;
39667
+ css /* scss */ `
39668
+ .o-autocomplete-dropdown {
39669
+ pointer-events: auto;
39670
+ cursor: pointer;
39671
+ background-color: #fff;
39672
+ max-width: 400px;
39673
+ z-index: 1;
39674
+
39675
+ .o-autocomplete-value-focus {
39676
+ background-color: #f2f2f2;
39651
39677
  }
39652
- input,
39653
- select {
39654
- width: 100%;
39655
- height: 100%;
39656
- box-sizing: border-box;
39678
+
39679
+ & > div {
39680
+ padding: 1px 5px 5px 5px;
39681
+ .o-autocomplete-description {
39682
+ padding-left: 5px;
39683
+ font-size: 11px;
39684
+ }
39657
39685
  }
39658
39686
  }
39659
39687
  `;
39660
- class GaugeChartDesignPanel extends owl.Component {
39661
- static template = "o-spreadsheet-GaugeChartDesignPanel";
39662
- static components = {
39663
- SidePanelCollapsible,
39664
- Section,
39665
- RoundColorPicker,
39666
- GeneralDesignEditor,
39667
- ChartErrorSection,
39668
- };
39688
+ class TextValueProvider extends owl.Component {
39689
+ static template = "o-spreadsheet-TextValueProvider";
39669
39690
  static props = {
39670
- figureId: String,
39671
- definition: Object,
39672
- updateChart: Function,
39673
- canUpdateChart: { type: Function, optional: true },
39691
+ proposals: Array,
39692
+ selectedIndex: { type: Number, optional: true },
39693
+ onValueSelected: Function,
39694
+ onValueHovered: Function,
39674
39695
  };
39675
- state;
39696
+ autoCompleteListRef = owl.useRef("autoCompleteList");
39676
39697
  setup() {
39677
- this.state = owl.useState({
39678
- sectionRuleDispatchResult: undefined,
39679
- sectionRule: deepCopy(this.props.definition.sectionRule),
39680
- });
39698
+ owl.useEffect(() => {
39699
+ const selectedIndex = this.props.selectedIndex;
39700
+ if (selectedIndex === undefined) {
39701
+ return;
39702
+ }
39703
+ const selectedElement = this.autoCompleteListRef.el?.children[selectedIndex];
39704
+ selectedElement?.scrollIntoView?.({ block: "nearest" });
39705
+ }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
39681
39706
  }
39682
- get designErrorMessages() {
39683
- const cancelledReasons = [...(this.state.sectionRuleDispatchResult?.reasons || [])];
39684
- return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
39707
+ }
39708
+
39709
+ class AutoCompleteStore extends SpreadsheetStore {
39710
+ mutators = ["useProvider", "moveSelection", "hide", "selectIndex"];
39711
+ selectedIndex = undefined;
39712
+ provider;
39713
+ get selectedProposal() {
39714
+ if (this.selectedIndex === undefined || this.provider === undefined) {
39715
+ return undefined;
39716
+ }
39717
+ return this.provider.proposals[this.selectedIndex];
39685
39718
  }
39686
- isRangeMinInvalid() {
39687
- return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */) ||
39688
- this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */) ||
39689
- this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */));
39719
+ useProvider(provider) {
39720
+ this.provider = provider;
39721
+ this.selectedIndex = provider.autoSelectFirstProposal ? 0 : undefined;
39690
39722
  }
39691
- isRangeMaxInvalid() {
39692
- return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */) ||
39693
- this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */) ||
39694
- this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */));
39723
+ hide() {
39724
+ this.provider = undefined;
39725
+ this.selectedIndex = undefined;
39695
39726
  }
39696
- // ---------------------------------------------------------------------------
39697
- // COLOR_SECTION_TEMPLATE
39698
- // ---------------------------------------------------------------------------
39699
- get isLowerInflectionPointInvalid() {
39700
- return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */) ||
39701
- this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeLowerBiggerThanUpper" /* CommandResult.GaugeLowerBiggerThanUpper */));
39727
+ selectIndex(index) {
39728
+ this.selectedIndex = index;
39702
39729
  }
39703
- get isUpperInflectionPointInvalid() {
39704
- return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */) ||
39705
- this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeLowerBiggerThanUpper" /* CommandResult.GaugeLowerBiggerThanUpper */));
39730
+ moveSelection(direction) {
39731
+ if (!this.provider) {
39732
+ return;
39733
+ }
39734
+ if (this.selectedIndex === undefined) {
39735
+ this.selectedIndex = 0;
39736
+ return;
39737
+ }
39738
+ if (direction === "previous") {
39739
+ this.selectedIndex--;
39740
+ if (this.selectedIndex < 0) {
39741
+ this.selectedIndex = this.provider.proposals.length - 1;
39742
+ }
39743
+ }
39744
+ else {
39745
+ this.selectedIndex = (this.selectedIndex + 1) % this.provider.proposals.length;
39746
+ }
39706
39747
  }
39707
- updateSectionColor(target, color) {
39708
- const sectionRule = deepCopy(this.state.sectionRule);
39709
- sectionRule.colors[target] = color;
39710
- this.updateSectionRule(sectionRule);
39748
+ }
39749
+
39750
+ class ContentEditableHelper {
39751
+ // todo make el private and expose dedicated methods
39752
+ el;
39753
+ constructor(el) {
39754
+ this.el = el;
39711
39755
  }
39712
- updateSectionRule(sectionRule) {
39713
- this.state.sectionRuleDispatchResult = this.props.updateChart(this.props.figureId, {
39714
- sectionRule,
39715
- });
39716
- if (this.state.sectionRuleDispatchResult.isSuccessful) {
39717
- this.state.sectionRule = deepCopy(sectionRule);
39756
+ updateEl(el) {
39757
+ this.el = el;
39758
+ }
39759
+ /**
39760
+ * select the text at position start to end, no matter the children
39761
+ */
39762
+ selectRange(start, end) {
39763
+ let selection = window.getSelection();
39764
+ const { start: currentStart, end: currentEnd } = this.getCurrentSelection();
39765
+ if (currentStart === start && currentEnd === end) {
39766
+ return;
39767
+ }
39768
+ if (selection.rangeCount === 0) {
39769
+ const range = document.createRange();
39770
+ selection.addRange(range);
39771
+ }
39772
+ const currentRange = selection.getRangeAt(0);
39773
+ let range;
39774
+ if (this.el.contains(currentRange.startContainer)) {
39775
+ range = currentRange;
39776
+ }
39777
+ else {
39778
+ range = document.createRange();
39779
+ selection.removeAllRanges();
39780
+ selection.addRange(range);
39781
+ }
39782
+ if (start === end && start === 0) {
39783
+ range.setStart(this.el, 0);
39784
+ range.setEnd(this.el, 0);
39785
+ }
39786
+ else {
39787
+ const textLength = this.getText().length;
39788
+ if (start < 0 || end > textLength) {
39789
+ console.warn(`wrong selection asked start ${start}, end ${end}, text content length ${textLength}`);
39790
+ if (start < 0)
39791
+ start = 0;
39792
+ if (end > textLength)
39793
+ end = textLength;
39794
+ if (start > textLength)
39795
+ start = textLength;
39796
+ }
39797
+ let startNode = this.findChildAtCharacterIndex(start);
39798
+ let endNode = this.findChildAtCharacterIndex(end);
39799
+ range.setStart(startNode.node, startNode.offset);
39800
+ range.setEnd(endNode.node, endNode.offset);
39718
39801
  }
39719
39802
  }
39720
- canUpdateSectionRule(sectionRule) {
39721
- this.state.sectionRuleDispatchResult = this.props.canUpdateChart(this.props.figureId, {
39722
- sectionRule,
39723
- });
39803
+ /**
39804
+ * finds the dom element that contains the character at `offset`
39805
+ */
39806
+ findChildAtCharacterIndex(offset) {
39807
+ let it = iterateChildren(this.el);
39808
+ let current, previous;
39809
+ let usedCharacters = offset;
39810
+ let isFirstParagraph = true;
39811
+ do {
39812
+ current = it.next();
39813
+ if (!current.done && !current.value.hasChildNodes()) {
39814
+ if (current.value.textContent && current.value.textContent.length < usedCharacters) {
39815
+ usedCharacters -= current.value.textContent.length;
39816
+ }
39817
+ else if (current.value.textContent &&
39818
+ current.value.textContent.length >= usedCharacters) {
39819
+ it.return(current.value);
39820
+ }
39821
+ previous = current.value;
39822
+ }
39823
+ // One new paragraph = one new line character, except for the first paragraph
39824
+ if (!current.done && current.value.nodeName === "P") {
39825
+ if (isFirstParagraph) {
39826
+ isFirstParagraph = false;
39827
+ }
39828
+ else {
39829
+ usedCharacters--;
39830
+ }
39831
+ }
39832
+ } while (!current.done && usedCharacters);
39833
+ if (current.value) {
39834
+ return { node: current.value, offset: usedCharacters };
39835
+ }
39836
+ return { node: previous, offset: usedCharacters };
39724
39837
  }
39725
- }
39726
-
39727
- class GeoChartRegionSelectSection extends owl.Component {
39728
- static template = "o-spreadsheet-GeoChartRegionSelectSection";
39729
- static components = { Section };
39730
- static props = {
39731
- figureId: String,
39732
- definition: Object,
39733
- updateChart: Function,
39734
- };
39735
- updateSelectedRegion(ev) {
39736
- const value = ev.target.value;
39737
- this.props.updateChart(this.props.figureId, { region: value });
39838
+ /**
39839
+ * Sets (or Replaces all) the text inside the root element in the form of distinctive paragraphs and
39840
+ * span for each element provided in `contents`.
39841
+ *
39842
+ * The function will apply the diff between the current content and the new content to avoid the systematic
39843
+ * destruction of DOM elements which interferes with IME[1]
39844
+ *
39845
+ * Each line of text will be encapsulated in a paragraph element.
39846
+ * Each span will have its own fontcolor and specific class if provided in the HtmlContent object.
39847
+ *
39848
+ * [1] https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor
39849
+ */
39850
+ setText(contents) {
39851
+ if (contents.length === 0) {
39852
+ this.removeAll();
39853
+ return;
39854
+ }
39855
+ const childElements = Array.from(this.el.childNodes);
39856
+ const contentLength = contents.length;
39857
+ for (let i = 0; i < contentLength; i++) {
39858
+ const line = contents[i];
39859
+ const childElement = childElements[i];
39860
+ let newChild = false;
39861
+ let p;
39862
+ if (childElement && childElement.nodeName === "P") {
39863
+ p = childElement;
39864
+ }
39865
+ else {
39866
+ newChild = true;
39867
+ p = document.createElement("p");
39868
+ }
39869
+ const lineLength = line.length;
39870
+ const existingChildren = Array.from(p.childNodes);
39871
+ for (let j = 0; j < lineLength; j++) {
39872
+ const content = line[j];
39873
+ const child = existingChildren[j];
39874
+ // child nodes can be multiple types of nodes: Span, Text, Div, etc...
39875
+ // We can only modify a node in place if it has the same type as the content
39876
+ // that we would insert, which are spans.
39877
+ // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
39878
+ // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)
39879
+ const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
39880
+ if (childIsSpan && compareContentToSpanElement(content, child)) {
39881
+ continue;
39882
+ }
39883
+ // this is an empty line in the content
39884
+ if (!content.value && !content.class) {
39885
+ if (child)
39886
+ p.removeChild(child);
39887
+ continue;
39888
+ }
39889
+ const span = document.createElement("span");
39890
+ span.innerText = content.value;
39891
+ span.style.color = content.color || "";
39892
+ if (content.class) {
39893
+ span.classList.add(content.class);
39894
+ }
39895
+ if (child) {
39896
+ p.replaceChild(span, child);
39897
+ }
39898
+ else {
39899
+ p.appendChild(span);
39900
+ }
39901
+ }
39902
+ if (existingChildren.length > lineLength) {
39903
+ for (let i = lineLength; i < existingChildren.length; i++) {
39904
+ p.removeChild(existingChildren[i]);
39905
+ }
39906
+ }
39907
+ // Empty line
39908
+ if (!p.hasChildNodes()) {
39909
+ const span = document.createElement("span");
39910
+ span.appendChild(document.createElement("br"));
39911
+ p.appendChild(span);
39912
+ }
39913
+ // replace p if necessary
39914
+ if (newChild) {
39915
+ if (childElement) {
39916
+ this.el.replaceChild(p, childElement);
39917
+ }
39918
+ else {
39919
+ this.el.appendChild(p);
39920
+ }
39921
+ }
39922
+ }
39923
+ if (childElements.length > contentLength) {
39924
+ for (let i = contentLength; i < childElements.length; i++) {
39925
+ this.el.removeChild(childElements[i]);
39926
+ }
39927
+ }
39738
39928
  }
39739
- get availableRegions() {
39740
- return this.env.model.getters.getGeoChartAvailableRegions();
39929
+ scrollSelectionIntoView() {
39930
+ const focusedNode = document.getSelection()?.focusNode;
39931
+ if (!focusedNode || !this.el.contains(focusedNode))
39932
+ return;
39933
+ const element = focusedNode instanceof HTMLElement ? focusedNode : focusedNode.parentElement;
39934
+ element?.scrollIntoView?.({ block: "nearest" });
39741
39935
  }
39742
- get selectedRegion() {
39743
- return this.props.definition.region || this.availableRegions[0]?.id;
39936
+ /**
39937
+ * remove the current selection of the user
39938
+ * */
39939
+ removeSelection() {
39940
+ let selection = window.getSelection();
39941
+ selection.removeAllRanges();
39744
39942
  }
39745
- }
39746
-
39747
- class GeoChartConfigPanel extends GenericChartConfigPanel {
39748
- static template = "o-spreadsheet-GeoChartConfigPanel";
39749
- static components = { ...GenericChartConfigPanel.components, GeoChartRegionSelectSection };
39750
- get dataRanges() {
39751
- return this.getDataSeriesRanges().slice(0, 1);
39943
+ removeAll() {
39944
+ if (this.el) {
39945
+ while (this.el.firstChild) {
39946
+ this.el.removeChild(this.el.firstChild);
39947
+ }
39948
+ }
39752
39949
  }
39753
- onDataSeriesConfirmed() {
39754
- this.dataSets = spreadRange(this.env.model.getters, this.dataSets).slice(0, 1);
39755
- this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
39756
- dataSets: this.dataSets,
39757
- });
39950
+ /**
39951
+ * finds the indexes of the current selection.
39952
+ * */
39953
+ getCurrentSelection() {
39954
+ return getCurrentSelection(this.el);
39758
39955
  }
39759
- getLabelRangeOptions() {
39760
- return [
39761
- {
39762
- name: "dataSetsHaveTitle",
39763
- label: this.dataSetsHaveTitleLabel,
39764
- value: this.props.definition.dataSetsHaveTitle,
39765
- onChange: this.onUpdateDataSetsHaveTitle.bind(this),
39766
- },
39767
- ];
39956
+ getText() {
39957
+ let text = "";
39958
+ let it = iterateChildren(this.el);
39959
+ let current = it.next();
39960
+ let isFirstParagraph = true;
39961
+ while (!current.done) {
39962
+ if (!current.value.hasChildNodes()) {
39963
+ text += current.value.textContent;
39964
+ }
39965
+ if (current.value.nodeName === "P" ||
39966
+ (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
39967
+ ) {
39968
+ if (isFirstParagraph) {
39969
+ isFirstParagraph = false;
39970
+ }
39971
+ else {
39972
+ text += NEWLINE;
39973
+ }
39974
+ }
39975
+ current = it.next();
39976
+ }
39977
+ return text;
39768
39978
  }
39769
39979
  }
39980
+ function compareContentToSpanElement(content, node) {
39981
+ const contentColor = content.color ? toHex(content.color) : "";
39982
+ const nodeColor = node.style?.color ? toHex(node.style.color) : "";
39983
+ const sameColor = contentColor === nodeColor;
39984
+ const sameClass = deepEquals([content.class], [...node.classList]);
39985
+ const sameContent = node.innerText === content.value;
39986
+ return sameColor && sameClass && sameContent;
39987
+ }
39770
39988
 
39771
- const DEFAULT_CUSTOM_COLOR_SCALE = {
39772
- minColor: "#FFF5EB",
39773
- midColor: "#FD8D3C",
39774
- maxColor: "#7F2704",
39775
- };
39776
- class GeoChartDesignPanel extends ChartWithAxisDesignPanel {
39777
- static template = "o-spreadsheet-GeoChartDesignPanel";
39778
- static components = { ...ChartWithAxisDesignPanel.components, RoundColorPicker };
39779
- colorScalesChoices = ChartTerms.GeoChart.ColorScales;
39780
- updateColorScaleType(ev) {
39781
- const value = ev.target.value;
39782
- value === "custom"
39783
- ? this.updateColorScale(DEFAULT_CUSTOM_COLOR_SCALE)
39784
- : this.updateColorScale(value);
39989
+ // -----------------------------------------------------------------------------
39990
+ // Formula Assistant component
39991
+ // -----------------------------------------------------------------------------
39992
+ css /* scss */ `
39993
+ .o-formula-assistant {
39994
+ background: #ffffff;
39995
+ .o-formula-assistant-head {
39996
+ background-color: #f2f2f2;
39997
+ padding: 10px;
39785
39998
  }
39786
- updateColorScale(colorScale) {
39787
- this.props.updateChart(this.props.figureId, { colorScale });
39999
+ .collapsed {
40000
+ transform: rotate(180deg);
39788
40001
  }
39789
- updateMissingValueColor(color) {
39790
- this.props.updateChart(this.props.figureId, { missingValueColor: color });
40002
+ .o-formula-assistant-core {
40003
+ border-bottom: 1px solid gray;
39791
40004
  }
39792
- updateLegendPosition(ev) {
39793
- const value = ev.target.value;
39794
- this.props.updateChart(this.props.figureId, { legendPosition: value });
40005
+ .o-formula-assistant-arg-description {
40006
+ font-size: 85%;
39795
40007
  }
39796
- get selectedColorScale() {
39797
- return typeof this.props.definition.colorScale === "object"
39798
- ? "custom"
39799
- : this.props.definition.colorScale || "oranges";
40008
+ .o-formula-assistant-focus {
40009
+ div:first-child,
40010
+ span {
40011
+ color: ${COMPOSER_ASSISTANT_COLOR};
40012
+ text-shadow: 0px 0px 1px ${COMPOSER_ASSISTANT_COLOR};
40013
+ }
40014
+ div:last-child {
40015
+ color: black;
40016
+ }
39800
40017
  }
39801
- get selectedMissingValueColor() {
39802
- return this.props.definition.missingValueColor || "#ffffff";
40018
+ .o-formula-assistant-gray {
40019
+ color: gray;
39803
40020
  }
39804
- get customColorScale() {
39805
- if (typeof this.props.definition.colorScale === "object") {
39806
- return this.props.definition.colorScale;
39807
- }
39808
- return undefined;
40021
+ }
40022
+ `;
40023
+ class FunctionDescriptionProvider extends owl.Component {
40024
+ static template = "o-spreadsheet-FunctionDescriptionProvider";
40025
+ static props = {
40026
+ functionName: String,
40027
+ functionDescription: Object,
40028
+ argToFocus: Number,
40029
+ };
40030
+ getContext() {
40031
+ return this.props;
39809
40032
  }
39810
- getCustomColorScaleColor(color) {
39811
- return this.customColorScale?.[color] ?? "";
39812
- }
39813
- setCustomColorScaleColor(colorType, color) {
39814
- if (!color && colorType !== "midColor") {
39815
- color = "#fff";
39816
- }
39817
- const customColorScale = this.customColorScale;
39818
- if (!customColorScale) {
39819
- return;
39820
- }
39821
- this.updateColorScale({ ...customColorScale, [colorType]: color });
40033
+ get formulaArgSeparator() {
40034
+ return this.env.model.getters.getLocale().formulaArgSeparator + " ";
39822
40035
  }
39823
40036
  }
39824
40037
 
39825
- class LineConfigPanel extends GenericChartConfigPanel {
39826
- static template = "o-spreadsheet-LineConfigPanel";
39827
- get canTreatLabelsAsText() {
39828
- const chart = this.env.model.getters.getChart(this.props.figureId);
39829
- if (chart && chart instanceof LineChart) {
39830
- return canChartParseLabels(chart.labelRange, this.env.model.getters);
39831
- }
39832
- return false;
39833
- }
39834
- get stackedLabel() {
39835
- const definition = this.props.definition;
39836
- return definition.fillArea
39837
- ? this.chartTerms.StackedAreaChart
39838
- : this.chartTerms.StackedLineChart;
39839
- }
39840
- getLabelRangeOptions() {
39841
- const options = super.getLabelRangeOptions();
39842
- if (this.canTreatLabelsAsText) {
39843
- options.push({
39844
- name: "labelsAsText",
39845
- value: this.props.definition.labelsAsText,
39846
- label: this.chartTerms.TreatLabelsAsText,
39847
- onChange: this.onUpdateLabelsAsText.bind(this),
39848
- });
39849
- }
39850
- return options;
39851
- }
39852
- onUpdateLabelsAsText(labelsAsText) {
39853
- this.props.updateChart(this.props.figureId, {
39854
- labelsAsText,
39855
- });
39856
- }
39857
- onUpdateStacked(stacked) {
39858
- this.props.updateChart(this.props.figureId, {
39859
- stacked,
39860
- });
39861
- }
39862
- onUpdateCumulative(cumulative) {
39863
- this.props.updateChart(this.props.figureId, {
39864
- cumulative,
39865
- });
39866
- }
39867
- }
40038
+ const functions = functionRegistry.content;
40039
+ const ASSISTANT_WIDTH = 300;
40040
+ const CLOSE_ICON_RADIUS = 9;
40041
+ const selectionIndicatorClass = "selector-flag";
40042
+ const backgroundClass = "background-flag";
40043
+ const selectionIndicatorColor = "#a9a9a9";
40044
+ const selectionIndicator = "␣";
40045
+ css /* scss */ `
40046
+ .o-composer-container {
40047
+ .o-composer {
40048
+ overflow-y: auto;
40049
+ overflow-x: hidden;
40050
+ word-break: break-all;
40051
+ padding-right: 2px;
39868
40052
 
39869
- class PieChartDesignPanel extends owl.Component {
39870
- static template = "o-spreadsheet-PieChartDesignPanel";
39871
- static components = {
39872
- GeneralDesignEditor,
39873
- Section,
39874
- Checkbox,
39875
- ChartLegend,
39876
- };
39877
- static props = {
39878
- figureId: String,
39879
- definition: Object,
39880
- updateChart: Function,
39881
- canUpdateChart: { type: Function, optional: true },
39882
- };
39883
- }
40053
+ box-sizing: border-box;
39884
40054
 
39885
- class RadarChartDesignPanel extends owl.Component {
39886
- static template = "o-spreadsheet-RadarChartDesignPanel";
39887
- static components = {
39888
- GeneralDesignEditor,
39889
- SeriesDesignEditor,
39890
- Section,
39891
- Checkbox,
39892
- ChartLegend,
39893
- };
39894
- static props = {
39895
- figureId: String,
39896
- definition: Object,
39897
- canUpdateChart: Function,
39898
- updateChart: Function,
39899
- };
39900
- }
40055
+ caret-color: black;
40056
+ padding-left: 3px;
40057
+ padding-right: 3px;
40058
+ outline: none;
39901
40059
 
39902
- class ScatterConfigPanel extends GenericChartConfigPanel {
39903
- static template = "o-spreadsheet-ScatterConfigPanel";
39904
- get canTreatLabelsAsText() {
39905
- const chart = this.env.model.getters.getChart(this.props.figureId);
39906
- if (chart && chart instanceof ScatterChart) {
39907
- return canChartParseLabels(chart.labelRange, this.env.model.getters);
39908
- }
39909
- return false;
39910
- }
39911
- onUpdateLabelsAsText(labelsAsText) {
39912
- this.props.updateChart(this.props.figureId, {
39913
- labelsAsText,
39914
- });
39915
- }
39916
- getLabelRangeOptions() {
39917
- const options = super.getLabelRangeOptions();
39918
- if (this.canTreatLabelsAsText) {
39919
- options.push({
39920
- name: "labelsAsText",
39921
- value: this.props.definition.labelsAsText,
39922
- label: this.chartTerms.TreatLabelsAsText,
39923
- onChange: this.onUpdateLabelsAsText.bind(this),
39924
- });
39925
- }
39926
- return options;
39927
- }
39928
- }
40060
+ p {
40061
+ margin-bottom: 0px;
39929
40062
 
39930
- class ScorecardChartConfigPanel extends owl.Component {
39931
- static template = "o-spreadsheet-ScorecardChartConfigPanel";
39932
- static components = { SelectionInput, ChartErrorSection, Section };
39933
- static props = {
39934
- figureId: String,
39935
- definition: Object,
39936
- updateChart: Function,
39937
- canUpdateChart: Function,
39938
- };
39939
- state = owl.useState({
39940
- keyValueDispatchResult: undefined,
39941
- baselineDispatchResult: undefined,
39942
- });
39943
- keyValue = this.props.definition.keyValue;
39944
- baseline = this.props.definition.baseline;
39945
- get errorMessages() {
39946
- const cancelledReasons = [
39947
- ...(this.state.keyValueDispatchResult?.reasons || []),
39948
- ...(this.state.baselineDispatchResult?.reasons || []),
39949
- ];
39950
- return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
39951
- }
39952
- get isKeyValueInvalid() {
39953
- return !!this.state.keyValueDispatchResult?.isCancelledBecause("InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */);
39954
- }
39955
- get isBaselineInvalid() {
39956
- return !!this.state.keyValueDispatchResult?.isCancelledBecause("InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */);
39957
- }
39958
- onKeyValueRangeChanged(ranges) {
39959
- this.keyValue = ranges[0];
39960
- this.state.keyValueDispatchResult = this.props.canUpdateChart(this.props.figureId, {
39961
- keyValue: this.keyValue,
39962
- });
39963
- }
39964
- updateKeyValueRange() {
39965
- this.state.keyValueDispatchResult = this.props.updateChart(this.props.figureId, {
39966
- keyValue: this.keyValue,
39967
- });
39968
- }
39969
- getKeyValueRange() {
39970
- return this.keyValue || "";
39971
- }
39972
- onBaselineRangeChanged(ranges) {
39973
- this.baseline = ranges[0];
39974
- this.state.baselineDispatchResult = this.props.canUpdateChart(this.props.figureId, {
39975
- baseline: this.baseline,
39976
- });
39977
- }
39978
- updateBaselineRange() {
39979
- this.state.baselineDispatchResult = this.props.updateChart(this.props.figureId, {
39980
- baseline: this.baseline,
39981
- });
39982
- }
39983
- getBaselineRange() {
39984
- return this.baseline || "";
39985
- }
39986
- updateBaselineMode(ev) {
39987
- this.props.updateChart(this.props.figureId, { baselineMode: ev.target.value });
39988
- }
39989
- }
40063
+ span {
40064
+ white-space: pre-wrap;
39990
40065
 
39991
- class ScorecardChartDesignPanel extends owl.Component {
39992
- static template = "o-spreadsheet-ScorecardChartDesignPanel";
39993
- static components = {
39994
- GeneralDesignEditor,
39995
- RoundColorPicker,
39996
- SidePanelCollapsible,
39997
- Section,
39998
- Checkbox,
39999
- };
40000
- static props = {
40001
- figureId: String,
40002
- definition: Object,
40003
- updateChart: Function,
40004
- canUpdateChart: { type: Function, optional: true },
40005
- };
40006
- get colorsSectionTitle() {
40007
- return this.props.definition.baselineMode === "progress"
40008
- ? _t("Progress bar colors")
40009
- : _t("Baseline colors");
40010
- }
40011
- get humanizeNumbersLabel() {
40012
- return _t("Humanize numbers");
40013
- }
40014
- get defaultScorecardTitleFontSize() {
40015
- return SCORECARD_CHART_TITLE_FONT_SIZE;
40016
- }
40017
- updateHumanizeNumbers(humanize) {
40018
- this.props.updateChart(this.props.figureId, { humanize });
40019
- }
40020
- translate(term) {
40021
- return _t(term);
40022
- }
40023
- updateBaselineDescr(ev) {
40024
- this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
40025
- }
40026
- setColor(color, colorPickerId) {
40027
- switch (colorPickerId) {
40028
- case "backgroundColor":
40029
- this.props.updateChart(this.props.figureId, { background: color });
40030
- break;
40031
- case "baselineColorDown":
40032
- this.props.updateChart(this.props.figureId, { baselineColorDown: color });
40033
- break;
40034
- case "baselineColorUp":
40035
- this.props.updateChart(this.props.figureId, { baselineColorUp: color });
40036
- break;
40037
- }
40038
- }
40039
- }
40066
+ &.${selectionIndicatorClass}:after {
40067
+ content: "${selectionIndicator}";
40068
+ color: ${selectionIndicatorColor};
40069
+ }
40040
40070
 
40041
- class WaterfallChartDesignPanel extends owl.Component {
40042
- static template = "o-spreadsheet-WaterfallChartDesignPanel";
40043
- static components = {
40044
- GeneralDesignEditor,
40045
- Checkbox,
40046
- SidePanelCollapsible,
40047
- Section,
40048
- RoundColorPicker,
40049
- AxisDesignEditor,
40050
- RadioSelection,
40051
- ChartLegend,
40052
- };
40053
- static props = {
40054
- figureId: String,
40055
- definition: Object,
40056
- updateChart: Function,
40057
- canUpdateChart: { type: Function, optional: true },
40058
- };
40059
- axisChoices = CHART_AXIS_CHOICES;
40060
- onUpdateShowSubTotals(showSubTotals) {
40061
- this.props.updateChart(this.props.figureId, { showSubTotals });
40062
- }
40063
- onUpdateShowConnectorLines(showConnectorLines) {
40064
- this.props.updateChart(this.props.figureId, { showConnectorLines });
40065
- }
40066
- onUpdateFirstValueAsSubtotal(firstValueAsSubtotal) {
40067
- this.props.updateChart(this.props.figureId, { firstValueAsSubtotal });
40068
- }
40069
- updateColor(colorName, color) {
40070
- this.props.updateChart(this.props.figureId, { [colorName]: color });
40071
- }
40072
- get axesList() {
40073
- return [
40074
- { id: "x", name: _t("Horizontal axis") },
40075
- { id: "y", name: _t("Vertical axis") },
40076
- ];
40077
- }
40078
- get positiveValuesColor() {
40079
- return (this.props.definition.positiveValuesColor ||
40080
- CHART_WATERFALL_POSITIVE_COLOR);
40081
- }
40082
- get negativeValuesColor() {
40083
- return (this.props.definition.negativeValuesColor ||
40084
- CHART_WATERFALL_NEGATIVE_COLOR);
40071
+ &.${backgroundClass} {
40072
+ border-radius: 5px;
40073
+ background-color: lightgray;
40074
+ padding: 0px 1.5px 1.5px 1.5px;
40075
+ }
40076
+ }
40077
+ }
40085
40078
  }
40086
- get subTotalValuesColor() {
40087
- return (this.props.definition.subTotalValuesColor ||
40088
- CHART_WATERFALL_SUBTOTAL_COLOR);
40079
+ .o-composer[placeholder]:empty:not(:focus):not(.active)::before {
40080
+ content: attr(placeholder);
40081
+ color: #bdbdbd;
40082
+ position: relative;
40083
+ top: 0%;
40084
+ pointer-events: none;
40089
40085
  }
40090
- updateVerticalAxisPosition(value) {
40091
- this.props.updateChart(this.props.figureId, {
40092
- verticalAxisPosition: value,
40093
- });
40086
+
40087
+ .fa-stack {
40088
+ /* reset stack size which is doubled by default */
40089
+ width: ${CLOSE_ICON_RADIUS * 2}px;
40090
+ height: ${CLOSE_ICON_RADIUS * 2}px;
40091
+ line-height: ${CLOSE_ICON_RADIUS * 2}px;
40094
40092
  }
40095
- }
40096
40093
 
40097
- const chartSidePanelComponentRegistry = new Registry();
40098
- chartSidePanelComponentRegistry
40099
- .add("line", {
40100
- configuration: LineConfigPanel,
40101
- design: ChartWithAxisDesignPanel,
40102
- })
40103
- .add("scatter", {
40104
- configuration: ScatterConfigPanel,
40105
- design: ChartWithAxisDesignPanel,
40106
- })
40107
- .add("bar", {
40108
- configuration: BarConfigPanel,
40109
- design: ChartWithAxisDesignPanel,
40110
- })
40111
- .add("combo", {
40112
- configuration: GenericChartConfigPanel,
40113
- design: ComboChartDesignPanel,
40114
- })
40115
- .add("pie", {
40116
- configuration: GenericChartConfigPanel,
40117
- design: PieChartDesignPanel,
40118
- })
40119
- .add("gauge", {
40120
- configuration: GaugeChartConfigPanel,
40121
- design: GaugeChartDesignPanel,
40122
- })
40123
- .add("scorecard", {
40124
- configuration: ScorecardChartConfigPanel,
40125
- design: ScorecardChartDesignPanel,
40126
- })
40127
- .add("waterfall", {
40128
- configuration: GenericChartConfigPanel,
40129
- design: WaterfallChartDesignPanel,
40130
- })
40131
- .add("pyramid", {
40132
- configuration: GenericChartConfigPanel,
40133
- design: ChartWithAxisDesignPanel,
40134
- })
40135
- .add("radar", {
40136
- configuration: GenericChartConfigPanel,
40137
- design: RadarChartDesignPanel,
40138
- })
40139
- .add("geo", {
40140
- configuration: GeoChartConfigPanel,
40141
- design: GeoChartDesignPanel,
40142
- });
40094
+ .force-open-assistant {
40095
+ left: -1px;
40096
+ top: -1px;
40143
40097
 
40144
- css /* scss */ `
40145
- .o-section .o-input.o-type-selector {
40146
- height: 30px;
40147
- padding-left: 35px;
40148
- padding-top: 5px;
40149
- }
40150
- .o-type-selector-preview {
40151
- left: 5px;
40152
- top: 3px;
40153
- .o-chart-preview {
40154
- width: 24px;
40155
- height: 24px;
40098
+ .fa-question-circle {
40099
+ color: ${PRIMARY_BUTTON_BG};
40100
+ }
40156
40101
  }
40157
- }
40158
40102
 
40159
- .o-popover .o-chart-select-popover {
40160
- box-sizing: border-box;
40161
- background: #fff;
40162
- .o-chart-type-item {
40163
- cursor: pointer;
40164
- padding: 3px 6px;
40165
- margin: 1px 2px;
40166
- &.selected,
40167
- &:hover {
40168
- border: 1px solid ${ACTION_COLOR};
40169
- background: ${BADGE_SELECTED_COLOR};
40170
- padding: 2px 5px;
40171
- }
40172
- .o-chart-preview {
40173
- width: 48px;
40174
- height: 48px;
40103
+ .o-composer-assistant {
40104
+ position: absolute;
40105
+ margin: 1px 4px;
40106
+
40107
+ .o-semi-bold {
40108
+ /* FIXME: to remove in favor of Bootstrap
40109
+ * 'fw-semibold' when we upgrade to Bootstrap 5.2
40110
+ */
40111
+ font-weight: 600 !important;
40175
40112
  }
40176
40113
  }
40177
40114
  }
40178
40115
  `;
40179
- class ChartTypePicker extends owl.Component {
40180
- static template = "o-spreadsheet-ChartTypePicker";
40181
- static components = { Section, Popover };
40182
- static props = { figureId: String, chartPanelStore: Object };
40183
- categories = chartCategories;
40184
- chartTypeByCategories = {};
40185
- popoverRef = owl.useRef("popoverRef");
40186
- selectRef = owl.useRef("selectRef");
40187
- state = owl.useState({ popoverProps: undefined, popoverStyle: "" });
40188
- setup() {
40189
- owl.useExternalListener(window, "pointerdown", this.onExternalClick, { capture: true });
40190
- for (const subtypeProperties of chartSubtypeRegistry.getAll()) {
40191
- if (this.chartTypeByCategories[subtypeProperties.category]) {
40192
- this.chartTypeByCategories[subtypeProperties.category].push(subtypeProperties);
40116
+ class Composer extends owl.Component {
40117
+ static template = "o-spreadsheet-Composer";
40118
+ static props = {
40119
+ focus: {
40120
+ validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value),
40121
+ },
40122
+ inputStyle: { type: String, optional: true },
40123
+ rect: { type: Object, optional: true },
40124
+ delimitation: { type: Object, optional: true },
40125
+ onComposerCellFocused: { type: Function, optional: true },
40126
+ onComposerContentFocused: Function,
40127
+ isDefaultFocus: { type: Boolean, optional: true },
40128
+ onInputContextMenu: { type: Function, optional: true },
40129
+ composerStore: Object,
40130
+ placeholder: { type: String, optional: true },
40131
+ };
40132
+ static components = { TextValueProvider, FunctionDescriptionProvider };
40133
+ static defaultProps = {
40134
+ inputStyle: "",
40135
+ isDefaultFocus: false,
40136
+ };
40137
+ DOMFocusableElementStore;
40138
+ composerRef = owl.useRef("o_composer");
40139
+ contentHelper = new ContentEditableHelper(this.composerRef.el);
40140
+ composerState = owl.useState({
40141
+ positionStart: 0,
40142
+ positionEnd: 0,
40143
+ });
40144
+ autoCompleteState;
40145
+ functionDescriptionState = owl.useState({
40146
+ showDescription: false,
40147
+ functionName: "",
40148
+ functionDescription: {},
40149
+ argToFocus: 0,
40150
+ });
40151
+ assistant = owl.useState({
40152
+ forcedClosed: false,
40153
+ });
40154
+ compositionActive = false;
40155
+ spreadsheetRect = useSpreadsheetRect();
40156
+ get assistantStyle() {
40157
+ const composerRect = this.composerRef.el.getBoundingClientRect();
40158
+ const assistantStyle = {};
40159
+ assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40160
+ const proposals = this.autoCompleteState.provider?.proposals;
40161
+ const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40162
+ if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
40163
+ assistantStyle.width = `${ASSISTANT_WIDTH}px`;
40164
+ }
40165
+ if (this.props.delimitation && this.props.rect) {
40166
+ const { x: cellX, y: cellY, height: cellHeight } = this.props.rect;
40167
+ const remainingHeight = this.props.delimitation.height - (cellY + cellHeight);
40168
+ assistantStyle["max-height"] = `${remainingHeight}px`;
40169
+ if (cellY > remainingHeight) {
40170
+ const availableSpaceAbove = cellY;
40171
+ assistantStyle["max-height"] = `${availableSpaceAbove - CLOSE_ICON_RADIUS}px`;
40172
+ // render top
40173
+ // We compensate 2 px of margin on the assistant style + 1px for design reasons
40174
+ assistantStyle.top = `-3px`;
40175
+ assistantStyle.transform = `translate(0, -100%)`;
40193
40176
  }
40194
- else {
40195
- this.chartTypeByCategories[subtypeProperties.category] = [subtypeProperties];
40177
+ if (cellX + ASSISTANT_WIDTH > this.props.delimitation.width) {
40178
+ // render left
40179
+ assistantStyle.right = `0px`;
40196
40180
  }
40197
40181
  }
40198
- }
40199
- onExternalClick(ev) {
40200
- if (isChildEvent(this.popoverRef.el?.parentElement, ev) ||
40201
- isChildEvent(this.selectRef.el, ev)) {
40202
- return;
40182
+ else {
40183
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40184
+ if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40185
+ this.spreadsheetRect.width) {
40186
+ assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40187
+ }
40203
40188
  }
40204
- this.closePopover();
40205
- }
40206
- onTypeChange(type) {
40207
- this.props.chartPanelStore.changeChartType(this.props.figureId, type);
40208
- this.closePopover();
40209
- }
40210
- getChartDefinition(figureId) {
40211
- return this.env.model.getters.getChartDefinition(figureId);
40189
+ return cssPropertiesToCss(assistantStyle);
40212
40190
  }
40213
- getSelectedChartSubtypeProperties() {
40214
- const definition = this.getChartDefinition(this.props.figureId);
40215
- const matchedChart = chartSubtypeRegistry
40216
- .getAll()
40217
- .find((c) => c.matcher?.(definition) || false);
40218
- return matchedChart || chartSubtypeRegistry.get(definition.type);
40191
+ // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40192
+ shouldProcessInputEvents = false;
40193
+ tokens = [];
40194
+ keyMapping = {
40195
+ Enter: (ev) => this.processEnterKey(ev, "down"),
40196
+ "Shift+Enter": (ev) => this.processEnterKey(ev, "up"),
40197
+ "Alt+Enter": this.processNewLineEvent,
40198
+ "Ctrl+Enter": this.processNewLineEvent,
40199
+ Escape: this.processEscapeKey,
40200
+ F2: (ev) => this.toggleEditionMode(ev),
40201
+ F4: (ev) => this.processF4Key(ev),
40202
+ Tab: (ev) => this.processTabKey(ev, "right"),
40203
+ "Shift+Tab": (ev) => this.processTabKey(ev, "left"),
40204
+ };
40205
+ keyCodeMapping = {
40206
+ NumpadDecimal: this.processNumpadDecimal,
40207
+ };
40208
+ setup() {
40209
+ this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
40210
+ this.autoCompleteState = useLocalStore(AutoCompleteStore);
40211
+ owl.onMounted(() => {
40212
+ const el = this.composerRef.el;
40213
+ if (this.props.isDefaultFocus) {
40214
+ this.DOMFocusableElementStore.setFocusableElement(el);
40215
+ }
40216
+ this.contentHelper.updateEl(el);
40217
+ });
40218
+ this.env.model.selection.observe(this, {
40219
+ handleEvent: () => this.autoCompleteState.hide(),
40220
+ });
40221
+ owl.onWillUnmount(() => {
40222
+ this.env.model.selection.detachObserver(this);
40223
+ });
40224
+ owl.useEffect(() => {
40225
+ this.processContent();
40226
+ if (document.activeElement === this.contentHelper.el &&
40227
+ this.props.composerStore.editionMode === "inactive" &&
40228
+ !this.props.isDefaultFocus) {
40229
+ this.DOMFocusableElementStore.focusableElement?.focus();
40230
+ }
40231
+ });
40232
+ owl.useEffect(() => {
40233
+ this.processTokenAtCursor();
40234
+ }, () => [this.props.composerStore.editionMode !== "inactive"]);
40219
40235
  }
40220
- onPointerDown(ev) {
40221
- if (this.state.popoverProps) {
40222
- this.closePopover();
40236
+ // ---------------------------------------------------------------------------
40237
+ // Handlers
40238
+ // ---------------------------------------------------------------------------
40239
+ processArrowKeys(ev) {
40240
+ const tokenAtCursor = this.props.composerStore.tokenAtCursor;
40241
+ if ((this.props.composerStore.isSelectingRange ||
40242
+ this.props.composerStore.editionMode === "inactive") &&
40243
+ !(["ArrowUp", "ArrowDown"].includes(ev.key) &&
40244
+ this.autoCompleteState.provider &&
40245
+ tokenAtCursor?.type !== "REFERENCE")) {
40246
+ this.functionDescriptionState.showDescription = false;
40247
+ this.autoCompleteState.hide();
40248
+ // Prevent the default content editable behavior which moves the cursor
40249
+ ev.preventDefault();
40250
+ ev.stopPropagation();
40251
+ updateSelectionWithArrowKeys(ev, this.env.model.selection);
40223
40252
  return;
40224
40253
  }
40225
- const target = ev.currentTarget;
40226
- const { bottom, right, width } = target.getBoundingClientRect();
40227
- this.state.popoverProps = {
40228
- anchorRect: { x: right, y: bottom, width: 0, height: 0 },
40229
- positioning: "TopRight",
40230
- verticalOffset: 0,
40231
- };
40232
- this.state.popoverStyle = cssPropertiesToCss({ width: `${width}px` });
40233
- }
40234
- closePopover() {
40235
- this.state.popoverProps = undefined;
40254
+ const content = this.props.composerStore.currentContent;
40255
+ if (this.props.focus === "cellFocus" &&
40256
+ !this.autoCompleteState.provider &&
40257
+ !content.startsWith("=")) {
40258
+ this.props.composerStore.stopEdition();
40259
+ return;
40260
+ }
40261
+ // All arrow keys are processed: up and down should move autocomplete, left
40262
+ // and right should move the cursor.
40263
+ ev.stopPropagation();
40264
+ this.handleArrowKeysForAutocomplete(ev);
40236
40265
  }
40237
- }
40238
-
40239
- class MainChartPanelStore extends SpreadsheetStore {
40240
- mutators = ["activatePanel", "changeChartType"];
40241
- panel = "configuration";
40242
- creationContexts = {};
40243
- activatePanel(panel) {
40244
- this.panel = panel;
40266
+ handleArrowKeysForAutocomplete(ev) {
40267
+ // only for arrow up and down
40268
+ if (["ArrowUp", "ArrowDown"].includes(ev.key) && this.autoCompleteState.provider) {
40269
+ ev.preventDefault();
40270
+ this.autoCompleteState.moveSelection(ev.key === "ArrowDown" ? "next" : "previous");
40271
+ }
40245
40272
  }
40246
- changeChartType(figureId, newDisplayType) {
40247
- const currentCreationContext = this.getters.getContextCreationChart(figureId);
40248
- const savedCreationContext = this.creationContexts[figureId] || {};
40249
- let newRanges = currentCreationContext?.range;
40250
- if (newRanges?.every((range, i) => deepEquals(range, savedCreationContext.range?.[i]))) {
40251
- newRanges = Object.assign([], savedCreationContext.range, currentCreationContext?.range);
40273
+ processTabKey(ev, direction) {
40274
+ ev.preventDefault();
40275
+ ev.stopPropagation();
40276
+ if (this.props.composerStore.editionMode !== "inactive") {
40277
+ const state = this.autoCompleteState;
40278
+ if (state.provider && state.selectedIndex !== undefined) {
40279
+ const autoCompleteValue = state.provider.proposals[state.selectedIndex]?.text;
40280
+ if (autoCompleteValue) {
40281
+ this.autoComplete(autoCompleteValue);
40282
+ return;
40283
+ }
40284
+ }
40285
+ this.props.composerStore.stopEdition(direction);
40252
40286
  }
40253
- this.creationContexts[figureId] = {
40254
- ...savedCreationContext,
40255
- ...currentCreationContext,
40256
- range: newRanges,
40257
- };
40258
- const sheetId = this.getters.getFigureSheetId(figureId);
40259
- if (!sheetId) {
40260
- return;
40287
+ }
40288
+ processEnterKey(ev, direction) {
40289
+ ev.preventDefault();
40290
+ ev.stopPropagation();
40291
+ const state = this.autoCompleteState;
40292
+ if (state.provider && state.selectedIndex !== undefined) {
40293
+ const autoCompleteValue = state.provider.proposals[state.selectedIndex]?.text;
40294
+ if (autoCompleteValue) {
40295
+ this.autoComplete(autoCompleteValue);
40296
+ return;
40297
+ }
40261
40298
  }
40262
- const definition = this.getChartDefinitionFromContextCreation(figureId, newDisplayType);
40263
- this.model.dispatch("UPDATE_CHART", {
40264
- definition,
40265
- id: figureId,
40266
- sheetId,
40299
+ this.props.composerStore.stopEdition(direction);
40300
+ }
40301
+ processNewLineEvent(ev) {
40302
+ ev.preventDefault();
40303
+ ev.stopPropagation();
40304
+ const content = this.contentHelper.getText();
40305
+ const selection = this.contentHelper.getCurrentSelection();
40306
+ const start = Math.min(selection.start, selection.end);
40307
+ const end = Math.max(selection.start, selection.end);
40308
+ this.props.composerStore.stopComposerRangeSelection();
40309
+ this.props.composerStore.setCurrentContent(content.slice(0, start) + NEWLINE + content.slice(end), {
40310
+ start: start + 1,
40311
+ end: start + 1,
40267
40312
  });
40313
+ this.processContent();
40268
40314
  }
40269
- getChartDefinitionFromContextCreation(figureId, newDisplayType) {
40270
- const newChartInfo = chartSubtypeRegistry.get(newDisplayType);
40271
- const ChartClass = chartRegistry.get(newChartInfo.chartType);
40272
- return {
40273
- ...ChartClass.getChartDefinitionFromContextCreation(this.creationContexts[figureId]),
40274
- ...newChartInfo.subtypeDefinition,
40275
- };
40315
+ processEscapeKey(ev) {
40316
+ this.props.composerStore.cancelEdition();
40317
+ ev.stopPropagation();
40318
+ ev.preventDefault();
40276
40319
  }
40277
- }
40278
-
40279
- css /* scss */ `
40280
- .o-chart {
40281
- .o-panel {
40282
- display: flex;
40283
- .o-panel-element {
40284
- flex: 1 0 auto;
40285
- padding: 8px 0px;
40286
- text-align: center;
40287
- cursor: pointer;
40288
- border-right: 1px solid ${GRAY_300};
40289
-
40290
- &.inactive {
40291
- color: ${TEXT_BODY};
40292
- background-color: ${GRAY_100};
40293
- border-bottom: 1px solid ${GRAY_300};
40294
- }
40295
-
40296
- &:not(.inactive) {
40297
- color: ${TEXT_HEADING};
40298
- border-bottom: 1px solid #fff;
40299
- }
40300
-
40301
- .fa {
40302
- margin-right: 4px;
40303
- }
40304
- }
40305
- .o-panel-element:last-child {
40306
- border-right: none;
40307
- }
40320
+ processF4Key(ev) {
40321
+ ev.stopPropagation();
40322
+ this.props.composerStore.cycleReferences();
40323
+ this.processContent();
40308
40324
  }
40309
- }
40310
- `;
40311
- class ChartPanel extends owl.Component {
40312
- static template = "o-spreadsheet-ChartPanel";
40313
- static components = { Section, ChartTypePicker };
40314
- static props = { onCloseSidePanel: Function, figureId: String };
40315
- store;
40316
- get figureId() {
40317
- return this.props.figureId;
40325
+ toggleEditionMode(ev) {
40326
+ ev.stopPropagation();
40327
+ this.props.composerStore.toggleEditionMode();
40328
+ this.processContent();
40318
40329
  }
40319
- setup() {
40320
- this.store = useLocalStore(MainChartPanelStore);
40330
+ processNumpadDecimal(ev) {
40331
+ ev.stopPropagation();
40332
+ ev.preventDefault();
40333
+ const locale = this.env.model.getters.getLocale();
40334
+ const selection = this.contentHelper.getCurrentSelection();
40335
+ const currentContent = this.props.composerStore.currentContent;
40336
+ const content = currentContent.slice(0, selection.start) +
40337
+ locale.decimalSeparator +
40338
+ currentContent.slice(selection.end);
40339
+ // Update composer even by hand rather than dispatching an InputEvent because untrusted inputs
40340
+ // events aren't handled natively by contentEditable
40341
+ this.props.composerStore.setCurrentContent(content, {
40342
+ start: selection.start + 1,
40343
+ end: selection.start + 1,
40344
+ });
40345
+ // We need to do the process content here in case there is no render between the keyDown and the
40346
+ // keyUp event
40347
+ this.processContent();
40321
40348
  }
40322
- updateChart(figureId, updateDefinition) {
40323
- if (figureId !== this.figureId) {
40349
+ onCompositionStart() {
40350
+ this.compositionActive = true;
40351
+ }
40352
+ onCompositionEnd() {
40353
+ this.compositionActive = false;
40354
+ }
40355
+ onKeydown(ev) {
40356
+ if (this.props.composerStore.editionMode === "inactive") {
40324
40357
  return;
40325
40358
  }
40326
- const definition = {
40327
- ...this.getChartDefinition(this.figureId),
40328
- ...updateDefinition,
40329
- };
40330
- return this.env.model.dispatch("UPDATE_CHART", {
40331
- definition,
40332
- id: figureId,
40333
- sheetId: this.env.model.getters.getFigureSheetId(figureId),
40334
- });
40359
+ if (ev.key.startsWith("Arrow")) {
40360
+ this.processArrowKeys(ev);
40361
+ return;
40362
+ }
40363
+ let handler = this.keyMapping[keyboardEventToShortcutString(ev)] ||
40364
+ this.keyCodeMapping[keyboardEventToShortcutString(ev, "code")];
40365
+ if (handler) {
40366
+ handler.call(this, ev);
40367
+ }
40368
+ else {
40369
+ ev.stopPropagation();
40370
+ }
40335
40371
  }
40336
- canUpdateChart(figureId, updateDefinition) {
40337
- if (figureId !== this.figureId || !this.env.model.getters.isChartDefined(figureId)) {
40338
- return;
40372
+ onPaste(ev) {
40373
+ if (this.props.composerStore.editionMode !== "inactive") {
40374
+ // let the browser clipboard work
40375
+ ev.stopPropagation();
40376
+ }
40377
+ else {
40378
+ // the user meant to paste in the sheet, not open the composer with the pasted content
40379
+ // While we're not editing, we still have the focus and should therefore prevent
40380
+ // the native "paste" to occur.
40381
+ ev.preventDefault();
40339
40382
  }
40340
- const definition = {
40341
- ...this.getChartDefinition(this.figureId),
40342
- ...updateDefinition,
40343
- };
40344
- return this.env.model.canDispatch("UPDATE_CHART", {
40345
- definition,
40346
- id: figureId,
40347
- sheetId: this.env.model.getters.getFigureSheetId(figureId),
40348
- });
40349
40383
  }
40350
- onTypeChange(type) {
40351
- if (!this.figureId) {
40384
+ /*
40385
+ * Triggered automatically by the content-editable between the keydown and key up
40386
+ * */
40387
+ onInput(ev) {
40388
+ if (!this.shouldProcessInputEvents) {
40352
40389
  return;
40353
40390
  }
40354
- this.store.changeChartType(this.figureId, type);
40355
- }
40356
- get chartPanel() {
40357
- if (!this.figureId) {
40358
- throw new Error("Chart not defined.");
40391
+ ev.stopPropagation();
40392
+ let content;
40393
+ if (this.props.composerStore.editionMode === "inactive") {
40394
+ content = ev.data || "";
40359
40395
  }
40360
- const type = this.env.model.getters.getChartType(this.figureId);
40361
- if (!type) {
40362
- throw new Error("Chart not defined.");
40396
+ else {
40397
+ content = this.contentHelper.getText();
40363
40398
  }
40364
- const chartPanel = chartSidePanelComponentRegistry.get(type);
40365
- if (!chartPanel) {
40366
- throw new Error(`Component is not defined for type ${type}`);
40399
+ if (this.props.focus === "inactive") {
40400
+ return this.props.onComposerCellFocused?.(content);
40367
40401
  }
40368
- return chartPanel;
40369
- }
40370
- getChartDefinition(figureId) {
40371
- return this.env.model.getters.getChartDefinition(figureId);
40372
- }
40373
- }
40374
-
40375
- class DOMFocusableElementStore {
40376
- mutators = ["setFocusableElement"];
40377
- focusableElement = undefined;
40378
- setFocusableElement(element) {
40379
- this.focusableElement = element;
40380
- }
40381
- }
40382
-
40383
- css /* scss */ `
40384
- .o-autocomplete-dropdown {
40385
- pointer-events: auto;
40386
- cursor: pointer;
40387
- background-color: #fff;
40388
- max-width: 400px;
40389
- z-index: 1;
40390
-
40391
- .o-autocomplete-value-focus {
40392
- background-color: #f2f2f2;
40393
- }
40394
-
40395
- & > div {
40396
- padding: 1px 5px 5px 5px;
40397
- .o-autocomplete-description {
40398
- padding-left: 5px;
40399
- font-size: 11px;
40400
- }
40402
+ let selection = this.contentHelper.getCurrentSelection();
40403
+ this.props.composerStore.stopComposerRangeSelection();
40404
+ this.props.composerStore.setCurrentContent(content, selection);
40405
+ this.processTokenAtCursor();
40401
40406
  }
40402
- }
40403
- `;
40404
- class TextValueProvider extends owl.Component {
40405
- static template = "o-spreadsheet-TextValueProvider";
40406
- static props = {
40407
- proposals: Array,
40408
- selectedIndex: { type: Number, optional: true },
40409
- onValueSelected: Function,
40410
- onValueHovered: Function,
40411
- };
40412
- autoCompleteListRef = owl.useRef("autoCompleteList");
40413
- setup() {
40414
- owl.useEffect(() => {
40415
- const selectedIndex = this.props.selectedIndex;
40416
- if (selectedIndex === undefined) {
40407
+ onKeyup(ev) {
40408
+ if (this.contentHelper.el === document.activeElement) {
40409
+ if (this.autoCompleteState.provider && ["ArrowUp", "ArrowDown"].includes(ev.key)) {
40417
40410
  return;
40418
40411
  }
40419
- const selectedElement = this.autoCompleteListRef.el?.children[selectedIndex];
40420
- selectedElement?.scrollIntoView?.({ block: "nearest" });
40421
- }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
40422
- }
40423
- }
40424
-
40425
- class AutoCompleteStore extends SpreadsheetStore {
40426
- mutators = ["useProvider", "moveSelection", "hide", "selectIndex"];
40427
- selectedIndex = undefined;
40428
- provider;
40429
- get selectedProposal() {
40430
- if (this.selectedIndex === undefined || this.provider === undefined) {
40431
- return undefined;
40412
+ if (this.props.composerStore.isSelectingRange && ev.key?.startsWith("Arrow")) {
40413
+ return;
40414
+ }
40415
+ const { start: oldStart, end: oldEnd } = this.props.composerStore.composerSelection;
40416
+ const { start, end } = this.contentHelper.getCurrentSelection();
40417
+ if (start !== oldStart || end !== oldEnd) {
40418
+ this.props.composerStore.changeComposerCursorSelection(start, end);
40419
+ }
40420
+ this.processTokenAtCursor();
40432
40421
  }
40433
- return this.provider.proposals[this.selectedIndex];
40434
- }
40435
- useProvider(provider) {
40436
- this.provider = provider;
40437
- this.selectedIndex = provider.autoSelectFirstProposal ? 0 : undefined;
40438
- }
40439
- hide() {
40440
- this.provider = undefined;
40441
- this.selectedIndex = undefined;
40442
- }
40443
- selectIndex(index) {
40444
- this.selectedIndex = index;
40445
40422
  }
40446
- moveSelection(direction) {
40447
- if (!this.provider) {
40423
+ onBlur(ev) {
40424
+ if (this.props.composerStore.editionMode === "inactive") {
40448
40425
  return;
40449
40426
  }
40450
- if (this.selectedIndex === undefined) {
40451
- this.selectedIndex = 0;
40427
+ const target = ev.relatedTarget;
40428
+ if (!target || !(target instanceof HTMLElement)) {
40429
+ this.props.composerStore.stopEdition();
40452
40430
  return;
40453
40431
  }
40454
- if (direction === "previous") {
40455
- this.selectedIndex--;
40456
- if (this.selectedIndex < 0) {
40457
- this.selectedIndex = this.provider.proposals.length - 1;
40458
- }
40432
+ if (target.attributes.getNamedItem("composerFocusableElement")) {
40433
+ this.contentHelper.el.focus();
40434
+ return;
40459
40435
  }
40460
- else {
40461
- this.selectedIndex = (this.selectedIndex + 1) % this.provider.proposals.length;
40436
+ if (target.classList.contains("o-composer")) {
40437
+ return;
40462
40438
  }
40439
+ this.props.composerStore.stopEdition();
40463
40440
  }
40464
- }
40465
-
40466
- class ContentEditableHelper {
40467
- // todo make el private and expose dedicated methods
40468
- el;
40469
- constructor(el) {
40470
- this.el = el;
40471
- }
40472
- updateEl(el) {
40473
- this.el = el;
40441
+ updateAutoCompleteIndex(index) {
40442
+ this.autoCompleteState.selectIndex(clip(0, index, 10));
40474
40443
  }
40475
40444
  /**
40476
- * select the text at position start to end, no matter the children
40445
+ * This is required to ensure the content helper selection is
40446
+ * properly updated on "onclick" events. Depending on the browser,
40447
+ * the callback onClick from the composer will be executed before
40448
+ * the selection was updated in the dom, which means we capture an
40449
+ * wrong selection which is then forced upon the content helper on
40450
+ * processContent.
40477
40451
  */
40478
- selectRange(start, end) {
40479
- let selection = window.getSelection();
40480
- const { start: currentStart, end: currentEnd } = this.getCurrentSelection();
40481
- if (currentStart === start && currentEnd === end) {
40452
+ onMousedown(ev) {
40453
+ if (ev.button > 0) {
40454
+ // not main button, probably a context menu
40482
40455
  return;
40483
40456
  }
40484
- if (selection.rangeCount === 0) {
40485
- const range = document.createRange();
40486
- selection.addRange(range);
40457
+ this.contentHelper.removeSelection();
40458
+ }
40459
+ onClick() {
40460
+ if (this.env.model.getters.isReadonly()) {
40461
+ return;
40487
40462
  }
40488
- const currentRange = selection.getRangeAt(0);
40489
- let range;
40490
- if (this.el.contains(currentRange.startContainer)) {
40491
- range = currentRange;
40463
+ const newSelection = this.contentHelper.getCurrentSelection();
40464
+ this.props.composerStore.stopComposerRangeSelection();
40465
+ this.props.onComposerContentFocused();
40466
+ this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
40467
+ this.processTokenAtCursor();
40468
+ }
40469
+ onDblClick() {
40470
+ if (this.env.model.getters.isReadonly()) {
40471
+ return;
40492
40472
  }
40493
- else {
40494
- range = document.createRange();
40495
- selection.removeAllRanges();
40496
- selection.addRange(range);
40473
+ const composerContent = this.props.composerStore.currentContent;
40474
+ const isValidFormula = composerContent.startsWith("=");
40475
+ if (isValidFormula) {
40476
+ const tokens = this.props.composerStore.currentTokens;
40477
+ const currentSelection = this.contentHelper.getCurrentSelection();
40478
+ if (currentSelection.start === currentSelection.end)
40479
+ return;
40480
+ const currentSelectedText = composerContent.substring(currentSelection.start, currentSelection.end);
40481
+ const token = tokens.filter((token) => token.value.includes(currentSelectedText) &&
40482
+ token.start <= currentSelection.start &&
40483
+ token.end >= currentSelection.end)[0];
40484
+ if (!token) {
40485
+ return;
40486
+ }
40487
+ if (token.type === "REFERENCE") {
40488
+ this.props.composerStore.changeComposerCursorSelection(token.start, token.end);
40489
+ }
40497
40490
  }
40498
- if (start === end && start === 0) {
40499
- range.setStart(this.el, 0);
40500
- range.setEnd(this.el, 0);
40491
+ }
40492
+ onContextMenu(ev) {
40493
+ if (this.props.composerStore.editionMode === "inactive") {
40494
+ this.props.onInputContextMenu?.(ev);
40501
40495
  }
40502
- else {
40503
- const textLength = this.getText().length;
40504
- if (start < 0 || end > textLength) {
40505
- console.warn(`wrong selection asked start ${start}, end ${end}, text content length ${textLength}`);
40506
- if (start < 0)
40507
- start = 0;
40508
- if (end > textLength)
40509
- end = textLength;
40510
- if (start > textLength)
40511
- start = textLength;
40496
+ }
40497
+ closeAssistant() {
40498
+ this.assistant.forcedClosed = true;
40499
+ }
40500
+ openAssistant() {
40501
+ this.assistant.forcedClosed = false;
40502
+ }
40503
+ // ---------------------------------------------------------------------------
40504
+ // Private
40505
+ // ---------------------------------------------------------------------------
40506
+ processContent() {
40507
+ if (this.compositionActive) {
40508
+ return;
40509
+ }
40510
+ this.shouldProcessInputEvents = false;
40511
+ if (this.props.focus !== "inactive" && document.activeElement !== this.contentHelper.el) {
40512
+ this.contentHelper.el.focus();
40513
+ }
40514
+ const content = this.getContentLines();
40515
+ this.contentHelper.setText(content);
40516
+ if (content.length !== 0 && content.length[0] !== 0) {
40517
+ if (this.props.focus !== "inactive") {
40518
+ // Put the cursor back where it was before the rendering
40519
+ const { start, end } = this.props.composerStore.composerSelection;
40520
+ this.contentHelper.selectRange(start, end);
40512
40521
  }
40513
- let startNode = this.findChildAtCharacterIndex(start);
40514
- let endNode = this.findChildAtCharacterIndex(end);
40515
- range.setStart(startNode.node, startNode.offset);
40516
- range.setEnd(endNode.node, endNode.offset);
40522
+ this.contentHelper.scrollSelectionIntoView();
40517
40523
  }
40524
+ this.shouldProcessInputEvents = true;
40518
40525
  }
40519
40526
  /**
40520
- * finds the dom element that contains the character at `offset`
40527
+ * Get the HTML content corresponding to the current composer token, divided by lines.
40521
40528
  */
40522
- findChildAtCharacterIndex(offset) {
40523
- let it = iterateChildren(this.el);
40524
- let current, previous;
40525
- let usedCharacters = offset;
40526
- let isFirstParagraph = true;
40527
- do {
40528
- current = it.next();
40529
- if (!current.done && !current.value.hasChildNodes()) {
40530
- if (current.value.textContent && current.value.textContent.length < usedCharacters) {
40531
- usedCharacters -= current.value.textContent.length;
40532
- }
40533
- else if (current.value.textContent &&
40534
- current.value.textContent.length >= usedCharacters) {
40535
- it.return(current.value);
40536
- }
40537
- previous = current.value;
40529
+ getContentLines() {
40530
+ let value = this.props.composerStore.currentContent;
40531
+ const isValidFormula = value.startsWith("=");
40532
+ if (value === "") {
40533
+ return [];
40534
+ }
40535
+ else if (isValidFormula && this.props.focus !== "inactive") {
40536
+ return this.splitHtmlContentIntoLines(this.getHtmlContentFromTokens());
40537
+ }
40538
+ return this.splitHtmlContentIntoLines([{ value }]);
40539
+ }
40540
+ getHtmlContentFromTokens() {
40541
+ const tokens = this.props.composerStore.currentTokens;
40542
+ const result = [];
40543
+ const { end, start } = this.props.composerStore.composerSelection;
40544
+ for (const token of tokens) {
40545
+ let color = token.color || DEFAULT_TOKEN_COLOR;
40546
+ if (token.isBlurred) {
40547
+ color = setColorAlpha(color, 0.5);
40538
40548
  }
40539
- // One new paragraph = one new line character, except for the first paragraph
40540
- if (!current.done && current.value.nodeName === "P") {
40541
- if (isFirstParagraph) {
40542
- isFirstParagraph = false;
40543
- }
40544
- else {
40545
- usedCharacters--;
40546
- }
40549
+ result.push({ value: token.value, color });
40550
+ if (token.type === "REFERENCE" &&
40551
+ this.props.composerStore.tokenAtCursor === token &&
40552
+ this.props.composerStore.editionMode === "selecting") {
40553
+ result[result.length - 1].class = "text-decoration-underline";
40554
+ }
40555
+ if (end === start && token.isParenthesisLinkedToCursor) {
40556
+ result[result.length - 1].class = backgroundClass;
40557
+ }
40558
+ if (this.props.composerStore.showSelectionIndicator && end === start && end === token.end) {
40559
+ result[result.length - 1].class = selectionIndicatorClass;
40547
40560
  }
40548
- } while (!current.done && usedCharacters);
40549
- if (current.value) {
40550
- return { node: current.value, offset: usedCharacters };
40551
40561
  }
40552
- return { node: previous, offset: usedCharacters };
40562
+ return result;
40553
40563
  }
40554
40564
  /**
40555
- * Sets (or Replaces all) the text inside the root element in the form of distinctive paragraphs and
40556
- * span for each element provided in `contents`.
40557
- *
40558
- * The function will apply the diff between the current content and the new content to avoid the systematic
40559
- * destruction of DOM elements which interferes with IME[1]
40560
- *
40561
- * Each line of text will be encapsulated in a paragraph element.
40562
- * Each span will have its own fontcolor and specific class if provided in the HtmlContent object.
40563
- *
40564
- * [1] https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor
40565
+ * Split an array of HTMLContents into lines. Each NEWLINE character encountered will create a new
40566
+ * line. Contents can be split into multiple parts if they contain multiple NEWLINE characters.
40565
40567
  */
40566
- setText(contents) {
40567
- if (contents.length === 0) {
40568
- this.removeAll();
40569
- return;
40570
- }
40571
- const childElements = Array.from(this.el.childNodes);
40572
- const contentLength = contents.length;
40573
- for (let i = 0; i < contentLength; i++) {
40574
- const line = contents[i];
40575
- const childElement = childElements[i];
40576
- let newChild = false;
40577
- let p;
40578
- if (childElement && childElement.nodeName === "P") {
40579
- p = childElement;
40580
- }
40581
- else {
40582
- newChild = true;
40583
- p = document.createElement("p");
40584
- }
40585
- const lineLength = line.length;
40586
- const existingChildren = Array.from(p.childNodes);
40587
- for (let j = 0; j < lineLength; j++) {
40588
- const content = line[j];
40589
- const child = existingChildren[j];
40590
- // child nodes can be multiple types of nodes: Span, Text, Div, etc...
40591
- // We can only modify a node in place if it has the same type as the content
40592
- // that we would insert, which are spans.
40593
- // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
40594
- // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)
40595
- const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
40596
- if (childIsSpan && compareContentToSpanElement(content, child)) {
40597
- continue;
40598
- }
40599
- // this is an empty line in the content
40600
- if (!content.value && !content.class) {
40601
- if (child)
40602
- p.removeChild(child);
40603
- continue;
40604
- }
40605
- const span = document.createElement("span");
40606
- span.innerText = content.value;
40607
- span.style.color = content.color || "";
40608
- if (content.class) {
40609
- span.classList.add(content.class);
40610
- }
40611
- if (child) {
40612
- p.replaceChild(span, child);
40613
- }
40614
- else {
40615
- p.appendChild(span);
40568
+ splitHtmlContentIntoLines(contents) {
40569
+ const contentSplitInLines = [];
40570
+ let currentLine = [];
40571
+ for (const content of contents) {
40572
+ if (content.value.includes(NEWLINE)) {
40573
+ const lines = content.value.split(NEWLINE);
40574
+ const lastLine = lines.pop();
40575
+ for (const line of lines) {
40576
+ currentLine.push({ color: content.color, value: line }); // don't copy class, only last line should keep it
40577
+ contentSplitInLines.push(currentLine);
40578
+ currentLine = [];
40616
40579
  }
40580
+ currentLine.push({ ...content, value: lastLine });
40617
40581
  }
40618
- if (existingChildren.length > lineLength) {
40619
- for (let i = lineLength; i < existingChildren.length; i++) {
40620
- p.removeChild(existingChildren[i]);
40621
- }
40582
+ else {
40583
+ currentLine.push(content);
40622
40584
  }
40623
- // Empty line
40624
- if (!p.hasChildNodes()) {
40625
- const span = document.createElement("span");
40626
- span.appendChild(document.createElement("br"));
40627
- p.appendChild(span);
40585
+ }
40586
+ if (currentLine.length) {
40587
+ contentSplitInLines.push(currentLine);
40588
+ }
40589
+ // Remove useless empty contents
40590
+ const filteredLines = [];
40591
+ for (const line of contentSplitInLines) {
40592
+ if (line.every(this.isContentEmpty)) {
40593
+ filteredLines.push([line[0]]);
40628
40594
  }
40629
- // replace p if necessary
40630
- if (newChild) {
40631
- if (childElement) {
40632
- this.el.replaceChild(p, childElement);
40633
- }
40634
- else {
40635
- this.el.appendChild(p);
40636
- }
40595
+ else {
40596
+ filteredLines.push(line.filter((content) => !this.isContentEmpty(content)));
40637
40597
  }
40638
40598
  }
40639
- if (childElements.length > contentLength) {
40640
- for (let i = contentLength; i < childElements.length; i++) {
40641
- this.el.removeChild(childElements[i]);
40599
+ return filteredLines;
40600
+ }
40601
+ isContentEmpty(content) {
40602
+ return !(content.value || content.class);
40603
+ }
40604
+ /**
40605
+ * Compute the state of the composer from the tokenAtCursor.
40606
+ * If the token is a function or symbol (that isn't a cell/range reference) we have to initialize
40607
+ * the autocomplete engine otherwise we initialize the formula assistant.
40608
+ */
40609
+ processTokenAtCursor() {
40610
+ let content = this.props.composerStore.currentContent;
40611
+ if (this.autoCompleteState.provider) {
40612
+ this.autoCompleteState.hide();
40613
+ }
40614
+ this.functionDescriptionState.showDescription = false;
40615
+ const autoCompleteProvider = this.props.composerStore.autocompleteProvider;
40616
+ if (autoCompleteProvider) {
40617
+ this.autoCompleteState.useProvider(autoCompleteProvider);
40618
+ }
40619
+ const token = this.props.composerStore.tokenAtCursor;
40620
+ if (content.startsWith("=") && token && token.type !== "SYMBOL") {
40621
+ const tokenContext = token.functionContext;
40622
+ const parentFunction = tokenContext?.parent.toUpperCase();
40623
+ if (tokenContext &&
40624
+ parentFunction &&
40625
+ parentFunction in functions &&
40626
+ token.type !== "UNKNOWN") {
40627
+ // initialize Formula Assistant
40628
+ const description = functions[parentFunction];
40629
+ const argPosition = tokenContext.argPosition;
40630
+ this.functionDescriptionState.functionName = parentFunction;
40631
+ this.functionDescriptionState.functionDescription = description;
40632
+ this.functionDescriptionState.argToFocus = description.getArgToFocus(argPosition + 1) - 1;
40633
+ this.functionDescriptionState.showDescription = true;
40642
40634
  }
40643
40635
  }
40644
40636
  }
40645
- scrollSelectionIntoView() {
40646
- const focusedNode = document.getSelection()?.focusNode;
40647
- if (!focusedNode || !this.el.contains(focusedNode))
40637
+ autoComplete(value) {
40638
+ if (!value || this.assistant.forcedClosed) {
40648
40639
  return;
40649
- const element = focusedNode instanceof HTMLElement ? focusedNode : focusedNode.parentElement;
40650
- element?.scrollIntoView?.({ block: "nearest" });
40640
+ }
40641
+ this.autoCompleteState.provider?.selectProposal(value);
40642
+ this.processTokenAtCursor();
40651
40643
  }
40652
- /**
40653
- * remove the current selection of the user
40654
- * */
40655
- removeSelection() {
40656
- let selection = window.getSelection();
40657
- selection.removeAllRanges();
40644
+ }
40645
+
40646
+ class StandaloneComposerStore extends AbstractComposerStore {
40647
+ args;
40648
+ constructor(get, args) {
40649
+ super(get);
40650
+ this.args = args;
40651
+ this._currentContent = this.getComposerContent();
40658
40652
  }
40659
- removeAll() {
40660
- if (this.el) {
40661
- while (this.el.firstChild) {
40662
- this.el.removeChild(this.el.firstChild);
40663
- }
40653
+ getAutoCompleteProviders() {
40654
+ const providersDefinitions = super.getAutoCompleteProviders();
40655
+ const contextualAutocomplete = this.args().contextualAutocomplete;
40656
+ if (contextualAutocomplete) {
40657
+ providersDefinitions.push(contextualAutocomplete);
40664
40658
  }
40659
+ return providersDefinitions;
40665
40660
  }
40666
40661
  /**
40667
- * finds the indexes of the current selection.
40662
+ * Replace the current reference selected by the new one.
40668
40663
  * */
40669
- getCurrentSelection() {
40670
- return getCurrentSelection(this.el);
40664
+ getZoneReference(zone) {
40665
+ const res = super.getZoneReference(zone);
40666
+ if (this.args().defaultStatic) {
40667
+ return setXcToFixedReferenceType(res, "colrow");
40668
+ }
40669
+ return res;
40671
40670
  }
40672
- getText() {
40673
- let text = "";
40674
- let it = iterateChildren(this.el);
40675
- let current = it.next();
40676
- let isFirstParagraph = true;
40677
- while (!current.done) {
40678
- if (!current.value.hasChildNodes()) {
40679
- text += current.value.textContent;
40680
- }
40681
- if (current.value.nodeName === "P" ||
40682
- (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
40683
- ) {
40684
- if (isFirstParagraph) {
40685
- isFirstParagraph = false;
40686
- }
40687
- else {
40688
- text += NEWLINE;
40671
+ getComposerContent() {
40672
+ if (this.editionMode === "inactive") {
40673
+ // References in the content might not be linked to the current active sheet
40674
+ // We here force the sheet name prefix for all references that are not in
40675
+ // the current active sheet
40676
+ const defaultRangeSheetId = this.args().defaultRangeSheetId;
40677
+ return rangeTokenize(this.args().content)
40678
+ .map((token) => {
40679
+ if (token.type === "REFERENCE") {
40680
+ const range = this.getters.getRangeFromSheetXC(defaultRangeSheetId, token.value);
40681
+ return this.getters.getRangeString(range, this.getters.getActiveSheetId());
40689
40682
  }
40683
+ return token.value;
40684
+ })
40685
+ .join("");
40686
+ }
40687
+ return this._currentContent;
40688
+ }
40689
+ stopEdition() {
40690
+ this._stopEdition();
40691
+ }
40692
+ confirmEdition(content) {
40693
+ this.args().onConfirm(content);
40694
+ }
40695
+ getTokenColor(token) {
40696
+ if (token.type === "SYMBOL") {
40697
+ const matchedColor = this.args().getContextualColoredSymbolToken?.(token);
40698
+ if (matchedColor) {
40699
+ return matchedColor;
40690
40700
  }
40691
- current = it.next();
40692
40701
  }
40693
- return text;
40702
+ return super.getTokenColor(token);
40694
40703
  }
40695
40704
  }
40696
- function compareContentToSpanElement(content, node) {
40697
- const contentColor = content.color ? toHex(content.color) : "";
40698
- const nodeColor = node.style?.color ? toHex(node.style.color) : "";
40699
- const sameColor = contentColor === nodeColor;
40700
- const sameClass = deepEquals([content.class], [...node.classList]);
40701
- const sameContent = node.innerText === content.value;
40702
- return sameColor && sameClass && sameContent;
40703
- }
40704
40705
 
40705
- // -----------------------------------------------------------------------------
40706
- // Formula Assistant component
40707
- // -----------------------------------------------------------------------------
40708
40706
  css /* scss */ `
40709
- .o-formula-assistant {
40710
- background: #ffffff;
40711
- .o-formula-assistant-head {
40712
- background-color: #f2f2f2;
40713
- padding: 10px;
40714
- }
40715
- .collapsed {
40716
- transform: rotate(180deg);
40717
- }
40718
- .o-formula-assistant-core {
40719
- border-bottom: 1px solid gray;
40720
- }
40721
- .o-formula-assistant-arg-description {
40722
- font-size: 85%;
40723
- }
40724
- .o-formula-assistant-focus {
40725
- div:first-child,
40726
- span {
40727
- color: ${COMPOSER_ASSISTANT_COLOR};
40728
- text-shadow: 0px 0px 1px ${COMPOSER_ASSISTANT_COLOR};
40707
+ .o-spreadsheet {
40708
+ .o-standalone-composer {
40709
+ min-height: 24px;
40710
+ box-sizing: border-box;
40711
+
40712
+ border-bottom: 1px solid;
40713
+ border-color: ${GRAY_300};
40714
+
40715
+ &.active {
40716
+ border-color: ${ACTION_COLOR};
40729
40717
  }
40730
- div:last-child {
40731
- color: black;
40718
+
40719
+ &.o-invalid {
40720
+ border-bottom: 2px solid red;
40721
+ }
40722
+
40723
+ /* As the standalone composer is potentially very small (eg. in a side panel), we remove the scrollbar display */
40724
+ scrollbar-width: none; /* Firefox */
40725
+ &::-webkit-scrollbar {
40726
+ display: none;
40732
40727
  }
40733
- }
40734
- .o-formula-assistant-gray {
40735
- color: gray;
40736
40728
  }
40737
40729
  }
40738
40730
  `;
40739
- class FunctionDescriptionProvider extends owl.Component {
40740
- static template = "o-spreadsheet-FunctionDescriptionProvider";
40731
+ class StandaloneComposer extends owl.Component {
40732
+ static template = "o-spreadsheet-StandaloneComposer";
40741
40733
  static props = {
40742
- functionName: String,
40743
- functionDescription: Object,
40744
- argToFocus: Number,
40734
+ composerContent: { type: String, optional: true },
40735
+ defaultRangeSheetId: { type: String, optional: true },
40736
+ defaultStatic: { type: Boolean, optional: true },
40737
+ onConfirm: Function,
40738
+ contextualAutocomplete: { type: Object, optional: true },
40739
+ placeholder: { type: String, optional: true },
40740
+ title: { type: String, optional: true },
40741
+ class: { type: String, optional: true },
40742
+ invalid: { type: Boolean, optional: true },
40743
+ getContextualColoredSymbolToken: { type: Function, optional: true },
40745
40744
  };
40746
- getContext() {
40747
- return this.props;
40745
+ static components = { Composer };
40746
+ static defaultProps = {
40747
+ composerContent: "",
40748
+ defaultStatic: false,
40749
+ };
40750
+ composerFocusStore;
40751
+ standaloneComposerStore;
40752
+ composerInterface;
40753
+ spreadsheetRect = useSpreadsheetRect();
40754
+ setup() {
40755
+ this.composerFocusStore = useStore(ComposerFocusStore);
40756
+ const standaloneComposerStore = useLocalStore(StandaloneComposerStore, () => ({
40757
+ onConfirm: this.props.onConfirm,
40758
+ content: this.props.composerContent,
40759
+ defaultStatic: this.props.defaultStatic ?? false,
40760
+ contextualAutocomplete: this.props.contextualAutocomplete,
40761
+ defaultRangeSheetId: this.props.defaultRangeSheetId,
40762
+ getContextualColoredSymbolToken: this.props.getContextualColoredSymbolToken,
40763
+ }));
40764
+ this.standaloneComposerStore = standaloneComposerStore;
40765
+ this.composerInterface = {
40766
+ id: "standaloneComposer",
40767
+ get editionMode() {
40768
+ return standaloneComposerStore.editionMode;
40769
+ },
40770
+ startEdition: this.standaloneComposerStore.startEdition,
40771
+ setCurrentContent: this.standaloneComposerStore.setCurrentContent,
40772
+ stopEdition: this.standaloneComposerStore.stopEdition,
40773
+ };
40748
40774
  }
40749
- get formulaArgSeparator() {
40750
- return this.env.model.getters.getLocale().formulaArgSeparator + " ";
40775
+ get focus() {
40776
+ return this.composerFocusStore.activeComposer === this.composerInterface
40777
+ ? this.composerFocusStore.focusMode
40778
+ : "inactive";
40779
+ }
40780
+ get composerStyle() {
40781
+ return this.props.invalid
40782
+ ? cssPropertiesToCss({ padding: "1px 0px 0px 0px" })
40783
+ : cssPropertiesToCss({ padding: "1px 0px" });
40784
+ }
40785
+ get containerClass() {
40786
+ const classes = [
40787
+ this.focus === "inactive" ? "" : "active",
40788
+ this.props.invalid ? "o-invalid" : "",
40789
+ this.props.class || "",
40790
+ ];
40791
+ return classes.join(" ");
40792
+ }
40793
+ onFocus(selection) {
40794
+ this.composerFocusStore.focusComposer(this.composerInterface, { selection });
40751
40795
  }
40752
40796
  }
40753
40797
 
40754
- const functions = functionRegistry.content;
40755
- const ASSISTANT_WIDTH = 300;
40756
- const CLOSE_ICON_RADIUS = 9;
40757
- const selectionIndicatorClass = "selector-flag";
40758
- const backgroundClass = "background-flag";
40759
- const selectionIndicatorColor = "#a9a9a9";
40760
- const selectionIndicator = "␣";
40761
40798
  css /* scss */ `
40762
- .o-composer-container {
40763
- .o-composer {
40764
- overflow-y: auto;
40765
- overflow-x: hidden;
40766
- word-break: break-all;
40767
- padding-right: 2px;
40799
+ .o-gauge-color-set {
40800
+ table {
40801
+ table-layout: fixed;
40802
+ margin-top: 2%;
40803
+ display: table;
40804
+ text-align: left;
40805
+ font-size: 12px;
40806
+ line-height: 18px;
40807
+ width: 100%;
40808
+ font-size: 12px;
40809
+ }
40768
40810
 
40811
+ td {
40769
40812
  box-sizing: border-box;
40770
-
40771
- caret-color: black;
40772
- padding-left: 3px;
40773
- padding-right: 3px;
40774
- outline: none;
40775
-
40776
- p {
40777
- margin-bottom: 0px;
40778
-
40779
- span {
40780
- white-space: pre-wrap;
40781
-
40782
- &.${selectionIndicatorClass}:after {
40783
- content: "${selectionIndicator}";
40784
- color: ${selectionIndicatorColor};
40785
- }
40786
-
40787
- &.${backgroundClass} {
40788
- border-radius: 5px;
40789
- background-color: lightgray;
40790
- padding: 0px 1.5px 1.5px 1.5px;
40791
- }
40792
- }
40793
- }
40813
+ height: 30px;
40814
+ padding: 6px 0;
40794
40815
  }
40795
- .o-composer[placeholder]:empty:not(:focus):not(.active)::before {
40796
- content: attr(placeholder);
40797
- color: #bdbdbd;
40798
- position: relative;
40799
- top: 0%;
40800
- pointer-events: none;
40816
+ th.o-gauge-color-set-colorPicker {
40817
+ width: 8%;
40801
40818
  }
40802
-
40803
- .fa-stack {
40804
- /* reset stack size which is doubled by default */
40805
- width: ${CLOSE_ICON_RADIUS * 2}px;
40806
- height: ${CLOSE_ICON_RADIUS * 2}px;
40807
- line-height: ${CLOSE_ICON_RADIUS * 2}px;
40819
+ th.o-gauge-color-set-text {
40820
+ width: 25%;
40808
40821
  }
40809
-
40810
- .force-open-assistant {
40811
- left: -1px;
40812
- top: -1px;
40813
-
40814
- .fa-question-circle {
40815
- color: ${PRIMARY_BUTTON_BG};
40816
- }
40822
+ th.o-gauge-color-set-operator {
40823
+ width: 10%;
40817
40824
  }
40818
-
40819
- .o-composer-assistant {
40820
- position: absolute;
40821
- margin: 1px 4px;
40822
-
40823
- .o-semi-bold {
40824
- /* FIXME: to remove in favor of Bootstrap
40825
- * 'fw-semibold' when we upgrade to Bootstrap 5.2
40826
- */
40827
- font-weight: 600 !important;
40828
- }
40825
+ th.o-gauge-color-set-value {
40826
+ width: 22%;
40827
+ }
40828
+ th.o-gauge-color-set-type {
40829
+ width: 30%;
40830
+ }
40831
+ input,
40832
+ select {
40833
+ width: 100%;
40834
+ height: 100%;
40835
+ box-sizing: border-box;
40829
40836
  }
40830
40837
  }
40831
40838
  `;
40832
- class Composer extends owl.Component {
40833
- static template = "o-spreadsheet-Composer";
40839
+ class GaugeChartDesignPanel extends owl.Component {
40840
+ static template = "o-spreadsheet-GaugeChartDesignPanel";
40841
+ static components = {
40842
+ SidePanelCollapsible,
40843
+ Section,
40844
+ RoundColorPicker,
40845
+ GeneralDesignEditor,
40846
+ ChartErrorSection,
40847
+ StandaloneComposer,
40848
+ };
40834
40849
  static props = {
40835
- focus: {
40836
- validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value),
40837
- },
40838
- inputStyle: { type: String, optional: true },
40839
- rect: { type: Object, optional: true },
40840
- delimitation: { type: Object, optional: true },
40841
- onComposerCellFocused: { type: Function, optional: true },
40842
- onComposerContentFocused: Function,
40843
- isDefaultFocus: { type: Boolean, optional: true },
40844
- onInputContextMenu: { type: Function, optional: true },
40845
- composerStore: Object,
40846
- placeholder: { type: String, optional: true },
40850
+ figureId: String,
40851
+ definition: Object,
40852
+ updateChart: Function,
40853
+ canUpdateChart: { type: Function, optional: true },
40847
40854
  };
40848
- static components = { TextValueProvider, FunctionDescriptionProvider };
40849
- static defaultProps = {
40850
- inputStyle: "",
40851
- isDefaultFocus: false,
40855
+ state;
40856
+ setup() {
40857
+ this.state = owl.useState({
40858
+ sectionRuleCancelledReasons: this.checkSectionRuleFormulasAreValid(this.props.definition.sectionRule),
40859
+ sectionRule: deepCopy(this.props.definition.sectionRule),
40860
+ });
40861
+ }
40862
+ get designErrorMessages() {
40863
+ const cancelledReasons = [...(this.state.sectionRuleCancelledReasons || [])];
40864
+ return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
40865
+ }
40866
+ get isRangeMinInvalid() {
40867
+ return !!(this.state.sectionRuleCancelledReasons?.includes("EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */) ||
40868
+ this.state.sectionRuleCancelledReasons?.includes("GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */));
40869
+ }
40870
+ get isRangeMaxInvalid() {
40871
+ return !!(this.state.sectionRuleCancelledReasons?.includes("EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */) ||
40872
+ this.state.sectionRuleCancelledReasons?.includes("GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */));
40873
+ }
40874
+ // ---------------------------------------------------------------------------
40875
+ // COLOR_SECTION_TEMPLATE
40876
+ // ---------------------------------------------------------------------------
40877
+ get isLowerInflectionPointInvalid() {
40878
+ return !!this.state.sectionRuleCancelledReasons?.includes("GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */);
40879
+ }
40880
+ get isUpperInflectionPointInvalid() {
40881
+ return !!this.state.sectionRuleCancelledReasons?.includes("GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */);
40882
+ }
40883
+ updateSectionColor(target, color) {
40884
+ const sectionRule = deepCopy(this.state.sectionRule);
40885
+ sectionRule.colors[target] = color;
40886
+ this.updateSectionRule(sectionRule);
40887
+ }
40888
+ updateSectionRule(sectionRule) {
40889
+ this.state.sectionRuleCancelledReasons = [];
40890
+ this.state.sectionRuleCancelledReasons.push(...this.checkSectionRuleFormulasAreValid(this.state.sectionRule));
40891
+ const dispatchResult = this.props.updateChart(this.props.figureId, {
40892
+ sectionRule,
40893
+ });
40894
+ if (dispatchResult.isSuccessful) {
40895
+ this.state.sectionRule = deepCopy(sectionRule);
40896
+ }
40897
+ else {
40898
+ this.state.sectionRuleCancelledReasons.push(...dispatchResult.reasons);
40899
+ }
40900
+ }
40901
+ onConfirmGaugeRange(editedRange, content) {
40902
+ this.state.sectionRule = { ...this.state.sectionRule, [editedRange]: content };
40903
+ this.updateSectionRule(this.state.sectionRule);
40904
+ }
40905
+ getGaugeInflectionComposerProps(sectionType) {
40906
+ const inflectionPointName = sectionType === "lowerColor" ? "lowerInflectionPoint" : "upperInflectionPoint";
40907
+ const inflectionPoint = this.state.sectionRule[inflectionPointName];
40908
+ return {
40909
+ onConfirm: (str) => {
40910
+ this.state.sectionRule = {
40911
+ ...this.state.sectionRule,
40912
+ [inflectionPointName]: { ...inflectionPoint, value: str },
40913
+ };
40914
+ this.updateSectionRule(this.state.sectionRule);
40915
+ },
40916
+ composerContent: inflectionPoint.value,
40917
+ invalid: sectionType === "lowerColor"
40918
+ ? this.isLowerInflectionPointInvalid
40919
+ : this.isUpperInflectionPointInvalid,
40920
+ defaultRangeSheetId: this.sheetId,
40921
+ class: inflectionPointName,
40922
+ placeholder: _t("Value"),
40923
+ title: _t("Value or formula"),
40924
+ };
40925
+ }
40926
+ checkSectionRuleFormulasAreValid(sectionRule) {
40927
+ const reasons = [];
40928
+ if (!this.valueIsValidNumber(sectionRule.rangeMin)) {
40929
+ reasons.push("GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */);
40930
+ }
40931
+ if (!this.valueIsValidNumber(sectionRule.rangeMax)) {
40932
+ reasons.push("GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */);
40933
+ }
40934
+ if (!this.valueIsValidNumber(sectionRule.lowerInflectionPoint.value)) {
40935
+ reasons.push("GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */);
40936
+ }
40937
+ if (!this.valueIsValidNumber(sectionRule.upperInflectionPoint.value)) {
40938
+ reasons.push("GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */);
40939
+ }
40940
+ return reasons;
40941
+ }
40942
+ valueIsValidNumber(value) {
40943
+ const locale = this.env.model.getters.getLocale();
40944
+ if (!value.startsWith("=")) {
40945
+ return tryToNumber(value, locale) !== undefined;
40946
+ }
40947
+ const evaluatedValue = this.env.model.getters.evaluateFormula(this.sheetId, value);
40948
+ if (isMatrix(evaluatedValue)) {
40949
+ return false;
40950
+ }
40951
+ return tryToNumber(evaluatedValue, locale) !== undefined;
40952
+ }
40953
+ get sheetId() {
40954
+ const chart = this.env.model.getters.getChart(this.props.figureId);
40955
+ if (!chart) {
40956
+ throw new Error("Chart not found with id " + this.props.figureId);
40957
+ }
40958
+ return chart.sheetId;
40959
+ }
40960
+ }
40961
+
40962
+ class GeoChartRegionSelectSection extends owl.Component {
40963
+ static template = "o-spreadsheet-GeoChartRegionSelectSection";
40964
+ static components = { Section };
40965
+ static props = {
40966
+ figureId: String,
40967
+ definition: Object,
40968
+ updateChart: Function,
40852
40969
  };
40853
- DOMFocusableElementStore;
40854
- composerRef = owl.useRef("o_composer");
40855
- contentHelper = new ContentEditableHelper(this.composerRef.el);
40856
- composerState = owl.useState({
40857
- positionStart: 0,
40858
- positionEnd: 0,
40859
- });
40860
- autoCompleteState;
40861
- functionDescriptionState = owl.useState({
40862
- showDescription: false,
40863
- functionName: "",
40864
- functionDescription: {},
40865
- argToFocus: 0,
40866
- });
40867
- assistant = owl.useState({
40868
- forcedClosed: false,
40869
- });
40870
- compositionActive = false;
40871
- spreadsheetRect = useSpreadsheetRect();
40872
- get assistantStyle() {
40873
- const composerRect = this.composerRef.el.getBoundingClientRect();
40874
- const assistantStyle = {};
40875
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40876
- const proposals = this.autoCompleteState.provider?.proposals;
40877
- const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40878
- if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
40879
- assistantStyle.width = `${ASSISTANT_WIDTH}px`;
40970
+ updateSelectedRegion(ev) {
40971
+ const value = ev.target.value;
40972
+ this.props.updateChart(this.props.figureId, { region: value });
40973
+ }
40974
+ get availableRegions() {
40975
+ return this.env.model.getters.getGeoChartAvailableRegions();
40976
+ }
40977
+ get selectedRegion() {
40978
+ return this.props.definition.region || this.availableRegions[0]?.id;
40979
+ }
40980
+ }
40981
+
40982
+ class GeoChartConfigPanel extends GenericChartConfigPanel {
40983
+ static template = "o-spreadsheet-GeoChartConfigPanel";
40984
+ static components = { ...GenericChartConfigPanel.components, GeoChartRegionSelectSection };
40985
+ get dataRanges() {
40986
+ return this.getDataSeriesRanges().slice(0, 1);
40987
+ }
40988
+ onDataSeriesConfirmed() {
40989
+ this.dataSets = spreadRange(this.env.model.getters, this.dataSets).slice(0, 1);
40990
+ this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
40991
+ dataSets: this.dataSets,
40992
+ });
40993
+ }
40994
+ getLabelRangeOptions() {
40995
+ return [
40996
+ {
40997
+ name: "dataSetsHaveTitle",
40998
+ label: this.dataSetsHaveTitleLabel,
40999
+ value: this.props.definition.dataSetsHaveTitle,
41000
+ onChange: this.onUpdateDataSetsHaveTitle.bind(this),
41001
+ },
41002
+ ];
41003
+ }
41004
+ }
41005
+
41006
+ const DEFAULT_CUSTOM_COLOR_SCALE = {
41007
+ minColor: "#FFF5EB",
41008
+ midColor: "#FD8D3C",
41009
+ maxColor: "#7F2704",
41010
+ };
41011
+ class GeoChartDesignPanel extends ChartWithAxisDesignPanel {
41012
+ static template = "o-spreadsheet-GeoChartDesignPanel";
41013
+ static components = { ...ChartWithAxisDesignPanel.components, RoundColorPicker };
41014
+ colorScalesChoices = ChartTerms.GeoChart.ColorScales;
41015
+ updateColorScaleType(ev) {
41016
+ const value = ev.target.value;
41017
+ value === "custom"
41018
+ ? this.updateColorScale(DEFAULT_CUSTOM_COLOR_SCALE)
41019
+ : this.updateColorScale(value);
41020
+ }
41021
+ updateColorScale(colorScale) {
41022
+ this.props.updateChart(this.props.figureId, { colorScale });
41023
+ }
41024
+ updateMissingValueColor(color) {
41025
+ this.props.updateChart(this.props.figureId, { missingValueColor: color });
41026
+ }
41027
+ updateLegendPosition(ev) {
41028
+ const value = ev.target.value;
41029
+ this.props.updateChart(this.props.figureId, { legendPosition: value });
41030
+ }
41031
+ get selectedColorScale() {
41032
+ return typeof this.props.definition.colorScale === "object"
41033
+ ? "custom"
41034
+ : this.props.definition.colorScale || "oranges";
41035
+ }
41036
+ get selectedMissingValueColor() {
41037
+ return this.props.definition.missingValueColor || "#ffffff";
41038
+ }
41039
+ get customColorScale() {
41040
+ if (typeof this.props.definition.colorScale === "object") {
41041
+ return this.props.definition.colorScale;
40880
41042
  }
40881
- if (this.props.delimitation && this.props.rect) {
40882
- const { x: cellX, y: cellY, height: cellHeight } = this.props.rect;
40883
- const remainingHeight = this.props.delimitation.height - (cellY + cellHeight);
40884
- assistantStyle["max-height"] = `${remainingHeight}px`;
40885
- if (cellY > remainingHeight) {
40886
- const availableSpaceAbove = cellY;
40887
- assistantStyle["max-height"] = `${availableSpaceAbove - CLOSE_ICON_RADIUS}px`;
40888
- // render top
40889
- // We compensate 2 px of margin on the assistant style + 1px for design reasons
40890
- assistantStyle.top = `-3px`;
40891
- assistantStyle.transform = `translate(0, -100%)`;
40892
- }
40893
- if (cellX + ASSISTANT_WIDTH > this.props.delimitation.width) {
40894
- // render left
40895
- assistantStyle.right = `0px`;
40896
- }
41043
+ return undefined;
41044
+ }
41045
+ getCustomColorScaleColor(color) {
41046
+ return this.customColorScale?.[color] ?? "";
41047
+ }
41048
+ setCustomColorScaleColor(colorType, color) {
41049
+ if (!color && colorType !== "midColor") {
41050
+ color = "#fff";
41051
+ }
41052
+ const customColorScale = this.customColorScale;
41053
+ if (!customColorScale) {
41054
+ return;
41055
+ }
41056
+ this.updateColorScale({ ...customColorScale, [colorType]: color });
41057
+ }
41058
+ }
41059
+
41060
+ class LineConfigPanel extends GenericChartConfigPanel {
41061
+ static template = "o-spreadsheet-LineConfigPanel";
41062
+ get canTreatLabelsAsText() {
41063
+ const chart = this.env.model.getters.getChart(this.props.figureId);
41064
+ if (chart && chart instanceof LineChart) {
41065
+ return canChartParseLabels(chart.labelRange, this.env.model.getters);
40897
41066
  }
40898
- else {
40899
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40900
- if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40901
- this.spreadsheetRect.width) {
40902
- assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40903
- }
41067
+ return false;
41068
+ }
41069
+ get stackedLabel() {
41070
+ const definition = this.props.definition;
41071
+ return definition.fillArea
41072
+ ? this.chartTerms.StackedAreaChart
41073
+ : this.chartTerms.StackedLineChart;
41074
+ }
41075
+ getLabelRangeOptions() {
41076
+ const options = super.getLabelRangeOptions();
41077
+ if (this.canTreatLabelsAsText) {
41078
+ options.push({
41079
+ name: "labelsAsText",
41080
+ value: this.props.definition.labelsAsText,
41081
+ label: this.chartTerms.TreatLabelsAsText,
41082
+ onChange: this.onUpdateLabelsAsText.bind(this),
41083
+ });
40904
41084
  }
40905
- return cssPropertiesToCss(assistantStyle);
41085
+ return options;
40906
41086
  }
40907
- // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40908
- shouldProcessInputEvents = false;
40909
- tokens = [];
40910
- keyMapping = {
40911
- Enter: (ev) => this.processEnterKey(ev, "down"),
40912
- "Shift+Enter": (ev) => this.processEnterKey(ev, "up"),
40913
- "Alt+Enter": this.processNewLineEvent,
40914
- "Ctrl+Enter": this.processNewLineEvent,
40915
- Escape: this.processEscapeKey,
40916
- F2: (ev) => this.toggleEditionMode(ev),
40917
- F4: (ev) => this.processF4Key(ev),
40918
- Tab: (ev) => this.processTabKey(ev, "right"),
40919
- "Shift+Tab": (ev) => this.processTabKey(ev, "left"),
40920
- };
40921
- keyCodeMapping = {
40922
- NumpadDecimal: this.processNumpadDecimal,
40923
- };
40924
- setup() {
40925
- this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
40926
- this.autoCompleteState = useLocalStore(AutoCompleteStore);
40927
- owl.onMounted(() => {
40928
- const el = this.composerRef.el;
40929
- if (this.props.isDefaultFocus) {
40930
- this.DOMFocusableElementStore.setFocusableElement(el);
40931
- }
40932
- this.contentHelper.updateEl(el);
40933
- });
40934
- this.env.model.selection.observe(this, {
40935
- handleEvent: () => this.autoCompleteState.hide(),
41087
+ onUpdateLabelsAsText(labelsAsText) {
41088
+ this.props.updateChart(this.props.figureId, {
41089
+ labelsAsText,
40936
41090
  });
40937
- owl.onWillUnmount(() => {
40938
- this.env.model.selection.detachObserver(this);
41091
+ }
41092
+ onUpdateStacked(stacked) {
41093
+ this.props.updateChart(this.props.figureId, {
41094
+ stacked,
40939
41095
  });
40940
- owl.useEffect(() => {
40941
- this.processContent();
40942
- if (document.activeElement === this.contentHelper.el &&
40943
- this.props.composerStore.editionMode === "inactive" &&
40944
- !this.props.isDefaultFocus) {
40945
- this.DOMFocusableElementStore.focusableElement?.focus();
40946
- }
41096
+ }
41097
+ onUpdateCumulative(cumulative) {
41098
+ this.props.updateChart(this.props.figureId, {
41099
+ cumulative,
40947
41100
  });
40948
- owl.useEffect(() => {
40949
- this.processTokenAtCursor();
40950
- }, () => [this.props.composerStore.editionMode !== "inactive"]);
40951
41101
  }
40952
- // ---------------------------------------------------------------------------
40953
- // Handlers
40954
- // ---------------------------------------------------------------------------
40955
- processArrowKeys(ev) {
40956
- const tokenAtCursor = this.props.composerStore.tokenAtCursor;
40957
- if ((this.props.composerStore.isSelectingRange ||
40958
- this.props.composerStore.editionMode === "inactive") &&
40959
- !(["ArrowUp", "ArrowDown"].includes(ev.key) &&
40960
- this.autoCompleteState.provider &&
40961
- tokenAtCursor?.type !== "REFERENCE")) {
40962
- this.functionDescriptionState.showDescription = false;
40963
- this.autoCompleteState.hide();
40964
- // Prevent the default content editable behavior which moves the cursor
40965
- ev.preventDefault();
40966
- ev.stopPropagation();
40967
- updateSelectionWithArrowKeys(ev, this.env.model.selection);
40968
- return;
40969
- }
40970
- const content = this.props.composerStore.currentContent;
40971
- if (this.props.focus === "cellFocus" &&
40972
- !this.autoCompleteState.provider &&
40973
- !content.startsWith("=")) {
40974
- this.props.composerStore.stopEdition();
40975
- return;
41102
+ }
41103
+
41104
+ class PieChartDesignPanel extends owl.Component {
41105
+ static template = "o-spreadsheet-PieChartDesignPanel";
41106
+ static components = {
41107
+ GeneralDesignEditor,
41108
+ Section,
41109
+ Checkbox,
41110
+ ChartLegend,
41111
+ };
41112
+ static props = {
41113
+ figureId: String,
41114
+ definition: Object,
41115
+ updateChart: Function,
41116
+ canUpdateChart: { type: Function, optional: true },
41117
+ };
41118
+ }
41119
+
41120
+ class RadarChartDesignPanel extends owl.Component {
41121
+ static template = "o-spreadsheet-RadarChartDesignPanel";
41122
+ static components = {
41123
+ GeneralDesignEditor,
41124
+ SeriesDesignEditor,
41125
+ Section,
41126
+ Checkbox,
41127
+ ChartLegend,
41128
+ };
41129
+ static props = {
41130
+ figureId: String,
41131
+ definition: Object,
41132
+ canUpdateChart: Function,
41133
+ updateChart: Function,
41134
+ };
41135
+ }
41136
+
41137
+ class ScatterConfigPanel extends GenericChartConfigPanel {
41138
+ static template = "o-spreadsheet-ScatterConfigPanel";
41139
+ get canTreatLabelsAsText() {
41140
+ const chart = this.env.model.getters.getChart(this.props.figureId);
41141
+ if (chart && chart instanceof ScatterChart) {
41142
+ return canChartParseLabels(chart.labelRange, this.env.model.getters);
40976
41143
  }
40977
- // All arrow keys are processed: up and down should move autocomplete, left
40978
- // and right should move the cursor.
40979
- ev.stopPropagation();
40980
- this.handleArrowKeysForAutocomplete(ev);
41144
+ return false;
40981
41145
  }
40982
- handleArrowKeysForAutocomplete(ev) {
40983
- // only for arrow up and down
40984
- if (["ArrowUp", "ArrowDown"].includes(ev.key) && this.autoCompleteState.provider) {
40985
- ev.preventDefault();
40986
- this.autoCompleteState.moveSelection(ev.key === "ArrowDown" ? "next" : "previous");
40987
- }
41146
+ onUpdateLabelsAsText(labelsAsText) {
41147
+ this.props.updateChart(this.props.figureId, {
41148
+ labelsAsText,
41149
+ });
40988
41150
  }
40989
- processTabKey(ev, direction) {
40990
- ev.preventDefault();
40991
- ev.stopPropagation();
40992
- if (this.props.composerStore.editionMode !== "inactive") {
40993
- const state = this.autoCompleteState;
40994
- if (state.provider && state.selectedIndex !== undefined) {
40995
- const autoCompleteValue = state.provider.proposals[state.selectedIndex]?.text;
40996
- if (autoCompleteValue) {
40997
- this.autoComplete(autoCompleteValue);
40998
- return;
40999
- }
41000
- }
41001
- this.props.composerStore.stopEdition(direction);
41151
+ getLabelRangeOptions() {
41152
+ const options = super.getLabelRangeOptions();
41153
+ if (this.canTreatLabelsAsText) {
41154
+ options.push({
41155
+ name: "labelsAsText",
41156
+ value: this.props.definition.labelsAsText,
41157
+ label: this.chartTerms.TreatLabelsAsText,
41158
+ onChange: this.onUpdateLabelsAsText.bind(this),
41159
+ });
41002
41160
  }
41161
+ return options;
41003
41162
  }
41004
- processEnterKey(ev, direction) {
41005
- ev.preventDefault();
41006
- ev.stopPropagation();
41007
- const state = this.autoCompleteState;
41008
- if (state.provider && state.selectedIndex !== undefined) {
41009
- const autoCompleteValue = state.provider.proposals[state.selectedIndex]?.text;
41010
- if (autoCompleteValue) {
41011
- this.autoComplete(autoCompleteValue);
41012
- return;
41013
- }
41014
- }
41015
- this.props.composerStore.stopEdition(direction);
41163
+ }
41164
+
41165
+ class ScorecardChartConfigPanel extends owl.Component {
41166
+ static template = "o-spreadsheet-ScorecardChartConfigPanel";
41167
+ static components = { SelectionInput, ChartErrorSection, Section };
41168
+ static props = {
41169
+ figureId: String,
41170
+ definition: Object,
41171
+ updateChart: Function,
41172
+ canUpdateChart: Function,
41173
+ };
41174
+ state = owl.useState({
41175
+ keyValueDispatchResult: undefined,
41176
+ baselineDispatchResult: undefined,
41177
+ });
41178
+ keyValue = this.props.definition.keyValue;
41179
+ baseline = this.props.definition.baseline;
41180
+ get errorMessages() {
41181
+ const cancelledReasons = [
41182
+ ...(this.state.keyValueDispatchResult?.reasons || []),
41183
+ ...(this.state.baselineDispatchResult?.reasons || []),
41184
+ ];
41185
+ return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
41016
41186
  }
41017
- processNewLineEvent(ev) {
41018
- ev.preventDefault();
41019
- ev.stopPropagation();
41020
- const content = this.contentHelper.getText();
41021
- const selection = this.contentHelper.getCurrentSelection();
41022
- const start = Math.min(selection.start, selection.end);
41023
- const end = Math.max(selection.start, selection.end);
41024
- this.props.composerStore.stopComposerRangeSelection();
41025
- this.props.composerStore.setCurrentContent(content.slice(0, start) + NEWLINE + content.slice(end), {
41026
- start: start + 1,
41027
- end: start + 1,
41187
+ get isKeyValueInvalid() {
41188
+ return !!this.state.keyValueDispatchResult?.isCancelledBecause("InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */);
41189
+ }
41190
+ get isBaselineInvalid() {
41191
+ return !!this.state.keyValueDispatchResult?.isCancelledBecause("InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */);
41192
+ }
41193
+ onKeyValueRangeChanged(ranges) {
41194
+ this.keyValue = ranges[0];
41195
+ this.state.keyValueDispatchResult = this.props.canUpdateChart(this.props.figureId, {
41196
+ keyValue: this.keyValue,
41028
41197
  });
41029
- this.processContent();
41030
41198
  }
41031
- processEscapeKey(ev) {
41032
- this.props.composerStore.cancelEdition();
41033
- ev.stopPropagation();
41034
- ev.preventDefault();
41199
+ updateKeyValueRange() {
41200
+ this.state.keyValueDispatchResult = this.props.updateChart(this.props.figureId, {
41201
+ keyValue: this.keyValue,
41202
+ });
41035
41203
  }
41036
- processF4Key(ev) {
41037
- ev.stopPropagation();
41038
- this.props.composerStore.cycleReferences();
41039
- this.processContent();
41204
+ getKeyValueRange() {
41205
+ return this.keyValue || "";
41040
41206
  }
41041
- toggleEditionMode(ev) {
41042
- ev.stopPropagation();
41043
- this.props.composerStore.toggleEditionMode();
41044
- this.processContent();
41207
+ onBaselineRangeChanged(ranges) {
41208
+ this.baseline = ranges[0];
41209
+ this.state.baselineDispatchResult = this.props.canUpdateChart(this.props.figureId, {
41210
+ baseline: this.baseline,
41211
+ });
41045
41212
  }
41046
- processNumpadDecimal(ev) {
41047
- ev.stopPropagation();
41048
- ev.preventDefault();
41049
- const locale = this.env.model.getters.getLocale();
41050
- const selection = this.contentHelper.getCurrentSelection();
41051
- const currentContent = this.props.composerStore.currentContent;
41052
- const content = currentContent.slice(0, selection.start) +
41053
- locale.decimalSeparator +
41054
- currentContent.slice(selection.end);
41055
- // Update composer even by hand rather than dispatching an InputEvent because untrusted inputs
41056
- // events aren't handled natively by contentEditable
41057
- this.props.composerStore.setCurrentContent(content, {
41058
- start: selection.start + 1,
41059
- end: selection.start + 1,
41213
+ updateBaselineRange() {
41214
+ this.state.baselineDispatchResult = this.props.updateChart(this.props.figureId, {
41215
+ baseline: this.baseline,
41060
41216
  });
41061
- // We need to do the process content here in case there is no render between the keyDown and the
41062
- // keyUp event
41063
- this.processContent();
41064
41217
  }
41065
- onCompositionStart() {
41066
- this.compositionActive = true;
41218
+ getBaselineRange() {
41219
+ return this.baseline || "";
41067
41220
  }
41068
- onCompositionEnd() {
41069
- this.compositionActive = false;
41221
+ updateBaselineMode(ev) {
41222
+ this.props.updateChart(this.props.figureId, { baselineMode: ev.target.value });
41070
41223
  }
41071
- onKeydown(ev) {
41072
- if (this.props.composerStore.editionMode === "inactive") {
41073
- return;
41074
- }
41075
- if (ev.key.startsWith("Arrow")) {
41076
- this.processArrowKeys(ev);
41077
- return;
41078
- }
41079
- let handler = this.keyMapping[keyboardEventToShortcutString(ev)] ||
41080
- this.keyCodeMapping[keyboardEventToShortcutString(ev, "code")];
41081
- if (handler) {
41082
- handler.call(this, ev);
41083
- }
41084
- else {
41085
- ev.stopPropagation();
41086
- }
41224
+ }
41225
+
41226
+ class ScorecardChartDesignPanel extends owl.Component {
41227
+ static template = "o-spreadsheet-ScorecardChartDesignPanel";
41228
+ static components = {
41229
+ GeneralDesignEditor,
41230
+ RoundColorPicker,
41231
+ SidePanelCollapsible,
41232
+ Section,
41233
+ Checkbox,
41234
+ };
41235
+ static props = {
41236
+ figureId: String,
41237
+ definition: Object,
41238
+ updateChart: Function,
41239
+ canUpdateChart: { type: Function, optional: true },
41240
+ };
41241
+ get colorsSectionTitle() {
41242
+ return this.props.definition.baselineMode === "progress"
41243
+ ? _t("Progress bar colors")
41244
+ : _t("Baseline colors");
41087
41245
  }
41088
- onPaste(ev) {
41089
- if (this.props.composerStore.editionMode !== "inactive") {
41090
- // let the browser clipboard work
41091
- ev.stopPropagation();
41092
- }
41093
- else {
41094
- // the user meant to paste in the sheet, not open the composer with the pasted content
41095
- // While we're not editing, we still have the focus and should therefore prevent
41096
- // the native "paste" to occur.
41097
- ev.preventDefault();
41098
- }
41246
+ get humanizeNumbersLabel() {
41247
+ return _t("Humanize numbers");
41099
41248
  }
41100
- /*
41101
- * Triggered automatically by the content-editable between the keydown and key up
41102
- * */
41103
- onInput(ev) {
41104
- if (!this.shouldProcessInputEvents) {
41105
- return;
41106
- }
41107
- ev.stopPropagation();
41108
- let content;
41109
- if (this.props.composerStore.editionMode === "inactive") {
41110
- content = ev.data || "";
41111
- }
41112
- else {
41113
- content = this.contentHelper.getText();
41114
- }
41115
- if (this.props.focus === "inactive") {
41116
- return this.props.onComposerCellFocused?.(content);
41117
- }
41118
- let selection = this.contentHelper.getCurrentSelection();
41119
- this.props.composerStore.stopComposerRangeSelection();
41120
- this.props.composerStore.setCurrentContent(content, selection);
41121
- this.processTokenAtCursor();
41249
+ get defaultScorecardTitleFontSize() {
41250
+ return SCORECARD_CHART_TITLE_FONT_SIZE;
41122
41251
  }
41123
- onKeyup(ev) {
41124
- if (this.contentHelper.el === document.activeElement) {
41125
- if (this.autoCompleteState.provider && ["ArrowUp", "ArrowDown"].includes(ev.key)) {
41126
- return;
41127
- }
41128
- if (this.props.composerStore.isSelectingRange && ev.key?.startsWith("Arrow")) {
41129
- return;
41130
- }
41131
- const { start: oldStart, end: oldEnd } = this.props.composerStore.composerSelection;
41132
- const { start, end } = this.contentHelper.getCurrentSelection();
41133
- if (start !== oldStart || end !== oldEnd) {
41134
- this.props.composerStore.changeComposerCursorSelection(start, end);
41135
- }
41136
- this.processTokenAtCursor();
41137
- }
41252
+ updateHumanizeNumbers(humanize) {
41253
+ this.props.updateChart(this.props.figureId, { humanize });
41138
41254
  }
41139
- onBlur(ev) {
41140
- if (this.props.composerStore.editionMode === "inactive") {
41141
- return;
41142
- }
41143
- const target = ev.relatedTarget;
41144
- if (!target || !(target instanceof HTMLElement)) {
41145
- this.props.composerStore.stopEdition();
41146
- return;
41147
- }
41148
- if (target.attributes.getNamedItem("composerFocusableElement")) {
41149
- this.contentHelper.el.focus();
41150
- return;
41151
- }
41152
- if (target.classList.contains("o-composer")) {
41153
- return;
41154
- }
41155
- this.props.composerStore.stopEdition();
41255
+ translate(term) {
41256
+ return _t(term);
41156
41257
  }
41157
- updateAutoCompleteIndex(index) {
41158
- this.autoCompleteState.selectIndex(clip(0, index, 10));
41258
+ updateBaselineDescr(ev) {
41259
+ this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
41159
41260
  }
41160
- /**
41161
- * This is required to ensure the content helper selection is
41162
- * properly updated on "onclick" events. Depending on the browser,
41163
- * the callback onClick from the composer will be executed before
41164
- * the selection was updated in the dom, which means we capture an
41165
- * wrong selection which is then forced upon the content helper on
41166
- * processContent.
41167
- */
41168
- onMousedown(ev) {
41169
- if (ev.button > 0) {
41170
- // not main button, probably a context menu
41171
- return;
41261
+ setColor(color, colorPickerId) {
41262
+ switch (colorPickerId) {
41263
+ case "backgroundColor":
41264
+ this.props.updateChart(this.props.figureId, { background: color });
41265
+ break;
41266
+ case "baselineColorDown":
41267
+ this.props.updateChart(this.props.figureId, { baselineColorDown: color });
41268
+ break;
41269
+ case "baselineColorUp":
41270
+ this.props.updateChart(this.props.figureId, { baselineColorUp: color });
41271
+ break;
41172
41272
  }
41173
- this.contentHelper.removeSelection();
41174
41273
  }
41175
- onClick() {
41176
- if (this.env.model.getters.isReadonly()) {
41177
- return;
41178
- }
41179
- const newSelection = this.contentHelper.getCurrentSelection();
41180
- this.props.composerStore.stopComposerRangeSelection();
41181
- this.props.onComposerContentFocused();
41182
- this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
41183
- this.processTokenAtCursor();
41274
+ }
41275
+
41276
+ class WaterfallChartDesignPanel extends owl.Component {
41277
+ static template = "o-spreadsheet-WaterfallChartDesignPanel";
41278
+ static components = {
41279
+ GeneralDesignEditor,
41280
+ Checkbox,
41281
+ SidePanelCollapsible,
41282
+ Section,
41283
+ RoundColorPicker,
41284
+ AxisDesignEditor,
41285
+ RadioSelection,
41286
+ ChartLegend,
41287
+ };
41288
+ static props = {
41289
+ figureId: String,
41290
+ definition: Object,
41291
+ updateChart: Function,
41292
+ canUpdateChart: { type: Function, optional: true },
41293
+ };
41294
+ axisChoices = CHART_AXIS_CHOICES;
41295
+ onUpdateShowSubTotals(showSubTotals) {
41296
+ this.props.updateChart(this.props.figureId, { showSubTotals });
41184
41297
  }
41185
- onDblClick() {
41186
- if (this.env.model.getters.isReadonly()) {
41187
- return;
41188
- }
41189
- const composerContent = this.props.composerStore.currentContent;
41190
- const isValidFormula = composerContent.startsWith("=");
41191
- if (isValidFormula) {
41192
- const tokens = this.props.composerStore.currentTokens;
41193
- const currentSelection = this.contentHelper.getCurrentSelection();
41194
- if (currentSelection.start === currentSelection.end)
41195
- return;
41196
- const currentSelectedText = composerContent.substring(currentSelection.start, currentSelection.end);
41197
- const token = tokens.filter((token) => token.value.includes(currentSelectedText) &&
41198
- token.start <= currentSelection.start &&
41199
- token.end >= currentSelection.end)[0];
41200
- if (!token) {
41201
- return;
41202
- }
41203
- if (token.type === "REFERENCE") {
41204
- this.props.composerStore.changeComposerCursorSelection(token.start, token.end);
41205
- }
41206
- }
41298
+ onUpdateShowConnectorLines(showConnectorLines) {
41299
+ this.props.updateChart(this.props.figureId, { showConnectorLines });
41207
41300
  }
41208
- onContextMenu(ev) {
41209
- if (this.props.composerStore.editionMode === "inactive") {
41210
- this.props.onInputContextMenu?.(ev);
41211
- }
41301
+ onUpdateFirstValueAsSubtotal(firstValueAsSubtotal) {
41302
+ this.props.updateChart(this.props.figureId, { firstValueAsSubtotal });
41212
41303
  }
41213
- closeAssistant() {
41214
- this.assistant.forcedClosed = true;
41304
+ updateColor(colorName, color) {
41305
+ this.props.updateChart(this.props.figureId, { [colorName]: color });
41215
41306
  }
41216
- openAssistant() {
41217
- this.assistant.forcedClosed = false;
41307
+ get axesList() {
41308
+ return [
41309
+ { id: "x", name: _t("Horizontal axis") },
41310
+ { id: "y", name: _t("Vertical axis") },
41311
+ ];
41312
+ }
41313
+ get positiveValuesColor() {
41314
+ return (this.props.definition.positiveValuesColor ||
41315
+ CHART_WATERFALL_POSITIVE_COLOR);
41316
+ }
41317
+ get negativeValuesColor() {
41318
+ return (this.props.definition.negativeValuesColor ||
41319
+ CHART_WATERFALL_NEGATIVE_COLOR);
41320
+ }
41321
+ get subTotalValuesColor() {
41322
+ return (this.props.definition.subTotalValuesColor ||
41323
+ CHART_WATERFALL_SUBTOTAL_COLOR);
41218
41324
  }
41219
- // ---------------------------------------------------------------------------
41220
- // Private
41221
- // ---------------------------------------------------------------------------
41222
- processContent() {
41223
- if (this.compositionActive) {
41224
- return;
41225
- }
41226
- this.shouldProcessInputEvents = false;
41227
- if (this.props.focus !== "inactive" && document.activeElement !== this.contentHelper.el) {
41228
- this.contentHelper.el.focus();
41229
- }
41230
- const content = this.getContentLines();
41231
- this.contentHelper.setText(content);
41232
- if (content.length !== 0 && content.length[0] !== 0) {
41233
- if (this.props.focus !== "inactive") {
41234
- // Put the cursor back where it was before the rendering
41235
- const { start, end } = this.props.composerStore.composerSelection;
41236
- this.contentHelper.selectRange(start, end);
41237
- }
41238
- this.contentHelper.scrollSelectionIntoView();
41239
- }
41240
- this.shouldProcessInputEvents = true;
41325
+ updateVerticalAxisPosition(value) {
41326
+ this.props.updateChart(this.props.figureId, {
41327
+ verticalAxisPosition: value,
41328
+ });
41241
41329
  }
41242
- /**
41243
- * Get the HTML content corresponding to the current composer token, divided by lines.
41244
- */
41245
- getContentLines() {
41246
- let value = this.props.composerStore.currentContent;
41247
- const isValidFormula = value.startsWith("=");
41248
- if (value === "") {
41249
- return [];
41250
- }
41251
- else if (isValidFormula && this.props.focus !== "inactive") {
41252
- return this.splitHtmlContentIntoLines(this.getHtmlContentFromTokens());
41253
- }
41254
- return this.splitHtmlContentIntoLines([{ value }]);
41330
+ }
41331
+
41332
+ const chartSidePanelComponentRegistry = new Registry();
41333
+ chartSidePanelComponentRegistry
41334
+ .add("line", {
41335
+ configuration: LineConfigPanel,
41336
+ design: ChartWithAxisDesignPanel,
41337
+ })
41338
+ .add("scatter", {
41339
+ configuration: ScatterConfigPanel,
41340
+ design: ChartWithAxisDesignPanel,
41341
+ })
41342
+ .add("bar", {
41343
+ configuration: BarConfigPanel,
41344
+ design: ChartWithAxisDesignPanel,
41345
+ })
41346
+ .add("combo", {
41347
+ configuration: GenericChartConfigPanel,
41348
+ design: ComboChartDesignPanel,
41349
+ })
41350
+ .add("pie", {
41351
+ configuration: GenericChartConfigPanel,
41352
+ design: PieChartDesignPanel,
41353
+ })
41354
+ .add("gauge", {
41355
+ configuration: GaugeChartConfigPanel,
41356
+ design: GaugeChartDesignPanel,
41357
+ })
41358
+ .add("scorecard", {
41359
+ configuration: ScorecardChartConfigPanel,
41360
+ design: ScorecardChartDesignPanel,
41361
+ })
41362
+ .add("waterfall", {
41363
+ configuration: GenericChartConfigPanel,
41364
+ design: WaterfallChartDesignPanel,
41365
+ })
41366
+ .add("pyramid", {
41367
+ configuration: GenericChartConfigPanel,
41368
+ design: ChartWithAxisDesignPanel,
41369
+ })
41370
+ .add("radar", {
41371
+ configuration: GenericChartConfigPanel,
41372
+ design: RadarChartDesignPanel,
41373
+ })
41374
+ .add("geo", {
41375
+ configuration: GeoChartConfigPanel,
41376
+ design: GeoChartDesignPanel,
41377
+ });
41378
+
41379
+ css /* scss */ `
41380
+ .o-section .o-input.o-type-selector {
41381
+ height: 30px;
41382
+ padding-left: 35px;
41383
+ padding-top: 5px;
41384
+ }
41385
+ .o-type-selector-preview {
41386
+ left: 5px;
41387
+ top: 3px;
41388
+ .o-chart-preview {
41389
+ width: 24px;
41390
+ height: 24px;
41255
41391
  }
41256
- getHtmlContentFromTokens() {
41257
- const tokens = this.props.composerStore.currentTokens;
41258
- const result = [];
41259
- const { end, start } = this.props.composerStore.composerSelection;
41260
- for (const token of tokens) {
41261
- let color = token.color || DEFAULT_TOKEN_COLOR;
41262
- if (token.isBlurred) {
41263
- color = setColorAlpha(color, 0.5);
41264
- }
41265
- result.push({ value: token.value, color });
41266
- if (token.type === "REFERENCE" &&
41267
- this.props.composerStore.tokenAtCursor === token &&
41268
- this.props.composerStore.editionMode === "selecting") {
41269
- result[result.length - 1].class = "text-decoration-underline";
41270
- }
41271
- if (end === start && token.isParenthesisLinkedToCursor) {
41272
- result[result.length - 1].class = backgroundClass;
41273
- }
41274
- if (this.props.composerStore.showSelectionIndicator && end === start && end === token.end) {
41275
- result[result.length - 1].class = selectionIndicatorClass;
41276
- }
41277
- }
41278
- return result;
41392
+ }
41393
+
41394
+ .o-popover .o-chart-select-popover {
41395
+ box-sizing: border-box;
41396
+ background: #fff;
41397
+ .o-chart-type-item {
41398
+ cursor: pointer;
41399
+ padding: 3px 6px;
41400
+ margin: 1px 2px;
41401
+ &.selected,
41402
+ &:hover {
41403
+ border: 1px solid ${ACTION_COLOR};
41404
+ background: ${BADGE_SELECTED_COLOR};
41405
+ padding: 2px 5px;
41406
+ }
41407
+ .o-chart-preview {
41408
+ width: 48px;
41409
+ height: 48px;
41410
+ }
41279
41411
  }
41280
- /**
41281
- * Split an array of HTMLContents into lines. Each NEWLINE character encountered will create a new
41282
- * line. Contents can be split into multiple parts if they contain multiple NEWLINE characters.
41283
- */
41284
- splitHtmlContentIntoLines(contents) {
41285
- const contentSplitInLines = [];
41286
- let currentLine = [];
41287
- for (const content of contents) {
41288
- if (content.value.includes(NEWLINE)) {
41289
- const lines = content.value.split(NEWLINE);
41290
- const lastLine = lines.pop();
41291
- for (const line of lines) {
41292
- currentLine.push({ color: content.color, value: line }); // don't copy class, only last line should keep it
41293
- contentSplitInLines.push(currentLine);
41294
- currentLine = [];
41295
- }
41296
- currentLine.push({ ...content, value: lastLine });
41412
+ }
41413
+ `;
41414
+ class ChartTypePicker extends owl.Component {
41415
+ static template = "o-spreadsheet-ChartTypePicker";
41416
+ static components = { Section, Popover };
41417
+ static props = { figureId: String, chartPanelStore: Object };
41418
+ categories = chartCategories;
41419
+ chartTypeByCategories = {};
41420
+ popoverRef = owl.useRef("popoverRef");
41421
+ selectRef = owl.useRef("selectRef");
41422
+ state = owl.useState({ popoverProps: undefined, popoverStyle: "" });
41423
+ setup() {
41424
+ owl.useExternalListener(window, "pointerdown", this.onExternalClick, { capture: true });
41425
+ for (const subtypeProperties of chartSubtypeRegistry.getAll()) {
41426
+ if (this.chartTypeByCategories[subtypeProperties.category]) {
41427
+ this.chartTypeByCategories[subtypeProperties.category].push(subtypeProperties);
41297
41428
  }
41298
41429
  else {
41299
- currentLine.push(content);
41430
+ this.chartTypeByCategories[subtypeProperties.category] = [subtypeProperties];
41300
41431
  }
41301
41432
  }
41302
- if (currentLine.length) {
41303
- contentSplitInLines.push(currentLine);
41304
- }
41305
- // Remove useless empty contents
41306
- const filteredLines = [];
41307
- for (const line of contentSplitInLines) {
41308
- if (line.every(this.isContentEmpty)) {
41309
- filteredLines.push([line[0]]);
41310
- }
41311
- else {
41312
- filteredLines.push(line.filter((content) => !this.isContentEmpty(content)));
41313
- }
41433
+ }
41434
+ onExternalClick(ev) {
41435
+ if (isChildEvent(this.popoverRef.el?.parentElement, ev) ||
41436
+ isChildEvent(this.selectRef.el, ev)) {
41437
+ return;
41314
41438
  }
41315
- return filteredLines;
41439
+ this.closePopover();
41316
41440
  }
41317
- isContentEmpty(content) {
41318
- return !(content.value || content.class);
41441
+ onTypeChange(type) {
41442
+ this.props.chartPanelStore.changeChartType(this.props.figureId, type);
41443
+ this.closePopover();
41319
41444
  }
41320
- /**
41321
- * Compute the state of the composer from the tokenAtCursor.
41322
- * If the token is a function or symbol (that isn't a cell/range reference) we have to initialize
41323
- * the autocomplete engine otherwise we initialize the formula assistant.
41324
- */
41325
- processTokenAtCursor() {
41326
- let content = this.props.composerStore.currentContent;
41327
- if (this.autoCompleteState.provider) {
41328
- this.autoCompleteState.hide();
41329
- }
41330
- this.functionDescriptionState.showDescription = false;
41331
- const autoCompleteProvider = this.props.composerStore.autocompleteProvider;
41332
- if (autoCompleteProvider) {
41333
- this.autoCompleteState.useProvider(autoCompleteProvider);
41334
- }
41335
- const token = this.props.composerStore.tokenAtCursor;
41336
- if (content.startsWith("=") && token && token.type !== "SYMBOL") {
41337
- const tokenContext = token.functionContext;
41338
- const parentFunction = tokenContext?.parent.toUpperCase();
41339
- if (tokenContext &&
41340
- parentFunction &&
41341
- parentFunction in functions &&
41342
- token.type !== "UNKNOWN") {
41343
- // initialize Formula Assistant
41344
- const description = functions[parentFunction];
41345
- const argPosition = tokenContext.argPosition;
41346
- this.functionDescriptionState.functionName = parentFunction;
41347
- this.functionDescriptionState.functionDescription = description;
41348
- this.functionDescriptionState.argToFocus = description.getArgToFocus(argPosition + 1) - 1;
41349
- this.functionDescriptionState.showDescription = true;
41350
- }
41351
- }
41445
+ getChartDefinition(figureId) {
41446
+ return this.env.model.getters.getChartDefinition(figureId);
41352
41447
  }
41353
- autoComplete(value) {
41354
- if (!value || this.assistant.forcedClosed) {
41448
+ getSelectedChartSubtypeProperties() {
41449
+ const definition = this.getChartDefinition(this.props.figureId);
41450
+ const matchedChart = chartSubtypeRegistry
41451
+ .getAll()
41452
+ .find((c) => c.matcher?.(definition) || false);
41453
+ return matchedChart || chartSubtypeRegistry.get(definition.type);
41454
+ }
41455
+ onPointerDown(ev) {
41456
+ if (this.state.popoverProps) {
41457
+ this.closePopover();
41355
41458
  return;
41356
41459
  }
41357
- this.autoCompleteState.provider?.selectProposal(value);
41358
- this.processTokenAtCursor();
41460
+ const target = ev.currentTarget;
41461
+ const { bottom, right, width } = target.getBoundingClientRect();
41462
+ this.state.popoverProps = {
41463
+ anchorRect: { x: right, y: bottom, width: 0, height: 0 },
41464
+ positioning: "TopRight",
41465
+ verticalOffset: 0,
41466
+ };
41467
+ this.state.popoverStyle = cssPropertiesToCss({ width: `${width}px` });
41468
+ }
41469
+ closePopover() {
41470
+ this.state.popoverProps = undefined;
41359
41471
  }
41360
41472
  }
41361
41473
 
41362
- class StandaloneComposerStore extends AbstractComposerStore {
41363
- args;
41364
- constructor(get, args) {
41365
- super(get);
41366
- this.args = args;
41367
- this._currentContent = this.getComposerContent();
41368
- }
41369
- getAutoCompleteProviders() {
41370
- const providersDefinitions = super.getAutoCompleteProviders();
41371
- const contextualAutocomplete = this.args().contextualAutocomplete;
41372
- if (contextualAutocomplete) {
41373
- providersDefinitions.push(contextualAutocomplete);
41374
- }
41375
- return providersDefinitions;
41474
+ class MainChartPanelStore extends SpreadsheetStore {
41475
+ mutators = ["activatePanel", "changeChartType"];
41476
+ panel = "configuration";
41477
+ creationContexts = {};
41478
+ activatePanel(panel) {
41479
+ this.panel = panel;
41376
41480
  }
41377
- /**
41378
- * Replace the current reference selected by the new one.
41379
- * */
41380
- getZoneReference(zone) {
41381
- const res = super.getZoneReference(zone);
41382
- if (this.args().defaultStatic) {
41383
- return setXcToFixedReferenceType(res, "colrow");
41481
+ changeChartType(figureId, newDisplayType) {
41482
+ const currentCreationContext = this.getters.getContextCreationChart(figureId);
41483
+ const savedCreationContext = this.creationContexts[figureId] || {};
41484
+ let newRanges = currentCreationContext?.range;
41485
+ if (newRanges?.every((range, i) => deepEquals(range, savedCreationContext.range?.[i]))) {
41486
+ newRanges = Object.assign([], savedCreationContext.range, currentCreationContext?.range);
41384
41487
  }
41385
- return res;
41386
- }
41387
- getComposerContent() {
41388
- if (this.editionMode === "inactive") {
41389
- // References in the content might not be linked to the current active sheet
41390
- // We here force the sheet name prefix for all references that are not in
41391
- // the current active sheet
41392
- const defaultRangeSheetId = this.args().defaultRangeSheetId;
41393
- return rangeTokenize(this.args().content)
41394
- .map((token) => {
41395
- if (token.type === "REFERENCE") {
41396
- const range = this.getters.getRangeFromSheetXC(defaultRangeSheetId, token.value);
41397
- return this.getters.getRangeString(range, this.getters.getActiveSheetId());
41398
- }
41399
- return token.value;
41400
- })
41401
- .join("");
41488
+ this.creationContexts[figureId] = {
41489
+ ...savedCreationContext,
41490
+ ...currentCreationContext,
41491
+ range: newRanges,
41492
+ };
41493
+ const sheetId = this.getters.getFigureSheetId(figureId);
41494
+ if (!sheetId) {
41495
+ return;
41402
41496
  }
41403
- return this._currentContent;
41404
- }
41405
- stopEdition() {
41406
- this._stopEdition();
41407
- }
41408
- confirmEdition(content) {
41409
- this.args().onConfirm(content);
41497
+ const definition = this.getChartDefinitionFromContextCreation(figureId, newDisplayType);
41498
+ this.model.dispatch("UPDATE_CHART", {
41499
+ definition,
41500
+ id: figureId,
41501
+ sheetId,
41502
+ });
41410
41503
  }
41411
- getTokenColor(token) {
41412
- if (token.type === "SYMBOL") {
41413
- const matchedColor = this.args().getContextualColoredSymbolToken?.(token);
41414
- if (matchedColor) {
41415
- return matchedColor;
41416
- }
41417
- }
41418
- return super.getTokenColor(token);
41504
+ getChartDefinitionFromContextCreation(figureId, newDisplayType) {
41505
+ const newChartInfo = chartSubtypeRegistry.get(newDisplayType);
41506
+ const ChartClass = chartRegistry.get(newChartInfo.chartType);
41507
+ return {
41508
+ ...ChartClass.getChartDefinitionFromContextCreation(this.creationContexts[figureId]),
41509
+ ...newChartInfo.subtypeDefinition,
41510
+ };
41419
41511
  }
41420
41512
  }
41421
41513
 
41422
41514
  css /* scss */ `
41423
- .o-spreadsheet {
41424
- .o-standalone-composer {
41425
- min-height: 24px;
41426
- box-sizing: border-box;
41515
+ .o-chart {
41516
+ .o-panel {
41517
+ display: flex;
41518
+ .o-panel-element {
41519
+ flex: 1 0 auto;
41520
+ padding: 8px 0px;
41521
+ text-align: center;
41522
+ cursor: pointer;
41523
+ border-right: 1px solid ${GRAY_300};
41427
41524
 
41428
- border-bottom: 1px solid;
41429
- border-color: ${GRAY_300};
41525
+ &.inactive {
41526
+ color: ${TEXT_BODY};
41527
+ background-color: ${GRAY_100};
41528
+ border-bottom: 1px solid ${GRAY_300};
41529
+ }
41430
41530
 
41431
- &.active {
41432
- border-color: ${ACTION_COLOR};
41433
- }
41531
+ &:not(.inactive) {
41532
+ color: ${TEXT_HEADING};
41533
+ border-bottom: 1px solid #fff;
41534
+ }
41434
41535
 
41435
- &.o-invalid {
41436
- border-bottom: 2px solid red;
41536
+ .fa {
41537
+ margin-right: 4px;
41538
+ }
41437
41539
  }
41438
-
41439
- /* As the standalone composer is potentially very small (eg. in a side panel), we remove the scrollbar display */
41440
- scrollbar-width: none; /* Firefox */
41441
- &::-webkit-scrollbar {
41442
- display: none;
41540
+ .o-panel-element:last-child {
41541
+ border-right: none;
41443
41542
  }
41444
41543
  }
41445
41544
  }
41446
41545
  `;
41447
- class StandaloneComposer extends owl.Component {
41448
- static template = "o-spreadsheet-StandaloneComposer";
41449
- static props = {
41450
- composerContent: { type: String, optional: true },
41451
- defaultRangeSheetId: { type: String, optional: true },
41452
- defaultStatic: { type: Boolean, optional: true },
41453
- onConfirm: Function,
41454
- contextualAutocomplete: { type: Object, optional: true },
41455
- placeholder: { type: String, optional: true },
41456
- class: { type: String, optional: true },
41457
- invalid: { type: Boolean, optional: true },
41458
- getContextualColoredSymbolToken: { type: Function, optional: true },
41459
- };
41460
- static components = { Composer };
41461
- static defaultProps = {
41462
- composerContent: "",
41463
- defaultStatic: false,
41464
- };
41465
- composerFocusStore;
41466
- standaloneComposerStore;
41467
- composerInterface;
41468
- spreadsheetRect = useSpreadsheetRect();
41546
+ class ChartPanel extends owl.Component {
41547
+ static template = "o-spreadsheet-ChartPanel";
41548
+ static components = { Section, ChartTypePicker };
41549
+ static props = { onCloseSidePanel: Function, figureId: String };
41550
+ store;
41551
+ get figureId() {
41552
+ return this.props.figureId;
41553
+ }
41469
41554
  setup() {
41470
- this.composerFocusStore = useStore(ComposerFocusStore);
41471
- const standaloneComposerStore = useLocalStore(StandaloneComposerStore, () => ({
41472
- onConfirm: this.props.onConfirm,
41473
- content: this.props.composerContent,
41474
- defaultStatic: this.props.defaultStatic ?? false,
41475
- contextualAutocomplete: this.props.contextualAutocomplete,
41476
- defaultRangeSheetId: this.props.defaultRangeSheetId,
41477
- getContextualColoredSymbolToken: this.props.getContextualColoredSymbolToken,
41478
- }));
41479
- this.standaloneComposerStore = standaloneComposerStore;
41480
- this.composerInterface = {
41481
- id: "standaloneComposer",
41482
- get editionMode() {
41483
- return standaloneComposerStore.editionMode;
41484
- },
41485
- startEdition: this.standaloneComposerStore.startEdition,
41486
- setCurrentContent: this.standaloneComposerStore.setCurrentContent,
41487
- stopEdition: this.standaloneComposerStore.stopEdition,
41555
+ this.store = useLocalStore(MainChartPanelStore);
41556
+ }
41557
+ updateChart(figureId, updateDefinition) {
41558
+ if (figureId !== this.figureId) {
41559
+ return;
41560
+ }
41561
+ const definition = {
41562
+ ...this.getChartDefinition(this.figureId),
41563
+ ...updateDefinition,
41488
41564
  };
41565
+ return this.env.model.dispatch("UPDATE_CHART", {
41566
+ definition,
41567
+ id: figureId,
41568
+ sheetId: this.env.model.getters.getFigureSheetId(figureId),
41569
+ });
41489
41570
  }
41490
- get focus() {
41491
- return this.composerFocusStore.activeComposer === this.composerInterface
41492
- ? this.composerFocusStore.focusMode
41493
- : "inactive";
41571
+ canUpdateChart(figureId, updateDefinition) {
41572
+ if (figureId !== this.figureId || !this.env.model.getters.isChartDefined(figureId)) {
41573
+ return;
41574
+ }
41575
+ const definition = {
41576
+ ...this.getChartDefinition(this.figureId),
41577
+ ...updateDefinition,
41578
+ };
41579
+ return this.env.model.canDispatch("UPDATE_CHART", {
41580
+ definition,
41581
+ id: figureId,
41582
+ sheetId: this.env.model.getters.getFigureSheetId(figureId),
41583
+ });
41494
41584
  }
41495
- get composerStyle() {
41496
- return this.props.invalid
41497
- ? cssPropertiesToCss({ padding: "1px 0px 0px 0px" })
41498
- : cssPropertiesToCss({ padding: "1px 0px" });
41585
+ onTypeChange(type) {
41586
+ if (!this.figureId) {
41587
+ return;
41588
+ }
41589
+ this.store.changeChartType(this.figureId, type);
41499
41590
  }
41500
- get containerClass() {
41501
- const classes = [
41502
- this.focus === "inactive" ? "" : "active",
41503
- this.props.invalid ? "o-invalid" : "",
41504
- this.props.class || "",
41505
- ];
41506
- return classes.join(" ");
41591
+ get chartPanel() {
41592
+ if (!this.figureId) {
41593
+ throw new Error("Chart not defined.");
41594
+ }
41595
+ const type = this.env.model.getters.getChartType(this.figureId);
41596
+ if (!type) {
41597
+ throw new Error("Chart not defined.");
41598
+ }
41599
+ const chartPanel = chartSidePanelComponentRegistry.get(type);
41600
+ if (!chartPanel) {
41601
+ throw new Error(`Component is not defined for type ${type}`);
41602
+ }
41603
+ return chartPanel;
41507
41604
  }
41508
- onFocus(selection) {
41509
- this.composerFocusStore.focusComposer(this.composerInterface, { selection });
41605
+ getChartDefinition(figureId) {
41606
+ return this.env.model.getters.getChartDefinition(figureId);
41510
41607
  }
41511
41608
  }
41512
41609
 
@@ -41766,7 +41863,7 @@ class ConditionalFormatPreviewList extends owl.Component {
41766
41863
  draggedItemId: cf.id,
41767
41864
  initialMousePosition: event.clientY,
41768
41865
  items: items,
41769
- containerEl: this.cfListRef.el,
41866
+ scrollableContainerEl: this.cfListRef.el,
41770
41867
  onDragEnd: (cfId, finalIndex) => this.onDragEnd(cfId, finalIndex),
41771
41868
  });
41772
41869
  }
@@ -44951,6 +45048,7 @@ class PivotLayoutConfigurator extends owl.Component {
44951
45048
  unusedGranularities: Object,
44952
45049
  dateGranularities: Array,
44953
45050
  datetimeGranularities: Array,
45051
+ getScrollableContainerEl: { type: Function, optional: true },
44954
45052
  pivotId: String,
44955
45053
  };
44956
45054
  dimensionsRef = owl.useRef("pivot-dimensions");
@@ -44984,7 +45082,7 @@ class PivotLayoutConfigurator extends owl.Component {
44984
45082
  draggedItemId: dimension.nameWithGranularity,
44985
45083
  initialMousePosition: event.clientY,
44986
45084
  items: draggableItems,
44987
- containerEl: this.dimensionsRef.el,
45085
+ scrollableContainerEl: this.props.getScrollableContainerEl?.() || this.dimensionsRef.el,
44988
45086
  onDragEnd: (dimensionName, finalIndex) => {
44989
45087
  const originalIndex = draggableIds.findIndex((id) => id === dimensionName);
44990
45088
  if (originalIndex === finalIndex) {
@@ -45033,7 +45131,7 @@ class PivotLayoutConfigurator extends owl.Component {
45033
45131
  draggedItemId: measure.id,
45034
45132
  initialMousePosition: event.clientY,
45035
45133
  items: draggableItems,
45036
- containerEl: this.dimensionsRef.el,
45134
+ scrollableContainerEl: this.props.getScrollableContainerEl?.() || this.dimensionsRef.el,
45037
45135
  onDragEnd: (measureName, finalIndex) => {
45038
45136
  const originalIndex = draggableIds.findIndex((id) => id === measureName);
45039
45137
  if (originalIndex === finalIndex) {
@@ -45666,7 +45764,7 @@ class SpreadsheetPivotTable {
45666
45764
  rowTreeToRows(tree, parentRow) {
45667
45765
  return tree.flatMap((node) => {
45668
45766
  const row = {
45669
- indent: parentRow ? parentRow.indent + 1 : 0,
45767
+ indent: parentRow ? parentRow.indent + 1 : 1,
45670
45768
  fields: [...(parentRow?.fields || []), node.field],
45671
45769
  values: [...(parentRow?.values || []), node.value],
45672
45770
  };
@@ -45722,7 +45820,7 @@ function dataEntriesToRows(dataEntries, index, rows, fields, values) {
45722
45820
  pivotTableRows.push({
45723
45821
  fields: _fields,
45724
45822
  values: _values,
45725
- indent: index,
45823
+ indent: index + 1,
45726
45824
  });
45727
45825
  const record = groups[value];
45728
45826
  if (record) {
@@ -46718,6 +46816,7 @@ class PivotSpreadsheetSidePanel extends owl.Component {
46718
46816
  };
46719
46817
  store;
46720
46818
  state;
46819
+ pivotSidePanelRef = owl.useRef("pivotSidePanel");
46721
46820
  setup() {
46722
46821
  this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);
46723
46822
  this.state = owl.useState({
@@ -46746,6 +46845,9 @@ class PivotSpreadsheetSidePanel extends owl.Component {
46746
46845
  get definition() {
46747
46846
  return this.store.definition;
46748
46847
  }
46848
+ getScrollableContainerEl() {
46849
+ return this.pivotSidePanelRef.el;
46850
+ }
46749
46851
  onSelectionChanged(ranges) {
46750
46852
  this.state.rangeHasChanged = true;
46751
46853
  this.state.range = ranges[0];
@@ -55924,6 +56026,8 @@ class RangeAdapter {
55924
56026
  this.getters = getters;
55925
56027
  }
55926
56028
  static getters = [
56029
+ "adaptFormulaStringDependencies",
56030
+ "copyFormulaStringForSheet",
55927
56031
  "extendRange",
55928
56032
  "getRangeString",
55929
56033
  "getRangeFromSheetXC",
@@ -56316,6 +56420,38 @@ class RangeAdapter {
56316
56420
  const unionOfZones = unionUnboundedZones(...zones);
56317
56421
  return this.getRangeFromZone(ranges[0].sheetId, unionOfZones);
56318
56422
  }
56423
+ adaptFormulaStringDependencies(sheetId, formula, applyChange) {
56424
+ if (!formula.startsWith("=")) {
56425
+ return formula;
56426
+ }
56427
+ const compiledFormula = compile(formula);
56428
+ const updatedDependencies = compiledFormula.dependencies.map((dep) => {
56429
+ const range = this.getters.getRangeFromSheetXC(sheetId, dep);
56430
+ const changedRange = applyChange(range);
56431
+ return changedRange.changeType === "NONE" ? range : changedRange.range;
56432
+ });
56433
+ return this.getters.getFormulaString(sheetId, compiledFormula.tokens, updatedDependencies);
56434
+ }
56435
+ /**
56436
+ * Copy a formula string to another sheet.
56437
+ *
56438
+ * @param mode
56439
+ * `keepSameReference` will make the formula reference the exact same ranges,
56440
+ * `moveReference` will change all the references to `sheetIdFrom` into references to `sheetIdTo`.
56441
+ */
56442
+ copyFormulaStringForSheet(sheetIdFrom, sheetIdTo, formula, mode) {
56443
+ if (!formula.startsWith("=")) {
56444
+ return formula;
56445
+ }
56446
+ const compiledFormula = compile(formula);
56447
+ const updatedDependencies = compiledFormula.dependencies.map((dep) => {
56448
+ const range = this.getters.getRangeFromSheetXC(sheetIdFrom, dep);
56449
+ return mode === "keepSameReference"
56450
+ ? range
56451
+ : duplicateRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, range);
56452
+ });
56453
+ return this.getters.getFormulaString(sheetIdTo, compiledFormula.tokens, updatedDependencies);
56454
+ }
56319
56455
  // ---------------------------------------------------------------------------
56320
56456
  // Private
56321
56457
  // ---------------------------------------------------------------------------
@@ -69213,7 +69349,7 @@ class BottomBar extends owl.Component {
69213
69349
  draggedItemId: sheetId,
69214
69350
  initialMousePosition: event.clientX,
69215
69351
  items: sheets,
69216
- containerEl: this.sheetListRef.el,
69352
+ scrollableContainerEl: this.sheetListRef.el,
69217
69353
  onDragEnd: (sheetId, finalIndex) => this.onDragEnd(sheetId, finalIndex),
69218
69354
  });
69219
69355
  }
@@ -69268,7 +69404,7 @@ class ClickableCellsStore extends SpreadsheetStore {
69268
69404
  this._registryItems = owl.markRaw(clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence));
69269
69405
  }
69270
69406
  }
69271
- getClickableAction(position) {
69407
+ getClickableItem(position) {
69272
69408
  const { sheetId, col, row } = position;
69273
69409
  const clickableCells = this._clickableCells;
69274
69410
  const xc = toXC(col, row);
@@ -69276,33 +69412,37 @@ class ClickableCellsStore extends SpreadsheetStore {
69276
69412
  clickableCells[sheetId] = {};
69277
69413
  }
69278
69414
  if (!(xc in clickableCells[sheetId])) {
69279
- clickableCells[sheetId][xc] = this.findClickableAction(position);
69415
+ const clickableCell = this.findClickableItem(position);
69416
+ if (clickableCell) {
69417
+ clickableCells[sheetId][xc] = clickableCell;
69418
+ }
69280
69419
  }
69281
69420
  return clickableCells[sheetId][xc];
69282
69421
  }
69283
- findClickableAction(position) {
69422
+ findClickableItem(position) {
69284
69423
  const getters = this.getters;
69285
69424
  for (const item of this._registryItems) {
69286
69425
  if (item.condition(position, getters)) {
69287
- return item.execute;
69426
+ return item;
69288
69427
  }
69289
69428
  }
69290
- return false;
69429
+ return undefined;
69291
69430
  }
69292
69431
  get clickableCells() {
69293
69432
  const cells = [];
69294
69433
  const getters = this.getters;
69295
69434
  const sheetId = getters.getActiveSheetId();
69296
69435
  for (const position of this.getters.getVisibleCellPositions()) {
69297
- const action = this.getClickableAction(position);
69298
- if (!action) {
69436
+ const item = this.getClickableItem(position);
69437
+ if (!item) {
69299
69438
  continue;
69300
69439
  }
69301
69440
  const zone = getters.expandZone(sheetId, positionToZone(position));
69302
69441
  cells.push({
69303
69442
  coordinates: getters.getVisibleRect(zone),
69304
69443
  position,
69305
- action,
69444
+ action: item.execute,
69445
+ title: item.title || "",
69306
69446
  });
69307
69447
  }
69308
69448
  return cells;
@@ -74678,6 +74818,9 @@ class Model extends EventBus {
74678
74818
  this.coreGetters.extendRange = this.range.extendRange.bind(this.range);
74679
74819
  this.coreGetters.getRangesUnion = this.range.getRangesUnion.bind(this.range);
74680
74820
  this.coreGetters.removeRangesSheetPrefix = this.range.removeRangesSheetPrefix.bind(this.range);
74821
+ this.coreGetters.adaptFormulaStringDependencies =
74822
+ this.range.adaptFormulaStringDependencies.bind(this.range);
74823
+ this.coreGetters.copyFormulaStringForSheet = this.range.copyFormulaStringForSheet.bind(this.range);
74681
74824
  this.getters = {
74682
74825
  isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
74683
74826
  isDashboard: () => this.config.mode === "dashboard",
@@ -75379,6 +75522,6 @@ exports.tokenColors = tokenColors;
75379
75522
  exports.tokenize = tokenize;
75380
75523
 
75381
75524
 
75382
- __info__.version = "18.2.0-alpha.6";
75383
- __info__.date = "2025-02-05T06:50:47.008Z";
75384
- __info__.hash = "dae9ab2";
75525
+ __info__.version = "18.2.0-alpha.7";
75526
+ __info__.date = "2025-02-10T09:01:19.353Z";
75527
+ __info__.hash = "0432f17";