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