@odoo/o-spreadsheet 16.3.0-alpha.1 → 16.3.0-alpha.3

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.
@@ -117,88 +117,88 @@ const FILTERS_COLOR = "#188038";
117
117
  const BACKGROUND_HEADER_FILTER_COLOR = "#E6F4EA";
118
118
  const BACKGROUND_HEADER_SELECTED_FILTER_COLOR = "#CEEAD6";
119
119
  const SEPARATOR_COLOR = "#E0E2E4";
120
- // Color picker
120
+ // Color picker defaults as upper case HEX to match `toHex`helper
121
121
  const COLOR_PICKER_DEFAULTS = [
122
122
  "#000000",
123
123
  "#434343",
124
124
  "#666666",
125
125
  "#999999",
126
- "#b7b7b7",
127
- "#cccccc",
128
- "#d9d9d9",
129
- "#efefef",
130
- "#f3f3f3",
131
- "#ffffff",
126
+ "#B7B7B7",
127
+ "#CCCCCC",
128
+ "#D9D9D9",
129
+ "#EFEFEF",
130
+ "#F3F3F3",
131
+ "#FFFFFF",
132
132
  "#980000",
133
- "#ff0000",
134
- "#ff9900",
135
- "#ffff00",
136
- "#00ff00",
137
- "#00ffff",
138
- "#4a86e8",
139
- "#0000ff",
140
- "#9900ff",
141
- "#ff00ff",
142
- "#e6b8af",
143
- "#f4cccc",
144
- "#fce5cd",
145
- "#fff2cc",
146
- "#d9ead3",
147
- "#d0e0e3",
148
- "#c9daf8",
149
- "#cfe2f3",
150
- "#d9d2e9",
151
- "#ead1dc",
152
- "#dd7e6b",
153
- "#ea9999",
154
- "#f9cb9c",
155
- "#ffe599",
156
- "#b6d7a8",
157
- "#a2c4c9",
158
- "#a4c2f4",
159
- "#9fc5e8",
160
- "#b4a7d6",
161
- "#d5a6bd",
162
- "#cc4125",
163
- "#e06666",
164
- "#f6b26b",
165
- "#ffd966",
166
- "#93c47d",
167
- "#76a5af",
168
- "#6d9eeb",
169
- "#6fa8dc",
170
- "#8e7cc3",
171
- "#c27ba0",
172
- "#a61c00",
173
- "#cc0000",
174
- "#e69138",
175
- "#f1c232",
176
- "#6aa84f",
177
- "#45818e",
178
- "#3c78d8",
179
- "#3d85c6",
180
- "#674ea7",
181
- "#a64d79",
182
- "#85200c",
133
+ "#FF0000",
134
+ "#FF9900",
135
+ "#FFFF00",
136
+ "#00FF00",
137
+ "#00FFFF",
138
+ "#4A86E8",
139
+ "#0000FF",
140
+ "#9900FF",
141
+ "#FF00FF",
142
+ "#E6B8AF",
143
+ "#F4CCCC",
144
+ "#FCE5CD",
145
+ "#FFF2CC",
146
+ "#D9EAD3",
147
+ "#D0E0E3",
148
+ "#C9DAF8",
149
+ "#CFE2F3",
150
+ "#D9D2E9",
151
+ "#EAD1DC",
152
+ "#DD7E6B",
153
+ "#EA9999",
154
+ "#F9CB9C",
155
+ "#FFE599",
156
+ "#B6D7A8",
157
+ "#A2C4C9",
158
+ "#A4C2F4",
159
+ "#9FC5E8",
160
+ "#B4A7D6",
161
+ "#D5A6BD",
162
+ "#CC4125",
163
+ "#E06666",
164
+ "#F6B26B",
165
+ "#FFD966",
166
+ "#93C47D",
167
+ "#76A5AF",
168
+ "#6D9EEB",
169
+ "#6FA8DC",
170
+ "#8E7CC3",
171
+ "#C27BA0",
172
+ "#A61C00",
173
+ "#CC0000",
174
+ "#E69138",
175
+ "#F1C232",
176
+ "#6AA84F",
177
+ "#45818E",
178
+ "#3C78D8",
179
+ "#3D85C6",
180
+ "#674EA7",
181
+ "#A64D79",
182
+ "#85200C",
183
183
  "#990000",
184
- "#b45f06",
185
- "#bf9000",
186
- "#38761d",
187
- "#134f5c",
188
- "#1155cc",
189
- "#0b5394",
190
- "#351c75",
191
- "#741b47",
192
- "#5b0f00",
184
+ "#B45F06",
185
+ "#BF9000",
186
+ "#38761D",
187
+ "#134F5C",
188
+ "#1155CC",
189
+ "#0B5394",
190
+ "#351C75",
191
+ "#741B47",
192
+ "#5B0F00",
193
193
  "#660000",
194
- "#783f04",
195
- "#7f6000",
196
- "#274e13",
197
- "#0c343d",
198
- "#1c4587",
194
+ "#783F04",
195
+ "#7F6000",
196
+ "#274E13",
197
+ "#0C343D",
198
+ "#1C4587",
199
199
  "#073763",
200
- "#20124d",
201
- "#4c1130",
200
+ "#20124D",
201
+ "#4C1130",
202
202
  ];
203
203
  // Dimensions
204
204
  const MIN_ROW_HEIGHT = 10;
@@ -227,8 +227,6 @@ const MENU_VERTICAL_PADDING = 8;
227
227
  const MENU_ITEM_HEIGHT = 28;
228
228
  const MENU_SEPARATOR_BORDER_WIDTH = 1;
229
229
  const MENU_SEPARATOR_PADDING = 5;
230
- const MENU_SEPARATOR_HEIGHT = MENU_SEPARATOR_BORDER_WIDTH + 2 * MENU_SEPARATOR_PADDING;
231
- const FIGURE_BORDER_SIZE = 1;
232
230
  // Fonts
233
231
  const DEFAULT_FONT_WEIGHT = "400";
234
232
  const DEFAULT_FONT_SIZE = 10;
@@ -238,7 +236,7 @@ const DEFAULT_FONT = "'Roboto', arial";
238
236
  const DEFAULT_BORDER_DESC = ["thin", "#000"];
239
237
  const DEFAULT_FILTER_BORDER_DESC = ["thin", FILTERS_COLOR];
240
238
  // DateTimeRegex
241
- const DATETIME_FORMAT = /[ymd:]/;
239
+ const DATETIME_FORMAT = /[ymdhs:]/;
242
240
  // Ranges
243
241
  const INCORRECT_RANGE_STRING = CellErrorType.InvalidReference;
244
242
  // Max Number of history steps kept in memory
@@ -248,6 +246,8 @@ const DEFAULT_REVISION_ID = "START_REVISION";
248
246
  // Figure
249
247
  const DEFAULT_FIGURE_HEIGHT = 335;
250
248
  const DEFAULT_FIGURE_WIDTH = 536;
249
+ const FIGURE_BORDER_WIDTH = 1;
250
+ const MIN_FIG_SIZE = 80;
251
251
  // Chart
252
252
  const MAX_CHAR_LABEL = 20;
253
253
  const FIGURE_ID_SPLITTER = "??";
@@ -258,7 +258,6 @@ const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
258
258
  const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#00A04A";
259
259
  const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#DC6965";
260
260
  const LINE_FILL_TRANSPARENCY = 0.4;
261
- const MIN_FIG_SIZE = 80;
262
261
  // session
263
262
  const DEBOUNCE_TIME = 200;
264
263
  const MESSAGE_VERSION = 1;
@@ -280,7 +279,8 @@ var ComponentsImportance;
280
279
  ComponentsImportance[ComponentsImportance["TopBarComposer"] = 21] = "TopBarComposer";
281
280
  ComponentsImportance[ComponentsImportance["IconPicker"] = 25] = "IconPicker";
282
281
  ComponentsImportance[ComponentsImportance["Popover"] = 30] = "Popover";
283
- ComponentsImportance[ComponentsImportance["ChartAnchor"] = 1000] = "ChartAnchor";
282
+ ComponentsImportance[ComponentsImportance["FigureAnchor"] = 1000] = "FigureAnchor";
283
+ ComponentsImportance[ComponentsImportance["FigureSnapLine"] = 1001] = "FigureSnapLine";
284
284
  })(ComponentsImportance || (ComponentsImportance = {}));
285
285
  const DEFAULT_SHEETVIEW_SIZE = 1000;
286
286
  const MAXIMAL_FREEZABLE_RATIO = 0.85;
