@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.
@@ -1746,6 +1746,9 @@ function parseFormat(formatString) {
1746
1746
  function formatValue(value, format) {
1747
1747
  switch (typeof value) {
1748
1748
  case "string":
1749
+ if (value.includes('\\"')) {
1750
+ return value.replace(/\\"/g, '"');
1751
+ }
1749
1752
  return value;
1750
1753
  case "boolean":
1751
1754
  return value ? "TRUE" : "FALSE";
@@ -9433,6 +9436,12 @@ const INSERT_LINK = (env) => {
9433
9436
  let { col, row } = env.model.getters.getActivePosition();
9434
9437
  env.model.dispatch("OPEN_CELL_POPOVER", { col, row, popoverType: "LinkEditor" });
9435
9438
  };
9439
+ const INSERT_LINK_NAME = (env) => {
9440
+ const sheetId = env.model.getters.getActiveSheetId();
9441
+ const { col, row } = env.model.getters.getActivePosition();
9442
+ const cell = env.model.getters.getEvaluatedCell({ sheetId, col, row });
9443
+ return cell && cell.link ? _lt("Edit link") : _lt("Insert link");
9444
+ };
9436
9445
  //------------------------------------------------------------------------------
9437
9446
  // Filters action
9438
9447
  //------------------------------------------------------------------------------
@@ -16657,7 +16666,7 @@ cellMenuRegistry
16657
16666
  })
16658
16667
  .add("insert_link", {
16659
16668
  ...insertLink,
16660
- name: _lt("Insert link"),
16669
+ name: INSERT_LINK_NAME,
16661
16670
  sequence: 150,
16662
16671
  separator: true,
16663
16672
  });
@@ -20984,6 +20993,7 @@ class Composer extends owl.Component {
20984
20993
  static components = { TextValueProvider, FunctionDescriptionProvider };
20985
20994
  static defaultProps = {
20986
20995
  inputStyle: "",
20996
+ isDefaultFocus: false,
20987
20997
  };
20988
20998
  composerRef = owl.useRef("o_composer");
20989
20999
  contentHelper = new ContentEditableHelper(this.composerRef.el);
@@ -21041,13 +21051,13 @@ class Composer extends owl.Component {
21041
21051
  setup() {
21042
21052
  owl.onMounted(() => {
21043
21053
  const el = this.composerRef.el;
21054
+ if (this.props.isDefaultFocus) {
21055
+ this.env.focusableElement.setFocusableElement(el);
21056
+ }
21044
21057
  this.contentHelper.updateEl(el);
21045
21058
  this.processContent();
21046
21059
  this.contentHelper.scrollSelectionIntoView();
21047
21060
  });
21048
- owl.onWillUnmount(() => {
21049
- this.props.onComposerUnmounted?.();
21050
- });
21051
21061
  owl.onPatched(() => {
21052
21062
  if (!this.isKeyStillDown) {
21053
21063
  this.processContent();
@@ -21058,7 +21068,8 @@ class Composer extends owl.Component {
21058
21068
  // Handlers
21059
21069
  // ---------------------------------------------------------------------------
21060
21070
  processArrowKeys(ev) {
21061
- if (this.env.model.getters.isSelectingForComposer()) {
21071
+ if (this.env.model.getters.isSelectingForComposer() ||
21072
+ this.env.model.getters.getEditionMode() === "inactive") {
21062
21073
  this.functionDescriptionState.showDescription = false;
21063
21074
  // Prevent the default content editable behavior which moves the cursor
21064
21075
  ev.preventDefault();
@@ -21098,21 +21109,23 @@ class Composer extends owl.Component {
21098
21109
  processTabKey(ev) {
21099
21110
  ev.preventDefault();
21100
21111
  ev.stopPropagation();
21101
- if (this.autoCompleteState.showProvider) {
21102
- const autoCompleteValue = this.autoCompleteState.values[this.autoCompleteState.selectedIndex]?.text;
21103
- if (autoCompleteValue) {
21104
- this.autoComplete(autoCompleteValue);
21105
- return;
21112
+ if (this.env.model.getters.getEditionMode() !== "inactive") {
21113
+ if (this.autoCompleteState.showProvider) {
21114
+ const autoCompleteValue = this.autoCompleteState.values[this.autoCompleteState.selectedIndex]?.text;
21115
+ if (autoCompleteValue) {
21116
+ this.autoComplete(autoCompleteValue);
21117
+ return;
21118
+ }
21106
21119
  }
21107
- }
21108
- else {
21109
- // when completing with tab, if there is no value to complete, the active cell will be moved to the right.
21110
- // we can't let the model think that it is for a ref selection.
21111
- // todo: check if this can be removed someday
21112
- this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21120
+ else {
21121
+ // when completing with tab, if there is no value to complete, the active cell will be moved to the right.
21122
+ // we can't let the model think that it is for a ref selection.
21123
+ // todo: check if this can be removed someday
21124
+ this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21125
+ }
21126
+ this.env.model.dispatch("STOP_EDITION");
21113
21127
  }
21114
21128
  const direction = ev.shiftKey ? "left" : "right";
21115
- this.env.model.dispatch("STOP_EDITION");
21116
21129
  this.env.model.selection.moveAnchorCell(direction, 1);
21117
21130
  }
21118
21131
  processEnterKey(ev) {
@@ -21156,6 +21169,9 @@ class Composer extends owl.Component {
21156
21169
  this.compositionActive = false;
21157
21170
  }
21158
21171
  onKeydown(ev) {
21172
+ if (this.env.model.getters.getEditionMode() === "inactive") {
21173
+ return;
21174
+ }
21159
21175
  let handler = this.keyMapping[ev.key];
21160
21176
  if (handler) {
21161
21177
  handler.call(this, ev);
@@ -21166,22 +21182,44 @@ class Composer extends owl.Component {
21166
21182
  }
21167
21183
  }
21168
21184
  updateCursorIfNeeded() {
21169
- if (!this.env.model.getters.isSelectingForComposer()) {
21185
+ const moveCursor = !this.env.model.getters.isSelectingForComposer() &&
21186
+ !(this.env.model.getters.getEditionMode() === "inactive");
21187
+ if (moveCursor) {
21170
21188
  const { start, end } = this.contentHelper.getCurrentSelection();
21171
21189
  this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", { start, end });
21172
21190
  this.isKeyStillDown = true;
21173
21191
  }
21174
21192
  }
21193
+ onPaste(ev) {
21194
+ if (this.env.model.getters.getEditionMode() !== "inactive") {
21195
+ ev.stopPropagation();
21196
+ }
21197
+ }
21175
21198
  /*
21176
21199
  * Triggered automatically by the content-editable between the keydown and key up
21177
21200
  * */
21178
- onInput() {
21179
- if (this.props.focus === "inactive" || !this.shouldProcessInputEvents) {
21201
+ onInput(ev) {
21202
+ if (!this.shouldProcessInputEvents) {
21180
21203
  return;
21181
21204
  }
21205
+ if (ev.inputType === "insertFromPaste" &&
21206
+ this.env.model.getters.getEditionMode() === "inactive") {
21207
+ return;
21208
+ }
21209
+ ev.stopPropagation();
21210
+ let content;
21211
+ if (this.env.model.getters.getEditionMode() === "inactive") {
21212
+ content = ev.data || "";
21213
+ }
21214
+ else {
21215
+ content = this.contentHelper.getText();
21216
+ }
21217
+ if (this.props.focus === "inactive") {
21218
+ return this.props.onComposerCellFocused?.(content);
21219
+ }
21182
21220
  this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21183
21221
  this.env.model.dispatch("SET_CURRENT_CONTENT", {
21184
- content: this.contentHelper.getText(),
21222
+ content,
21185
21223
  selection: this.contentHelper.getCurrentSelection(),
21186
21224
  });
21187
21225
  }
@@ -21252,9 +21290,8 @@ class Composer extends owl.Component {
21252
21290
  }
21253
21291
  const newSelection = this.contentHelper.getCurrentSelection();
21254
21292
  this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
21255
- if (this.props.focus === "inactive") {
21256
- this.props.onComposerContentFocused(newSelection);
21257
- }
21293
+ this.props.onComposerContentFocused();
21294
+ if (this.props.focus === "inactive") ;
21258
21295
  this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", newSelection);
21259
21296
  this.processTokenAtCursor();
21260
21297
  }
@@ -21398,7 +21435,7 @@ class Composer extends owl.Component {
21398
21435
  const highlights = this.env.model.getters.getHighlights();
21399
21436
  const refSheet = sheetName
21400
21437
  ? this.env.model.getters.getSheetIdByName(sheetName)
21401
- : this.env.model.getters.getCurrentEditedCell().sheetId;
21438
+ : this.env.model.getters.getCurrentEditedCell()?.sheetId;
21402
21439
  const highlight = highlights.find((highlight) => {
21403
21440
  if (highlight.sheetId !== refSheet)
21404
21441
  return false;
@@ -21475,12 +21512,13 @@ class Composer extends owl.Component {
21475
21512
  }
21476
21513
  }
21477
21514
  Composer.props = {
21478
- focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
21479
- onComposerContentFocused: Function,
21480
21515
  inputStyle: { type: String, optional: true },
21481
21516
  rect: { type: Object, optional: true },
21482
21517
  delimitation: { type: Object, optional: true },
21483
- onComposerUnmounted: { type: Function, optional: true },
21518
+ focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
21519
+ onComposerCellFocused: { type: Function, optional: true },
21520
+ onComposerContentFocused: Function,
21521
+ isDefaultFocus: { type: Boolean, optional: true },
21484
21522
  };
21485
21523
 
21486
21524
  const COMPOSER_BORDER_WIDTH = 3 * 0.4 * window.devicePixelRatio || 1;
@@ -21514,51 +21552,26 @@ css /* scss */ `
21514
21552
  class GridComposer extends owl.Component {
21515
21553
  static template = "o-spreadsheet-GridComposer";
21516
21554
  static components = { Composer };
21517
- gridComposerRef;
21518
- zone;
21519
- rect;
21555
+ // TODORAR see if we can keep it undefined
21556
+ rect = this.defaultRect;
21557
+ isEditing = false;
21520
21558
  isCellReferenceVisible;
21521
- composerState;
21559
+ get defaultRect() {
21560
+ return { x: 0, y: 0, width: 0, height: 0 };
21561
+ }
21522
21562
  setup() {
21523
- this.gridComposerRef = owl.useRef("gridComposer");
21524
- this.composerState = owl.useState({
21525
- rect: undefined,
21526
- delimitation: undefined,
21527
- });
21528
- const { sheetId, col, row } = this.env.model.getters.getActivePosition();
21529
- this.zone = this.env.model.getters.expandZone(sheetId, positionToZone({ col, row }));
21530
- this.rect = this.env.model.getters.getVisibleRect(this.zone);
21531
- this.isCellReferenceVisible = false;
21532
- owl.onMounted(() => {
21533
- const el = this.gridComposerRef.el;
21534
- this.composerState.rect = {
21535
- x: this.rect.x,
21536
- y: this.rect.y,
21537
- width: el.clientWidth,
21538
- height: el.clientHeight,
21539
- };
21540
- this.composerState.delimitation = {
21541
- width: el.parentElement.clientWidth,
21542
- height: el.parentElement.clientHeight,
21543
- };
21544
- });
21545
21563
  owl.onWillUpdateProps(() => {
21546
- if (this.isCellReferenceVisible) {
21547
- return;
21548
- }
21549
- const sheetId = this.env.model.getters.getActiveSheetId();
21550
- const zone = this.env.model.getters.getSelectedZone();
21551
- const rect = this.env.model.getters.getVisibleRect(zone);
21552
- if (!deepEquals(rect, this.rect) ||
21553
- sheetId !== this.env.model.getters.getCurrentEditedCell().sheetId) {
21554
- this.isCellReferenceVisible = true;
21555
- }
21564
+ this.updateComponentPosition();
21565
+ this.updateCellReferenceVisibility();
21556
21566
  });
21557
21567
  }
21558
21568
  get shouldDisplayCellReference() {
21559
21569
  return this.isCellReferenceVisible;
21560
21570
  }
21561
21571
  get cellReference() {
21572
+ if (!this.env.model.getters.getCurrentEditedCell()) {
21573
+ return "";
21574
+ }
21562
21575
  const { col, row, sheetId } = this.env.model.getters.getCurrentEditedCell();
21563
21576
  const prefixSheet = sheetId !== this.env.model.getters.getActiveSheetId();
21564
21577
  return `${prefixSheet ? getCanonicalSheetName(this.env.model.getters.getSheetName(sheetId)) + "!" : ""}${toXC(col, row)}`;
@@ -21570,7 +21583,27 @@ class GridComposer extends owl.Component {
21570
21583
  top: `${top - GRID_CELL_REFERENCE_TOP_OFFSET}px`,
21571
21584
  });
21572
21585
  }
21586
+ get composerProps() {
21587
+ const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
21588
+ return {
21589
+ rect: { ...this.rect },
21590
+ delimitation: {
21591
+ width,
21592
+ height,
21593
+ },
21594
+ focus: this.props.focus,
21595
+ isDefaultFocus: true,
21596
+ onComposerContentFocused: this.props.onComposerContentFocused,
21597
+ onComposerCellFocused: this.props.onComposerCellFocused,
21598
+ };
21599
+ }
21573
21600
  get containerStyle() {
21601
+ if (this.env.model.getters.getEditionMode() === "inactive" || !this.rect) {
21602
+ return `
21603
+ position: absolute;
21604
+ z-index: -1000;
21605
+ `;
21606
+ }
21574
21607
  const isFormula = this.env.model.getters.getCurrentContent().startsWith("=");
21575
21608
  const cell = this.env.model.getters.getActiveCell();
21576
21609
  const position = this.env.model.getters.getActivePosition();
@@ -21590,6 +21623,8 @@ class GridComposer extends owl.Component {
21590
21623
  if (!isFormula) {
21591
21624
  textAlign = style.align || cell.defaultAlign;
21592
21625
  }
21626
+ const maxHeight = this.props.gridDims.height - this.rect.y;
21627
+ const maxWidth = this.props.gridDims.width - this.rect.x;
21593
21628
  /**
21594
21629
  * min-size is on the container, not the composer element, because we want to have the same size as the cell by default,
21595
21630
  * including all the paddings/margins of the composer
@@ -21601,6 +21636,8 @@ class GridComposer extends owl.Component {
21601
21636
  top: `${top}px`,
21602
21637
  "min-width": `${width + 1}px`,
21603
21638
  "min-height": `${height + 1}px`,
21639
+ "max-width": `${maxWidth}px`,
21640
+ "max-height": `${maxHeight}px`,
21604
21641
  background,
21605
21642
  color,
21606
21643
  "font-size": `${fontSizeInPixels(fontSize)}px`,
@@ -21610,20 +21647,38 @@ class GridComposer extends owl.Component {
21610
21647
  "text-align": textAlign,
21611
21648
  });
21612
21649
  }
21613
- get composerStyle() {
21614
- const maxHeight = this.props.gridDims.height - this.rect.y;
21615
- const maxWidth = this.props.gridDims.width - this.rect.x;
21616
- return cssPropertiesToCss({
21617
- "max-width": `${maxWidth}px`,
21618
- "max-height": `${maxHeight}px`,
21619
- });
21650
+ updateComponentPosition() {
21651
+ const isEditing = this.env.model.getters.getEditionMode() !== "inactive";
21652
+ if (this.isEditing !== isEditing) {
21653
+ this.isEditing = isEditing;
21654
+ if (!isEditing) {
21655
+ this.rect = this.defaultRect;
21656
+ this.env.focusableElement.focus();
21657
+ return;
21658
+ }
21659
+ const position = this.env.model.getters.getActivePosition();
21660
+ const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
21661
+ this.rect = this.env.model.getters.getVisibleRect(zone);
21662
+ }
21663
+ }
21664
+ updateCellReferenceVisibility() {
21665
+ if (this.isCellReferenceVisible || this.env.model.getters.getEditionMode() === "inactive") {
21666
+ return;
21667
+ }
21668
+ const sheetId = this.env.model.getters.getActiveSheetId();
21669
+ const zone = this.env.model.getters.getSelectedZone();
21670
+ const rect = this.env.model.getters.getVisibleRect(zone);
21671
+ if (!deepEquals(rect, this.rect) ||
21672
+ sheetId !== this.env.model.getters.getCurrentEditedCell().sheetId) {
21673
+ this.isCellReferenceVisible = true;
21674
+ }
21620
21675
  }
21621
21676
  }
21622
21677
  GridComposer.props = {
21623
21678
  focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
21624
- onComposerUnmounted: Function,
21625
21679
  onComposerContentFocused: Function,
21626
21680
  gridDims: Object,
21681
+ onComposerCellFocused: Function,
21627
21682
  };
21628
21683
 
21629
21684
  const CSS$1 = css /* scss */ `
@@ -22492,7 +22547,6 @@ class GridOverlay extends owl.Component {
22492
22547
  this.props.onCellDoubleClicked(col, row);
22493
22548
  }
22494
22549
  onContextMenu(ev) {
22495
- ev.preventDefault();
22496
22550
  const [col, row] = this.getCartesianCoordinates(ev);
22497
22551
  this.props.onCellRightClicked(col, row, { x: ev.clientX, y: ev.clientY });
22498
22552
  }
@@ -23586,7 +23640,6 @@ class Grid extends owl.Component {
23586
23640
  HEADER_WIDTH = HEADER_WIDTH;
23587
23641
  menuState;
23588
23642
  gridRef;
23589
- hiddenInput;
23590
23643
  onMouseWheel;
23591
23644
  canvasPosition;
23592
23645
  hoveredCell;
@@ -23597,17 +23650,16 @@ class Grid extends owl.Component {
23597
23650
  menuItems: [],
23598
23651
  });
23599
23652
  this.gridRef = owl.useRef("grid");
23600
- this.hiddenInput = owl.useRef("hiddenInput");
23601
23653
  this.canvasPosition = useAbsoluteBoundingRect(this.gridRef);
23602
23654
  this.hoveredCell = owl.useState({ col: undefined, row: undefined });
23603
23655
  owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
23604
23656
  owl.useExternalListener(document.body, "cut", this.copy.bind(this, true));
23605
23657
  owl.useExternalListener(document.body, "copy", this.copy.bind(this, false));
23606
23658
  owl.useExternalListener(document.body, "paste", this.paste);
23607
- owl.onMounted(() => this.focus());
23608
- this.props.exposeFocus(() => this.focus());
23659
+ owl.onMounted(() => this.focusDefaultElement());
23660
+ this.props.exposeFocus(() => this.focusDefaultElement());
23609
23661
  useGridDrawing("canvas", this.env.model, () => this.env.model.getters.getSheetViewDimensionWithHeaders());
23610
- owl.useEffect(() => this.focus(), () => [this.env.model.getters.getActiveSheetId()]);
23662
+ owl.useEffect(() => this.focusDefaultElement(), () => [this.env.model.getters.getActiveSheetId()]);
23611
23663
  this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {
23612
23664
  this.moveCanvas(deltaX, deltaY);
23613
23665
  this.hoveredCell.col = undefined;
@@ -23630,7 +23682,7 @@ class Grid extends owl.Component {
23630
23682
  if (this.env.model.getters.hasOpenedPopover()) {
23631
23683
  this.closeOpenedPopover();
23632
23684
  }
23633
- this.focus();
23685
+ this.focusDefaultElement();
23634
23686
  }
23635
23687
  // this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key
23636
23688
  // down itself
@@ -23800,10 +23852,10 @@ class Grid extends owl.Component {
23800
23852
  PAGEUP: () => this.env.model.dispatch("SHIFT_VIEWPORT_UP"),
23801
23853
  "CTRL+K": () => INSERT_LINK(this.env),
23802
23854
  };
23803
- focus() {
23855
+ focusDefaultElement() {
23804
23856
  if (!this.env.model.getters.getSelectedFigureId() &&
23805
23857
  this.env.model.getters.getEditionMode() === "inactive") {
23806
- this.hiddenInput.el?.focus();
23858
+ this.env.focusableElement.focus();
23807
23859
  }
23808
23860
  }
23809
23861
  get gridEl() {
@@ -23947,19 +23999,6 @@ class Grid extends owl.Component {
23947
23999
  return;
23948
24000
  }
23949
24001
  }
23950
- onInput(ev) {
23951
- // the user meant to paste in the sheet, not open the composer with the pasted content
23952
- if (!ev.isComposing && ev.inputType === "insertFromPaste") {
23953
- return;
23954
- }
23955
- if (ev.data) {
23956
- // if the user types a character on the grid, it means he wants to start composing the selected cell with that
23957
- // character
23958
- ev.preventDefault();
23959
- ev.stopPropagation();
23960
- this.props.onGridComposerCellFocused(ev.data);
23961
- }
23962
- }
23963
24002
  // ---------------------------------------------------------------------------
23964
24003
  // Context Menu
23965
24004
  // ---------------------------------------------------------------------------
@@ -24069,7 +24108,7 @@ class Grid extends owl.Component {
24069
24108
  }
24070
24109
  closeMenu() {
24071
24110
  this.menuState.isOpen = false;
24072
- this.focus();
24111
+ this.focusDefaultElement();
24073
24112
  }
24074
24113
  }
24075
24114
  Grid.props = {
@@ -28729,6 +28768,31 @@ const MIGRATIONS = [
28729
28768
  return data;
28730
28769
  },
28731
28770
  },
28771
+ {
28772
+ description: "Fix datafilter duplication",
28773
+ from: 12,
28774
+ to: 12.5,
28775
+ applyMigration(data) {
28776
+ for (let sheet of data.sheets || []) {
28777
+ let knownDataFilterZones = [];
28778
+ for (let filterTable of sheet.filterTables || []) {
28779
+ const zone = toZone(filterTable.range);
28780
+ // See commit message for the details
28781
+ const intersectZoneIndex = knownDataFilterZones.findIndex((knownZone) => overlap(knownZone, zone));
28782
+ if (intersectZoneIndex !== -1) {
28783
+ knownDataFilterZones[intersectZoneIndex] = zone;
28784
+ }
28785
+ else {
28786
+ knownDataFilterZones.push(zone);
28787
+ }
28788
+ }
28789
+ sheet.filterTables = knownDataFilterZones.map((zone) => ({
28790
+ range: zoneToXc(zone),
28791
+ }));
28792
+ }
28793
+ return data;
28794
+ },
28795
+ },
28732
28796
  {
28733
28797
  description: "Change Border description structure",
28734
28798
  from: 12,
@@ -29630,9 +29694,9 @@ class CellPlugin extends CorePlugin {
29630
29694
  allowDispatch(cmd) {
29631
29695
  switch (cmd.type) {
29632
29696
  case "UPDATE_CELL":
29633
- return this.checkCellOutOfSheet(cmd);
29697
+ return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessUpdateCell);
29634
29698
  case "CLEAR_CELL":
29635
- return this.checkValidations(cmd, this.chainValidations(this.checkCellOutOfSheet, this.checkUselessClearCell));
29699
+ return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessClearCell);
29636
29700
  default:
29637
29701
  return 0 /* CommandResult.Success */;
29638
29702
  }
@@ -29764,10 +29828,6 @@ class CellPlugin extends CorePlugin {
29764
29828
  format: cell.format ? getItemId(cell.format, formats) : undefined,
29765
29829
  content: cell.content || undefined,
29766
29830
  };
29767
- // if there is a formula but no dependencies (maybe because the cell is in error), no need to recompute the formula text
29768
- if (cell.isFormula && cell.dependencies.length) {
29769
- cells[xc].content = this.buildFormulaContent(_sheet.id, cell, cell.dependencies, true);
29770
- }
29771
29831
  }
29772
29832
  _sheet.cells = cells;
29773
29833
  }
@@ -29930,7 +29990,7 @@ class CellPlugin extends CorePlugin {
29930
29990
  else {
29931
29991
  style = before ? before.style : undefined;
29932
29992
  }
29933
- let format = ("format" in after ? after.format : before && before.format) || detectFormat(afterContent);
29993
+ const format = "format" in after ? after.format : before && before.format;
29934
29994
  /* Read the following IF as:
29935
29995
  * we need to remove the cell if it is completely empty, but we can know if it completely empty if:
29936
29996
  * - the command says the new content is empty and has no border/format/style
@@ -29974,7 +30034,7 @@ class CellPlugin extends CorePlugin {
29974
30034
  id,
29975
30035
  content,
29976
30036
  style,
29977
- format,
30037
+ format: format || detectFormat(content),
29978
30038
  isFormula: false,
29979
30039
  };
29980
30040
  }
@@ -30051,6 +30111,18 @@ class CellPlugin extends CorePlugin {
30051
30111
  }
30052
30112
  return 0 /* CommandResult.Success */;
30053
30113
  }
30114
+ checkUselessUpdateCell(cmd) {
30115
+ const cell = this.getters.getCell(cmd);
30116
+ const hasContent = "content" in cmd || "formula" in cmd;
30117
+ const hasStyle = "style" in cmd;
30118
+ const hasFormat = "format" in cmd;
30119
+ if ((!hasContent || cell?.content === cmd.content) &&
30120
+ (!hasStyle || deepEquals(cell?.style, cmd.style)) &&
30121
+ (!hasFormat || cell?.format === cmd.format)) {
30122
+ return 94 /* CommandResult.NoChanges */;
30123
+ }
30124
+ return 0 /* CommandResult.Success */;
30125
+ }
30054
30126
  }
30055
30127
 
30056
30128
  class ChartPlugin extends CorePlugin {
@@ -33842,6 +33914,10 @@ class EvaluationPlugin extends UIPlugin {
33842
33914
  if (cell.format !== evaluatedCell.format) {
33843
33915
  exportedCellData.computedFormat = evaluatedCell.format;
33844
33916
  }
33917
+ // if there is a formula but no dependencies (maybe because the cell is in error), no need to recompute the formula text
33918
+ if (cell.isFormula && cell.dependencies.length) {
33919
+ exportedCellData.content = this.getters.buildFormulaContent(sheet.id, cell, cell.dependencies, true);
33920
+ }
33845
33921
  }
33846
33922
  }
33847
33923
  }
@@ -35710,8 +35786,7 @@ class AutofillPlugin extends UIPlugin {
35710
35786
  let row = zone.bottom;
35711
35787
  if (col > 0) {
35712
35788
  let leftPosition = { sheetId, col: col - 1, row };
35713
- while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty ||
35714
- this.getters.getCell(leftPosition)?.content) {
35789
+ while (this.getters.getCell(leftPosition)?.content) {
35715
35790
  row += 1;
35716
35791
  leftPosition = { sheetId, col: col - 1, row };
35717
35792
  }
@@ -35720,8 +35795,7 @@ class AutofillPlugin extends UIPlugin {
35720
35795
  col = zone.right;
35721
35796
  if (col <= this.getters.getNumberCols(sheetId)) {
35722
35797
  let rightPosition = { sheetId, col: col + 1, row };
35723
- while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty ||
35724
- this.getters.getCell(rightPosition)?.content) {
35798
+ while (this.getters.getCell(rightPosition)?.content) {
35725
35799
  row += 1;
35726
35800
  rightPosition = { sheetId, col: col + 1, row };
35727
35801
  }
@@ -41186,7 +41260,7 @@ class EditionPlugin extends UIPlugin {
41186
41260
  getZoneReference(zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {
41187
41261
  const sheetId = this.getters.getActiveSheetId();
41188
41262
  let selectedXc = this.getters.zoneToXC(sheetId, zone, fixedParts);
41189
- if (this.getters.getCurrentEditedCell().sheetId !== this.getters.getActiveSheetId()) {
41263
+ if (this.getters.getCurrentEditedCell()?.sheetId !== this.getters.getActiveSheetId()) {
41190
41264
  const sheetName = getCanonicalSheetName(this.getters.getSheetName(this.getters.getActiveSheetId()));
41191
41265
  selectedXc = `${sheetName}!${selectedXc}`;
41192
41266
  }
@@ -41201,7 +41275,7 @@ class EditionPlugin extends UIPlugin {
41201
41275
  _fixedParts.pop();
41202
41276
  }
41203
41277
  const newRange = range.clone({ parts: _fixedParts });
41204
- return this.getters.getSelectionRangeString(newRange, this.getters.getCurrentEditedCell().sheetId);
41278
+ return this.getters.getSelectionRangeString(newRange, this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId());
41205
41279
  }
41206
41280
  /**
41207
41281
  * Replace the current selection by a new text.
@@ -41234,7 +41308,7 @@ class EditionPlugin extends UIPlugin {
41234
41308
  if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
41235
41309
  return;
41236
41310
  }
41237
- const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
41311
+ const editionSheetId = this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId();
41238
41312
  const XCs = this.getReferencedRanges().map((range) => this.getters.getRangeString(range, editionSheetId));
41239
41313
  const colorsToKeep = {};
41240
41314
  for (const xc of XCs) {
@@ -41263,7 +41337,7 @@ class EditionPlugin extends UIPlugin {
41263
41337
  if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
41264
41338
  return [];
41265
41339
  }
41266
- const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
41340
+ const editionSheetId = this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId();
41267
41341
  const rangeColor = (rangeString) => {
41268
41342
  const colorIndex = this.colorIndexByRange[rangeString];
41269
41343
  return colors$1[colorIndex % colors$1.length];
@@ -41281,7 +41355,7 @@ class EditionPlugin extends UIPlugin {
41281
41355
  * Return ranges currently referenced in the composer
41282
41356
  */
41283
41357
  getReferencedRanges() {
41284
- const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
41358
+ const editionSheetId = this.getters.getCurrentEditedCell()?.sheetId || this.getters.getActiveSheetId();
41285
41359
  const referenceRanges = this.currentTokens
41286
41360
  .filter((token) => token.type === "REFERENCE")
41287
41361
  .map((token) => this.getters.getRangeFromSheetXC(editionSheetId, token.value));
@@ -42092,6 +42166,16 @@ class ImageProvider {
42092
42166
  }
42093
42167
  }
42094
42168
 
42169
+ class FocusableElement {
42170
+ focusableElement = undefined;
42171
+ setFocusableElement(element) {
42172
+ this.focusableElement = element;
42173
+ }
42174
+ focus() {
42175
+ this.focusableElement?.focus();
42176
+ }
42177
+ }
42178
+
42095
42179
  const RIPPLE_KEY_FRAMES = [
42096
42180
  { transform: "scale(0)" },
42097
42181
  { transform: "scale(0.8)", offset: 0.33 },
@@ -44120,6 +44204,7 @@ class Spreadsheet extends owl.Component {
44120
44204
  _t: Spreadsheet._t,
44121
44205
  clipboard: this.env.clipboard || instantiateClipboard(),
44122
44206
  startCellEdition: (content) => this.onGridComposerCellFocused(content),
44207
+ focusableElement: new FocusableElement(),
44123
44208
  });
44124
44209
  owl.useExternalListener(window, "resize", () => this.render(true));
44125
44210
  owl.useExternalListener(window, "beforeunload", this.unbindModelEvents.bind(this));
@@ -44226,17 +44311,18 @@ class Spreadsheet extends owl.Component {
44226
44311
  this.model.dispatch("UNFOCUS_SELECTION_INPUT");
44227
44312
  this.composer.topBarFocus = "contentFocus";
44228
44313
  this.composer.gridFocusMode = "inactive";
44229
- this.setComposerContent({ selection } || {});
44314
+ this.setComposerContent({ selection });
44230
44315
  }
44231
- onGridComposerContentFocused() {
44316
+ onGridComposerContentFocused(selection) {
44232
44317
  if (this.model.getters.isReadonly()) {
44233
44318
  return;
44234
44319
  }
44235
44320
  this.model.dispatch("UNFOCUS_SELECTION_INPUT");
44236
44321
  this.composer.topBarFocus = "inactive";
44237
44322
  this.composer.gridFocusMode = "contentFocus";
44238
- this.setComposerContent({});
44323
+ this.setComposerContent({ selection });
44239
44324
  }
44325
+ // TODO: either both are defined or none of them. change those args to an object
44240
44326
  onGridComposerCellFocused(content, selection) {
44241
44327
  if (this.model.getters.isReadonly()) {
44242
44328
  return;
@@ -45634,11 +45720,10 @@ class SelectionStreamProcessorImpl {
45634
45720
  * next cluster if the given cell is outside a cluster or at the border of a cluster in the given direction.
45635
45721
  */
45636
45722
  getEndOfCluster(startPosition, dim, dir) {
45637
- const sheet = this.getters.getActiveSheet();
45638
45723
  let currentPosition = startPosition;
45639
45724
  // If both the current cell and the next cell are not empty, we want to go to the end of the cluster
45640
45725
  const nextCellPosition = this.getNextCellPosition(startPosition, dim, dir);
45641
- let mode = !this.isCellEmpty(currentPosition, sheet.id) && !this.isCellEmpty(nextCellPosition, sheet.id)
45726
+ let mode = !this.isEvaluatedCellEmpty(currentPosition) && !this.isEvaluatedCellEmpty(nextCellPosition)
45642
45727
  ? "endOfCluster"
45643
45728
  : "nextCluster";
45644
45729
  while (true) {
@@ -45648,7 +45733,7 @@ class SelectionStreamProcessorImpl {
45648
45733
  currentPosition.row === nextCellPosition.row) {
45649
45734
  break;
45650
45735
  }
45651
- const isNextCellEmpty = this.isCellEmpty(nextCellPosition, sheet.id);
45736
+ const isNextCellEmpty = this.isEvaluatedCellEmpty(nextCellPosition);
45652
45737
  if (mode === "endOfCluster" && isNextCellEmpty) {
45653
45738
  break;
45654
45739
  }
@@ -45662,14 +45747,24 @@ class SelectionStreamProcessorImpl {
45662
45747
  return dim === "cols" ? currentPosition.col : currentPosition.row;
45663
45748
  }
45664
45749
  /**
45665
- * Check if a cell is empty or undefined in the model. If the cell is part of a merge,
45666
- * check if the merge containing the cell is empty.
45750
+ * Check if a cell evaluated value is empty. If the cell is part of a merge,
45751
+ * the check applies to the main cell of the merge.
45667
45752
  */
45668
- isCellEmpty({ col, row }, sheetId = this.getters.getActiveSheetId()) {
45753
+ isEvaluatedCellEmpty({ col, row }) {
45754
+ const sheetId = this.getters.getActiveSheetId();
45669
45755
  const position = this.getters.getMainCellPosition({ sheetId, col, row });
45670
45756
  const cell = this.getters.getEvaluatedCell(position);
45671
45757
  return cell.type === CellValueType.empty;
45672
45758
  }
45759
+ /**
45760
+ * Checks if a cell is empty (i.e. does not have a content). If the cell is part of a merge,
45761
+ * the check applies to the main cell of the merge.
45762
+ */
45763
+ isCellEmpty({ col, row }) {
45764
+ const sheetId = this.getters.getActiveSheetId();
45765
+ const position = this.getters.getMainCellPosition({ sheetId, col, row });
45766
+ return !this.getters.getCell(position)?.content;
45767
+ }
45673
45768
  /** Computes the next cell position in the given direction by crossing through merges and skipping hidden cells.
45674
45769
  *
45675
45770
  * This has the same behaviour as getNextAvailablePosition() for certain arguments, but use this method instead
@@ -47938,6 +48033,6 @@ exports.setTranslationMethod = setTranslationMethod;
47938
48033
  exports.tokenize = tokenize;
47939
48034
 
47940
48035
 
47941
- __info__.version = '16.3.24';
47942
- __info__.date = '2024-02-02T13:45:48.440Z';
47943
- __info__.hash = '0bf5f45';
48036
+ __info__.version = '16.3.26';
48037
+ __info__.date = '2024-02-16T14:22:11.270Z';
48038
+ __info__.hash = 'ab9abb6';