@odoo/o-spreadsheet 16.3.24 → 16.3.26

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.
@@ -1743,6 +1743,9 @@
1743
1743
  function formatValue(value, format) {
1744
1744
  switch (typeof value) {
1745
1745
  case "string":
1746
+ if (value.includes('\\"')) {
1747
+ return value.replace(/\\"/g, '"');
1748
+ }
1746
1749
  return value;
1747
1750
  case "boolean":
1748
1751
  return value ? "TRUE" : "FALSE";
@@ -9430,6 +9433,12 @@
9430
9433
  let { col, row } = env.model.getters.getActivePosition();
9431
9434
  env.model.dispatch("OPEN_CELL_POPOVER", { col, row, popoverType: "LinkEditor" });
9432
9435
  };
9436
+ const INSERT_LINK_NAME = (env) => {
9437
+ const sheetId = env.model.getters.getActiveSheetId();
9438
+ const { col, row } = env.model.getters.getActivePosition();
9439
+ const cell = env.model.getters.getEvaluatedCell({ sheetId, col, row });
9440
+ return cell && cell.link ? _lt("Edit link") : _lt("Insert link");
9441
+ };
9433
9442
  //------------------------------------------------------------------------------
9434
9443
  // Filters action
9435
9444
  //------------------------------------------------------------------------------
@@ -16654,7 +16663,7 @@
16654
16663
  })
16655
16664
  .add("insert_link", {
16656
16665
  ...insertLink,
16657
- name: _lt("Insert link"),
16666
+ name: INSERT_LINK_NAME,
16658
16667
  sequence: 150,
16659
16668
  separator: true,
16660
16669
  });
@@ -20981,6 +20990,7 @@
20981
20990
  static components = { TextValueProvider, FunctionDescriptionProvider };
20982
20991
  static defaultProps = {
20983
20992
  inputStyle: "",
20993
+ isDefaultFocus: false,
20984
20994
  };
20985
20995
  composerRef = owl.useRef("o_composer");
20986
20996
  contentHelper = new ContentEditableHelper(this.composerRef.el);