@@ -717,7 +717,7 @@ function getUnquotedSheetName(sheetName) {
717
717
  * '\w' captures [0-9][a-z][A-Z] and _.
718
718
  * @param sheetName Name of the sheet
719
719
  */
720
- function getComposerSheetName(sheetName) {
720
+ function getCanonicalSheetName(sheetName) {
721
721
  var _a;
722
722
  if (((_a = sheetName.match(/\w/g)) === null || _a === void 0 ? void 0 : _a.length) !== sheetName.length) {
723
723
  sheetName = `'${sheetName}'`;
@@ -738,8 +738,9 @@ function getDefaultCellHeight(cell) {
738
738
  return DEFAULT_CELL_HEIGHT;
739
739
  }
740
740
  const fontSize = computeTextFontSizeInPixels(cell.style);
741
- const multiLineText = cell.content.split(NEWLINE);
742
- return computeTextLinesHeight(fontSize, multiLineText.length) + 2 * PADDING_AUTORESIZE_VERTICAL;
741
+ // the number of lines should be computed from the formula result, but it's not evaluated at this point
742
+ const numberOfLines = cell.isFormula ? 1 : cell.content.split(NEWLINE).length;
743
+ return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;
743
744
  }
744
745
  function computeTextWidth(context, text, style) {
745
746
  context.save();
@@ -1091,6 +1092,8 @@ function replaceSpecialSpaces(text) {
1091
1092
  return text;
1092
1093
  }
1093
1094
 
1095
+ const RBA_REGEX = /rgba?\(|\s+|\)/gi;
1096
+ const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/g;
1094
1097
  const colors$1 = [
1095
1098
  "#eb6d00",
1096
1099
  "#0074d9",
@@ -1116,7 +1119,10 @@ function colorNumberString(color) {
1116
1119
  }
1117
1120
  /**
1118
1121
  * Converts any CSS color value to a standardized hex6 value.
1119
- * Accepts: hex3, hex6, hex8 and rgb (rgba is not supported)
1122
+ * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
1123
+ *
1124
+ * [1] under the form rgb(r, g, b, a?) or rgba(r, g, b, a?)
1125
+ * with r,g,b ∈ [0, 255] and a ∈ [0, 1]
1120
1126
  *
1121
1127
  * toHex("#ABC")
1122
1128
  * >> "#AABBCC"
@@ -1127,27 +1133,31 @@ function colorNumberString(color) {
1127
1133
  * toHex("rgb(30, 80, 16)")
1128
1134
  * >> "#1E5010"
1129
1135
  *
1136
+ * * toHex("rgb(30, 80, 16, 0.5)")
1137
+ * >> "#1E501080"
1138
+ *
1130
1139
  */
1131
1140
  function toHex(color) {
1132
- if (color.includes("rgba")) {
1133
- throw new Error(`rgba() conversion currently not supported: ${color}`);
1134
- }
1135
- if (color.includes("rgb")) {
1136
- return rgbToHex6(color);
1141
+ let hexColor = color;
1142
+ if (color.startsWith("rgb")) {
1143
+ hexColor = rgbaStringToHex(color);
1137
1144
  }
1138
- color = color.replace("#", "").toUpperCase();
1139
- if (color.length === 3 || color.length === 4) {
1140
- color = color.split("").reduce((acc, h) => acc + h + h, "");
1145
+ else {
1146
+ hexColor = color.replace("#", "").toUpperCase();
1147
+ if (hexColor.length === 3 || hexColor.length === 4) {
1148
+ hexColor = hexColor.split("").reduce((acc, h) => acc + h + h, "");
1149
+ }
1150
+ hexColor = `#${hexColor}`;
1141
1151
  }
1142
- if (color.replace(/[a-f0-9]/gi, "") !== "") {
1143
- throw new Error("invalid color");
1152
+ if (!hexColor.match(HEX_MATCH)) {
1153
+ throw new Error(`invalid color input: ${color}`);
1144
1154
  }
1145
- return "#" + color;
1155
+ return hexColor;
1146
1156
  }
1147
1157
  function isColorValid(color) {
1148
1158
  try {
1149
- const { r, g, b, a } = colorToRGBA(color);
1150
- return (isColorValueValid(r) && isColorValueValid(g) && isColorValueValid(b) && isColorValueValid(a));
1159
+ toHex(color);
1160
+ return true;
1151
1161
  }
1152
1162
  catch (error) {
1153
1163
  return false;
@@ -1180,15 +1190,29 @@ function relativeLuminance(color) {
1180
1190
  /**
1181
1191
  * Convert a CSS rgb color string to a standardized hex6 color value.
1182
1192
  *
1183
- * rgbToHex6("rgb(30, 80, 16)")
1193
+ * rgbaStringToHex("rgb(30, 80, 16)")
1184
1194
  * >> "#1E5010"
1195
+ *
1196
+ * rgbaStringToHex("rgba(30, 80, 16, 0.5)")
1197
+ * >> "#1E501080"
1198
+ *
1199
+ * DOES NOT SUPPORT NON INTEGER RGB VALUES
1185
1200
  */
1186
- function rgbToHex6(color) {
1187
- return ("#" +
1188
- concat(color
1189
- .slice(4, -1)
1190
- .split(",")
1191
- .map((valueString) => parseInt(valueString, 10).toString(16).padStart(2, "0"))).toUpperCase());
1201
+ function rgbaStringToHex(color) {
1202
+ const stringVals = color.replace(RBA_REGEX, "").split(",");
1203
+ let alphaHex = 255;
1204
+ if (stringVals.length !== 3 && stringVals.length !== 4) {
1205
+ throw new Error("invalid color");
1206
+ }
1207
+ else if (stringVals.length === 4) {
1208
+ const alpha = parseFloat(stringVals.pop() || "1");
1209
+ alphaHex = Math.round((alpha || 1) * 255);
1210
+ }
1211
+ const vals = stringVals.map((val) => parseInt(val, 10));
1212
+ if (alphaHex !== 255) {
1213
+ vals.push(alphaHex);
1214
+ }
1215
+ return "#" + concat(vals.map((value) => value.toString(16).padStart(2, "0"))).toUpperCase();
1192
1216
  }
1193
1217
  /**
1194
1218
  * RGBA to HEX representation (#RRGGBBAA).
@@ -1210,7 +1234,7 @@ function rgbaToHex(rgba) {
1210
1234
  a = "0" + a;
1211
1235
  if (a === "ff")
1212
1236
  a = "";
1213
- return "#" + r + g + b + a;
1237
+ return ("#" + r + g + b + a).toUpperCase();
1214
1238
  }
1215
1239
  /**
1216
1240
  * Color string to RGBA representation
@@ -1331,6 +1355,9 @@ function rgbaToHSLA(rgba) {
1331
1355
  s = +(s * 100).toFixed(1);
1332
1356
  l = +(l * 100).toFixed(1);
1333
1357
  return { a: rgba.a, h, s, l };
1358
+ }
1359
+ function isSameColor(color1, color2) {
1360
+ return isColorValid(color1) && isColorValid(color2) && toHex(color1) === toHex(color2);
1334
1361
  }
1335
1362
 
1336
1363
  /** Reference of a cell (eg. A1, $B$5) */
@@ -1482,7 +1509,7 @@ function scrollDelay(value) {
1482
1509
  * formula, commas are used to separate arguments
1483
1510
  * - it does not support % symbol, in formulas % is an operator
1484
1511
  */
1485
- const formulaNumberRegexp = /^-?\d+(\.?\d*(e\d+)?)?|^-?\.\d+/;
1512
+ const formulaNumberRegexp = /(^-?\d+(\.?\d*(e\d+)?)?|^-?\.\d+)(?!\w|!)/;
1486
1513
  const pIntegerAndDecimals = "(\\d+(,\\d{3,})*(\\.\\d*)?)"; // pattern that match integer number with or without decimal digits
1487
1514
  const pOnlyDecimals = "(\\.\\d+)"; // pattern that match only expression with decimal digits
1488
1515
  const pScientificFormat = "(e(\\+|-)?\\d+)?"; // pattern that match scientific format between zero and one time (should be placed before pPercentFormat)
@@ -3055,6 +3082,9 @@ function cellTextStyleToCss(style) {
3055
3082
  }
3056
3083
  return attributes;
3057
3084
  }
3085
+ /**
3086
+ * Transform CSS properties into a CSS string.
3087
+ */
3058
3088
  function cssPropertiesToCss(attributes) {
3059
3089
  const str = Object.entries(attributes)
3060
3090
  .filter(([attName, attValue]) => attValue !== undefined)
@@ -3088,6 +3118,7 @@ const CHART_PADDING_RATIO = 0.02;
3088
3118
  const LINE_HEIGHT = 1.2;
3089
3119
  css /* scss */ `
3090
3120
  div.o-scorecard {
3121
+ font-family: ${DEFAULT_FONT};
3091
3122
  user-select: none;
3092
3123
  background-color: white;
3093
3124
  display: flex;
@@ -5075,6 +5106,10 @@ function usePopoverContainer() {
5075
5106
  function rectIntersection(rect1, rect2) {
5076
5107
  return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2)));
5077
5108
  }
5109
+ /** Compute the union of the rectangles, ie. the smallest rectangle that contain them all */
5110
+ function rectUnion(...rects) {
5111
+ return zoneToRect(union(...rects.map(rectToZone)));
5112
+ }
5078
5113
  function rectToZone(rect) {
5079
5114
  return {
5080
5115
  left: rect.x,
@@ -5371,24 +5406,30 @@ class Menu extends owl.Component {
5371
5406
  }
5372
5407
  });
5373
5408
  }
5374
- get visibleMenuItems() {
5375
- return this.props.menuItems.filter((x) => x.isVisible(this.env));
5409
+ get menuItemsAndSeparators() {
5410
+ const menuItemsAndSeparators = [];
5411
+ for (let i = 0; i < this.props.menuItems.length; i++) {
5412
+ const menuItem = this.props.menuItems[i];
5413
+ if (menuItem.isVisible(this.env)) {
5414
+ menuItemsAndSeparators.push(menuItem);
5415
+ }
5416
+ if (menuItem.separator &&
5417
+ i !== this.props.menuItems.length - 1 && // no separator at the end
5418
+ menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator" // no double separator
5419
+ ) {
5420
+ menuItemsAndSeparators.push("separator");
5421
+ }
5422
+ }
5423
+ if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") {
5424
+ return [];
5425
+ }
5426
+ return menuItemsAndSeparators;
5376
5427
  }
5377
5428
  get subMenuPosition() {
5378
5429
  const position = Object.assign({}, this.subMenu.position);
5379
5430
  position.y -= this.subMenu.scrollOffset || 0;
5380
5431
  return position;
5381
5432
  }
5382
- get menuHeight() {
5383
- const menuItems = this.visibleMenuItems;
5384
- let menuItemsHeight = this.getMenuItemsHeight(menuItems);
5385
- // We don't display separator at the end of a menu
5386
- if (menuItems[menuItems.length - 1].separator) {
5387
- menuItemsHeight -= MENU_SEPARATOR_HEIGHT;
5388
- }
5389
- const menuHeight = 2 * MENU_VERTICAL_PADDING + menuItemsHeight;
5390
- return this.props.maxHeight ? Math.min(menuHeight, this.props.maxHeight) : menuHeight;
5391
- }
5392
5433
  get popoverProps() {
5393
5434
  const isRoot = this.props.depth === 1;
5394
5435
  return {
@@ -5402,7 +5443,6 @@ class Menu extends owl.Component {
5402
5443
  verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
5403
5444
  onPopoverHidden: () => this.closeSubMenu(),
5404
5445
  onPopoverMoved: () => this.closeSubMenu(),
5405
- maxHeight: this.menuHeight,
5406
5446
  };
5407
5447
  }
5408
5448
  getColor(menu) {
@@ -5418,14 +5458,6 @@ class Menu extends owl.Component {
5418
5458
  this.closeSubMenu();
5419
5459
  this.props.onClose();
5420
5460
  }
5421
- /**
5422
- * Return the number of pixels between the top of the menu
5423
- * and the menu item at a given index.
5424
- */
5425
- subMenuVerticalPosition(menuIndex) {
5426
- const menusAbove = this.visibleMenuItems.slice(0, menuIndex);
5427
- return this.position.y + this.getMenuItemsHeight(menusAbove) + MENU_VERTICAL_PADDING;
5428
- }
5429
5461
  onExternalClick(ev) {
5430
5462
  // Don't close a root menu when clicked to open the submenus.
5431
5463
  const el = this.menuRef.el;
@@ -5435,10 +5467,6 @@ class Menu extends owl.Component {
5435
5467
  ev.closedMenuId = this.props.menuId;
5436
5468
  this.close();
5437
5469
  }
5438
- getMenuItemsHeight(menuItems) {
5439
- const numberOfSeparators = menuItems.filter((m) => m.separator).length;
5440
- return MENU_ITEM_HEIGHT * menuItems.length + MENU_SEPARATOR_HEIGHT * numberOfSeparators;
5441
- }
5442
5470
  getName(menu) {
5443
5471
  return menu.name(this.env);
5444
5472
  }
@@ -5458,8 +5486,11 @@ class Menu extends owl.Component {
5458
5486
  * If the given menu is not disabled, open it's submenu at the
5459
5487
  * correct position according to available surrounding space.
5460
5488
  */
5461
- openSubMenu(menu, menuIndex) {
5462
- const y = this.subMenuVerticalPosition(menuIndex);
5489
+ openSubMenu(menu, menuIndex, ev) {
5490
+ const parentMenuEl = ev.currentTarget;
5491
+ if (!parentMenuEl)
5492
+ return;
5493
+ const y = parentMenuEl.getBoundingClientRect().top;
5463
5494
  this.subMenu.position = {
5464
5495
  x: this.position.x + this.props.depth * MENU_WIDTH,
5465
5496
  y: y - (this.subMenu.scrollOffset || 0),
@@ -5476,20 +5507,20 @@ class Menu extends owl.Component {
5476
5507
  this.subMenu.isOpen = false;
5477
5508
  this.subMenu.parentMenu = undefined;
5478
5509
  }
5479
- onClickMenu(menu, menuIndex) {
5510
+ onClickMenu(menu, menuIndex, ev) {
5480
5511
  if (this.isEnabled(menu)) {
5481
5512
  if (this.isRoot(menu)) {
5482
- this.openSubMenu(menu, menuIndex);
5513
+ this.openSubMenu(menu, menuIndex, ev);
5483
5514
  }
5484
5515
  else {
5485
5516
  this.activateMenu(menu);
5486
5517
  }
5487
5518
  }
5488
5519
  }
5489
- onMouseOver(menu, position) {
5520
+ onMouseOver(menu, position, ev) {
5490
5521
  if (menu.isEnabled(this.env)) {
5491
5522
  if (this.isRoot(menu)) {
5492
- this.openSubMenu(menu, position);
5523
+ this.openSubMenu(menu, position, ev);
5493
5524
  }
5494
5525
  else {
5495
5526
  this.closeSubMenu();
@@ -6066,8 +6097,8 @@ function createDataSets(getters, dataSetsString, sheetId, dataSetsHaveTitle) {
6066
6097
  const dataSets = [];
6067
6098
  for (const sheetXC of dataSetsString) {
6068
6099
  const dataRange = getters.getRangeFromSheetXC(sheetId, sheetXC);
6069
- const { unboundedZone: zone, sheetId: dataSetSheetId, invalidSheetName } = dataRange;
6070
- if (invalidSheetName) {
6100
+ const { unboundedZone: zone, sheetId: dataSetSheetId, invalidSheetName, invalidXc } = dataRange;
6101
+ if (invalidSheetName || invalidXc) {
6071
6102
  continue;
6072
6103
  }
6073
6104
  // It's a rectangle. We treat all columns (arbitrary) as different data series.
@@ -6092,14 +6123,8 @@ function createDataSets(getters, dataSetsString, sheetId, dataSetsHaveTitle) {
6092
6123
  : undefined));
6093
6124
  }
6094
6125
  }
6095
- else if (zone.left === zone.right && zone.top === zone.bottom) {
6096
- // A single cell. If it's only the title, the dataset is not added.
6097
- if (!dataSetsHaveTitle) {
6098
- dataSets.push(createDataSet(getters, dataSetSheetId, zone, undefined));
6099
- }
6100
- }
6101
6126
  else {
6102
- /* 1 row or 1 column */
6127
+ /* 1 cell, 1 row or 1 column */
6103
6128
  dataSets.push(createDataSet(getters, dataSetSheetId, zone, dataSetsHaveTitle
6104
6129
  ? {
6105
6130
  top: zone.top,
@@ -6153,6 +6178,18 @@ function toExcelDataset(getters, ds) {
6153
6178
  range: getters.getRangeString(dataRange, "forceSheetReference"),
6154
6179
  };
6155
6180
  }
6181
+ function toExcelLabelRange(getters, labelRange, shouldRemoveFirstLabel) {
6182
+ if (!labelRange)
6183
+ return undefined;
6184
+ let zone = {
6185
+ ...labelRange.zone,
6186
+ };
6187
+ if (shouldRemoveFirstLabel && labelRange.zone.bottom > labelRange.zone.top) {
6188
+ zone.top = zone.top + 1;
6189
+ }
6190
+ const range = labelRange.clone({ zone });
6191
+ return getters.getRangeString(range, "forceSheetReference");
6192
+ }
6156
6193
  /**
6157
6194
  * Transform a chart definition which supports dataSets (dataSets and LabelRange)
6158
6195
  * with an executed command
@@ -6237,6 +6274,20 @@ function checkLabelRange(definition) {
6237
6274
  }
6238
6275
  return 0 /* CommandResult.Success */;
6239
6276
  }
6277
+ function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
6278
+ if (!dataSetsHaveTitle)
6279
+ return false;
6280
+ if (!labelRange)
6281
+ return false;
6282
+ if (!dataset)
6283
+ return true;
6284
+ const datasetLength = getZoneArea(dataset.dataRange.zone);
6285
+ const labelLength = getZoneArea(labelRange.zone);
6286
+ if (labelLength < datasetLength) {
6287
+ return false;
6288
+ }
6289
+ return true;
6290
+ }
6240
6291
  // ---------------------------------------------------------------------------
6241
6292
  // Scorecard
6242
6293
  // ---------------------------------------------------------------------------
@@ -6606,6 +6657,7 @@ class BarChart extends AbstractChart {
6606
6657
  this.legendPosition = definition.legendPosition;
6607
6658
  this.stacked = definition.stacked;
6608
6659
  this.aggregated = definition.aggregated;
6660
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
6609
6661
  }
6610
6662
  static transformDefinition(definition, executed) {
6611
6663
  return transformChartDefinitionWithDataSetsWithZone(definition, executed);
@@ -6673,11 +6725,13 @@ class BarChart extends AbstractChart {
6673
6725
  const dataSets = this.dataSets
6674
6726
  .map((ds) => toExcelDataset(this.getters, ds))
6675
6727
  .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ?
6728
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
6676
6729
  return {
6677
6730
  ...this.getDefinition(),
6678
6731
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
6679
6732
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
6680
6733
  dataSets,
6734
+ labelRange,
6681
6735
  };
6682
6736
  }
6683
6737
  updateRanges(applyChange) {
@@ -6745,6 +6799,11 @@ function createBarChartRuntime(chart, getters) {
6745
6799
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
6746
6800
  let labels = labelValues.formattedValues;
6747
6801
  let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
6802
+ if (chart.dataSetsHaveTitle &&
6803
+ dataSetsValues[0] &&
6804
+ labels.length > dataSetsValues[0].data.length) {
6805
+ labels.shift();
6806
+ }
6748
6807
  ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
6749
6808
  if (chart.aggregated) {
6750
6809
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
@@ -7167,6 +7226,7 @@ class LineChart extends AbstractChart {
7167
7226
  this.labelsAsText = definition.labelsAsText;
7168
7227
  this.stacked = definition.stacked;
7169
7228
  this.aggregated = definition.aggregated;
7229
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
7170
7230
  }
7171
7231
  static validateChartDefinition(validator, definition) {
7172
7232
  return validator.checkValidations(definition, checkDataset, checkLabelRange);
@@ -7234,11 +7294,13 @@ class LineChart extends AbstractChart {
7234
7294
  const dataSets = this.dataSets
7235
7295
  .map((ds) => toExcelDataset(this.getters, ds))
7236
7296
  .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ?
7297
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
7237
7298
  return {
7238
7299
  ...this.getDefinition(),
7239
7300
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
7240
7301
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
7241
7302
  dataSets,
7303
+ labelRange,
7242
7304
  };
7243
7305
  }
7244
7306
  copyForSheetId(sheetId) {
@@ -7376,6 +7438,11 @@ function createLineChartRuntime(chart, getters) {
7376
7438
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
7377
7439
  let labels = axisType === "linear" ? labelValues.values : labelValues.formattedValues;
7378
7440
  let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
7441
+ if (chart.dataSetsHaveTitle &&
7442
+ dataSetsValues[0] &&
7443
+ labels.length > dataSetsValues[0].data.length) {
7444
+ labels.shift();
7445
+ }
7379
7446
  ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
7380
7447
  if (axisType === "time") {
7381
7448
  ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));
@@ -7434,6 +7501,7 @@ class PieChart extends AbstractChart {
7434
7501
  this.background = definition.background;
7435
7502
  this.legendPosition = definition.legendPosition;
7436
7503
  this.aggregated = definition.aggregated;
7504
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
7437
7505
  }
7438
7506
  static transformDefinition(definition, executed) {
7439
7507
  return transformChartDefinitionWithDataSetsWithZone(definition, executed);
@@ -7497,12 +7565,14 @@ class PieChart extends AbstractChart {
7497
7565
  const dataSets = this.dataSets
7498
7566
  .map((ds) => toExcelDataset(this.getters, ds))
7499
7567
  .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ?
7568
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
7500
7569
  return {
7501
7570
  ...this.getDefinition(),
7502
7571
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
7503
7572
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
7504
7573
  verticalAxisPosition: "left",
7505
7574
  dataSets,
7575
+ labelRange,
7506
7576
  };
7507
7577
  }
7508
7578
  updateRanges(applyChange) {
@@ -7548,6 +7618,11 @@ function createPieChartRuntime(chart, getters) {
7548
7618
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
7549
7619
  let labels = labelValues.formattedValues;
7550
7620
  let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
7621
+ if (chart.dataSetsHaveTitle &&
7622
+ dataSetsValues[0] &&
7623
+ labels.length > dataSetsValues[0].data.length) {
7624
+ labels.shift();
7625
+ }
7551
7626
  ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
7552
7627
  if (chart.aggregated) {
7553
7628
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
@@ -7804,6 +7879,10 @@ css /* scss */ `
7804
7879
  }
7805
7880
  `;
7806
7881
  class ChartFigure extends owl.Component {
7882
+ onDoubleClick() {
7883
+ this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
7884
+ this.env.openSidePanel("ChartPanel");
7885
+ }
7807
7886
  get chartType() {
7808
7887
  return this.env.model.getters.getChartType(this.props.figure.id);
7809
7888
  }
@@ -8179,7 +8258,6 @@ function getSmartChartDefinition(zone, getters) {
8179
8258
  labelRangeXc = zoneToXc({
8180
8259
  ...zone,
8181
8260
  right: zone.left,
8182
- top: dataSetsHaveTitle ? zone.top + 1 : zone.top,
8183
8261
  });
8184
8262
  }
8185
8263
  // Only display legend for several datasets.
@@ -15097,7 +15175,7 @@ const ADDRESS = {
15097
15175
  cellReference = rowPart + colPart;
15098
15176
  }
15099
15177
  if (sheet !== undefined) {
15100
- return `${getComposerSheetName(toString(sheet))}!${cellReference}`;
15178
+ return `${getCanonicalSheetName(toString(sheet))}!${cellReference}`;
15101
15179
  }
15102
15180
  return cellReference;
15103
15181
  },
@@ -16922,6 +17000,22 @@ class LineBarPieConfigPanel extends owl.Component {
16922
17000
  aggregated: ev.target.checked,
16923
17001
  });
16924
17002
  }
17003
+ calculateHeaderPosition() {
17004
+ if (this.isDatasetInvalid || this.isLabelInvalid) {
17005
+ return undefined;
17006
+ }
17007
+ const getters = this.env.model.getters;
17008
+ const sheetId = getters.getActiveSheetId();
17009
+ const labelRange = createRange(getters, sheetId, this.labelRange);
17010
+ const dataSets = createDataSets(getters, this.dataSeriesRanges, sheetId, this.props.definition.dataSetsHaveTitle);
17011
+ if (dataSets.length) {
17012
+ return dataSets[0].dataRange.zone.top + 1;
17013
+ }
17014
+ else if (labelRange) {
17015
+ return labelRange.zone.top + 1;
17016
+ }
17017
+ return undefined;
17018
+ }
16925
17019
  }
16926
17020
  LineBarPieConfigPanel.template = "o-spreadsheet-LineBarPieConfigPanel";
16927
17021
  LineBarPieConfigPanel.components = { SelectionInput };
@@ -17169,6 +17263,9 @@ class ColorPicker extends owl.Component {
17169
17263
  top,
17170
17264
  });
17171
17265
  }
17266
+ isSameColor(color1, color2) {
17267
+ return isSameColor(color1, color2);
17268
+ }
17172
17269
  }
17173
17270
  ColorPicker.template = "o-spreadsheet-ColorPicker";
17174
17271
  ColorPicker.defaultProps = {
@@ -18841,7 +18938,7 @@ css /*SCSS*/ `
18841
18938
  box-sizing: content-box;
18842
18939
 
18843
18940
  .o-fig-anchor {
18844
- z-index: ${ComponentsImportance.ChartAnchor};
18941
+ z-index: ${ComponentsImportance.FigureAnchor};
18845
18942
  position: absolute;
18846
18943
  width: ${ANCHOR_SIZE}px;
18847
18944
  height: ${ANCHOR_SIZE}px;
@@ -19410,6 +19507,8 @@ class ContentEditableHelper {
19410
19507
  start = 0;
19411
19508
  if (end > textLength)
19412
19509
  end = textLength;
19510
+ if (start > textLength)
19511
+ start = textLength;
19413
19512
  }
19414
19513
  let startNode = this.findChildAtCharacterIndex(start);
19415
19514
  let endNode = this.findChildAtCharacterIndex(end);
@@ -19679,6 +19778,7 @@ css /* scss */ `
19679
19778
  padding-right: 2px;
19680
19779
 
19681
19780
  box-sizing: border-box;
19781
+ font-family: ${DEFAULT_FONT};
19682
19782
 
19683
19783
  caret-color: black;
19684
19784
  padding-left: 3px;
@@ -19708,13 +19808,6 @@ css /* scss */ `
19708
19808
  pointer-events: none;
19709
19809
  }
19710
19810
  }
19711
-
19712
- .o-topbar-composer .o-composer:empty:not(:focus)::before {
19713
- /* svg free of use from https://uxwing.com/formula-fx-icon/ */
19714
- content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 121.8 122.9' width='16' height='16' focusable='false'%3E%3Cpath d='m28 34-4 5v2h10l-6 40c-4 22-6 28-7 30-2 2-3 3-5 3-3 0-7-2-9-4H4c-2 2-4 4-4 7s4 6 8 6 9-2 15-8c8-7 13-17 18-39l7-35 13-1 3-6H49c4-23 7-27 11-27 2 0 5 2 8 6h4c1-1 4-4 4-7 0-2-3-6-9-6-5 0-13 4-20 10-6 7-9 14-11 24h-8zm41 16c4-5 7-7 8-7s2 1 5 9l3 12c-7 11-12 17-16 17l-3-1-2-1c-3 0-6 3-6 7s3 7 7 7c6 0 12-6 22-23l3 10c3 9 6 13 10 13 5 0 11-4 18-15l-3-4c-4 6-7 8-8 8-2 0-4-3-6-10l-5-15 8-10 6-4 3 1 3 2c2 0 6-3 6-7s-2-7-6-7c-6 0-11 5-21 20l-2-6c-3-9-5-14-9-14-5 0-12 6-18 15l3 3z' fill='%23BDBDBD' /%3E%3C/svg%3E");
19715
- position: relative;
19716
- top: 20%;
19717
- }
19718
19811
  `;
19719
19812
  class Composer extends owl.Component {
19720
19813
  constructor() {
@@ -20128,7 +20221,7 @@ class Composer extends owl.Component {
20128
20221
  const highlights = this.env.model.getters.getHighlights();
20129
20222
  const refSheet = sheetName
20130
20223
  ? this.env.model.getters.getSheetIdByName(sheetName)
20131
- : this.env.model.getters.getEditionSheet();
20224
+ : this.env.model.getters.getCurrentEditedCell().sheetId;
20132
20225
  const highlight = highlights.find((highlight) => {
20133
20226
  if (highlight.sheetId !== refSheet)
20134
20227
  return false;
@@ -20289,7 +20382,7 @@ class GridComposer extends owl.Component {
20289
20382
  get cellReference() {
20290
20383
  const { col, row, sheetId } = this.env.model.getters.getCurrentEditedCell();
20291
20384
  const prefixSheet = sheetId !== this.env.model.getters.getActiveSheetId();
20292
- return `${prefixSheet ? getComposerSheetName(this.env.model.getters.getSheetName(sheetId)) + "!" : ""}${toXC(col, row)}`;
20385
+ return `${prefixSheet ? getCanonicalSheetName(this.env.model.getters.getSheetName(sheetId)) + "!" : ""}${toXC(col, row)}`;
20293
20386
  }
20294
20387
  get cellReferenceStyle() {
20295
20388
  const { x: left, y: top } = this.rect;
@@ -20442,6 +20535,269 @@ FilterIconsOverlay.props = {
20442
20535
  gridPosition: { type: Object, optional: true },
20443
20536
  };
20444
20537
 
20538
+ /**
20539
+ * Transform a figure with coordinates from the model, to coordinates as they are shown on the screen,
20540
+ * taking into account the scroll position of the active sheet and the frozen panes.
20541
+ */
20542
+ function internalFigureToScreen(getters, fig) {
20543
+ return { ...fig, ...internalToScreenCoordinates(getters, { x: fig.x, y: fig.y }) };
20544
+ }
20545
+ /**
20546
+ * Transform a figure with coordinates as they are shown on the screen, to coordinates as they are in the model,
20547
+ * taking into account the scroll position of the active sheet and the frozen panes.
20548
+ *
20549
+ * Note that this isn't exactly the reverse operation as internalFigureToScreen, because the figure will always be on top
20550
+ * of the frozen panes.
20551
+ */
20552
+ function screenFigureToInternal(getters, fig) {
20553
+ return { ...fig, ...screenCoordinatesToInternal(getters, { x: fig.x, y: fig.y }) };
20554
+ }
20555
+ function internalToScreenCoordinates(getters, { x, y }) {
20556
+ const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates();
20557
+ const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
20558
+ x = x < viewportX ? x : x - scrollX;
20559
+ y = y < viewportY ? y : y - scrollY;
20560
+ return { x, y };
20561
+ }
20562
+ function screenCoordinatesToInternal(getters, { x, y }) {
20563
+ const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates();
20564
+ const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
20565
+ x = viewportX && x < viewportX ? x : x + scrollX;
20566
+ y = viewportY && y < viewportY ? y : y + scrollY;
20567
+ return { x, y };
20568
+ }
20569
+
20570
+ function dragFigureForMove({ x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, initialFigure, { x: viewportX, y: viewportY }, { maxX, maxY }, { scrollX, scrollY }) {
20571
+ const minX = viewportX ? 0 : -scrollX;
20572
+ const minY = viewportY ? 0 : -scrollY;
20573
+ const deltaX = mouseX - mouseInitialX;
20574
+ const newX = clamp(initialFigure.x + deltaX, minX, maxX - initialFigure.width - scrollX);
20575
+ const deltaY = mouseY - mouseInitialY;
20576
+ const newY = clamp(initialFigure.y + deltaY, minY, maxY - initialFigure.height - scrollY);
20577
+ return { ...initialFigure, x: newX, y: newY };
20578
+ }
20579
+ function clamp(value, min, max) {
20580
+ return Math.min(Math.max(value, min), max);
20581
+ }
20582
+ function dragFigureForResize(initialFigure, dirX, dirY, { x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, keepRatio, minFigSize) {
20583
+ let { x, y, width, height } = initialFigure;
20584
+ if (keepRatio && dirX != 0 && dirY != 0) {
20585
+ const deltaX = Math.min(dirX * (mouseInitialX - mouseX), initialFigure.width - minFigSize);
20586
+ const deltaY = Math.min(dirY * (mouseInitialY - mouseY), initialFigure.height - minFigSize);
20587
+ const fraction = Math.min(deltaX / initialFigure.width, deltaY / initialFigure.height);
20588
+ width = initialFigure.width * (1 - fraction);
20589
+ height = initialFigure.height * (1 - fraction);
20590
+ if (dirX < 0) {
20591
+ x = initialFigure.x + initialFigure.width * fraction;
20592
+ }
20593
+ if (dirY < 0) {
20594
+ y = initialFigure.y + initialFigure.height * fraction;
20595
+ }
20596
+ }
20597
+ else {
20598
+ const deltaX = Math.max(dirX * (mouseX - mouseInitialX), minFigSize - initialFigure.width);
20599
+ const deltaY = Math.max(dirY * (mouseY - mouseInitialY), minFigSize - initialFigure.height);
20600
+ width = initialFigure.width + deltaX;
20601
+ height = initialFigure.height + deltaY;
20602
+ if (dirX < 0) {
20603
+ x = initialFigure.x - deltaX;
20604
+ }
20605
+ if (dirY < 0) {
20606
+ y = initialFigure.y - deltaY;
20607
+ }
20608
+ }
20609
+ return { ...initialFigure, x, y, width, height };
20610
+ }
20611
+
20612
+ const SNAP_MARGIN = 5;
20613
+ /**
20614
+ * Try to snap the given figure to other figures when moving the figure, and return the snapped
20615
+ * figure and the possible snap lines, if any were found
20616
+ */
20617
+ function snapForMove(getters, figureToSnap, otherFigures) {
20618
+ const snappedFigure = { ...figureToSnap };
20619
+ const verticalSnapLine = getSnapLine(getters, snappedFigure, ["hCenter", "right", "left"], otherFigures, ["hCenter", "right", "left"]);
20620
+ const horizontalSnapLine = getSnapLine(getters, snappedFigure, ["vCenter", "bottom", "top"], otherFigures, ["vCenter", "bottom", "top"]);
20621
+ const { y: viewportY, x: viewportX } = getters.getMainViewportCoordinates();
20622
+ const { scrollY, scrollX } = getters.getActiveSheetScrollInfo();
20623
+ // If the snap cause the figure to change pane, we need to also apply the scroll as an offset
20624
+ if (horizontalSnapLine) {
20625
+ snappedFigure.y -= horizontalSnapLine.snapOffset;
20626
+ const isBaseFigFrozenY = figureToSnap.y < viewportY;
20627
+ const isSnappedFrozenY = snappedFigure.y < viewportY;
20628
+ if (isBaseFigFrozenY && !isSnappedFrozenY)
20629
+ snappedFigure.y += scrollY;
20630
+ else if (!isBaseFigFrozenY && isSnappedFrozenY)
20631
+ snappedFigure.y -= scrollY;
20632
+ }
20633
+ if (verticalSnapLine) {
20634
+ snappedFigure.x -= verticalSnapLine.snapOffset;
20635
+ const isBaseFigFrozenX = figureToSnap.x < viewportX;
20636
+ const isSnappedFrozenX = snappedFigure.x < viewportX;
20637
+ if (isBaseFigFrozenX && !isSnappedFrozenX)
20638
+ snappedFigure.x += scrollX;
20639
+ else if (!isBaseFigFrozenX && isSnappedFrozenX)
20640
+ snappedFigure.x -= scrollX;
20641
+ }
20642
+ return { snappedFigure, verticalSnapLine, horizontalSnapLine };
20643
+ }
20644
+ /**
20645
+ * Try to snap the given figure to the other figures when resizing the figure, and return the snapped
20646
+ * figure and the possible snap lines, if any were found
20647
+ */
20648
+ function snapForResize(getters, resizeDirX, resizeDirY, figureToSnap, otherFigures) {
20649
+ const snappedFigure = { ...figureToSnap };
20650
+ // Vertical snap line
20651
+ const verticalSnapLine = getSnapLine(getters, snappedFigure, [resizeDirX === -1 ? "left" : "right"], otherFigures, ["right", "left"]);
20652
+ if (verticalSnapLine) {
20653
+ if (resizeDirX === 1) {
20654
+ snappedFigure.width -= verticalSnapLine.snapOffset;
20655
+ }
20656
+ else if (resizeDirX === -1) {
20657
+ snappedFigure.x -= verticalSnapLine.snapOffset;
20658
+ snappedFigure.width += verticalSnapLine.snapOffset;
20659
+ }
20660
+ }
20661
+ // Horizontal snap line
20662
+ const horizontalSnapLine = getSnapLine(getters, snappedFigure, [resizeDirY === -1 ? "top" : "bottom"], otherFigures, ["bottom", "top"]);
20663
+ if (horizontalSnapLine) {
20664
+ if (resizeDirY === 1) {
20665
+ snappedFigure.height -= horizontalSnapLine.snapOffset;
20666
+ }
20667
+ else if (resizeDirY === -1) {
20668
+ snappedFigure.y -= horizontalSnapLine.snapOffset;
20669
+ snappedFigure.height += horizontalSnapLine.snapOffset;
20670
+ }
20671
+ }
20672
+ snappedFigure.x = Math.round(snappedFigure.x);
20673
+ snappedFigure.y = Math.round(snappedFigure.y);
20674
+ snappedFigure.height = Math.round(snappedFigure.height);
20675
+ snappedFigure.width = Math.round(snappedFigure.width);
20676
+ return { snappedFigure, verticalSnapLine, horizontalSnapLine };
20677
+ }
20678
+ /**
20679
+ * Get the position of snap axes for the given figure
20680
+ *
20681
+ * @param figure the figure
20682
+ * @param axesTypes the list of axis types to return the positions of
20683
+ */
20684
+ function getVisibleAxes(getters, figure, axesTypes) {
20685
+ const axes = axesTypes.map((axisType) => getAxis(figure, axisType));
20686
+ return axes
20687
+ .filter((axis) => isAxisVisible(getters, figure, axis))
20688
+ .map((axis) => getAxisScreenPosition(getters, figure, axis));
20689
+ }
20690
+ /**
20691
+ * We need two positions for the figure axis :
20692
+ * - the position (core) of the axis in the figure. This is used to know whether or not the axis is
20693
+ * displayed, or is hidden by the scroll/the frozen panes
20694
+ * - the position in the screen, which is used to find snap matches. We cannot use the core position for this,
20695
+ * because figures partially in frozen panes aren't displayed at their actual coordinates
20696
+ */
20697
+ function getAxisScreenPosition(getters, figure, figureAxis) {
20698
+ const screenFigure = internalFigureToScreen(getters, figure);
20699
+ return getAxis(screenFigure, figureAxis.axisType);
20700
+ }
20701
+ function isAxisVisible(getters, figure, axis) {
20702
+ const { x: mainViewportX, y: mainViewportY } = getters.getMainViewportCoordinates();
20703
+ const axisStartEndPositions = [];
20704
+ switch (axis.axisType) {
20705
+ case "top":
20706
+ case "bottom":
20707
+ case "vCenter":
20708
+ if (figure.y < mainViewportY)
20709
+ return true;
20710
+ axisStartEndPositions.push({ x: figure.x, y: axis.position });
20711
+ axisStartEndPositions.push({ x: figure.x + figure.width, y: axis.position });
20712
+ break;
20713
+ case "left":
20714
+ case "right":
20715
+ case "hCenter":
20716
+ if (figure.x < mainViewportX)
20717
+ return true;
20718
+ axisStartEndPositions.push({ x: axis.position, y: figure.y });
20719
+ axisStartEndPositions.push({ x: axis.position, y: figure.y + figure.height });
20720
+ break;
20721
+ }
20722
+ return axisStartEndPositions.some(getters.isPositionVisible);
20723
+ }
20724
+ /**
20725
+ * Get a snap line for the given figure, if the figure can snap to any other figure
20726
+ *
20727
+ * @param figureToSnap figure to get the snap line for
20728
+ * @param figAxesTypes figure axes of the given figure to be considered to find a snap line
20729
+ * @param otherFigures figures to match against the snapped figure to find a snap line
20730
+ * @param otherAxesTypes figure axes of the other figures to be considered to find a snap line
20731
+ */
20732
+ function getSnapLine(getters, figureToSnap, figAxesTypes, otherFigures, otherAxesTypes) {
20733
+ const axesOfFigure = getVisibleAxes(getters, figureToSnap, figAxesTypes);
20734
+ let closestMatch = undefined;
20735
+ for (const otherFigure of otherFigures) {
20736
+ const axesOfOtherFig = getVisibleAxes(getters, otherFigure, otherAxesTypes);
20737
+ for (const axisOfFigure of axesOfFigure) {
20738
+ for (const axisOfOtherFig of axesOfOtherFig) {
20739
+ if (!canSnap(axisOfFigure.position, axisOfOtherFig.position))
20740
+ continue;
20741
+ const snapOffset = axisOfFigure.position - axisOfOtherFig.position;
20742
+ if (closestMatch && snapOffset === closestMatch.snapOffset) {
20743
+ closestMatch.matchedFigIds.push(otherFigure.id);
20744
+ }
20745
+ else if (!closestMatch || Math.abs(snapOffset) <= Math.abs(closestMatch.snapOffset)) {
20746
+ closestMatch = {
20747
+ matchedFigIds: [otherFigure.id],
20748
+ snapOffset,
20749
+ snappedAxisType: axisOfFigure.axisType,
20750
+ position: axisOfOtherFig.position,
20751
+ };
20752
+ }
20753
+ }
20754
+ }
20755
+ }
20756
+ return closestMatch;
20757
+ }
20758
+ /** Check if two axes are close enough to snap */
20759
+ function canSnap(axisPosition1, axisPosition2) {
20760
+ return Math.abs(axisPosition1 - axisPosition2) <= SNAP_MARGIN;
20761
+ }
20762
+ function getAxis(fig, axisType) {
20763
+ let position = 0;
20764
+ switch (axisType) {
20765
+ case "top":
20766
+ position = fig.y;
20767
+ break;
20768
+ case "bottom":
20769
+ position = fig.y + fig.height - FIGURE_BORDER_WIDTH;
20770
+ break;
20771
+ case "vCenter":
20772
+ position = fig.y + Math.floor(fig.height / 2) - FIGURE_BORDER_WIDTH;
20773
+ break;
20774
+ case "left":
20775
+ position = fig.x;
20776
+ break;
20777
+ case "right":
20778
+ position = fig.x + fig.width - FIGURE_BORDER_WIDTH;
20779
+ break;
20780
+ case "hCenter":
20781
+ position = fig.x + Math.floor(fig.width / 2) - FIGURE_BORDER_WIDTH;
20782
+ break;
20783
+ }
20784
+ return { position, axisType: axisType };
20785
+ }
20786
+
20787
+ css /*SCSS*/ `
20788
+ .o-figure-snap-line {
20789
+ position: relative;
20790
+ z-index: ${ComponentsImportance.FigureSnapLine};
20791
+ &.vertical {
20792
+ width: 0px;
20793
+ border-left: 1px dashed black;
20794
+ }
20795
+ &.horizontal {
20796
+ border-top: 1px dashed black;
20797
+ height: 0px;
20798
+ }
20799
+ }
20800
+ `;
20445
20801
  /**
20446
20802
  * Each figure ⭐ is positioned inside a container `div` placed and sized
20447
20803
  * according to the split pane the figure is part of, or a separate container for the figure
@@ -20506,11 +20862,9 @@ class FiguresContainer extends owl.Component {
20506
20862
  constructor() {
20507
20863
  super(...arguments);
20508
20864
  this.dnd = owl.useState({
20509
- figId: undefined,
20510
- x: 0,
20511
- y: 0,
20512
- width: 0,
20513
- height: 0,
20865
+ draggedFigure: undefined,
20866
+ horizontalSnap: undefined,
20867
+ verticalSnap: undefined,
20514
20868
  });
20515
20869
  }
20516
20870
  setup() {
@@ -20526,9 +20880,11 @@ class FiguresContainer extends owl.Component {
20526
20880
  });
20527
20881
  }
20528
20882
  getVisibleFigures() {
20883
+ var _a;
20529
20884
  const visibleFigures = this.env.model.getters.getVisibleFigures();
20530
- if (this.dnd.figId && !visibleFigures.some((figure) => figure.id === this.dnd.figId)) {
20531
- visibleFigures.push(this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.figId));
20885
+ if (this.dnd.draggedFigure &&
20886
+ !visibleFigures.some((figure) => { var _a; return figure.id === ((_a = this.dnd.draggedFigure) === null || _a === void 0 ? void 0 : _a.id); })) {
20887
+ visibleFigures.push(this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), (_a = this.dnd.draggedFigure) === null || _a === void 0 ? void 0 : _a.id));
20532
20888
  }
20533
20889
  return visibleFigures;
20534
20890
  }
@@ -20551,7 +20907,7 @@ class FiguresContainer extends owl.Component {
20551
20907
  });
20552
20908
  }
20553
20909
  }
20554
- if (this.dnd.figId) {
20910
+ if (this.dnd.draggedFigure) {
20555
20911
  containers.push({
20556
20912
  type: "dnd",
20557
20913
  figures: [this.getDndFigure()],
@@ -20562,19 +20918,25 @@ class FiguresContainer extends owl.Component {
20562
20918
  return containers;
20563
20919
  }
20564
20920
  getContainerStyle(container) {
20565
- const { width: viewWidth, height: viewHeight } = this.env.model.getters.getMainViewportRect();
20566
- const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
20567
- const left = ["bottomRight", "topRight"].includes(container) ? viewportX : 0;
20568
- const width = viewWidth - left;
20569
- const top = ["bottomRight", "bottomLeft"].includes(container) ? viewportY : 0;
20570
- const height = viewHeight - top;
20921
+ return this.rectToCss(this.getContainerRect(container));
20922
+ }
20923
+ rectToCss(rect) {
20571
20924
  return cssPropertiesToCss({
20572
- left: `${left}px`,
20573
- top: `${top}px`,
20574
- width: `${width}px`,
20575
- height: `${height}px`,
20925
+ left: `${rect.x}px`,
20926
+ top: `${rect.y}px`,
20927
+ width: `${rect.width}px`,
20928
+ height: `${rect.height}px`,
20576
20929
  });
20577
20930
  }
20931
+ getContainerRect(container) {
20932
+ const { width: viewWidth, height: viewHeight } = this.env.model.getters.getMainViewportRect();
20933
+ const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
20934
+ const x = ["bottomRight", "topRight"].includes(container) ? viewportX : 0;
20935
+ const width = viewWidth - x;
20936
+ const y = ["bottomRight", "bottomLeft"].includes(container) ? viewportY : 0;
20937
+ const height = viewHeight - y;
20938
+ return { x, y, width, height };
20939
+ }
20578
20940
  getInverseViewportPositionStyle(container) {
20579
20941
  const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
20580
20942
  const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
@@ -20586,8 +20948,9 @@ class FiguresContainer extends owl.Component {
20586
20948
  });
20587
20949
  }
20588
20950
  getFigureContainer(figure) {
20951
+ var _a;
20589
20952
  const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
20590
- if (figure.id === this.dnd.figId) {
20953
+ if (figure.id === ((_a = this.dnd.draggedFigure) === null || _a === void 0 ? void 0 : _a.id)) {
20591
20954
  return "dnd";
20592
20955
  }
20593
20956
  else if (figure.x < viewportX && figure.y < viewportY) {
@@ -20613,133 +20976,141 @@ class FiguresContainer extends owl.Component {
20613
20976
  return;
20614
20977
  }
20615
20978
  const sheetId = this.env.model.getters.getActiveSheetId();
20616
- const mouseInitialX = ev.clientX;
20617
- const mouseInitialY = ev.clientY;
20618
- const { x: dndInitialX, y: dndInitialY } = this.internalToScreenCoordinates(figure);
20619
- this.dnd.x = dndInitialX;
20620
- this.dnd.y = dndInitialY;
20621
- this.dnd.width = figure.width;
20622
- this.dnd.height = figure.height;
20979
+ const initialMousePosition = { x: ev.clientX, y: ev.clientY };
20980
+ const maxDimensions = {
20981
+ maxX: this.env.model.getters.getColDimensions(sheetId, this.env.model.getters.getNumberCols(sheetId) - 1).end,
20982
+ maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end,
20983
+ };
20984
+ const { x, y } = internalFigureToScreen(this.env.model.getters, figure);
20985
+ const initialFig = { ...figure, x, y };
20623
20986
  const onMouseMove = (ev) => {
20624
- const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
20625
- const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
20626
- const minX = viewportX ? 0 : -scrollX;
20627
- const minY = viewportY ? 0 : -scrollY;
20628
- this.dnd.figId = figure.id;
20629
- const newX = ev.clientX;
20630
- let deltaX = newX - mouseInitialX;
20631
- this.dnd.x = Math.max(dndInitialX + deltaX, minX);
20632
- const newY = ev.clientY;
20633
- let deltaY = newY - mouseInitialY;
20634
- this.dnd.y = Math.max(dndInitialY + deltaY, minY);
20987
+ const getters = this.env.model.getters;
20988
+ const currentMousePosition = { x: ev.clientX, y: ev.clientY };
20989
+ const draggedFigure = dragFigureForMove(currentMousePosition, initialMousePosition, initialFig, this.env.model.getters.getMainViewportCoordinates(), maxDimensions, getters.getActiveSheetScrollInfo());
20990
+ const otherFigures = this.getOtherFigures(figure.id);
20991
+ const internalDragged = screenFigureToInternal(getters, draggedFigure);
20992
+ const snapResult = snapForMove(getters, internalDragged, otherFigures);
20993
+ this.dnd.draggedFigure = internalFigureToScreen(getters, snapResult.snappedFigure);
20994
+ this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);
20995
+ this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);
20635
20996
  };
20636
20997
  const onMouseUp = (ev) => {
20637
- if (!this.dnd.figId) {
20998
+ if (!this.dnd.draggedFigure) {
20638
20999
  return;
20639
21000
  }
20640
- let { x, y } = this.screenCoordinatesToInternal(this.dnd);
20641
- this.dnd.figId = undefined;
21001
+ let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure);
21002
+ this.dnd.draggedFigure = undefined;
21003
+ this.dnd.horizontalSnap = undefined;
21004
+ this.dnd.verticalSnap = undefined;
20642
21005
  this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
20643
21006
  };
20644
21007
  startDnd(onMouseMove, onMouseUp);
20645
21008
  }
21009
+ /**
21010
+ * Initialize the resize of a figure with mouse movements
21011
+ *
21012
+ * @param dirX X direction of the resize. -1 : resize from the left border of the figure, 0 : no resize in X, 1 :
21013
+ * resize from the right border of the figure
21014
+ * @param dirY Y direction of the resize. -1 : resize from the top border of the figure, 0 : no resize in Y, 1 :
21015
+ * resize from the bottom border of the figure
21016
+ * @param ev Mouse Event
21017
+ */
20646
21018
  startResize(figure, dirX, dirY, ev) {
20647
21019
  ev.stopPropagation();
20648
- const initialX = ev.clientX;
20649
- const initialY = ev.clientY;
20650
- const { x: dndInitialX, y: dndInitialY } = this.internalToScreenCoordinates(figure);
20651
- this.dnd.x = dndInitialX;
20652
- this.dnd.y = dndInitialY;
20653
- this.dnd.width = figure.width;
20654
- this.dnd.height = figure.height;
21020
+ const initialMousePosition = { x: ev.clientX, y: ev.clientY };
21021
+ const { x, y } = internalFigureToScreen(this.env.model.getters, figure);
21022
+ const initialFig = { ...figure, x, y };
20655
21023
  const keepRatio = figureRegistry.get(figure.tag).keepRatio || false;
20656
21024
  const minFigSize = figureRegistry.get(figure.tag).minFigSize || MIN_FIG_SIZE;
20657
- let onMouseMove;
20658
- if (keepRatio && dirX != 0 && dirY != 0) {
20659
- onMouseMove = (ev) => {
20660
- this.dnd.figId = figure.id;
20661
- const deltaX = Math.min(dirX * (initialX - ev.clientX), figure.width - minFigSize);
20662
- const deltaY = Math.min(dirY * (initialY - ev.clientY), figure.height - minFigSize);
20663
- const fraction = Math.min(deltaX / figure.width, deltaY / figure.height);
20664
- this.dnd.width = figure.width * (1 - fraction);
20665
- this.dnd.height = figure.height * (1 - fraction);
20666
- if (dirX < 0) {
20667
- this.dnd.x = dndInitialX + figure.width * fraction;
20668
- }
20669
- if (dirY < 0) {
20670
- this.dnd.y = dndInitialY + figure.height * fraction;
20671
- }
20672
- };
20673
- }
20674
- else {
20675
- onMouseMove = (ev) => {
20676
- this.dnd.figId = figure.id;
20677
- const deltaX = Math.max(dirX * (ev.clientX - initialX), minFigSize - figure.width);
20678
- const deltaY = Math.max(dirY * (ev.clientY - initialY), minFigSize - figure.height);
20679
- this.dnd.width = figure.width + deltaX;
20680
- this.dnd.height = figure.height + deltaY;
20681
- if (dirX < 0) {
20682
- this.dnd.x = dndInitialX - deltaX;
20683
- }
20684
- if (dirY < 0) {
20685
- this.dnd.y = dndInitialY - deltaY;
20686
- }
20687
- };
20688
- }
21025
+ const onMouseMove = (ev) => {
21026
+ const currentMousePosition = { x: ev.clientX, y: ev.clientY };
21027
+ const draggedFigure = dragFigureForResize(initialFig, dirX, dirY, currentMousePosition, initialMousePosition, keepRatio, minFigSize);
21028
+ const otherFigures = this.getOtherFigures(figure.id);
21029
+ const snapResult = snapForResize(this.env.model.getters, dirX, dirY, draggedFigure, otherFigures);
21030
+ this.dnd.draggedFigure = snapResult.snappedFigure;
21031
+ this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);
21032
+ this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);
21033
+ };
20689
21034
  const onMouseUp = (ev) => {
20690
- if (!this.dnd.figId) {
21035
+ if (!this.dnd.draggedFigure) {
20691
21036
  return;
20692
21037
  }
20693
- this.dnd.figId = undefined;
20694
- let { x, y } = this.screenCoordinatesToInternal(this.dnd);
21038
+ let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure);
20695
21039
  const update = { x, y };
20696
21040
  if (dirX) {
20697
- update.width = this.dnd.width;
21041
+ update.width = this.dnd.draggedFigure.width;
20698
21042
  }
20699
21043
  if (dirY) {
20700
- update.height = this.dnd.height;
21044
+ update.height = this.dnd.draggedFigure.height;
20701
21045
  }
20702
21046
  this.env.model.dispatch("UPDATE_FIGURE", {
20703
21047
  sheetId: this.env.model.getters.getActiveSheetId(),
20704
21048
  id: figure.id,
20705
21049
  ...update,
20706
21050
  });
21051
+ this.dnd.draggedFigure = undefined;
21052
+ this.dnd.horizontalSnap = undefined;
21053
+ this.dnd.verticalSnap = undefined;
20707
21054
  };
20708
21055
  startDnd(onMouseMove, onMouseUp);
20709
21056
  }
21057
+ getOtherFigures(figId) {
21058
+ return this.getVisibleFigures().filter((f) => f.id !== figId);
21059
+ }
20710
21060
  getDndFigure() {
20711
- const figure = this.getVisibleFigures().find((fig) => fig.id === this.dnd.figId);
21061
+ const figure = this.getVisibleFigures().find((fig) => { var _a; return fig.id === ((_a = this.dnd.draggedFigure) === null || _a === void 0 ? void 0 : _a.id); });
20712
21062
  if (!figure)
20713
21063
  throw new Error("Dnd figure not found");
20714
21064
  return {
20715
21065
  ...figure,
20716
- x: this.dnd.x,
20717
- y: this.dnd.y,
20718
- width: this.dnd.width,
20719
- height: this.dnd.height,
21066
+ ...this.dnd.draggedFigure,
20720
21067
  };
20721
21068
  }
20722
21069
  getFigureStyle(figure) {
20723
- if (figure.id !== this.dnd.figId)
21070
+ var _a;
21071
+ if (figure.id !== ((_a = this.dnd.draggedFigure) === null || _a === void 0 ? void 0 : _a.id))
20724
21072
  return "";
20725
21073
  return cssPropertiesToCss({
20726
21074
  opacity: "0.9",
20727
21075
  cursor: "grabbing",
20728
21076
  });
20729
21077
  }
20730
- internalToScreenCoordinates({ x, y }) {
20731
- const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
20732
- const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
20733
- x = x < viewportX ? x : x - scrollX;
20734
- y = y < viewportY ? y : y - scrollY;
20735
- return { x, y };
21078
+ getSnap(snapLine) {
21079
+ if (!snapLine || !this.dnd.draggedFigure)
21080
+ return undefined;
21081
+ const figureVisibleRects = snapLine.matchedFigIds
21082
+ .map((id) => this.getVisibleFigures().find((fig) => fig.id === id))
21083
+ .filter(isDefined$1)
21084
+ .map((fig) => {
21085
+ const figOnSCreen = internalFigureToScreen(this.env.model.getters, fig);
21086
+ const container = this.getFigureContainer(fig);
21087
+ return rectIntersection(figOnSCreen, this.getContainerRect(container));
21088
+ })
21089
+ .filter(isDefined$1);
21090
+ const containerRect = rectUnion(this.dnd.draggedFigure, ...figureVisibleRects);
21091
+ return {
21092
+ line: snapLine,
21093
+ containerStyle: this.rectToCss(containerRect),
21094
+ lineStyle: this.getSnapLineStyle(snapLine, containerRect),
21095
+ };
20736
21096
  }
20737
- screenCoordinatesToInternal({ x, y }) {
20738
- const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
20739
- const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
20740
- x = viewportX && x < viewportX ? x : x + scrollX;
20741
- y = viewportY && y < viewportY ? y : y + scrollY;
20742
- return { x, y };
21097
+ getSnapLineStyle(snapLine, containerRect) {
21098
+ if (!snapLine)
21099
+ return "";
21100
+ if (["top", "vCenter", "bottom"].includes(snapLine.snappedAxisType)) {
21101
+ return cssPropertiesToCss({
21102
+ top: `${snapLine.position - containerRect.y}px`,
21103
+ left: `0px`,
21104
+ width: `100%`,
21105
+ });
21106
+ }
21107
+ else {
21108
+ return cssPropertiesToCss({
21109
+ top: `0px`,
21110
+ left: `${snapLine.position - containerRect.x}px`,
21111
+ height: `100%`,
21112
+ });
21113
+ }
20743
21114
  }
20744
21115
  }
20745
21116
  FiguresContainer.template = "o-spreadsheet-FiguresContainer";
@@ -20886,6 +21257,9 @@ class GridOverlay extends owl.Component {
20886
21257
  owl.onMounted(() => {
20887
21258
  resizeObserver.observe(this.gridOverlayEl);
20888
21259
  });
21260
+ owl.onWillUnmount(() => {
21261
+ resizeObserver.disconnect();
21262
+ });
20889
21263
  useTouchMove(this.gridOverlay, this.props.onGridMoved, () => {
20890
21264
  const { scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();
20891
21265
  return scrollY > 0;
@@ -22306,6 +22680,10 @@ class Grid extends owl.Component {
22306
22680
  }
22307
22681
  }
22308
22682
  onInput(ev) {
22683
+ // the user meant to paste in the sheet, not open the composer with the pasted content
22684
+ if (!ev.isComposing && ev.inputType === "insertFromPaste") {
22685
+ return;
22686
+ }
22309
22687
  if (ev.data) {
22310
22688
  // if the user types a character on the grid, it means he wants to start composing the selected cell with that
22311
22689
  // character
@@ -23154,6 +23532,70 @@ function fixXlsxUnicode(str) {
23154
23532
  });
23155
23533
  }
23156
23534
 
23535
+ const XLSX_DATE_FORMAT_REGEX = /^(yy|yyyy|m{1,5}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/m|\s|-|\/|\.|:)+$/i;
23536
+ /**
23537
+ * Convert excel format to o_spreadsheet format
23538
+ *
23539
+ * Excel format are defined in openXML §18.8.31
23540
+ */
23541
+ function convertXlsxFormat(numFmtId, formats, warningManager) {
23542
+ var _a, _b, _c;
23543
+ if (numFmtId === 0) {
23544
+ return undefined;
23545
+ }
23546
+ // Format is either defined in the imported data, or the formatId is defined in openXML §18.8.30
23547
+ let format = XLSX_FORMATS_CONVERSION_MAP[numFmtId] || ((_a = formats.find((f) => f.id === numFmtId)) === null || _a === void 0 ? void 0 : _a.format);
23548
+ if (format) {
23549
+ try {
23550
+ let convertedFormat = format.replace(/(.*?);.*/, "$1"); // only take first part of multi-part format
23551
+ convertedFormat = convertedFormat.replace(/\[(.*)-[A-Z0-9]{3}\]/g, "[$1]"); // remove currency and locale/date system/number system info (ECMA §18.8.31)
23552
+ convertedFormat = convertedFormat.replace(/\[\$\]/g, ""); // remove empty bocks
23553
+ // Quotes in format escape sequences of characters. ATM we only support [$...] blocks to escape characters, and only one of them per format
23554
+ const numberOfQuotes = ((_b = convertedFormat.match(/"/g)) === null || _b === void 0 ? void 0 : _b.length) || 0;
23555
+ const numberOfOpenBrackets = ((_c = convertedFormat.match(/\[/g)) === null || _c === void 0 ? void 0 : _c.length) || 0;
23556
+ if (numberOfQuotes / 2 + numberOfOpenBrackets > 1) {
23557
+ throw new Error("Multiple escaped blocks in format");
23558
+ }
23559
+ convertedFormat = convertedFormat.replace(/"(.*)"/g, "[$$$1]"); // replace '"..."' by '[$...]'
23560
+ convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ == ignore width of next char for align purposes. Not supported ATM
23561
+ convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * == repeat next character enough to fill the line. Not supported ATM
23562
+ convertedFormat = convertedFormat.replace(/\\ /g, " "); // unescape spaces
23563
+ convertedFormat = convertedFormat.replace(/\\./g, (match) => match[1]); // unescape other characters
23564
+ if (isXlsxDateFormat(convertedFormat)) {
23565
+ convertedFormat = convertDateFormat$1(convertedFormat);
23566
+ }
23567
+ if (isFormatSupported(convertedFormat)) {
23568
+ return convertedFormat;
23569
+ }
23570
+ }
23571
+ catch (e) { }
23572
+ }
23573
+ warningManager.generateNotSupportedWarning(WarningTypes.NumFmtIdNotSupported, format || `nmFmtId ${numFmtId}`);
23574
+ return undefined;
23575
+ }
23576
+ function isFormatSupported(format) {
23577
+ try {
23578
+ formatValue(0, format);
23579
+ return true;
23580
+ }
23581
+ catch (e) {
23582
+ return false;
23583
+ }
23584
+ }
23585
+ function isXlsxDateFormat(format) {
23586
+ return format.match(XLSX_DATE_FORMAT_REGEX) !== null;
23587
+ }
23588
+ function convertDateFormat$1(format) {
23589
+ // Some of these aren't defined neither in the OpenXML spec not the Xlsx extension of OpenXML,
23590
+ // but can still occur and are supported by Excel/Google sheets
23591
+ format = format.toLowerCase();
23592
+ format = format.replace(/mmmmm/g, "mmm");
23593
+ format = format.replace(/am\/pm|a\/m/g, "a");
23594
+ format = format.replace(/hhhh/g, "hh");
23595
+ format = format.replace(/\bh\b/g, "hh");
23596
+ return format;
23597
+ }
23598
+
23157
23599
  function convertBorders(data, warningManager) {
23158
23600
  const borderArray = data.borders.map((border) => {
23159
23601
  addBorderWarnings(border, warningManager);
@@ -23216,41 +23658,6 @@ function convertFormats(data, warningManager) {
23216
23658
  }
23217
23659
  return arrayToObject(formats, 1);
23218
23660
  }
23219
- /**
23220
- * Convert excel format to o_spreadsheet format
23221
- *
23222
- * Excel format are defined in openXML §18.8.31
23223
- */
23224
- function convertXlsxFormat(numFmtId, formats, warningManager) {
23225
- var _a, _b, _c;
23226
- if (numFmtId === 0) {
23227
- return undefined;
23228
- }
23229
- // Format is either defined in the imported data, or the formatId is defined in openXML §18.8.30
23230
- let format = XLSX_FORMATS_CONVERSION_MAP[numFmtId] || ((_a = formats.find((f) => f.id === numFmtId)) === null || _a === void 0 ? void 0 : _a.format);
23231
- if (format) {
23232
- try {
23233
- let convertedFormat = format.replace(/(.*?);.*/, "$1"); // only take first part of multi-part format
23234
- convertedFormat = convertedFormat.replace(/\[(.*)-[A-Z0-9]{3}\]/g, "[$1]"); // remove currency and locale/date system/number system info (ECMA §18.8.31)
23235
- convertedFormat = convertedFormat.replace(/\[\$\]/g, ""); // remove empty bocks
23236
- // Quotes in format escape sequences of characters. ATM we only support [$...] blocks to escape characters, and only one of them per format
23237
- const numberOfQuotes = ((_b = convertedFormat.match(/"/g)) === null || _b === void 0 ? void 0 : _b.length) || 0;
23238
- const numberOfOpenBrackets = ((_c = convertedFormat.match(/\[/g)) === null || _c === void 0 ? void 0 : _c.length) || 0;
23239
- if (numberOfQuotes / 2 + numberOfOpenBrackets > 1) {
23240
- throw new Error("Multiple escaped blocks in format");
23241
- }
23242
- convertedFormat = convertedFormat.replace(/"(.*)"/g, "[$$$1]"); // replace '"..."' by '[$...]'
23243
- convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ == ignore with of next char for align purposes. Not supported ATM
23244
- convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * == repeat next character enough to fill the line. Not supported ATM
23245
- convertedFormat = convertedFormat.replace(/\\ /g, " "); // unescape spaces
23246
- formatValue(0, convertedFormat);
23247
- return convertedFormat;
23248
- }
23249
- catch (e) { }
23250
- }
23251
- warningManager.generateNotSupportedWarning(WarningTypes.NumFmtIdNotSupported, format || `nmFmtId ${numFmtId}`);
23252
- return undefined;
23253
- }
23254
23661
  // ---------------------------------------------------------------------------
23255
23662
  // Warnings
23256
23663
  // ---------------------------------------------------------------------------
@@ -23805,16 +24212,18 @@ function convertFigure(figure, id, sheetData) {
23805
24212
  };
23806
24213
  }
23807
24214
  function convertChartData(chartData) {
23808
- var _a;
23809
- const labelRange = (_a = chartData.dataSets[0].label) === null || _a === void 0 ? void 0 : _a.replace(/\$/g, "");
23810
- let dataSets = chartData.dataSets.map((data) => data.range.replace(/\$/g, ""));
24215
+ const dataSetsHaveTitle = chartData.dataSets[0].label !== undefined;
24216
+ const labelRange = chartData.labelRange
24217
+ ? convertExcelRangeToSheetXC(chartData.labelRange, dataSetsHaveTitle)
24218
+ : undefined;
24219
+ let dataSets = chartData.dataSets.map((data) => convertExcelRangeToSheetXC(data.range, dataSetsHaveTitle));
23811
24220
  // For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset
23812
24221
  if (chartData.type === "pie") {
23813
24222
  dataSets.reverse();
23814
24223
  }
23815
24224
  return {
23816
24225
  dataSets,
23817
- dataSetsHaveTitle: false,
24226
+ dataSetsHaveTitle,
23818
24227
  labelRange,
23819
24228
  title: chartData.title || "",
23820
24229
  type: chartData.type,
@@ -23825,6 +24234,28 @@ function convertChartData(chartData) {
23825
24234
  aggregated: false,
23826
24235
  labelsAsText: false,
23827
24236
  };
24237
+ }
24238
+ function convertExcelRangeToSheetXC(range, dataSetsHaveTitle) {
24239
+ let { sheetName, xc } = splitReference(range);
24240
+ if (sheetName) {
24241
+ sheetName = getCanonicalSheetName(sheetName) + "!";
24242
+ }
24243
+ else {
24244
+ sheetName = "";
24245
+ }
24246
+ let zone = toUnboundedZone(xc);
24247
+ if (dataSetsHaveTitle && zone.bottom !== undefined && zone.right !== undefined) {
24248
+ const height = zone.bottom - zone.top + 1;
24249
+ const width = zone.right - zone.left + 1;
24250
+ if (height === 1) {
24251
+ zone = { ...zone, left: zone.left - 1 };
24252
+ }
24253
+ else if (width === 1) {
24254
+ zone = { ...zone, top: zone.top - 1 };
24255
+ }
24256
+ }
24257
+ const dataXC = zoneToXc(zone);
24258
+ return sheetName + dataXC;
23828
24259
  }
23829
24260
 
23830
24261
  /**
@@ -24892,6 +25323,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
24892
25323
  title: chartTitle,
24893
25324
  type: CHART_TYPE_CONVERSION_MAP[chartType],
24894
25325
  dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`)),
25326
+ labelRange: this.extractChildTextContent(rootChartElement, "c:ser c:cat c:f"),
24895
25327
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
24896
25328
  default: "ffffff",
24897
25329
  }).asString(),
@@ -24911,7 +25343,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
24911
25343
  extractChartDatasets(chartElement) {
24912
25344
  return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
24913
25345
  return {
24914
- label: this.extractChildTextContent(chartDataElement, "c:cat c:f"),
25346
+ label: this.extractChildTextContent(chartDataElement, "c:tx c:f"),
24915
25347
  range: this.extractChildTextContent(chartDataElement, "c:val c:f", { required: true }),
24916
25348
  };
24917
25349
  });
@@ -28828,6 +29260,39 @@ class FigurePlugin extends CorePlugin {
28828
29260
  case "DELETE_FIGURE":
28829
29261
  this.removeFigure(cmd.id, cmd.sheetId);
28830
29262
  break;
29263
+ case "REMOVE_COLUMNS_ROWS":
29264
+ this.onRowColDelete(cmd.sheetId, cmd.dimension);
29265
+ }
29266
+ }
29267
+ onRowColDelete(sheetId, dimension) {
29268
+ dimension === "ROW" ? this.onRowDeletion(sheetId) : this.onColDeletion(sheetId);
29269
+ }
29270
+ onRowDeletion(sheetId) {
29271
+ const numHeader = this.getters.getNumberRows(sheetId);
29272
+ let gridHeight = 0;
29273
+ for (let i = 0; i < numHeader; i++) {
29274
+ gridHeight += this.getters.getRowSize(sheetId, i);
29275
+ }
29276
+ const figures = this.getters.getFigures(sheetId);
29277
+ for (const figure of figures) {
29278
+ const newY = Math.min(figure.y, gridHeight - figure.height);
29279
+ if (newY !== figure.y) {
29280
+ this.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, y: newY });
29281
+ }
29282
+ }
29283
+ }
29284
+ onColDeletion(sheetId) {
29285
+ const numHeader = this.getters.getNumberCols(sheetId);
29286
+ let gridWidth = 0;
29287
+ for (let i = 0; i < numHeader; i++) {
29288
+ gridWidth += this.getters.getColSize(sheetId, i);
29289
+ }
29290
+ const figures = this.getters.getFigures(sheetId);
29291
+ for (const figure of figures) {
29292
+ const newX = Math.min(figure.x, gridWidth - figure.width);
29293
+ if (newX !== figure.x) {
29294
+ this.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x: newX });
29295
+ }
28831
29296
  }
28832
29297
  }
28833
29298
  updateFigure(sheetId, figure) {
@@ -30242,6 +30707,7 @@ class RangeAdapter {
30242
30707
  }
30243
30708
  else if (range.zone[start] >= min && range.zone[end] <= max) {
30244
30709
  changeType = "REMOVE";
30710
+ newRange = range.clone({ ...this.getInvalidRange() });
30245
30711
  }
30246
30712
  else if (range.zone[start] <= max && range.zone[end] >= max) {
30247
30713
  const toRemove = max - range.zone[start] + 1;
@@ -30307,8 +30773,10 @@ class RangeAdapter {
30307
30773
  return { changeType: "NONE" };
30308
30774
  }
30309
30775
  const invalidSheetName = this.getters.getSheetName(cmd.sheetId);
30310
- const sheetId = "";
30311
- range = range.clone({ sheetId, invalidSheetName });
30776
+ range = range.clone({
30777
+ ...this.getInvalidRange(),
30778
+ invalidSheetName,
30779
+ });
30312
30780
  return { changeType: "REMOVE", range };
30313
30781
  }, cmd.sheetId);
30314
30782
  break;
@@ -30495,7 +30963,7 @@ class RangeAdapter {
30495
30963
  sheetName = rangeImpl.invalidSheetName;
30496
30964
  }
30497
30965
  else {
30498
- sheetName = getComposerSheetName(this.getters.getSheetName(rangeImpl.sheetId));
30966
+ sheetName = getCanonicalSheetName(this.getters.getSheetName(rangeImpl.sheetId));
30499
30967
  }
30500
30968
  }
