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