@@ -21038,13 +21048,13 @@
21038
21048
  setup() {
21039
21049
  owl.onMounted(() => {
21040
21050
  const el = this.composerRef.el;
21051
+ if (this.props.isDefaultFocus) {
21052
+ this.env.focusableElement.setFocusableElement(el);
21053
+ }
21041
21054
  this.contentHelper.updateEl(el);
21042
21055
  this.processContent();
21043
21056
  this.contentHelper.scrollSelectionIntoView();
21044
21057
  });
21045
- owl.onWillUnmount(() => {
21046
- this.props.onComposerUnmounted?.();
21047
- });
21048
21058
  owl.onPatched(() => {
21049
21059
  if (!this.isKeyStillDown) {
21050
21060
  this.processContent();
@@ -21055,7 +21065,8 @@
21055
21065
  // Handlers
21056
21066
  // ---------------------------------------------------------------------------
21057
21067
  processArrowKeys(ev) {
21058
- if (this.env.model.getters.isSelectingForComposer()) {
21068
+ if (this.env.model.getters.isSelectingForComposer() ||
21069
+ this.env.model.getters.getEditionMode() === "inactive") {
21059
21070
  this.functionDescriptionState.showDescription = false;
21060
21071
  // Prevent the default content editable behavior which moves the cursor
21061
21072
  ev.preventDefault();
@@ -21095,21 +21106,23 @@
21095
21106
  processTabKey(ev) {
21096
21107
  ev.preventDefault();
21097
21108
  ev.stopPropagation();
21098
- if (this.autoCompleteState.showProvider) {
21099
- const autoCompleteValue = this.autoCompleteState.values[this.autoCompleteState.selectedIndex]?.text;
21100
- if (autoCompleteValue) {
21101
- this.autoComplete(autoCompleteValue);
21102
- return;
21109
+ if (this.env.model.getters.getEditionMode() !== "inactive") {
21110
+ if (this.autoCompleteState.showProvider) {
21111
+ const autoCompleteValue = this.autoCompleteState.values[this.autoCompleteState.selectedIndex]?.text;
21112
+ if (autoCompleteValue) {
21113
+ this.autoComplete(autoCompleteValue);
21114
+ return;
21115
+ }
21103
21116
  }
21104
- }
21105
- else {
21106
- // when completing with tab, if there is no value to complete, the active cell will be moved to the right.
21107
- // we can't let the model think that it is for a ref selection.
21108
- // todo: check if this can be removed someday
21109
- this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21117
+ else {
21118
+ // when completing with tab, if there is no value to complete, the active cell will be moved to the right.
21119
+ // we can't let the model think that it is for a ref selection.
21120
+ // todo: check if this can be removed someday
21121
+ this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21122
+ }
21123
+ this.env.model.dispatch("STOP_EDITION");
21110
21124
  }
21111
21125
  const direction = ev.shiftKey ? "left" : "right";
21112
- this.env.model.dispatch("STOP_EDITION");
21113
21126
  this.env.model.selection.moveAnchorCell(direction, 1);
21114
21127
  }
21115
21128
  processEnterKey(ev) {
@@ -21153,6 +21166,9 @@
21153
21166
  this.compositionActive = false;
21154
21167
  }
21155
21168
  onKeydown(ev) {
21169
+ if (this.env.model.getters.getEditionMode() === "inactive") {
21170
+ return;
21171
+ }
21156
21172
  let handler = this.keyMapping[ev.key];
21157
21173
  if (handler) {
21158
21174
  handler.call(this, ev);
@@ -21163,22 +21179,44 @@
21163
21179
  }
21164
21180
  }
21165
21181
  updateCursorIfNeeded() {
21166
- if (!this.env.model.getters.isSelectingForComposer()) {
21182
+ const moveCursor = !this.env.model.getters.isSelectingForComposer() &&
21183
+ !(this.env.model.getters.getEditionMode() === "inactive");
21184
+ if (moveCursor) {
21167
21185
  const { start, end } = this.contentHelper.getCurrentSelection();
21168
21186
  this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", { start, end });
21169
21187
  this.isKeyStillDown = true;
21170
21188
  }
21171
21189
  }
21190
+ onPaste(ev) {
21191
+ if (this.env.model.getters.getEditionMode() !== "inactive") {
21192
+ ev.stopPropagation();
21193
+ }
21194
+ }
21172
21195
  /*
21173
21196
  * Triggered automatically by the content-editable between the keydown and key up
21174
21197
  * */
21175
- onInput() {
21176
- if (this.props.focus === "inactive" || !this.shouldProcessInputEvents) {
21198
+ onInput(ev) {
21199
+ if (!this.shouldProcessInputEvents) {
21177
21200
  return;
21178
21201
  }
21202
+ if (ev.inputType === "insertFromPaste" &&
21203
+ this.env.model.getters.getEditionMode() === "inactive") {
21204
+ return;
21205
+ }
21206
+ ev.stopPropagation();
21207
+ let content;
21208
+ if (this.env.model.getters.getEditionMode() === "inactive") {
21209
+ content = ev.data || "";
21210
+ }
21211
+ else {
21212
+ content = this.contentHelper.getText();
21213
+ }
21214
+ if (this.props.focus === "inactive") {
21215
+ return this.props.onComposerCellFocused?.(content);
21216
+ }
21179
21217
  this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21180
21218
  this.env.model.dispatch("SET_CURRENT_CONTENT", {
21181
- content: this.contentHelper.getText(),
21219
+ content,
21182
21220
  selection: this.contentHelper.getCurrentSelection(),
21183
21221
  });
21184
21222
  }
@@ -21249,9 +21287,8 @@
21249
21287
  }
21250
21288
  const newSelection = this.contentHelper.getCurrentSelection();
21251
21289
  this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21252
- if (this.props.focus === "inactive") {
21253
- this.props.onComposerContentFocused(newSelection);
21254
- }
21290
+ this.props.onComposerContentFocused();
21291
+ if (this.props.focus === "inactive") ;
21255
21292
  this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", newSelection);
21256
21293
  this.processTokenAtCursor();
21257
21294
  }
@@ -21395,7 +21432,7 @@
21395
21432
  const highlights = this.env.model.getters.getHighlights();
21396
21433
  const refSheet = sheetName
21397
21434
  ? this.env.model.getters.getSheetIdByName(sheetName)
21398
- : this.env.model.getters.getCurrentEditedCell().sheetId;
21435
+ : this.env.model.getters.getCurrentEditedCell()?.sheetId;
21399
21436
  const highlight = highlights.find((highlight) => {
21400
21437
  if (highlight.sheetId !== refSheet)
21401
21438
  return false;
@@ -21472,12 +21509,13 @@
21472
21509
  }
21473
21510
  }
21474
21511
  Composer.props = {
21475
- focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
21476
- onComposerContentFocused: Function,
21477
21512
  inputStyle: { type: String, optional: true },
21478
21513
  rect: { type: Object, optional: true },
21479
21514
  delimitation: { type: Object, optional: true },
21480
- onComposerUnmounted: { type: Function, optional: true },
21515
+ focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
21516
+ onComposerCellFocused: { type: Function, optional: true },
21517
+ onComposerContentFocused: Function,
21518
+ isDefaultFocus: { type: Boolean, optional: true },
21481
21519
  };
21482
21520
 
21483
21521
  const COMPOSER_BORDER_WIDTH = 3 * 0.4 * window.devicePixelRatio || 1;
@@ -21511,51 +21549,26 @@
21511
21549
  class GridComposer extends owl.Component {
21512
21550
  static template = "o-spreadsheet-GridComposer";
21513
21551
  static components = { Composer };
21514
- gridComposerRef;
21515
- zone;
21516
- rect;
21552
+ // TODORAR see if we can keep it undefined
21553
+ rect = this.defaultRect;
21554
+ isEditing = false;
21517
21555
  isCellReferenceVisible;
21518
- composerState;
21556
+ get defaultRect() {
21557
+ return { x: 0, y: 0, width: 0, height: 0 };
21558
+ }
21519
21559
  setup() {
21520
- this.gridComposerRef = owl.useRef("gridComposer");
21521
- this.composerState = owl.useState({
21522
- rect: undefined,
21523
- delimitation: undefined,
21524
- });
21525
- const { sheetId, col, row } = this.env.model.getters.getActivePosition();
21526
- this.zone = this.env.model.getters.expandZone(sheetId, positionToZone({ col, row }));
21527
- this.rect = this.env.model.getters.getVisibleRect(this.zone);
21528
- this.isCellReferenceVisible = false;
21529
- owl.onMounted(() => {
21530
- const el = this.gridComposerRef.el;
21531
- this.composerState.rect = {
21532
- x: this.rect.x,
21533
- y: this.rect.y,
21534
- width: el.clientWidth,
21535
- height: el.clientHeight,
21536
- };
21537
- this.composerState.delimitation = {
21538
- width: el.parentElement.clientWidth,
21539
- height: el.parentElement.clientHeight,
21540
- };
21541
- });
21542
21560
  owl.onWillUpdateProps(() => {
21543
- if (this.isCellReferenceVisible) {
21544
- return;
21545
- }
21546
- const sheetId = this.env.model.getters.getActiveSheetId();
21547
- const zone = this.env.model.getters.getSelectedZone();
21548
- const rect = this.env.model.getters.getVisibleRect(zone);
21549
- if (!deepEquals(rect, this.rect) ||
21550
- sheetId !== this.env.model.getters.getCurrentEditedCell().sheetId) {
21551
- this.isCellReferenceVisible = true;
21552
- }
21561
+ this.updateComponentPosition();
21562
+ this.updateCellReferenceVisibility();
21553
21563
  });
21554
21564
  }
21555
21565
  get shouldDisplayCellReference() {
21556
21566
  return this.isCellReferenceVisible;
21557
21567
  }
21558
21568
  get cellReference() {
21569
+ if (!this.env.model.getters.getCurrentEditedCell()) {
21570
+ return "";
21571
+ }
21559
21572
  const { col, row, sheetId } = this.env.model.getters.getCurrentEditedCell();
21560
21573
  const prefixSheet = sheetId !== this.env.model.getters.getActiveSheetId();
21561
21574
  return `${prefixSheet ? getCanonicalSheetName(this.env.model.getters.getSheetName(sheetId)) + "!" : ""}${toXC(col, row)}`;
@@ -21567,7 +21580,27 @@
21567
21580
  top: `${top - GRID_CELL_REFERENCE_TOP_OFFSET}px`,
21568
21581
  });
21569
21582
  }
21583
+ get composerProps() {
21584
+ const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
21585
+ return {
21586
+ rect: { ...this.rect },
21587
+ delimitation: {
21588
+ width,
21589
+ height,
21590
+ },
21591
+ focus: this.props.focus,
21592
+ isDefaultFocus: true,
21593
+ onComposerContentFocused: this.props.onComposerContentFocused,
21594
+ onComposerCellFocused: this.props.onComposerCellFocused,
21595
+ };
21596
+ }
21570
21597
  get containerStyle() {
21598
+ if (this.env.model.getters.getEditionMode() === "inactive" || !this.rect) {
21599
+ return `
21600
+ position: absolute;
21601
+ z-index: -1000;
21602
+ `;
21603
+ }
21571
21604
  const isFormula = this.env.model.getters.getCurrentContent().startsWith("=");
21572
21605
  const cell = this.env.model.getters.getActiveCell();
21573
21606
  const position = this.env.model.getters.getActivePosition();
@@ -21587,6 +21620,8 @@
21587
21620
  if (!isFormula) {
21588
21621
  textAlign = style.align || cell.defaultAlign;
21589
21622
  }
21623
+ const maxHeight = this.props.gridDims.height - this.rect.y;
21624
+ const maxWidth = this.props.gridDims.width - this.rect.x;
21590
21625
  /**
21591
21626
  * min-size is on the container, not the composer element, because we want to have the same size as the cell by default,
21592
21627
  * including all the paddings/margins of the composer
@@ -21598,6 +21633,8 @@
21598
21633
  top: `${top}px`,
21599
21634
  "min-width": `${width + 1}px`,
21600
21635
  "min-height": `${height + 1}px`,
21636
+ "max-width": `${maxWidth}px`,
21637
+ "max-height": `${maxHeight}px`,
21601
21638
  background,
21602
21639
  color,
21603
21640
  "font-size": `${fontSizeInPixels(fontSize)}px`,
@@ -21607,20 +21644,38 @@
21607
21644
  "text-align": textAlign,
21608
21645
  });
21609
21646
  }
21610
- get composerStyle() {
21611
- const maxHeight = this.props.gridDims.height - this.rect.y;
21612
- const maxWidth = this.props.gridDims.width - this.rect.x;
21613
- return cssPropertiesToCss({
21614
- "max-width": `${maxWidth}px`,
21615
- "max-height": `${maxHeight}px`,
21616
- });
21647
+ updateComponentPosition() {
21648
+ const isEditing = this.env.model.getters.getEditionMode() !== "inactive";
21649
+ if (this.isEditing !== isEditing) {
21650
+ this.isEditing = isEditing;
21651
+ if (!isEditing) {
21652
+ this.rect = this.defaultRect;
21653
+ this.env.focusableElement.focus();
21654
+ return;
21655
+ }
21656
+ const position = this.env.model.getters.getActivePosition();
21657
+ const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
21658
+ this.rect = this.env.model.getters.getVisibleRect(zone);
21659
+ }
21660
+ }
21661
+ updateCellReferenceVisibility() {
21662
+ if (this.isCellReferenceVisible || this.env.model.getters.getEditionMode() === "inactive") {
21663
+ return;
21664
+ }
21665
+ const sheetId = this.env.model.getters.getActiveSheetId();
21666
+ const zone = this.env.model.getters.getSelectedZone();
21667
+ const rect = this.env.model.getters.getVisibleRect(zone);
21668
+ if (!deepEquals(rect, this.rect) ||
21669
+ sheetId !== this.env.model.getters.getCurrentEditedCell().sheetId) {
21670
+ this.isCellReferenceVisible = true;
21671
+ }
21617
21672
  }
21618
21673
  }
21619
21674
  GridComposer.props = {
21620
21675
  focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
21621
- onComposerUnmounted: Function,
21622
21676
  onComposerContentFocused: Function,
21623
21677
  gridDims: Object,
21678
+ onComposerCellFocused: Function,
21624
21679
  };
21625
21680
 
21626
21681
  const CSS$1 = css /* scss */ `
@@ -22489,7 +22544,6 @@
22489
22544
  this.props.onCellDoubleClicked(col, row);
22490
22545
  }
22491
22546
  onContextMenu(ev) {
22492
- ev.preventDefault();
22493
22547
  const [col, row] = this.getCartesianCoordinates(ev);
22494
22548
  this.props.onCellRightClicked(col, row, { x: ev.clientX, y: ev.clientY });
22495
22549
  }
@@ -23583,7 +23637,6 @@
23583
23637
  HEADER_WIDTH = HEADER_WIDTH;
23584
23638
  menuState;
23585
23639
  gridRef;
23586
- hiddenInput;
23587
23640
  onMouseWheel;
23588
23641
  canvasPosition;
23589
23642
  hoveredCell;
@@ -23594,17 +23647,16 @@
23594
23647
  menuItems: [],
23595
23648
  });
23596
23649
  this.gridRef = owl.useRef("grid");
23597
- this.hiddenInput = owl.useRef("hiddenInput");
23598
23650
  this.canvasPosition = useAbsoluteBoundingRect(this.gridRef);
23599
23651
  this.hoveredCell = owl.useState({ col: undefined, row: undefined });
23600
23652
  owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
23601
23653
  owl.useExternalListener(document.body, "cut", this.copy.bind(this, true));
23602
23654
  owl.useExternalListener(document.body, "copy", this.copy.bind(this, false));
23603
23655
  owl.useExternalListener(document.body, "paste", this.paste);
23604
- owl.onMounted(() => this.focus());
23605
- this.props.exposeFocus(() => this.focus());
23656
+ owl.onMounted(() => this.focusDefaultElement());
23657
+ this.props.exposeFocus(() => this.focusDefaultElement());
23606
23658
  useGridDrawing("canvas", this.env.model, () => this.env.model.getters.getSheetViewDimensionWithHeaders());
23607
- owl.useEffect(() => this.focus(), () => [this.env.model.getters.getActiveSheetId()]);
23659
+ owl.useEffect(() => this.focusDefaultElement(), () => [this.env.model.getters.getActiveSheetId()]);
23608
23660
  this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {
23609
23661
  this.moveCanvas(deltaX, deltaY);
23610
23662
  this.hoveredCell.col = undefined;
@@ -23627,7 +23679,7 @@
23627
23679
  if (this.env.model.getters.hasOpenedPopover()) {
23628
23680
  this.closeOpenedPopover();
23629
23681
  }
23630
- this.focus();
23682
+ this.focusDefaultElement();
23631
23683
  }
23632
23684
  // this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key
23633
23685
  // down itself
@@ -23797,10 +23849,10 @@
23797
23849
  PAGEUP: () => this.env.model.dispatch("SHIFT_VIEWPORT_UP"),
23798
23850
  "CTRL+K": () => INSERT_LINK(this.env),
23799
23851
  };
23800
- focus() {
23852
+ focusDefaultElement() {
23801
23853
  if (!this.env.model.getters.getSelectedFigureId() &&
23802
23854
  this.env.model.getters.getEditionMode() === "inactive") {
23803
- this.hiddenInput.el?.focus();
23855
+ this.env.focusableElement.focus();
23804
23856
  }
23805
23857
  }
23806
23858
  get gridEl() {
@@ -23944,19 +23996,6 @@
23944
23996
  return;
23945
23997
  }
23946
23998
  }
23947
- onInput(ev) {
23948
- // the user meant to paste in the sheet, not open the composer with the pasted content
23949
- if (!ev.isComposing && ev.inputType === "insertFromPaste") {
23950
- return;
23951
- }
23952
- if (ev.data) {
23953
- // if the user types a character on the grid, it means he wants to start composing the selected cell with that
23954
- // character
23955
- ev.preventDefault();
23956
- ev.stopPropagation();
23957
- this.props.onGridComposerCellFocused(ev.data);
23958
- }
23959
- }
23960
23999
  // ---------------------------------------------------------------------------
23961
24000
  // Context Menu
23962
24001
  // ---------------------------------------------------------------------------
@@ -24066,7 +24105,7 @@
24066
24105
  }
24067
24106
  closeMenu() {
24068
24107
  this.menuState.isOpen = false;
24069
- this.focus();
24108
+ this.focusDefaultElement();
24070
24109
  }
24071
24110
  }
24072
24111
  Grid.props = {
@@ -28726,6 +28765,31 @@
28726
28765
  return data;
28727
28766
  },
28728
28767
  },
28768
+ {
28769
+ description: "Fix datafilter duplication",
28770
+ from: 12,
28771
+ to: 12.5,
28772
+ applyMigration(data) {
28773
+ for (let sheet of data.sheets || []) {
28774
+ let knownDataFilterZones = [];
28775
+ for (let filterTable of sheet.filterTables || []) {
28776
+ const zone = toZone(filterTable.range);
28777
+ // See commit message for the details
28778
+ const intersectZoneIndex = knownDataFilterZones.findIndex((knownZone) => overlap(knownZone, zone));
28779
+ if (intersectZoneIndex !== -1) {
28780
+ knownDataFilterZones[intersectZoneIndex] = zone;
28781
+ }
28782
+ else {
28783
+ knownDataFilterZones.push(zone);
28784
+ }
28785
+ }
28786
+ sheet.filterTables = knownDataFilterZones.map((zone) => ({
28787
+ range: zoneToXc(zone),
28788
+ }));
28789
+ }
28790
+ return data;
28791
+ },
28792
+ },
28729
28793
  {
28730
28794
  description: "Change Border description structure",
28731
28795
  from: 12,
@@ -29627,9 +29691,9 @@
29627
29691
  allowDispatch(cmd) {
29628
29692
  switch (cmd.type) {
29629
29693
  case "UPDATE_CELL":
29630
- return this.checkCellOutOfSheet(cmd);
29694
+ return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessUpdateCell);
29631
29695
  case "CLEAR_CELL":
29632
- return this.checkValidations(cmd, this.chainValidations(this.checkCellOutOfSheet, this.checkUselessClearCell));
29696
+ return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessClearCell);
29633
29697
  default:
29634
29698
  return 0 /* CommandResult.Success */;
29635
29699
  }
@@ -29761,10 +29825,6 @@
29761
29825
  format: cell.format ? getItemId(cell.format, formats) : undefined,
29762
29826
  content: cell.content || undefined,
29763
29827
  };
29764
- // if there is a formula but no dependencies (maybe because the cell is in error), no need to recompute the formula text
29765
- if (cell.isFormula && cell.dependencies.length) {
29766
- cells[xc].content = this.buildFormulaContent(_sheet.id, cell, cell.dependencies, true);
29767
- }
29768
29828
  }
29769
29829
  _sheet.cells = cells;
29770
29830
  }
@@ -29927,7 +29987,7 @@
29927
29987
  else {
29928
29988
  style = before ? before.style : undefined;
29929
29989
  }
29930
- let format = ("format" in after ? after.format : before && before.format) || detectFormat(afterContent);
29990
+ const format = "format" in after ? after.format : before && before.format;
29931
29991
  /* Read the following IF as:
29932
29992
  * we need to remove the cell if it is completely empty, but we can know if it completely empty if:
29933
29993
  * - the command says the new content is empty and has no border/format/style
@@ -29971,7 +30031,7 @@
29971
30031
  id,
29972
30032
  content,
29973
30033
  style,
29974
- format,
30034
+ format: format || detectFormat(content),
29975
30035
  isFormula: false,
29976
30036
  };
29977
30037
  }
@@ -30048,6 +30108,18 @@
30048
30108
  }
30049
30109
  return 0 /* CommandResult.Success */;
30050
30110
  }
30111
+ checkUselessUpdateCell(cmd) {
30112
+ const cell = this.getters.getCell(cmd);
30113
+ const hasContent = "content" in cmd || "formula" in cmd;
30114
+ const hasStyle = "style" in cmd;
30115
+ const hasFormat = "format" in cmd;
30116
+ if ((!hasContent || cell?.content === cmd.content) &&
30117
+ (!hasStyle || deepEquals(cell?.style, cmd.style)) &&
30118
+ (!hasFormat || cell?.format === cmd.format)) {
30119
+ return 94 /* CommandResult.NoChanges */;
30120
+ }
30121
+ return 0 /* CommandResult.Success */;
30122
+ }
30051
30123
  }
30052
30124
 
30053
30125
  class ChartPlugin extends CorePlugin {
@@ -33839,6 +33911,10 @@
33839
33911
  if (cell.format !== evaluatedCell.format) {
33840
33912
  exportedCellData.computedFormat = evaluatedCell.format;
33841
33913
  }
33914
+ // if there is a formula but no dependencies (maybe because the cell is in error), no need to recompute the formula text
33915
+ if (cell.isFormula && cell.dependencies.length) {
33916
+ exportedCellData.content = this.getters.buildFormulaContent(sheet.id, cell, cell.dependencies, true);
33917
+ }
33842
33918
  }
33843
33919
  }
33844
33920
  }
@@ -35707,8 +35783,7 @@
35707
35783
  let row = zone.bottom;
35708
35784
  if (col > 0) {
35709
35785
  let leftPosition = { sheetId, col: col - 1, row };
35710
- while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty ||
35711
- this.getters.getCell(leftPosition)?.content) {
35786
+ while (this.getters.getCell(leftPosition)?.content) {
35712
35787
  row += 1;
35713
35788
  leftPosition = { sheetId, col: col - 1, row };
35714
35789
  }
@@ -35717,8 +35792,7 @@
35717
35792
  col = zone.right;
35718
35793
  if (col <= this.getters.getNumberCols(sheetId)) {
35719
35794
  let rightPosition = { sheetId, col: col + 1, row };
35720
- while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty ||
35721
- this.getters.getCell(rightPosition)?.content) {
35795
+ while (this.getters.getCell(rightPosition)?.content) {
35722
35796
  row += 1;
35723
35797
  rightPosition = { sheetId, col: col + 1, row };
35724
35798
  }
@@ -41183,7 +41257,7 @@
41183
41257
  getZoneReference(zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {
41184
41258
  const sheetId = this.getters.getActiveSheetId();
41185
41259
  let selectedXc = this.getters.zoneToXC(sheetId, zone, fixedParts);
41186
- if (this.getters.getCurrentEditedCell().sheetId !== this.getters.getActiveSheetId()) {
41260
+ if (this.getters.getCurrentEditedCell()?.sheetId !== this.getters.getActiveSheetId()) {
41187
41261
  const sheetName = getCanonicalSheetName(this.getters.getSheetName(this.getters.getActiveSheetId()));
41188
41262
  selectedXc = `${sheetName}!${selectedXc}`;
41189
41263
  }
@@ -41198,7 +41272,7 @@
41198
41272
  _fixedParts.pop();
41199
41273
  }
41200
41274
  const newRange = range.clone({ parts: _fixedParts });
41201
- return this.getters.getSelectionRangeString(newRange, this.getters.getCurrentEditedCell().sheetId);
41275
+ return this.getters.getSelectionRangeString(newRange, this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId());
41202
41276
  }
41203
41277
  /**
41204
41278
  * Replace the current selection by a new text.
@@ -41231,7 +41305,7 @@
41231
41305
  if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
41232
41306
  return;
41233
41307
  }
41234
- const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
41308
+ const editionSheetId = this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId();
41235
41309
  const XCs = this.getReferencedRanges().map((range) => this.getters.getRangeString(range, editionSheetId));
41236
41310
  const colorsToKeep = {};
41237
41311
  for (const xc of XCs) {
@@ -41260,7 +41334,7 @@
41260
41334
  if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
41261
41335
  return [];
41262
41336
  }
41263
- const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
41337
+ const editionSheetId = this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId();
41264
41338
  const rangeColor = (rangeString) => {
41265
41339
  const colorIndex = this.colorIndexByRange[rangeString];
41266
41340
  return colors$1[colorIndex % colors$1.length];
@@ -41278,7 +41352,7 @@
41278
41352
  * Return ranges currently referenced in the composer
41279
41353
  */
41280
41354
  getReferencedRanges() {
41281
- const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
41355
+ const editionSheetId = this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId();
41282
41356
  const referenceRanges = this.currentTokens
41283
41357
  .filter((token) => token.type === "REFERENCE")
41284
41358
  .map((token) => this.getters.getRangeFromSheetXC(editionSheetId, token.value));
@@ -42089,6 +42163,16 @@
42089
42163
  }
42090
42164
  }
42091
42165
 
42166
+ class FocusableElement {
42167
+ focusableElement = undefined;
42168
+ setFocusableElement(element) {
42169
+ this.focusableElement = element;
42170
+ }
42171
+ focus() {
42172
+ this.focusableElement?.focus();
42173
+ }
42174
+ }
42175
+
42092
42176
  const RIPPLE_KEY_FRAMES = [
42093
42177
  { transform: "scale(0)" },
42094
42178
  { transform: "scale(0.8)", offset: 0.33 },
@@ -44117,6 +44201,7 @@
44117
44201
  _t: Spreadsheet._t,
44118
44202
  clipboard: this.env.clipboard || instantiateClipboard(),
44119
44203
  startCellEdition: (content) => this.onGridComposerCellFocused(content),
44204
+ focusableElement: new FocusableElement(),
44120
44205
  });
44121
44206
  owl.useExternalListener(window, "resize", () => this.render(true));
44122
44207
  owl.useExternalListener(window, "beforeunload", this.unbindModelEvents.bind(this));
@@ -44223,17 +44308,18 @@
44223
44308
  this.model.dispatch("UNFOCUS_SELECTION_INPUT");
44224
44309
  this.composer.topBarFocus = "contentFocus";
44225
44310
  this.composer.gridFocusMode = "inactive";
44226
- this.setComposerContent({ selection } || {});
44311
+ this.setComposerContent({ selection });
44227
44312
  }
44228
- onGridComposerContentFocused() {
44313
+ onGridComposerContentFocused(selection) {
44229
44314
  if (this.model.getters.isReadonly()) {
44230
44315
  return;
44231
44316
  }
44232
44317
  this.model.dispatch("UNFOCUS_SELECTION_INPUT");
44233
44318
  this.composer.topBarFocus = "inactive";
44234
44319
  this.composer.gridFocusMode = "contentFocus";
44235
- this.setComposerContent({});
44320
+ this.setComposerContent({ selection });
44236
44321
  }
44322
+ // TODO: either both are defined or none of them. change those args to an object
44237
44323
  onGridComposerCellFocused(content, selection) {
44238
44324
  if (this.model.getters.isReadonly()) {
44239
44325
  return;
@@ -45631,11 +45717,10 @@
45631
45717
  * next cluster if the given cell is outside a cluster or at the border of a cluster in the given direction.
45632
45718
  */
45633
45719
  getEndOfCluster(startPosition, dim, dir) {
45634
- const sheet = this.getters.getActiveSheet();
45635
45720
  let currentPosition = startPosition;
45636
45721
  // If both the current cell and the next cell are not empty, we want to go to the end of the cluster
45637
45722
  const nextCellPosition = this.getNextCellPosition(startPosition, dim, dir);
45638
- let mode = !this.isCellEmpty(currentPosition, sheet.id) && !this.isCellEmpty(nextCellPosition, sheet.id)
45723
+ let mode = !this.isEvaluatedCellEmpty(currentPosition) && !this.isEvaluatedCellEmpty(nextCellPosition)
45639
45724
  ? "endOfCluster"
45640
45725
  : "nextCluster";
45641
45726
  while (true) {
@@ -45645,7 +45730,7 @@
45645
45730
  currentPosition.row === nextCellPosition.row) {
45646
45731
  break;
45647
45732
  }
45648
- const isNextCellEmpty = this.isCellEmpty(nextCellPosition, sheet.id);
45733
+ const isNextCellEmpty = this.isEvaluatedCellEmpty(nextCellPosition);
45649
45734
  if (mode === "endOfCluster" && isNextCellEmpty) {
45650
45735
  break;
45651
45736
  }
@@ -45659,14 +45744,24 @@
45659
45744
  return dim === "cols" ? currentPosition.col : currentPosition.row;
45660
45745
  }
45661
45746
  /**
45662
- * Check if a cell is empty or undefined in the model. If the cell is part of a merge,
45663
- * check if the merge containing the cell is empty.
45747
+ * Check if a cell evaluated value is empty. If the cell is part of a merge,
45748
+ * the check applies to the main cell of the merge.
45664
45749
  */
45665
- isCellEmpty({ col, row }, sheetId = this.getters.getActiveSheetId()) {
45750
+ isEvaluatedCellEmpty({ col, row }) {
45751
+ const sheetId = this.getters.getActiveSheetId();
45666
45752
  const position = this.getters.getMainCellPosition({ sheetId, col, row });
45667
45753
  const cell = this.getters.getEvaluatedCell(position);
45668
45754
  return cell.type === CellValueType.empty;
45669
45755
  }
45756
+ /**
45757
+ * Checks if a cell is empty (i.e. does not have a content). If the cell is part of a merge,
45758
+ * the check applies to the main cell of the merge.
45759
+ */
45760
+ isCellEmpty({ col, row }) {
45761
+ const sheetId = this.getters.getActiveSheetId();
45762
+ const position = this.getters.getMainCellPosition({ sheetId, col, row });
45763
+ return !this.getters.getCell(position)?.content;
45764
+ }
45670
45765
  /** Computes the next cell position in the given direction by crossing through merges and skipping hidden cells.
45671
45766
  *
45672
45767
  * This has the same behaviour as getNextAvailablePosition() for certain arguments, but use this method instead
@@ -47937,9 +48032,9 @@
47937
48032
  Object.defineProperty(exports, '__esModule', { value: true });
47938
48033
 
47939
48034
 
47940
- __info__.version = '16.3.24';
47941
- __info__.date = '2024-02-02T13:45:48.440Z';
47942
- __info__.hash = '0bf5f45';
48035
+ __info__.version = '16.3.26';
48036
+ __info__.date = '2024-02-16T14:22:11.270Z';
48037
+ __info__.hash = 'ab9abb6';
47943
48038
 
47944
48039
 
47945
48040
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);