30501
30969
  if (prefixSheet && !sheetName) {
@@ -30568,6 +31036,15 @@ class RangeAdapter {
30568
31036
  }
30569
31037
  return str;
30570
31038
  }
31039
+ getInvalidRange() {
31040
+ return {
31041
+ parts: [],
31042
+ prefixSheet: false,
31043
+ zone: { left: -1, top: -1, right: -1, bottom: -1 },
31044
+ sheetId: "",
31045
+ invalidXc: INCORRECT_RANGE_STRING,
31046
+ };
31047
+ }
30571
31048
  }
30572
31049
  RangeAdapter.getters = [
30573
31050
  "getRangeString",
@@ -31537,9 +32014,7 @@ class CustomColorsPlugin extends UIPlugin {
31537
32014
  // remove duplicates first to check validity on a reduced
31538
32015
  // set of colors, then normalize to HEX and remove duplicates
31539
32016
  // again
31540
- [...new Set([...usedColors, ...this.customColors])]
31541
- .filter(isColorValid)
31542
- .map((c) => toHex(c).toLowerCase())),
32017
+ [...new Set([...usedColors, ...this.customColors])].filter(isColorValid).map(toHex)),
31543
32018
  ]).filter((color) => !COLOR_PICKER_DEFAULTS.includes(color));
31544
32019
  }
31545
32020
  getColorsFromCells(cells) {
@@ -31600,7 +32075,7 @@ class CustomColorsPlugin extends UIPlugin {
31600
32075
  return [...chartsColors];
31601
32076
  }
31602
32077
  tryToAddColor(color) {
31603
- const formattedColor = toHex(color).toLowerCase();
32078
+ const formattedColor = toHex(color);
31604
32079
  if (color && !COLOR_PICKER_DEFAULTS.includes(formattedColor)) {
31605
32080
  this.customColors.add(formattedColor);
31606
32081
  }
@@ -32615,8 +33090,8 @@ class InternalViewport {
32615
33090
  this.getters = getters;
32616
33091
  this.sheetId = sheetId;
32617
33092
  this.boundaries = boundaries;
32618
- this.width = sizeInGrid.width;
32619
- this.height = sizeInGrid.height;
33093
+ this.viewportWidth = sizeInGrid.width;
33094
+ this.viewportHeight = sizeInGrid.height;
32620
33095
  this.offsetScrollbarX = offsets.x;
32621
33096
  this.offsetScrollbarY = offsets.y;
32622
33097
  this.canScrollVertically = options.canScrollVertically;
@@ -32627,6 +33102,10 @@ class InternalViewport {
32627
33102
  this.adjustViewportOffsetY();
32628
33103
  }
32629
33104
  // PUBLIC
33105
+ /** Returns the maximum size (in Pixels) of the viewport relative to its allocated client size
33106
+ * When the viewport grid size is smaller than its client width (resp. height), it will return
33107
+ * the client width (resp. height).
33108
+ */
32630
33109
  getMaxSize() {
32631
33110
  const lastCol = this.getters.findLastVisibleColRowIndex(this.sheetId, "COL", {
32632
33111
  first: this.boundaries.left,
@@ -32638,20 +33117,24 @@ class InternalViewport {
32638
33117
  });
32639
33118
  const { end: lastColEnd, size: lastColSize } = this.getters.getColDimensions(this.sheetId, lastCol);
32640
33119
  const { end: lastRowEnd, size: lastRowSize } = this.getters.getRowDimensions(this.sheetId, lastRow);
32641
- const leftColIndex = this.searchHeaderIndex("COL", lastColEnd - this.width, 0);
33120
+ const leftColIndex = this.searchHeaderIndex("COL", lastColEnd - this.viewportWidth, 0);
32642
33121
  const leftColSize = this.getters.getColSize(this.sheetId, leftColIndex);
32643
- const leftRowIndex = this.searchHeaderIndex("ROW", lastRowEnd - this.height, 0);
33122
+ const leftRowIndex = this.searchHeaderIndex("ROW", lastRowEnd - this.viewportHeight, 0);
32644
33123
  const topRowSize = this.getters.getRowSize(this.sheetId, leftRowIndex);
32645
- const width = lastColEnd -
32646
- this.offsetCorrectionX +
32647
- (this.canScrollHorizontally
32648
- ? Math.max(DEFAULT_CELL_WIDTH, Math.min(leftColSize, this.width - lastColSize))
32649
- : 0);
32650
- const height = lastRowEnd -
32651
- this.offsetCorrectionY +
32652
- (this.canScrollVertically
32653
- ? Math.max(DEFAULT_CELL_HEIGHT + 5, Math.min(topRowSize, this.height - lastRowSize))
32654
- : 0);
33124
+ let width = lastColEnd - this.offsetCorrectionX;
33125
+ if (this.canScrollHorizontally) {
33126
+ width += Math.max(DEFAULT_CELL_WIDTH, // leave some minimal space to let the user know they scrolled all the way
33127
+ Math.min(leftColSize, this.viewportWidth - lastColSize) // Add pixels that allows the snapping at maximum horizontal scroll
33128
+ );
33129
+ width = Math.max(width, this.viewportWidth); // if the viewport grid size is smaller than its client width, return client width
33130
+ }
33131
+ let height = lastRowEnd - this.offsetCorrectionY;
33132
+ if (this.canScrollVertically) {
33133
+ height += Math.max(DEFAULT_CELL_HEIGHT + 5, // leave some space to let the user know they scrolled all the way
33134
+ Math.min(topRowSize, this.viewportHeight - lastRowSize) // Add pixels that allows the snapping at maximum vertical scroll
33135
+ );
33136
+ height = Math.max(height, this.viewportHeight); // if the viewport grid size is smaller than its client height, return client height
33137
+ }
32655
33138
  return { width, height };
32656
33139
  }
32657
33140
  /**
@@ -32660,7 +33143,7 @@ class InternalViewport {
32660
33143
  * It returns -1 if no column is found.
32661
33144
  */
32662
33145
  getColIndex(x, absolute = false) {
32663
- if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.width) {
33146
+ if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.viewportWidth) {
32664
33147
  return -1;
32665
33148
  }
32666
33149
  return this.searchHeaderIndex("COL", x - this.offsetCorrectionX, this.left, absolute);
@@ -32671,7 +33154,7 @@ class InternalViewport {
32671
33154
  * It returns -1 if no row is found.
32672
33155
  */
32673
33156
  getRowIndex(y, absolute = false) {
32674
- if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.height) {
33157
+ if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.viewportHeight) {
32675
33158
  return -1;
32676
33159
  }
32677
33160
  return this.searchHeaderIndex("ROW", y - this.offsetCorrectionY, this.top, absolute);
@@ -32698,7 +33181,7 @@ class InternalViewport {
32698
33181
  adjustPositionX(col) {
32699
33182
  const sheetId = this.sheetId;
32700
33183
  const { start, end } = this.getters.getColDimensions(sheetId, col);
32701
- while (end > this.offsetX + this.offsetCorrectionX + this.width &&
33184
+ while (end > this.offsetX + this.offsetCorrectionX + this.viewportWidth &&
32702
33185
  this.offsetX + this.offsetCorrectionX < start) {
32703
33186
  this.offsetX = this.getters.getColDimensions(sheetId, this.left).end - this.offsetCorrectionX;
32704
33187
  this.offsetScrollbarX = this.offsetX;
@@ -32720,7 +33203,7 @@ class InternalViewport {
32720
33203
  adjustPositionY(row) {
32721
33204
  const sheetId = this.sheetId;
32722
33205
  while (this.getters.getRowDimensions(sheetId, row).end >
32723
- this.offsetY + this.height + this.offsetCorrectionY &&
33206
+ this.offsetY + this.viewportHeight + this.offsetCorrectionY &&
32724
33207
  this.offsetY + this.offsetCorrectionY < this.getters.getRowDimensions(sheetId, row).start) {
32725
33208
  this.offsetY = this.getters.getRowDimensions(sheetId, this.top).end - this.offsetCorrectionY;
32726
33209
  this.offsetScrollbarY = this.offsetY;
@@ -32758,8 +33241,8 @@ class InternalViewport {
32758
33241
  const x = this.getters.getColRowOffset("COL", this.zone.left, targetZone.left) +
32759
33242
  this.offsetCorrectionX;
32760
33243
  const y = this.getters.getColRowOffset("ROW", this.zone.top, targetZone.top) + this.offsetCorrectionY;
32761
- const width = Math.min(this.getters.getColRowOffset("COL", targetZone.left, targetZone.right + 1), this.width);
32762
- const height = Math.min(this.getters.getColRowOffset("ROW", targetZone.top, targetZone.bottom + 1), this.height);
33244
+ const width = Math.min(this.getters.getColRowOffset("COL", targetZone.left, targetZone.right + 1), this.viewportWidth);
33245
+ const height = Math.min(this.getters.getColRowOffset("ROW", targetZone.top, targetZone.bottom + 1), this.viewportHeight);
32763
33246
  return {
32764
33247
  x,
32765
33248
  y,
@@ -32822,12 +33305,12 @@ class InternalViewport {
32822
33305
  adjustViewportOffsetX() {
32823
33306
  if (this.canScrollHorizontally) {
32824
33307
  const { width: viewportWidth } = this.getMaxSize();
32825
- if (this.width + this.offsetScrollbarX > viewportWidth) {
32826
- this.offsetScrollbarX = Math.max(0, viewportWidth - this.width);
33308
+ if (this.viewportWidth + this.offsetScrollbarX > viewportWidth) {
33309
+ this.offsetScrollbarX = Math.max(0, viewportWidth - this.viewportWidth);
32827
33310
  }
32828
33311
  }
32829
33312
  this.left = this.getColIndex(this.offsetScrollbarX, true);
32830
- this.right = this.getColIndex(this.offsetScrollbarX + this.width, true);
33313
+ this.right = this.getColIndex(this.offsetScrollbarX + this.viewportWidth, true);
32831
33314
  if (this.right === -1) {
32832
33315
  this.right = this.boundaries.right;
32833
33316
  }
@@ -32839,12 +33322,12 @@ class InternalViewport {
32839
33322
  adjustViewportOffsetY() {
32840
33323
  if (this.canScrollVertically) {
32841
33324
  const { height: paneHeight } = this.getMaxSize();
32842
- if (this.height + this.offsetScrollbarY > paneHeight) {
32843
- this.offsetScrollbarY = Math.max(0, paneHeight - this.height);
33325
+ if (this.viewportHeight + this.offsetScrollbarY > paneHeight) {
33326
+ this.offsetScrollbarY = Math.max(0, paneHeight - this.viewportHeight);
32844
33327
  }
32845
33328
  }
32846
33329
  this.top = this.getRowIndex(this.offsetScrollbarY, true);
32847
- this.bottom = this.getRowIndex(this.offsetScrollbarY + this.width, true);
33330
+ this.bottom = this.getRowIndex(this.offsetScrollbarY + this.viewportWidth, true);
32848
33331
  if (this.bottom === -1) {
32849
33332
  this.bottom = this.boundaries.bottom;
32850
33333
  }
@@ -32855,7 +33338,7 @@ class InternalViewport {
32855
33338
  adjustViewportZoneX() {
32856
33339
  const sheetId = this.sheetId;
32857
33340
  this.left = this.searchHeaderIndex("COL", this.offsetScrollbarX, this.boundaries.left);
32858
- this.right = Math.min(this.boundaries.right, this.searchHeaderIndex("COL", this.width, this.left));
33341
+ this.right = Math.min(this.boundaries.right, this.searchHeaderIndex("COL", this.viewportWidth, this.left));
32859
33342
  if (this.right === -1) {
32860
33343
  this.right = this.getters.getNumberCols(sheetId) - 1;
32861
33344
  }
@@ -32868,7 +33351,7 @@ class InternalViewport {
32868
33351
  adjustViewportZoneY() {
32869
33352
  const sheetId = this.sheetId;
32870
33353
  this.top = this.searchHeaderIndex("ROW", this.offsetScrollbarY, this.boundaries.top);
32871
- this.bottom = Math.min(this.boundaries.bottom, this.searchHeaderIndex("ROW", this.height, this.top));
33354
+ this.bottom = Math.min(this.boundaries.bottom, this.searchHeaderIndex("ROW", this.viewportHeight, this.top));
32872
33355
  if (this.bottom === -1) {
32873
33356
  this.bottom = this.getters.getNumberRows(sheetId) - 1;
32874
33357
  }
@@ -33131,8 +33614,7 @@ class SheetViewPlugin extends UIPlugin {
33131
33614
  return [...new Set(viewports.map((v) => range(v.top, v.bottom + 1)).flat())].filter((row) => !this.getters.isHeaderHidden(sheetId, "ROW", row));
33132
33615
  }
33133
33616
  /**
33134
- * Return the main viewport maximum size. That is the zone dimension
33135
- * with some bottom and right padding.
33617
+ * Return the main viewport maximum size relative to the client size.
33136
33618
  */
33137
33619
  getMainViewportRect() {
33138
33620
  const sheetId = this.getters.getActiveSheetId();
@@ -33148,8 +33630,8 @@ class SheetViewPlugin extends UIPlugin {
33148
33630
  const { width, height } = this.getMainViewportRect();
33149
33631
  const viewport = this.getMainInternalViewport(sheetId);
33150
33632
  return {
33151
- maxOffsetX: Math.max(0, width - viewport.width + 1),
33152
- maxOffsetY: Math.max(0, height - viewport.height + 1),
33633
+ maxOffsetX: Math.max(0, width - viewport.viewportWidth + 1),
33634
+ maxOffsetY: Math.max(0, height - viewport.viewportHeight + 1),
33153
33635
  };
33154
33636
  }
33155
33637
  getColRowOffsetInViewport(dimension, referenceIndex, index) {
@@ -33464,6 +33946,20 @@ class SheetViewPlugin extends UIPlugin {
33464
33946
  }
33465
33947
  return result;
33466
33948
  }
33949
+ isPositionVisible(position) {
33950
+ const { scrollX, scrollY } = this.getters.getActiveSheetScrollInfo();
33951
+ const { x: mainViewportX, y: mainViewportY } = this.getters.getMainViewportCoordinates();
33952
+ const { width, height } = this.getters.getSheetViewDimension();
33953
+ if (position.x >= mainViewportX &&
33954
+ (position.x < mainViewportX + scrollX || position.x > width + scrollX + mainViewportX)) {
33955
+ return false;
33956
+ }
33957
+ if (position.y >= mainViewportY &&
33958
+ (position.y < mainViewportY + scrollY || position.y > height + scrollY + mainViewportY)) {
33959
+ return false;
33960
+ }
33961
+ return true;
33962
+ }
33467
33963
  getFrozenSheetViewRatio(sheetId) {
33468
33964
  const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
33469
33965
  const offsetCorrectionX = this.getters.getColDimensions(sheetId, xSplit).start;
@@ -33492,6 +33988,7 @@ SheetViewPlugin.getters = [
33492
33988
  "getSheetViewVisibleCols",
33493
33989
  "getSheetViewVisibleRows",
33494
33990
  "getFrozenSheetViewRatio",
33991
+ "isPositionVisible",
33495
33992
  ];
33496
33993
 
33497
33994
  /**
@@ -35354,7 +35851,7 @@ class SelectionInputPlugin extends UIPlugin {
35354
35851
  const inputSheetId = this.activeSheet;
35355
35852
  const sheetId = this.getters.getActiveSheetId();
35356
35853
  const sheetName = this.getters.getSheetName(sheetId);
35357
- this.add([sheetId === inputSheetId ? xc : `${getComposerSheetName(sheetName)}!${xc}`]);
35854
+ this.add([sheetId === inputSheetId ? xc : `${getCanonicalSheetName(sheetName)}!${xc}`]);
35358
35855
  }
35359
35856
  handle(cmd) {
35360
35857
  switch (cmd.type) {
@@ -37601,13 +38098,19 @@ class ClipboardFigureState {
37601
38098
  */
37602
38099
  paste(target) {
37603
38100
  const sheetId = this.getters.getActiveSheetId();
38101
+ const { width, height } = this.copiedFigure;
38102
+ const numCols = this.getters.getNumberCols(sheetId);
38103
+ const numRows = this.getters.getNumberRows(sheetId);
38104
+ const targetX = this.getters.getColDimensions(sheetId, target[0].left).start;
38105
+ const targetY = this.getters.getRowDimensions(sheetId, target[0].top).start;
38106
+ const maxX = this.getters.getColDimensions(sheetId, numCols - 1).end;
38107
+ const maxY = this.getters.getRowDimensions(sheetId, numRows - 1).end;
37604
38108
  const position = {
37605
- x: this.getters.getColDimensions(sheetId, target[0].left).start,
37606
- y: this.getters.getRowDimensions(sheetId, target[0].top).start,
38109
+ x: maxX < width ? 0 : Math.min(targetX, maxX - width),
38110
+ y: maxY < height ? 0 : Math.min(targetY, maxY - height),
37607
38111
  };
37608
- const size = { height: this.copiedFigure.height, width: this.copiedFigure.width };
37609
38112
  const newId = new UuidGenerator().uuidv4();
37610
- this.copiedFigureContent.paste(sheetId, newId, position, size);
38113
+ this.copiedFigureContent.paste(sheetId, newId, position, { height, width });
37611
38114
  if (this.operation === "CUT") {
37612
38115
  this.dispatch("DELETE_FIGURE", {
37613
38116
  sheetId: this.copiedFigureContent.sheetId,
@@ -37973,7 +38476,7 @@ function loopThroughReferenceType(token) {
37973
38476
  return token;
37974
38477
  const { xc, sheetName } = splitReference(token.value);
37975
38478
  const [left, right] = xc.split(":");
37976
- const sheetRef = sheetName ? `${getComposerSheetName(sheetName)}!` : "";
38479
+ const sheetRef = sheetName ? `${getCanonicalSheetName(sheetName)}!` : "";
37977
38480
  const updatedLeft = getTokenNextReferenceType(left);
37978
38481
  const updatedRight = right ? `:${getTokenNextReferenceType(right)}` : "";
37979
38482
  return { ...token, value: sheetRef + updatedLeft + updatedRight };
@@ -38232,9 +38735,6 @@ class EditionPlugin extends UIPlugin {
38232
38735
  }
38233
38736
  return this.currentContent;
38234
38737
  }
38235
- getEditionSheet() {
38236
- return this.sheetId;
38237
- }
38238
38738
  getComposerSelection() {
38239
38739
  return {
38240
38740
  start: this.selectionStart,
@@ -38518,8 +39018,8 @@ class EditionPlugin extends UIPlugin {
38518
39018
  getZoneReference(zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {
38519
39019
  const sheetId = this.getters.getActiveSheetId();
38520
39020
  let selectedXc = this.getters.zoneToXC(sheetId, zone, fixedParts);
38521
- if (this.getters.getEditionSheet() !== this.getters.getActiveSheetId()) {
38522
- const sheetName = getComposerSheetName(this.getters.getSheetName(this.getters.getActiveSheetId()));
39021
+ if (this.getters.getCurrentEditedCell().sheetId !== this.getters.getActiveSheetId()) {
39022
+ const sheetName = getCanonicalSheetName(this.getters.getSheetName(this.getters.getActiveSheetId()));
38523
39023
  selectedXc = `${sheetName}!${selectedXc}`;
38524
39024
  }
38525
39025
  return selectedXc;
@@ -38533,7 +39033,7 @@ class EditionPlugin extends UIPlugin {
38533
39033
  _fixedParts.pop();
38534
39034
  }
38535
39035
  const newRange = range.clone({ parts: _fixedParts });
38536
- return this.getters.getSelectionRangeString(newRange, this.getters.getEditionSheet());
39036
+ return this.getters.getSelectionRangeString(newRange, this.getters.getCurrentEditedCell().sheetId);
38537
39037
  }
38538
39038
  /**
38539
39039
  * Replace the current selection by a new text.
@@ -38566,7 +39066,7 @@ class EditionPlugin extends UIPlugin {
38566
39066
  if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
38567
39067
  return;
38568
39068
  }
38569
- const editionSheetId = this.getters.getEditionSheet();
39069
+ const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
38570
39070
  const XCs = this.getReferencedRanges().map((range) => this.getters.getRangeString(range, editionSheetId));
38571
39071
  const colorsToKeep = {};
38572
39072
  for (const xc of XCs) {
@@ -38595,7 +39095,7 @@ class EditionPlugin extends UIPlugin {
38595
39095
  if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
38596
39096
  return [];
38597
39097
  }
38598
- const editionSheetId = this.getters.getEditionSheet();
39098
+ const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
38599
39099
  const rangeColor = (rangeString) => {
38600
39100
  const colorIndex = this.colorIndexByRange[rangeString];
38601
39101
  return colors$1[colorIndex % colors$1.length];
@@ -38613,7 +39113,7 @@ class EditionPlugin extends UIPlugin {
38613
39113
  * Return ranges currently referenced in the composer
38614
39114
  */
38615
39115
  getReferencedRanges() {
38616
- const editionSheetId = this.getters.getEditionSheet();
39116
+ const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
38617
39117
  return this.currentTokens
38618
39118
  .filter((token) => token.type === "REFERENCE")
38619
39119
  .map((token) => this.getters.getRangeFromSheetXC(editionSheetId, token.value));
@@ -38672,7 +39172,6 @@ EditionPlugin.getters = [
38672
39172
  "isSelectingForComposer",
38673
39173
  "showSelectionIndicator",
38674
39174
  "getCurrentContent",
38675
- "getEditionSheet",
38676
39175
  "getComposerSelection",
38677
39176
  "getCurrentTokens",
38678
39177
  "getTokenAtCursor",
@@ -39995,10 +40494,6 @@ css /* scss */ `
39995
40494
  .o-bottom-bar-arrows {
39996
40495
  .o-bottom-bar-arrow {
39997
40496
  cursor: pointer;
39998
- &.o-disabled {
39999
- opacity: 0.4;
40000
- cursor: default;
40001
- }
40002
40497
  &:hover:not([class*="o-disabled"]) {
40003
40498
  .o-icon {
40004
40499
  opacity: 0.9;
@@ -40591,6 +41086,11 @@ css /* scss */ `
40591
41086
  margin-top: -1px;
40592
41087
  border: 1px solid;
40593
41088
  z-index: ${ComponentsImportance.TopBarComposer};
41089
+
41090
+ .o-composer:empty:not(:focus)::before {
41091
+ /* svg free of use from https://uxwing.com/formula-fx-icon/ */
41092
+ content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -10 121.8 142.9' width='24' height='24' focusable='false'%3E%3Cpath d='m28 34-4 5v2h10l-6 40c-4 22-6 28-7 30-2 2-3 3-5 3-3 0-7-2-9-4H4c-2 2-4 4-4 7s4 6 8 6 9-2 15-8c8-7 13-17 18-39l7-35 13-1 3-6H49c4-23 7-27 11-27 2 0 5 2 8 6h4c1-1 4-4 4-7 0-2-3-6-9-6-5 0-13 4-20 10-6 7-9 14-11 24h-8zm41 16c4-5 7-7 8-7s2 1 5 9l3 12c-7 11-12 17-16 17l-3-1-2-1c-3 0-6 3-6 7s3 7 7 7c6 0 12-6 22-23l3 10c3 9 6 13 10 13 5 0 11-4 18-15l-3-4c-4 6-7 8-8 8-2 0-4-3-6-10l-5-15 8-10 6-4 3 1 3 2c2 0 6-3 6-7s-2-7-6-7c-6 0-11 5-21 20l-2-6c-3-9-5-14-9-14-5 0-12 6-18 15l3 3z' fill='%23BDBDBD' /%3E%3C/svg%3E");
41093
+ }
40594
41094
  }
40595
41095
  `;
40596
41096
  class TopBarComposer extends owl.Component {
@@ -40831,11 +41331,6 @@ css /* scss */ `
40831
41331
  margin: 0 6px;
40832
41332
  }
40833
41333
 
40834
- .o-disabled {
40835
- opacity: 0.6;
40836
- cursor: default;
40837
- }
40838
-
40839
41334
  .o-dropdown {
40840
41335
  position: relative;
40841
41336
  display: flex;
@@ -40997,6 +41492,7 @@ class TopBar extends owl.Component {
40997
41492
  this.state.menuState.parentMenu = menu;
40998
41493
  this.isSelectingMenu = true;
40999
41494
  this.openedEl = ev.target;
41495
+ this.env.model.dispatch("STOP_EDITION");
41000
41496
  }
41001
41497
  closeMenus() {
41002
41498
  this.state.activeTool = "";
@@ -41209,10 +41705,12 @@ css /* scss */ `
41209
41705
  button {
41210
41706
  color: #333;
41211
41707
  }
41212
-
41213
- * {
41214
- font-family: "Roboto", "RobotoDraft", Helvetica, Arial, sans-serif;
41708
+ .o-disabled {
41709
+ opacity: 0.4;
41710
+ pointer: default;
41711
+ pointer-events: none;
41215
41712
  }
41713
+
41216
41714
  &,
41217
41715
  *,
41218
41716
  *:before,
@@ -41370,8 +41868,10 @@ class Spreadsheet extends owl.Component {
41370
41868
  this.sidePanel.isOpen = true;
41371
41869
  }
41372
41870
  closeSidePanel() {
41871
+ var _a, _b;
41373
41872
  this.sidePanel.isOpen = false;
41374
41873
  this.focusGrid();
41874
+ (_b = (_a = this.sidePanel.panelProps) === null || _a === void 0 ? void 0 : _a.onCloseSidePanel) === null || _b === void 0 ? void 0 : _b.call(_a);
41375
41875
  }
41376
41876
  toggleSidePanel(panel, panelProps) {
41377
41877
  if (this.sidePanel.isOpen && panel === this.sidePanel.component) {
@@ -43810,7 +44310,7 @@ function figureCoordinates(headers, position) {
43810
44310
  if (currentPosition <= position && position < currentPosition + header.size) {
43811
44311
  return {
43812
44312
  index: parseInt(headerIndex),
43813
- offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),
44313
+ offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH),
43814
44314
  };
43815
44315
  }
43816
44316
  else {
@@ -43819,7 +44319,7 @@ function figureCoordinates(headers, position) {
43819
44319
  }
43820
44320
  return {
43821
44321
  index: headers.length - 1,
43822
- offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),
44322
+ offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH),
43823
44323
  };
43824
44324
  }
43825
44325
  function createChartDrawing(figure, sheet, chartRelId) {
@@ -44579,6 +45079,13 @@ class Model extends EventBus {
44579
45079
  * Internal status of the model. Important for command handling coordination
44580
45080
  */
44581
45081
  this.status = 0 /* Status.Ready */;
45082
+ /**
45083
+ * Check if a command can be dispatched, and returns a DispatchResult object with the possible
45084
+ * reasons the dispatch failed.
45085
+ */
45086
+ this.canDispatch = (type, payload) => {
45087
+ return this.checkDispatchAllowed({ ...payload, type });
45088
+ };
44582
45089
  /**
44583
45090
  * The dispatch method is the only entry point to manipulate data in the model.
44584
45091
  * This is through this method that commands are dispatched most of the time
@@ -44722,7 +45229,10 @@ class Model extends EventBus {
44722
45229
  owl.markRaw(this);
44723
45230
  }
44724
45231
  get handlers() {
44725
- return [this.range, ...this.corePlugins, ...this.allUIPlugins, this.history];
45232
+ return [...this.coreHandlers, ...this.allUIPlugins, this.history];
45233
+ }
45234
+ get coreHandlers() {
45235
+ return [this.range, ...this.corePlugins];
44726
45236
  }
44727
45237
  get allUIPlugins() {
44728
45238
  return [...this.statefulUIPlugins, ...this.coreViewsPlugins, ...this.featurePlugins];
@@ -44850,6 +45360,16 @@ class Model extends EventBus {
44850
45360
  * Check if the given command is allowed by all the plugins and the history.
44851
45361
  */
44852
45362
  checkDispatchAllowed(command) {
45363
+ if (isCoreCommand(command)) {
45364
+ return this.checkDispatchAllowedCoreCommand(command);
45365
+ }
45366
+ return this.checkDispatchAllowedLocalCommand(command);
45367
+ }
45368
+ checkDispatchAllowedCoreCommand(command) {
45369
+ const results = this.coreHandlers.map((handler) => handler.allowDispatch(command));
45370
+ return new DispatchResult(results.flat());
45371
+ }
45372
+ checkDispatchAllowedLocalCommand(command) {
44853
45373
  const results = this.handlers.map((handler) => handler.allowDispatch(command));
44854
45374
  return new DispatchResult(results.flat());
44855
45375
  }
@@ -45078,6 +45598,6 @@ exports.setTranslationMethod = setTranslationMethod;
45078
45598
  exports.tokenize = tokenize;
45079
45599
 
45080
45600
 
45081
- __info__.version = '16.3.0-alpha.1';
45082
- __info__.date = '2023-03-16T12:12:26.034Z';
45083
- __info__.hash = 'f41d64a';
45601
+ __info__.version = '16.3.0-alpha.3';
45602
+ __info__.date = '2023-04-13T08:30:41.165Z';
45603
+ __info__.hash = '573e679';