@odoo/o-spreadsheet 18.4.0-alpha.6 → 18.4.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.4.0-alpha.6
6
- * @date 2025-05-30T08:44:33.216Z
7
- * @hash eecf7e4
5
+ * @version 18.4.0-alpha.8
6
+ * @date 2025-06-12T09:53:48.133Z
7
+ * @hash 9b7a8d0
8
8
  */
9
9
 
10
10
  'use strict';
@@ -132,7 +132,7 @@ class Registry {
132
132
 
133
133
  const CANVAS_SHIFT = 0.5;
134
134
  // Colors
135
- const HIGHLIGHT_COLOR = "#37A850";
135
+ const HIGHLIGHT_COLOR = "#017E84";
136
136
  const BACKGROUND_GRAY_COLOR = "#f5f5f5";
137
137
  const BACKGROUND_HEADER_COLOR = "#F8F9FA";
138
138
  const BACKGROUND_HEADER_SELECTED_COLOR = "#E8EAED";
@@ -145,7 +145,7 @@ const CELL_BORDER_COLOR = "#E2E3E3";
145
145
  const BACKGROUND_CHART_COLOR = "#FFFFFF";
146
146
  const DISABLED_TEXT_COLOR = "#CACACA";
147
147
  const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;
148
- const LINK_COLOR = "#017E84";
148
+ const LINK_COLOR = HIGHLIGHT_COLOR;
149
149
  const FILTERS_COLOR = "#188038";
150
150
  const SEPARATOR_COLOR = "#E0E2E4";
151
151
  const ICONS_COLOR = "#4A4F59";
@@ -174,7 +174,7 @@ const BUTTON_HOVER_BG = GRAY_300;
174
174
  const BUTTON_HOVER_TEXT_COLOR = "#111827";
175
175
  const BUTTON_ACTIVE_BG = "#e6f2f3";
176
176
  const BUTTON_ACTIVE_TEXT_COLOR = "#111827";
177
- const ACTION_COLOR = "#017E84";
177
+ const ACTION_COLOR = HIGHLIGHT_COLOR;
178
178
  const ACTION_COLOR_HOVER = "#01585c";
179
179
  const ALERT_WARNING_BG = "#FBEBCC";
180
180
  const ALERT_WARNING_BORDER = "#F8E2B3";
@@ -281,8 +281,10 @@ const MIN_ROW_HEIGHT = 10;
281
281
  const MIN_COL_WIDTH = 5;
282
282
  const HEADER_HEIGHT = 26;
283
283
  const HEADER_WIDTH = 48;
284
- const TOPBAR_TOOLBAR_HEIGHT = 34;
285
- const BOTTOMBAR_HEIGHT = 36;
284
+ const DESKTOP_TOPBAR_TOOLBAR_HEIGHT = 34;
285
+ const MOBILE_TOPBAR_TOOLBAR_HEIGHT = 44;
286
+ const DESKTOP_BOTTOMBAR_HEIGHT = 36;
287
+ const MOBILE_BOTTOMBAR_HEIGHT = 44;
286
288
  const DEFAULT_CELL_WIDTH = 96;
287
289
  const DEFAULT_CELL_HEIGHT = 23;
288
290
  const SCROLLBAR_WIDTH = 15;
@@ -303,7 +305,8 @@ const MOBILE_WIDTH_BREAKPOINT = 768;
303
305
  // Menus
304
306
  const MENU_WIDTH = 250;
305
307
  const MENU_VERTICAL_PADDING = 6;
306
- const MENU_ITEM_HEIGHT = 26;
308
+ const DESKTOP_MENU_ITEM_HEIGHT = 26;
309
+ const MOBILE_MENU_ITEM_HEIGHT = 35;
307
310
  const MENU_ITEM_PADDING_HORIZONTAL = 11;
308
311
  const MENU_ITEM_PADDING_VERTICAL = 4;
309
312
  const MENU_SEPARATOR_BORDER_WIDTH = 1;
@@ -401,6 +404,7 @@ const PIVOT_TABLE_CONFIG = {
401
404
  automaticAutofill: false,
402
405
  };
403
406
  const PIVOT_INDENT = 15;
407
+ const PIVOT_COLLAPSE_ICON_SIZE = 12;
404
408
  const DEFAULT_CURRENCY = {
405
409
  symbol: "$",
406
410
  position: "before",
@@ -1564,6 +1568,19 @@ class AlternatingColorGenerator extends ColorGenerator {
1564
1568
  this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
1565
1569
  }
1566
1570
  }
1571
+ class AlternatingColorMap {
1572
+ availableColors;
1573
+ colors = {};
1574
+ constructor(paletteSize = 12) {
1575
+ this.availableColors = new AlternatingColorGenerator(paletteSize);
1576
+ }
1577
+ get(id) {
1578
+ if (!this.colors[id]) {
1579
+ this.colors[id] = this.availableColors.next();
1580
+ }
1581
+ return this.colors[id];
1582
+ }
1583
+ }
1567
1584
  /**
1568
1585
  * Returns a function that maps a value to a color using a color scale defined by the given
1569
1586
  * color/threshold values pairs.
@@ -5003,7 +5020,9 @@ function isTextFormat(format) {
5003
5020
  }
5004
5021
 
5005
5022
  function evaluateLiteral(literalCell, localeFormat) {
5006
- const value = isTextFormat(localeFormat.format) ? literalCell.content : literalCell.parsedValue;
5023
+ const value = isTextFormat(localeFormat.format) && literalCell.parsedValue !== null
5024
+ ? literalCell.content
5025
+ : literalCell.parsedValue;
5007
5026
  const functionResult = { value, format: localeFormat.format };
5008
5027
  return createEvaluatedCell(functionResult, localeFormat.locale);
5009
5028
  }
@@ -5052,6 +5071,9 @@ function _createEvaluatedCell(functionResult, locale, cell) {
5052
5071
  if (isEvaluationError(value)) {
5053
5072
  return errorCell(value, message);
5054
5073
  }
5074
+ if (value === null) {
5075
+ return emptyCell(format);
5076
+ }
5055
5077
  if (isTextFormat(format)) {
5056
5078
  // TO DO:
5057
5079
  // with the next line, the value of the cell is transformed depending on the format.
@@ -5059,9 +5081,6 @@ function _createEvaluatedCell(functionResult, locale, cell) {
5059
5081
  // to interpret the value as a number.
5060
5082
  return textCell(toString(value), format, formattedValue);
5061
5083
  }
5062
- if (value === null) {
5063
- return emptyCell(format);
5064
- }
5065
5084
  if (typeof value === "number") {
5066
5085
  if (isDateTimeFormat(format || "")) {
5067
5086
  return dateTimeCell(value, format, formattedValue);
@@ -19349,8 +19368,9 @@ const PIVOT = {
19349
19368
  arg("include_total (boolean, default=TRUE)", _t("Whether to include total/sub-totals or not.")),
19350
19369
  arg("include_column_titles (boolean, default=TRUE)", _t("Whether to include the column titles or not.")),
19351
19370
  arg("column_count (number, optional)", _t("number of columns")),
19371
+ arg("include_measure_titles (boolean, default=TRUE)", _t("Whether to include the measure titles row or not.")),
19352
19372
  ],
19353
- compute: function (pivotFormulaId, rowCount = { value: 10000 }, includeTotal = { value: true }, includeColumnHeaders = { value: true }, columnCount = { value: Number.MAX_VALUE }) {
19373
+ compute: function (pivotFormulaId, rowCount = { value: 10000 }, includeTotal = { value: true }, includeColumnHeaders = { value: true }, columnCount = { value: Number.MAX_VALUE }, includeMeasureTitles = { value: true }) {
19354
19374
  const _pivotFormulaId = toString(pivotFormulaId);
19355
19375
  const _rowCount = toNumber(rowCount, this.locale);
19356
19376
  if (_rowCount < 0) {
@@ -19360,8 +19380,11 @@ const PIVOT = {
19360
19380
  if (_columnCount < 0) {
19361
19381
  return new EvaluationError(_t("The number of columns must be positive."));
19362
19382
  }
19363
- const _includeColumnHeaders = toBoolean(includeColumnHeaders);
19364
- const _includedTotal = toBoolean(includeTotal);
19383
+ const visibilityOptions = {
19384
+ displayColumnHeaders: toBoolean(includeColumnHeaders),
19385
+ displayTotals: toBoolean(includeTotal),
19386
+ displayMeasuresRow: toBoolean(includeMeasureTitles),
19387
+ };
19365
19388
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
19366
19389
  const pivot = this.getters.getPivot(pivotId);
19367
19390
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
@@ -19372,9 +19395,15 @@ const PIVOT = {
19372
19395
  return error;
19373
19396
  }
19374
19397
  const table = pivot.getCollapsedTableStructure();
19375
- const cells = table.getPivotCells(_includedTotal, _includeColumnHeaders);
19376
- const headerRows = _includeColumnHeaders ? table.columns.length : 0;
19377
- const pivotTitle = this.getters.getPivotDisplayName(pivotId);
19398
+ const cells = table.getPivotCells(visibilityOptions);
19399
+ let headerRows = 0;
19400
+ if (visibilityOptions.displayColumnHeaders) {
19401
+ headerRows = table.columns.length - 1;
19402
+ }
19403
+ if (visibilityOptions.displayMeasuresRow) {
19404
+ headerRows++;
19405
+ }
19406
+ const pivotTitle = this.getters.getPivotName(pivotId);
19378
19407
  const tableHeight = Math.min(headerRows + _rowCount, cells[0].length);
19379
19408
  if (tableHeight === 0) {
19380
19409
  return [[{ value: pivotTitle }]];
@@ -19402,7 +19431,7 @@ const PIVOT = {
19402
19431
  }
19403
19432
  }
19404
19433
  }
19405
- if (_includeColumnHeaders) {
19434
+ if (visibilityOptions.displayColumnHeaders || visibilityOptions.displayMeasuresRow) {
19406
19435
  result[0][0] = { value: pivotTitle };
19407
19436
  }
19408
19437
  return result;
@@ -20558,7 +20587,7 @@ const TEXT = {
20558
20587
  description: _t("Converts a number to text according to a specified format."),
20559
20588
  args: [
20560
20589
  arg("number (number)", _t("The number, date or time to format.")),
20561
- arg("format (string)", _t("The pattern by which to format the number, enclosed in quotation marks.")),
20590
+ arg("format (string)", _t('The case-sensitive format of the result, enclosed in quotation marks. Examples: "0.00" rounded to 2 decimal places, "hh:mm:ss" for hour:minutes:seconds.')),
20562
20591
  ],
20563
20592
  compute: function (number, format) {
20564
20593
  const _number = toNumber(number, this.locale);
@@ -21665,8 +21694,6 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21665
21694
  if (isNaN(value)) {
21666
21695
  continue;
21667
21696
  }
21668
- const axisId = chart.config.type === "radar" ? dataset.rAxisID : dataset.yAxisID;
21669
- const displayValue = options.callback(Number(value), axisId);
21670
21697
  const point = dataset.data[i];
21671
21698
  const xPosition = point.x;
21672
21699
  let yPosition = 0;
@@ -21690,7 +21717,8 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21690
21717
  textsPositions[xPosition].push(yPosition);
21691
21718
  ctx.fillStyle = point.options.backgroundColor;
21692
21719
  ctx.strokeStyle = options.background || "#ffffff";
21693
- drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
21720
+ const valueToDisplay = options.callback(Number(value), dataset, i);
21721
+ drawTextWithBackground(valueToDisplay, xPosition, yPosition, ctx);
21694
21722
  }
21695
21723
  }
21696
21724
  }
@@ -21707,7 +21735,7 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21707
21735
  if (isNaN(value)) {
21708
21736
  continue;
21709
21737
  }
21710
- const displayValue = options.callback(value, dataset.xAxisID);
21738
+ const displayValue = options.callback(value, dataset, i);
21711
21739
  const point = dataset.data[i];
21712
21740
  const yPosition = point.y;
21713
21741
  let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
@@ -21742,10 +21770,22 @@ function drawPieChartValues(chart, options, ctx) {
21742
21770
  const midAngle = (startAngle + endAngle) / 2;
21743
21771
  const midRadius = (innerRadius + outerRadius) / 2;
21744
21772
  const x = bar.x + midRadius * Math.cos(midAngle);
21745
- const y = bar.y + midRadius * Math.sin(midAngle) + 7;
21773
+ const y = bar.y + midRadius * Math.sin(midAngle);
21774
+ const displayValue = options.callback(value, dataset, i);
21775
+ const textHeight = 12; // ChartJS default
21776
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: textHeight }, "px");
21777
+ const radius = outerRadius - innerRadius;
21778
+ // Check if the text fits in the slice. Not perfect, but good enough heuristic.
21779
+ if (textWidth >= radius || radius < textHeight) {
21780
+ continue;
21781
+ }
21782
+ const sliceAngle = endAngle - startAngle;
21783
+ const midWidth = 2 * midRadius * Math.tan(sliceAngle / 2);
21784
+ if (sliceAngle < Math.PI / 2 && (textWidth >= midWidth || midWidth < textHeight)) {
21785
+ continue;
21786
+ }
21746
21787
  ctx.fillStyle = chartFontColor(options.background);
21747
21788
  ctx.strokeStyle = options.background || "#ffffff";
21748
- const displayValue = options.callback(value, "y");
21749
21789
  drawTextWithBackground(displayValue, x, y, ctx);
21750
21790
  }
21751
21791
  }
@@ -23492,7 +23532,7 @@ function isMacOS() {
23492
23532
  * On Mac, this is the "meta" or "command" key.
23493
23533
  */
23494
23534
  function isCtrlKey(ev) {
23495
- return isMacOS() ? ev.metaKey : ev.ctrlKey;
23535
+ return isMacOS() || isIOS() ? ev.metaKey : ev.ctrlKey;
23496
23536
  }
23497
23537
  /**
23498
23538
  * @param {MouseEvent} ev - The mouse event.
@@ -23531,6 +23571,23 @@ function downloadFile(dataUrl, fileName) {
23531
23571
  function isBrowserFirefox() {
23532
23572
  return /Firefox/i.test(navigator.userAgent);
23533
23573
  }
23574
+ // Mobile detection
23575
+ function maxTouchPoints() {
23576
+ return navigator.maxTouchPoints || 1;
23577
+ }
23578
+ function isAndroid() {
23579
+ return /Android/i.test(navigator.userAgent);
23580
+ }
23581
+ function isIOS() {
23582
+ return (/(iPad|iPhone|iPod)/i.test(navigator.userAgent) ||
23583
+ (navigator.platform === "MacIntel" && maxTouchPoints() > 1));
23584
+ }
23585
+ function isOtherMobileOS() {
23586
+ return /(webOS|BlackBerry|Windows Phone)/i.test(navigator.userAgent);
23587
+ }
23588
+ function isMobileOS() {
23589
+ return isAndroid() || isIOS() || isOtherMobileOS();
23590
+ }
23534
23591
 
23535
23592
  /**
23536
23593
  * Convert a JS color hexadecimal to an excel compatible color.
@@ -25051,14 +25108,14 @@ function getPieChartLegend(definition, args) {
25051
25108
  ...getLegendDisplayOptions(definition),
25052
25109
  labels: {
25053
25110
  usePointStyle: true,
25054
- generateLabels: (c) => c.data.labels?.map((label, index) => ({
25111
+ generateLabels: (c) => (c.data.labels?.map((label, index) => ({
25055
25112
  text: truncateLabel(String(label)),
25056
25113
  strokeStyle: colors[index],
25057
25114
  fillStyle: colors[index],
25058
25115
  pointStyle: "rect",
25059
25116
  lineWidth: 2,
25060
25117
  fontColor,
25061
- })) || [],
25118
+ })) || []).filter((label) => label.text),
25062
25119
  filter: (legendItem, data) => {
25063
25120
  return "datasetIndex" in legendItem
25064
25121
  ? !data.datasets[legendItem.datasetIndex].hidden
@@ -25218,7 +25275,8 @@ function getCustomLegendLabels(fontColor, legendLabelConfig) {
25218
25275
  labels: {
25219
25276
  color: fontColor,
25220
25277
  usePointStyle: true,
25221
- generateLabels: (chart) => chart.data.datasets.map((dataset, index) => {
25278
+ generateLabels: (chart) => chart.data.datasets
25279
+ .map((dataset, index) => {
25222
25280
  if (isTrendLineAxis(dataset["xAxisID"])) {
25223
25281
  return {
25224
25282
  text: truncateLabel(dataset.label),
@@ -25240,7 +25298,8 @@ function getCustomLegendLabels(fontColor, legendLabelConfig) {
25240
25298
  datasetIndex: index,
25241
25299
  ...legendLabelConfig,
25242
25300
  };
25243
- }),
25301
+ })
25302
+ .filter((label) => label.text),
25244
25303
  filter: (legendItem, data) => {
25245
25304
  return "datasetIndex" in legendItem
25246
25305
  ? !data.datasets[legendItem.datasetIndex].hidden
@@ -25594,7 +25653,10 @@ function getChartShowValues(definition, args) {
25594
25653
  horizontal: "horizontal" in definition && definition.horizontal,
25595
25654
  showValues: "showValues" in definition ? !!definition.showValues : false,
25596
25655
  background: definition.background,
25597
- callback: formatChartDatasetValue(axisFormats, locale),
25656
+ callback: (value, dataset) => {
25657
+ const axisId = getDatasetAxisId(definition, dataset);
25658
+ return formatChartDatasetValue(axisFormats, locale)(value, axisId);
25659
+ },
25598
25660
  };
25599
25661
  }
25600
25662
  function getSunburstShowValues(definition, args) {
@@ -25612,6 +25674,45 @@ function getSunburstShowValues(definition, args) {
25612
25674
  },
25613
25675
  };
25614
25676
  }
25677
+ function getPyramidChartShowValues(definition, args) {
25678
+ const { axisFormats, locale } = args;
25679
+ return {
25680
+ horizontal: true,
25681
+ showValues: "showValues" in definition ? !!definition.showValues : false,
25682
+ background: definition.background,
25683
+ callback: (value, dataset) => {
25684
+ value = Math.abs(Number(value));
25685
+ return formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25686
+ },
25687
+ };
25688
+ }
25689
+ function getWaterfallChartShowValues(definition, args) {
25690
+ const { axisFormats, locale, dataSetsValues } = args;
25691
+ const subtotalIndexes = dataSetsValues.reduce((subtotalIndexes, ds) => {
25692
+ subtotalIndexes.push((subtotalIndexes.at(-1) || -1) + ds.data.length + 1);
25693
+ return subtotalIndexes;
25694
+ }, []);
25695
+ return {
25696
+ showValues: "showValues" in definition ? !!definition.showValues : false,
25697
+ background: definition.background,
25698
+ callback: (value, dataset, index) => {
25699
+ const raw = dataset._dataset.data[index];
25700
+ const delta = raw[1] - raw[0];
25701
+ let sign = delta >= 0 ? "+" : "";
25702
+ if (definition.showSubTotals && subtotalIndexes.includes(index) && sign === "+") {
25703
+ sign = "";
25704
+ }
25705
+ return `${sign}${formatChartDatasetValue(axisFormats, locale)(delta, dataset.yAxisID)}`;
25706
+ },
25707
+ };
25708
+ }
25709
+ function getDatasetAxisId(definition, dataset) {
25710
+ if (dataset.rAxisID) {
25711
+ return dataset.rAxisID;
25712
+ }
25713
+ const axisId = "horizontal" in definition && definition.horizontal ? dataset.xAxisID : dataset.yAxisID;
25714
+ return axisId || "y";
25715
+ }
25615
25716
 
25616
25717
  function getChartTitle(definition) {
25617
25718
  const chartTitle = definition.title;
@@ -26014,6 +26115,7 @@ var CHART_RUNTIME_HELPERS = /*#__PURE__*/Object.freeze({
26014
26115
  getPieChartTooltip: getPieChartTooltip,
26015
26116
  getPyramidChartData: getPyramidChartData,
26016
26117
  getPyramidChartScales: getPyramidChartScales,
26118
+ getPyramidChartShowValues: getPyramidChartShowValues,
26017
26119
  getPyramidChartTooltip: getPyramidChartTooltip,
26018
26120
  getRadarChartData: getRadarChartData,
26019
26121
  getRadarChartDatasets: getRadarChartDatasets,
@@ -26034,6 +26136,7 @@ var CHART_RUNTIME_HELPERS = /*#__PURE__*/Object.freeze({
26034
26136
  getTrendDatasetForLineChart: getTrendDatasetForLineChart,
26035
26137
  getWaterfallChartLegend: getWaterfallChartLegend,
26036
26138
  getWaterfallChartScales: getWaterfallChartScales,
26139
+ getWaterfallChartShowValues: getWaterfallChartShowValues,
26037
26140
  getWaterfallChartTooltip: getWaterfallChartTooltip,
26038
26141
  getWaterfallDatasetAndLabels: getWaterfallDatasetAndLabels,
26039
26142
  makeDatasetsCumulative: makeDatasetsCumulative
@@ -27346,7 +27449,7 @@ function createPyramidChartRuntime(chart, getters) {
27346
27449
  title: getChartTitle(definition),
27347
27450
  legend: getBarChartLegend(definition),
27348
27451
  tooltip: getPyramidChartTooltip(definition, chartData),
27349
- chartShowValuesPlugin: getChartShowValues(definition, chartData),
27452
+ chartShowValuesPlugin: getPyramidChartShowValues(definition, chartData),
27350
27453
  },
27351
27454
  },
27352
27455
  };
@@ -28094,7 +28197,7 @@ function createWaterfallChartRuntime(chart, getters) {
28094
28197
  title: getChartTitle(definition),
28095
28198
  legend: getWaterfallChartLegend(definition),
28096
28199
  tooltip: getWaterfallChartTooltip(definition, chartData),
28097
- chartShowValuesPlugin: getChartShowValues(definition, chartData),
28200
+ chartShowValuesPlugin: getWaterfallChartShowValues(definition, chartData),
28098
28201
  waterfallLinesPlugin: { showConnectorLines: definition.showConnectorLines },
28099
28202
  },
28100
28203
  },
@@ -28886,6 +28989,7 @@ function getChartMenuActions(figureId, onFigureDeleted, env) {
28886
28989
  env.openSidePanel("ChartPanel");
28887
28990
  },
28888
28991
  icon: "o-spreadsheet-Icon.EDIT",
28992
+ isEnabled: (env) => !env.isSmall,
28889
28993
  },
28890
28994
  getCopyMenuItem(figureId, env),
28891
28995
  getCutMenuItem(figureId, env),
@@ -29100,6 +29204,147 @@ function useTimeOut() {
29100
29204
  };
29101
29205
  }
29102
29206
 
29207
+ //------------------------------------------------------------------------------
29208
+ // Context Menu Component
29209
+ //------------------------------------------------------------------------------
29210
+ css /* scss */ `
29211
+ .o-menu {
29212
+ background-color: white;
29213
+ user-select: none;
29214
+
29215
+ .o-menu-item {
29216
+ height: ${DESKTOP_MENU_ITEM_HEIGHT}px;
29217
+ padding: ${MENU_ITEM_PADDING_VERTICAL}px ${MENU_ITEM_PADDING_HORIZONTAL}px;
29218
+ cursor: pointer;
29219
+ user-select: none;
29220
+
29221
+ .o-menu-item-name {
29222
+ min-width: 40%;
29223
+ }
29224
+
29225
+ .o-menu-item-icon {
29226
+ display: inline-block;
29227
+ margin: 0px 8px 0px 0px;
29228
+ width: ${DESKTOP_MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;
29229
+ line-height: ${DESKTOP_MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;
29230
+ }
29231
+
29232
+ &:not(.disabled) {
29233
+ &:hover,
29234
+ &.o-menu-item-active {
29235
+ background-color: ${BUTTON_ACTIVE_BG};
29236
+ color: ${BUTTON_ACTIVE_TEXT_COLOR};
29237
+ }
29238
+ .o-menu-item-description {
29239
+ color: grey;
29240
+ }
29241
+ .o-menu-item-icon {
29242
+ .o-icon {
29243
+ color: ${ICONS_COLOR};
29244
+ }
29245
+ }
29246
+ }
29247
+ &.disabled {
29248
+ color: ${DISABLED_TEXT_COLOR};
29249
+ cursor: not-allowed;
29250
+ }
29251
+ }
29252
+ }
29253
+
29254
+ .o-spreadsheet-mobile {
29255
+ .o-menu-item {
29256
+ height: ${MOBILE_MENU_ITEM_HEIGHT}px;
29257
+ }
29258
+ }
29259
+ `;
29260
+ class Menu extends owl.Component {
29261
+ static template = "o-spreadsheet-Menu";
29262
+ static props = {
29263
+ menuItems: Array,
29264
+ onClose: Function,
29265
+ onClickMenu: { type: Function, optional: true },
29266
+ onMouseEnter: { type: Function, optional: true },
29267
+ onMouseOver: { type: Function, optional: true },
29268
+ onMouseLeave: { type: Function, optional: true },
29269
+ width: { type: Number, optional: true },
29270
+ isActive: { type: Function, optional: true },
29271
+ onScroll: { type: Function, optional: true },
29272
+ };
29273
+ static components = {};
29274
+ static defaultProps = {};
29275
+ hoveredMenu = undefined;
29276
+ setup() {
29277
+ owl.onWillUnmount(() => {
29278
+ this.hoveredMenu?.onStopHover?.(this.env);
29279
+ });
29280
+ }
29281
+ get menuItemsAndSeparators() {
29282
+ const menuItemsAndSeparators = [];
29283
+ for (let i = 0; i < this.props.menuItems.length; i++) {
29284
+ const menuItem = this.props.menuItems[i];
29285
+ if (menuItem.isVisible(this.env)) {
29286
+ menuItemsAndSeparators.push(menuItem);
29287
+ }
29288
+ if (menuItem.separator &&
29289
+ i !== this.props.menuItems.length - 1 && // no separator at the end
29290
+ menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator" // no double separator
29291
+ ) {
29292
+ menuItemsAndSeparators.push("separator");
29293
+ }
29294
+ }
29295
+ if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") {
29296
+ menuItemsAndSeparators.pop();
29297
+ }
29298
+ if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") {
29299
+ return [];
29300
+ }
29301
+ return menuItemsAndSeparators;
29302
+ }
29303
+ get childrenHaveIcon() {
29304
+ return this.props.menuItems.some((menuItem) => !!this.getIconName(menuItem));
29305
+ }
29306
+ getIconName(menu) {
29307
+ if (menu.icon(this.env)) {
29308
+ return menu.icon(this.env);
29309
+ }
29310
+ if (menu.isActive?.(this.env)) {
29311
+ return "o-spreadsheet-Icon.CHECK";
29312
+ }
29313
+ return "";
29314
+ }
29315
+ getColor(menu) {
29316
+ return cssPropertiesToCss({ color: menu.textColor });
29317
+ }
29318
+ getIconColor(menu) {
29319
+ return cssPropertiesToCss({ color: menu.iconColor });
29320
+ }
29321
+ getName(menu) {
29322
+ return menu.name(this.env);
29323
+ }
29324
+ isRoot(menu) {
29325
+ return !menu.execute;
29326
+ }
29327
+ isEnabled(menu) {
29328
+ if (menu.isEnabled(this.env)) {
29329
+ return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
29330
+ }
29331
+ return false;
29332
+ }
29333
+ get menuStyle() {
29334
+ return this.props.width ? cssPropertiesToCss({ width: this.props.width + "px" }) : "";
29335
+ }
29336
+ onMouseEnter(menu, ev) {
29337
+ this.hoveredMenu = menu;
29338
+ menu.onStartHover?.(this.env);
29339
+ this.props.onMouseEnter?.(menu, ev);
29340
+ }
29341
+ onMouseLeave(menu, ev) {
29342
+ this.hoveredMenu = undefined;
29343
+ menu.onStopHover?.(this.env);
29344
+ this.props.onMouseLeave?.(menu, ev);
29345
+ }
29346
+ }
29347
+
29103
29348
  /**
29104
29349
  * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap
29105
29350
  */
@@ -29128,6 +29373,9 @@ function zoneToRect(zone) {
29128
29373
  height: zone.bottom - zone.top,
29129
29374
  };
29130
29375
  }
29376
+ function isPointInsideRect(x, y, rect) {
29377
+ return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
29378
+ }
29131
29379
 
29132
29380
  /**
29133
29381
  * Return the o-spreadsheet element position relative
@@ -29425,57 +29673,17 @@ class TopRightPopoverContext extends PopoverPositionContext {
29425
29673
  }
29426
29674
 
29427
29675
  //------------------------------------------------------------------------------
29428
- // Context Menu Component
29676
+ // Context MenuPopover Component
29429
29677
  //------------------------------------------------------------------------------
29430
29678
  css /* scss */ `
29431
- .o-menu {
29432
- background-color: white;
29679
+ .o-menu-wrapper {
29433
29680
  padding: ${MENU_VERTICAL_PADDING}px 0px;
29434
- width: ${MENU_WIDTH}px;
29435
- user-select: none;
29436
-
29437
- .o-menu-item {
29438
- height: ${MENU_ITEM_HEIGHT}px;
29439
- padding: ${MENU_ITEM_PADDING_VERTICAL}px ${MENU_ITEM_PADDING_HORIZONTAL}px;
29440
- cursor: pointer;
29441
- user-select: none;
29442
-
29443
- .o-menu-item-name {
29444
- min-width: 40%;
29445
- }
29446
-
29447
- .o-menu-item-icon {
29448
- display: inline-block;
29449
- margin: 0px 8px 0px 0px;
29450
- width: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;
29451
- line-height: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;
29452
- }
29453
-
29454
- &:not(.disabled) {
29455
- &:hover,
29456
- &.o-menu-item-active {
29457
- background-color: ${BUTTON_ACTIVE_BG};
29458
- color: ${BUTTON_ACTIVE_TEXT_COLOR};
29459
- }
29460
- .o-menu-item-description {
29461
- color: grey;
29462
- }
29463
- .o-menu-item-icon {
29464
- .o-icon {
29465
- color: ${ICONS_COLOR};
29466
- }
29467
- }
29468
- }
29469
- &.disabled {
29470
- color: ${DISABLED_TEXT_COLOR};
29471
- cursor: not-allowed;
29472
- }
29473
- }
29681
+ background-color: white;
29474
29682
  }
29475
29683
  `;
29476
29684
  const TIMEOUT_DELAY = 250;
29477
- class Menu extends owl.Component {
29478
- static template = "o-spreadsheet-Menu";
29685
+ class MenuPopover extends owl.Component {
29686
+ static template = "o-spreadsheet-Menu-Popover";
29479
29687
  static props = {
29480
29688
  anchorRect: Object,
29481
29689
  popoverPositioning: { type: String, optional: true },
@@ -29488,7 +29696,7 @@ class Menu extends owl.Component {
29488
29696
  onMouseOver: { type: Function, optional: true },
29489
29697
  width: { type: Number, optional: true },
29490
29698
  };
29491
- static components = { Menu, Popover };
29699
+ static components = { MenuPopover, Menu, Popover };
29492
29700
  static defaultProps = {
29493
29701
  depth: 1,
29494
29702
  popoverPositioning: "top-right",
@@ -29515,27 +29723,18 @@ class Menu extends owl.Component {
29515
29723
  this.hoveredMenu?.onStopHover?.(this.env);
29516
29724
  });
29517
29725
  }
29518
- get menuItemsAndSeparators() {
29519
- const menuItemsAndSeparators = [];
29520
- for (let i = 0; i < this.props.menuItems.length; i++) {
29521
- const menuItem = this.props.menuItems[i];
29522
- if (menuItem.isVisible(this.env)) {
29523
- menuItemsAndSeparators.push(menuItem);
29524
- }
29525
- if (menuItem.separator &&
29526
- i !== this.props.menuItems.length - 1 && // no separator at the end
29527
- menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator" // no double separator
29528
- ) {
29529
- menuItemsAndSeparators.push("separator");
29530
- }
29531
- }
29532
- if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") {
29533
- menuItemsAndSeparators.pop();
29534
- }
29535
- if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") {
29536
- return [];
29537
- }
29538
- return menuItemsAndSeparators;
29726
+ get menuProps() {
29727
+ return {
29728
+ menuItems: this.props.menuItems,
29729
+ onClose: this.close.bind(this),
29730
+ // @ts-ignore
29731
+ onClickMenu: this.onClickMenu.bind(this),
29732
+ onMouseOver: this.onMouseOver.bind(this),
29733
+ onMouseLeave: this.onMouseLeave.bind(this),
29734
+ width: this.props.width || MENU_WIDTH,
29735
+ isActive: this.isActive.bind(this),
29736
+ onScroll: this.onScroll.bind(this),
29737
+ };
29539
29738
  }
29540
29739
  get subMenuAnchorRect() {
29541
29740
  const anchorRect = Object.assign({}, this.subMenu.anchorRect);
@@ -29549,12 +29748,13 @@ class Menu extends owl.Component {
29549
29748
  x: this.props.anchorRect.x,
29550
29749
  y: this.props.anchorRect.y,
29551
29750
  width: isRoot ? this.props.anchorRect.width : this.props.width || MENU_WIDTH,
29552
- height: isRoot ? this.props.anchorRect.height : MENU_ITEM_HEIGHT,
29751
+ height: isRoot ? this.props.anchorRect.height : DESKTOP_MENU_ITEM_HEIGHT,
29553
29752
  },
29554
29753
  positioning: this.props.popoverPositioning,
29555
29754
  verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
29556
29755
  onPopoverHidden: () => this.closeSubMenu(),
29557
29756
  onPopoverMoved: () => this.closeSubMenu(),
29757
+ maxHeight: this.props.maxHeight,
29558
29758
  };
29559
29759
  }
29560
29760
  get childrenHaveIcon() {
@@ -29624,7 +29824,7 @@ class Menu extends owl.Component {
29624
29824
  x: getRefBoundingRect(this.menuRef).x,
29625
29825
  y: y - (this.subMenu.scrollOffset || 0),
29626
29826
  width: this.props.width || MENU_WIDTH,
29627
- height: MENU_ITEM_HEIGHT,
29827
+ height: DESKTOP_MENU_ITEM_HEIGHT,
29628
29828
  };
29629
29829
  this.subMenu.menuItems = menu.children(this.env);
29630
29830
  this.subMenu.isOpen = true;
@@ -29673,14 +29873,8 @@ class Menu extends owl.Component {
29673
29873
  this.subMenu.isHoveringChild = true;
29674
29874
  this.openingTimeOut.clear();
29675
29875
  }
29676
- onMouseEnter(menu, ev) {
29677
- this.hoveredMenu = menu;
29678
- menu.onStartHover?.(this.env);
29679
- }
29680
29876
  onMouseLeave(menu) {
29681
29877
  this.openingTimeOut.schedule(this.closeSubMenu.bind(this), TIMEOUT_DELAY);
29682
- this.hoveredMenu = undefined;
29683
- menu.onStopHover?.(this.env);
29684
29878
  }
29685
29879
  get menuStyle() {
29686
29880
  return this.props.width ? cssPropertiesToCss({ width: this.props.width + "px" }) : "";
@@ -29689,7 +29883,7 @@ class Menu extends owl.Component {
29689
29883
 
29690
29884
  class ChartDashboardMenu extends owl.Component {
29691
29885
  static template = "spreadsheet.ChartDashboardMenu";
29692
- static components = { Menu };
29886
+ static components = { MenuPopover };
29693
29887
  static props = { figureUI: Object };
29694
29888
  originalChartDefinition;
29695
29889
  fullScreenFigureStore;
@@ -29953,7 +30147,7 @@ class FigureComponent extends owl.Component {
29953
30147
  onMouseDown: { type: Function, optional: true },
29954
30148
  onClickAnchor: { type: Function, optional: true },
29955
30149
  };
29956
- static components = { Menu };
30150
+ static components = { MenuPopover };
29957
30151
  static defaultProps = {
29958
30152
  onFigureDeleted: () => { },
29959
30153
  onMouseDown: () => { },
@@ -30040,7 +30234,14 @@ class FigureComponent extends owl.Component {
30040
30234
  this.props.onClickAnchor(dirX, dirY, ev);
30041
30235
  }
30042
30236
  onMouseDown(ev) {
30043
- this.props.onMouseDown(ev);
30237
+ if (!this.env.isMobile()) {
30238
+ this.props.onMouseDown(ev);
30239
+ }
30240
+ }
30241
+ onClick(ev) {
30242
+ if (this.env.isMobile()) {
30243
+ this.props.onMouseDown(ev);
30244
+ }
30044
30245
  }
30045
30246
  onKeyDown(ev) {
30046
30247
  const keyDownShortcut = keyboardEventToShortcutString(ev);
@@ -31507,15 +31708,15 @@ class HighlightStore extends SpreadsheetStore {
31507
31708
  const activeSheetId = this.getters.getActiveSheetId();
31508
31709
  return this.providers
31509
31710
  .flatMap((h) => h.highlights)
31510
- .filter((h) => h.sheetId === activeSheetId)
31711
+ .filter((h) => h.range.sheetId === activeSheetId)
31511
31712
  .map((highlight) => {
31512
- const { numberOfRows, numberOfCols } = zoneToDimension(highlight.zone);
31713
+ const { numberOfRows, numberOfCols } = zoneToDimension(highlight.range.zone);
31513
31714
  const zone = numberOfRows * numberOfCols === 1
31514
- ? this.getters.expandZone(highlight.sheetId, highlight.zone)
31515
- : highlight.zone;
31715
+ ? this.getters.expandZone(highlight.range.sheetId, highlight.range.zone)
31716
+ : highlight.range.unboundedZone;
31516
31717
  return {
31517
31718
  ...highlight,
31518
- zone,
31719
+ range: this.model.getters.getRangeFromZone(highlight.range.sheetId, zone),
31519
31720
  };
31520
31721
  });
31521
31722
  }
@@ -31528,7 +31729,7 @@ class HighlightStore extends SpreadsheetStore {
31528
31729
  drawLayer(ctx, layer) {
31529
31730
  if (layer === "Highlights") {
31530
31731
  for (const highlight of this.highlights) {
31531
- const rect = this.getters.getVisibleRect(highlight.zone);
31732
+ const rect = this.getters.getVisibleRect(highlight.range.zone);
31532
31733
  drawHighlight(ctx, highlight, rect);
31533
31734
  }
31534
31735
  }
@@ -32156,12 +32357,12 @@ class AbstractComposerStore extends SpreadsheetStore {
32156
32357
  rangeColor(xc, sheetName) {
32157
32358
  const refSheet = sheetName ? this.model.getters.getSheetIdByName(sheetName) : this.sheetId;
32158
32359
  const highlight = this.highlights.find((highlight) => {
32159
- if (highlight.sheetId !== refSheet)
32360
+ if (highlight.range.sheetId !== refSheet)
32160
32361
  return false;
32161
32362
  const range = this.model.getters.getRangeFromSheetXC(refSheet, xc);
32162
32363
  let zone = range.zone;
32163
32364
  zone = getZoneArea(zone) === 1 ? this.model.getters.expandZone(refSheet, zone) : zone;
32164
- return isEqual(zone, highlight.zone);
32365
+ return isEqual(zone, highlight.range.zone);
32165
32366
  });
32166
32367
  return highlight && highlight.color ? highlight.color : undefined;
32167
32368
  }
@@ -32271,11 +32472,10 @@ class AbstractComposerStore extends SpreadsheetStore {
32271
32472
  const { numberOfRows, numberOfCols } = zoneToDimension(range.zone);
32272
32473
  const zone = numberOfRows * numberOfCols === 1
32273
32474
  ? this.getters.expandZone(range.sheetId, range.zone)
32274
- : range.zone;
32475
+ : range.unboundedZone;
32275
32476
  return {
32276
- zone,
32477
+ range: this.model.getters.getRangeFromZone(range.sheetId, zone),
32277
32478
  color: rangeColor(rangeString),
32278
- sheetId: range.sheetId,
32279
32479
  interactive: true,
32280
32480
  };
32281
32481
  });
@@ -32528,11 +32728,15 @@ class Composer extends owl.Component {
32528
32728
  onInputContextMenu: { type: Function, optional: true },
32529
32729
  composerStore: Object,
32530
32730
  placeholder: { type: String, optional: true },
32731
+ inputMode: { type: String, optional: true },
32732
+ showAssistant: { type: Boolean, optional: true },
32531
32733
  };
32532
32734
  static components = { TextValueProvider, FunctionDescriptionProvider, SpeechBubble };
32533
32735
  static defaultProps = {
32534
32736
  inputStyle: "",
32535
32737
  isDefaultFocus: false,
32738
+ inputMode: "text",
32739
+ showAssistant: true,
32536
32740
  };
32537
32741
  DOMFocusableElementStore;
32538
32742
  composerRef = owl.useRef("o_composer");
@@ -32583,8 +32787,7 @@ class Composer extends owl.Component {
32583
32787
  assistantStyle["max-height"] = `${availableSpaceAbove - CLOSE_ICON_RADIUS}px`;
32584
32788
  // render top
32585
32789
  // We compensate 2 px of margin on the assistant style + 1px for design reasons
32586
- assistantStyle.top = `-3px`;
32587
- assistantStyle.transform = `translate(0, -100%)`;
32790
+ assistantStyle.transform = `translate(0, calc(-100% - ${cellHeight + 3}px))`;
32588
32791
  }
32589
32792
  if (cellX + ASSISTANT_WIDTH > this.props.delimitation.width) {
32590
32793
  // render left
@@ -32863,6 +33066,9 @@ class Composer extends owl.Component {
32863
33066
  // not main button, probably a context menu
32864
33067
  return;
32865
33068
  }
33069
+ if (this.env.isMobile() && !isIOS()) {
33070
+ return;
33071
+ }
32866
33072
  this.contentHelper.removeSelection();
32867
33073
  }
32868
33074
  onMouseup() {
@@ -33486,7 +33692,7 @@ class ListCriterionForm extends CriterionForm {
33486
33692
  */
33487
33693
  function startDnd(onPointerMove, onPointerUp) {
33488
33694
  const removeListeners = () => {
33489
- window.removeEventListener("pointerup", _onPointerUp);
33695
+ window.removeEventListener("pointerup", _onPointerUp, { capture: true });
33490
33696
  window.removeEventListener("dragstart", _onDragStart);
33491
33697
  window.removeEventListener("pointermove", onPointerMove);
33492
33698
  window.removeEventListener("wheel", onPointerMove);
@@ -33498,7 +33704,7 @@ function startDnd(onPointerMove, onPointerUp) {
33498
33704
  function _onDragStart(ev) {
33499
33705
  ev.preventDefault();
33500
33706
  }
33501
- window.addEventListener("pointerup", _onPointerUp);
33707
+ window.addEventListener("pointerup", _onPointerUp, { capture: true });
33502
33708
  window.addEventListener("dragstart", _onDragStart);
33503
33709
  window.addEventListener("pointermove", onPointerMove);
33504
33710
  // mouse wheel on window is by default a passive event.
@@ -34120,9 +34326,9 @@ class SelectionInputStore extends SpreadsheetStore {
34120
34326
  .filter((reference) => this.shouldBeHighlighted(this.inputSheetId, reference));
34121
34327
  return XCs.map((xc) => {
34122
34328
  const { sheetName } = splitReference(xc);
34329
+ const sheetId = (sheetName && this.getters.getSheetIdByName(sheetName)) || this.inputSheetId;
34123
34330
  return {
34124
- zone: this.getters.getRangeFromSheetXC(this.inputSheetId, xc).zone,
34125
- sheetId: (sheetName && this.getters.getSheetIdByName(sheetName)) || this.inputSheetId,
34331
+ range: this.getters.getRangeFromSheetXC(sheetId, xc),
34126
34332
  color,
34127
34333
  interactive: true,
34128
34334
  };
@@ -34591,7 +34797,7 @@ function getCriterionMenuItems(callback, availableTypes) {
34591
34797
  return createActions(actionSpecs);
34592
34798
  }
34593
34799
 
34594
- /** This component looks like a select input, but on click it opens a Menu with the items given as props instead of a dropdown */
34800
+ /** This component looks like a select input, but on click it opens a MenuPopover with the items given as props instead of a dropdown */
34595
34801
  class SelectMenu extends owl.Component {
34596
34802
  static template = "o-spreadsheet-SelectMenu";
34597
34803
  static props = {
@@ -34599,7 +34805,7 @@ class SelectMenu extends owl.Component {
34599
34805
  selectedValue: String,
34600
34806
  class: { type: String, optional: true },
34601
34807
  };
34602
- static components = { Menu };
34808
+ static components = { MenuPopover };
34603
34809
  menuId = new UuidGenerator().uuidv4();
34604
34810
  selectRef = owl.useRef("select");
34605
34811
  state = owl.useState({
@@ -34772,17 +34978,22 @@ class FilterMenuValueList extends owl.Component {
34772
34978
  static components = { FilterMenuValueItem };
34773
34979
  state = owl.useState({
34774
34980
  values: [],
34981
+ displayedValues: [],
34775
34982
  textFilter: "",
34776
34983
  selectedValue: undefined,
34984
+ numberOfDisplayedValues: 50,
34985
+ hasMoreValues: false,
34777
34986
  });
34778
34987
  searchBar = owl.useRef("filterMenuSearchBar");
34779
34988
  setup() {
34780
34989
  owl.onWillUpdateProps((nextProps) => {
34781
34990
  if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
34782
34991
  this.state.values = this.getFilterHiddenValues(nextProps.filterPosition);
34992
+ this.computeDisplayedValues();
34783
34993
  }
34784
34994
  });
34785
34995
  this.state.values = this.getFilterHiddenValues(this.props.filterPosition);
34996
+ this.computeDisplayedValues();
34786
34997
  }
34787
34998
  getFilterHiddenValues(position) {
34788
34999
  const sheetId = this.env.model.getters.getActiveSheetId();
@@ -34800,21 +35011,28 @@ class FilterMenuValueList extends owl.Component {
34800
35011
  }
34801
35012
  const cellValues = cells.map((val) => val.cellValue);
34802
35013
  const filterValues = filterValue?.filterType === "values" ? filterValue.hiddenValues : [];
34803
- const strValues = [...cellValues, ...filterValues];
34804
- const normalizedFilteredValues = filterValues.map(toLowerCase);
34805
- // Set with lowercase values to avoid duplicates
34806
- const normalizedValues = [...new Set(strValues.map(toLowerCase))];
34807
- const sortedValues = normalizedValues.sort((val1, val2) => val1.localeCompare(val2, undefined, { numeric: true, sensitivity: "base" }));
34808
- return sortedValues.map((normalizedValue) => {
34809
- let checked = false;
34810
- if (filterValue?.filterType !== "criterion") {
34811
- checked = normalizedFilteredValues.findIndex((val) => val === normalizedValue) === -1;
35014
+ const normalizedFilteredValues = new Set(filterValues.map(toLowerCase));
35015
+ const set = new Set();
35016
+ const values = [];
35017
+ const addValue = (value) => {
35018
+ const normalizedValue = toLowerCase(value);
35019
+ if (!set.has(normalizedValue)) {
35020
+ values.push({
35021
+ string: value || "",
35022
+ checked: filterValue?.filterType !== "criterion"
35023
+ ? !normalizedFilteredValues.has(normalizedValue)
35024
+ : false,
35025
+ normalizedValue,
35026
+ });
35027
+ set.add(normalizedValue);
34812
35028
  }
34813
- return {
34814
- checked,
34815
- string: strValues.find((val) => toLowerCase(val) === normalizedValue) || "",
34816
- };
34817
- });
35029
+ };
35030
+ cellValues.forEach(addValue);
35031
+ filterValues.forEach(addValue);
35032
+ return values.sort((val1, val2) => val1.normalizedValue.localeCompare(val2.normalizedValue, undefined, {
35033
+ numeric: true,
35034
+ sensitivity: "base",
35035
+ }));
34818
35036
  }
34819
35037
  checkValue(value) {
34820
35038
  this.state.selectedValue = value.string;
@@ -34826,25 +35044,37 @@ class FilterMenuValueList extends owl.Component {
34826
35044
  this.state.selectedValue = value.string;
34827
35045
  }
34828
35046
  selectAll() {
34829
- this.displayedValues.forEach((value) => (value.checked = true));
34830
- this.updateHiddenValues();
35047
+ this.state.displayedValues.forEach((value) => (value.checked = true));
35048
+ this.props.onUpdateHiddenValues([]);
34831
35049
  }
34832
35050
  clearAll() {
34833
- this.displayedValues.forEach((value) => (value.checked = false));
34834
- this.updateHiddenValues();
35051
+ this.state.displayedValues.forEach((value) => (value.checked = false));
35052
+ const hiddenValues = this.state.values.map((val) => val.string);
35053
+ this.props.onUpdateHiddenValues(hiddenValues);
34835
35054
  }
34836
35055
  updateHiddenValues() {
34837
35056
  const hiddenValues = this.state.values.filter((val) => !val.checked).map((val) => val.string);
34838
35057
  this.props.onUpdateHiddenValues(hiddenValues);
34839
35058
  }
34840
- get displayedValues() {
34841
- if (!this.state.textFilter) {
34842
- return this.state.values;
34843
- }
34844
- return fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
35059
+ updateSearch(ev) {
35060
+ const target = ev.target;
35061
+ this.state.textFilter = target.value;
35062
+ this.state.selectedValue = undefined;
35063
+ this.computeDisplayedValues();
35064
+ }
35065
+ computeDisplayedValues() {
35066
+ const values = !this.state.textFilter
35067
+ ? this.state.values
35068
+ : fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
35069
+ this.state.displayedValues = values.slice(0, this.state.numberOfDisplayedValues);
35070
+ this.state.hasMoreValues = values.length > this.state.numberOfDisplayedValues;
35071
+ }
35072
+ loadMoreValues() {
35073
+ this.state.numberOfDisplayedValues += 100;
35074
+ this.computeDisplayedValues();
34845
35075
  }
34846
35076
  onKeyDown(ev) {
34847
- const displayedValues = this.displayedValues;
35077
+ const displayedValues = this.state.displayedValues;
34848
35078
  if (displayedValues.length === 0)
34849
35079
  return;
34850
35080
  let selectedIndex = undefined;
@@ -35347,7 +35577,7 @@ class MenuItemRegistry extends Registry {
35347
35577
  }
35348
35578
 
35349
35579
  //------------------------------------------------------------------------------
35350
- // Link Menu Registry
35580
+ // Link MenuPopover Registry
35351
35581
  //------------------------------------------------------------------------------
35352
35582
  const linkMenuRegistry = new MenuItemRegistry();
35353
35583
  linkMenuRegistry.add("sheet", {
@@ -35409,7 +35639,7 @@ class LinkEditor extends owl.Component {
35409
35639
  cellPosition: Object,
35410
35640
  onClosed: { type: Function, optional: true },
35411
35641
  };
35412
- static components = { Menu };
35642
+ static components = { MenuPopover };
35413
35643
  menuItems = linkMenuRegistry.getMenuItems();
35414
35644
  link = owl.useState(this.defaultState);
35415
35645
  menu = owl.useState({
@@ -35866,58 +36096,149 @@ css /* scss */ `
35866
36096
  const ARROW_DOWN = {
35867
36097
  width: 448,
35868
36098
  height: 512,
35869
- fillColor: "#E06666",
35870
- path: "M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z",
36099
+ paths: [
36100
+ {
36101
+ fillColor: "#E06666",
36102
+ path: "M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z",
36103
+ },
36104
+ ],
35871
36105
  };
35872
36106
  const ARROW_UP = {
35873
36107
  width: 448,
35874
36108
  height: 512,
35875
- fillColor: "#6AA84F",
35876
- path: "M34.9 289.5l-22.2-22.2c-9.4-9.4-9.4-24.6 0-33.9L207 39c9.4-9.4 24.6-9.4 33.9 0l194.3 194.3c9.4 9.4 9.4 24.6 0 33.9L413 289.4c-9.5 9.5-25 9.3-34.3-.4L264 168.6V456c0 13.3-10.7 24-24 24h-32c-13.3 0-24-10.7-24-24V168.6L69.2 289.1c-9.3 9.8-24.8 10-34.3.4z",
36109
+ paths: [
36110
+ {
36111
+ fillColor: "#6AA84F",
36112
+ path: "M34.9 289.5l-22.2-22.2c-9.4-9.4-9.4-24.6 0-33.9L207 39c9.4-9.4 24.6-9.4 33.9 0l194.3 194.3c9.4 9.4 9.4 24.6 0 33.9L413 289.4c-9.5 9.5-25 9.3-34.3-.4L264 168.6V456c0 13.3-10.7 24-24 24h-32c-13.3 0-24-10.7-24-24V168.6L69.2 289.1c-9.3 9.8-24.8 10-34.3.4z",
36113
+ },
36114
+ ],
35877
36115
  };
35878
36116
  const ARROW_RIGHT = {
35879
36117
  width: 448,
35880
36118
  height: 512,
35881
- fillColor: "#F0AD4E",
35882
- path: "M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z",
36119
+ paths: [
36120
+ {
36121
+ fillColor: "#F0AD4E",
36122
+ path: "M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z",
36123
+ },
36124
+ ],
35883
36125
  };
35884
36126
  const SMILE = {
35885
36127
  width: 496,
35886
36128
  height: 512,
35887
- fillColor: "#6AA84F",
35888
- path: "M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z",
36129
+ paths: [
36130
+ {
36131
+ fillColor: "#6AA84F",
36132
+ path: "M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z",
36133
+ },
36134
+ ],
35889
36135
  };
35890
36136
  const MEH = {
35891
36137
  width: 496,
35892
36138
  height: 512,
35893
- fillColor: "#F0AD4E",
35894
- path: "M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm8 144H160c-13.2 0-24 10.8-24 24s10.8 24 24 24h176c13.2 0 24-10.8 24-24s-10.8-24-24-24z",
36139
+ paths: [
36140
+ {
36141
+ fillColor: "#F0AD4E",
36142
+ path: "M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm8 144H160c-13.2 0-24 10.8-24 24s10.8 24 24 24h176c13.2 0 24-10.8 24-24s-10.8-24-24-24z",
36143
+ },
36144
+ ],
35895
36145
  };
35896
36146
  const FROWN = {
35897
36147
  width: 496,
35898
36148
  height: 512,
35899
- fillColor: "#E06666",
35900
- path: "M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm-80 128c-40.2 0-78 17.7-103.8 48.6-8.5 10.2-7.1 25.3 3.1 33.8 10.2 8.4 25.3 7.1 33.8-3.1 16.6-19.9 41-31.4 66.9-31.4s50.3 11.4 66.9 31.4c8.1 9.7 23.1 11.9 33.8 3.1 10.2-8.5 11.5-23.6 3.1-33.8C326 321.7 288.2 304 248 304z",
36149
+ paths: [
36150
+ {
36151
+ fillColor: "#E06666",
36152
+ path: "M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm-80 128c-40.2 0-78 17.7-103.8 48.6-8.5 10.2-7.1 25.3 3.1 33.8 10.2 8.4 25.3 7.1 33.8-3.1 16.6-19.9 41-31.4 66.9-31.4s50.3 11.4 66.9 31.4c8.1 9.7 23.1 11.9 33.8 3.1 10.2-8.5 11.5-23.6 3.1-33.8C326 321.7 288.2 304 248 304z",
36153
+ },
36154
+ ],
35901
36155
  };
35902
36156
  const DOT_PATH = "M256 9 a247 247 0 1 0.1 0 0";
35903
36157
  const GREEN_DOT = {
35904
36158
  width: 512,
35905
36159
  height: 512,
35906
- fillColor: "#6AA84F",
35907
- path: DOT_PATH,
36160
+ paths: [{ fillColor: "#6AA84F", path: DOT_PATH }],
35908
36161
  };
35909
36162
  const YELLOW_DOT = {
35910
36163
  width: 512,
35911
36164
  height: 512,
35912
- fillColor: "#F0AD4E",
35913
- path: DOT_PATH,
36165
+ paths: [{ fillColor: "#F0AD4E", path: DOT_PATH }],
35914
36166
  };
35915
36167
  const RED_DOT = {
35916
36168
  width: 512,
35917
36169
  height: 512,
35918
- fillColor: "#E06666",
35919
- path: DOT_PATH,
36170
+ paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36171
+ };
36172
+ const CARET_DOWN = {
36173
+ width: 512,
36174
+ height: 512,
36175
+ paths: [{ fillColor: TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
36176
+ };
36177
+ const HOVERED_CARET_DOWN = {
36178
+ width: 512,
36179
+ height: 512,
36180
+ paths: [
36181
+ { fillColor: TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
36182
+ { fillColor: "#fff", path: "M120 195 h270 l-135 130" },
36183
+ ],
36184
+ };
36185
+ const CHECKBOX_UNCHECKED = {
36186
+ width: 512,
36187
+ height: 512,
36188
+ paths: [{ fillColor: GRAY_300, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36189
+ };
36190
+ const CHECKBOX_UNCHECKED_HOVERED = {
36191
+ width: 512,
36192
+ height: 512,
36193
+ paths: [{ fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36194
+ };
36195
+ const CHECKBOX_CHECKED = {
36196
+ width: 512,
36197
+ height: 512,
36198
+ paths: [
36199
+ { fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422" },
36200
+ { fillColor: "#FFF", path: "M165,240 l45,45 l135,-135 h60 l-195,195 l-105,-105" },
36201
+ ],
35920
36202
  };
36203
+ function getPivotIconSvg(isCollapsed, isHovered) {
36204
+ const symbolPath = isCollapsed
36205
+ ? "M149,235 h213 v43 h-213 M235,149 h43 v213 h-43" // +
36206
+ : "M149,235 h213 v43 h-213"; // -
36207
+ return {
36208
+ width: 512,
36209
+ height: 512,
36210
+ paths: [
36211
+ { path: "M21,21 h469 v469 h-469", fillColor: isHovered ? GRAY_900 : "#777" }, // borders
36212
+ { path: "M64,64 v384 h384 v-384", fillColor: isHovered ? GRAY_200 : "#eee" }, // background
36213
+ { path: symbolPath, fillColor: isHovered ? GRAY_900 : "#777" },
36214
+ ],
36215
+ };
36216
+ }
36217
+ function getDataFilterIcon(isActive, isHighContrast, isHovered) {
36218
+ const symbolPath = isActive
36219
+ ? "M18.6 3.5H4.29c-.7 0-1.06.85-.56 1.35l6.1 6.1v6.8c0 .26.13.5.34.65l2.64 1.85a.79.79 0 0 0 1.25-.65v-8.64l6.1-6.1a.79.79 0 0 0-.56-1.35"
36220
+ : "M 339.667 681 L 510.333 681 L 510.333 595.667 L 339.667 595.667 L 339.667 681 Z M 41 169 L 41 254.333 L 809 254.333 L 809 169 L 41 169 Z M 169 467.667 L 681 467.667 L 681 382.333 L 169 382.333 L 169 467.667 Z";
36221
+ const hoverBackgroundPath = isActive ? "M0,0 h24 v24 h-24" : "M0,0 h850 v850 h-850";
36222
+ const colors = { iconColor: FILTERS_COLOR, hoverBackgroundColor: FILTERS_COLOR };
36223
+ if (isHovered && !isHighContrast) {
36224
+ colors.iconColor = "#fff";
36225
+ }
36226
+ else if (!isHovered && isHighContrast) {
36227
+ colors.iconColor = "#defade";
36228
+ }
36229
+ else if (isHovered && isHighContrast) {
36230
+ colors.iconColor = FILTERS_COLOR;
36231
+ colors.hoverBackgroundColor = "#fff";
36232
+ }
36233
+ return {
36234
+ width: isActive ? 24 : 850,
36235
+ height: isActive ? 24 : 850,
36236
+ paths: [
36237
+ isHovered ? { path: hoverBackgroundPath, fillColor: colors.hoverBackgroundColor } : undefined,
36238
+ { path: symbolPath, fillColor: colors.iconColor },
36239
+ ].filter(isDefined),
36240
+ };
36241
+ }
35921
36242
  const ICONS = {
35922
36243
  arrowGood: {
35923
36244
  template: "ARROW_UP",
@@ -35973,6 +36294,15 @@ const ICON_SETS = {
35973
36294
  bad: "dotBad",
35974
36295
  },
35975
36296
  };
36297
+ const path2DCache = {};
36298
+ function getPath2D(svgPath) {
36299
+ if (path2DCache[svgPath]) {
36300
+ return path2DCache[svgPath];
36301
+ }
36302
+ const path2D = new Path2D(svgPath);
36303
+ path2DCache[svgPath] = path2D;
36304
+ return path2D;
36305
+ }
35976
36306
 
35977
36307
  /**
35978
36308
  * Map of the different types of conversions warnings and their name in error messages
@@ -38217,7 +38547,7 @@ class XlsxBaseExtractor {
38217
38547
  */
38218
38548
  handleMissingValue(parentElement, missingElementName, optionalArgs) {
38219
38549
  if (optionalArgs?.required) {
38220
- if (optionalArgs?.default) {
38550
+ if (optionalArgs?.default !== undefined) {
38221
38551
  this.warningManager.addParsingWarning(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, replacing it by the default value ${optionalArgs.default}`);
38222
38552
  }
38223
38553
  else {
@@ -41944,6 +42274,7 @@ const findAndReplace = {
41944
42274
  execute: (env) => {
41945
42275
  env.openSidePanel("FindAndReplace", {});
41946
42276
  },
42277
+ isEnabled: (env) => !env.isSmall,
41947
42278
  icon: "o-spreadsheet-Icon.SEARCH",
41948
42279
  };
41949
42280
  const deleteValues = {
@@ -42200,11 +42531,13 @@ const insertCellShiftRight = {
42200
42531
  const insertChart = {
42201
42532
  name: _t("Chart"),
42202
42533
  execute: CREATE_CHART,
42534
+ isEnabled: (env) => !env.isSmall,
42203
42535
  icon: "o-spreadsheet-Icon.INSERT_CHART",
42204
42536
  };
42205
42537
  const insertPivot = {
42206
42538
  name: _t("Pivot table"),
42207
42539
  execute: CREATE_PIVOT,
42540
+ isEnabled: (env) => !env.isSmall,
42208
42541
  icon: "o-spreadsheet-Icon.PIVOT",
42209
42542
  };
42210
42543
  const insertImage = {
@@ -42212,12 +42545,14 @@ const insertImage = {
42212
42545
  description: "Ctrl+O",
42213
42546
  execute: CREATE_IMAGE,
42214
42547
  isVisible: (env) => env.imageProvider !== undefined,
42548
+ isEnabled: (env) => !env.isSmall,
42215
42549
  icon: "o-spreadsheet-Icon.INSERT_IMAGE",
42216
42550
  };
42217
42551
  const insertTable = {
42218
42552
  name: () => _t("Table"),
42219
42553
  execute: INSERT_TABLE,
42220
42554
  isVisible: (env) => IS_SELECTION_CONTINUOUS(env) && !env.model.getters.getFirstTableInSelection(),
42555
+ isEnabled: (env) => !env.isSmall,
42221
42556
  icon: "o-spreadsheet-Icon.PAINT_TABLE",
42222
42557
  };
42223
42558
  const insertFunction = {
@@ -42323,6 +42658,7 @@ const insertDropdown = {
42323
42658
  },
42324
42659
  });
42325
42660
  },
42661
+ isEnabled: (env) => !env.isSmall,
42326
42662
  icon: "o-spreadsheet-Icon.INSERT_DROPDOWN",
42327
42663
  };
42328
42664
  const insertSheet = {
@@ -42592,7 +42928,7 @@ const pivotProperties = {
42592
42928
  isVisible: (env) => {
42593
42929
  const position = env.model.getters.getActivePosition();
42594
42930
  const pivotId = env.model.getters.getPivotIdFromPosition(position);
42595
- return (pivotId && env.model.getters.isExistingPivot(pivotId)) || false;
42931
+ return (!env.isSmall && pivotId && env.model.getters.isExistingPivot(pivotId)) || false;
42596
42932
  },
42597
42933
  isReadonlyAllowed: true,
42598
42934
  icon: "o-spreadsheet-Icon.PIVOT",
@@ -42710,7 +43046,7 @@ function isPivotSortMenuItemActive(env, order) {
42710
43046
  }
42711
43047
 
42712
43048
  //------------------------------------------------------------------------------
42713
- // Context Menu Registry
43049
+ // Context MenuPopover Registry
42714
43050
  //------------------------------------------------------------------------------
42715
43051
  const cellMenuRegistry = new MenuItemRegistry();
42716
43052
  cellMenuRegistry
@@ -42793,6 +43129,7 @@ cellMenuRegistry
42793
43129
  .add("edit_table", {
42794
43130
  ...editTable,
42795
43131
  isVisible: SELECTION_CONTAINS_SINGLE_TABLE,
43132
+ isEnabled: (env) => !env.isSmall,
42796
43133
  sequence: 140,
42797
43134
  })
42798
43135
  .add("delete_table", {
@@ -42861,6 +43198,7 @@ const removeDuplicates = {
42861
43198
  }
42862
43199
  env.openSidePanel("RemoveDuplicates", {});
42863
43200
  },
43201
+ isEnabled: (env) => !env.isSmall,
42864
43202
  };
42865
43203
  const trimWhitespace = {
42866
43204
  name: _t("Trim whitespace"),
@@ -42888,7 +43226,7 @@ const splitToColumns = {
42888
43226
  name: _t("Split text to columns"),
42889
43227
  sequence: 1,
42890
43228
  execute: (env) => env.openSidePanel("SplitToColumns", {}),
42891
- isEnabled: (env) => env.model.getters.isSingleColSelected(),
43229
+ isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
42892
43230
  icon: "o-spreadsheet-Icon.SPLIT_TEXT",
42893
43231
  };
42894
43232
  const reinsertDynamicPivotMenu = {
@@ -42975,7 +43313,7 @@ const formatNumberAccounting = createFormatActionSpec({
42975
43313
  const EXAMPLE_DATE = parseLiteral("2023/09/26 10:43:00 PM", DEFAULT_LOCALE);
42976
43314
  const formatCustomCurrency = {
42977
43315
  name: _t("Custom currency"),
42978
- isVisible: (env) => env.loadCurrencies !== undefined,
43316
+ isVisible: (env) => env.loadCurrencies !== undefined && !env.isSmall,
42979
43317
  execute: (env) => env.openSidePanel("CustomCurrency", {}),
42980
43318
  };
42981
43319
  const formatNumberDate = createFormatActionSpec({
@@ -43198,6 +43536,7 @@ const formatWrappingClip = {
43198
43536
  const formatCF = {
43199
43537
  name: _t("Conditional formatting"),
43200
43538
  execute: OPEN_CF_SIDEPANEL_ACTION,
43539
+ isEnabled: (env) => !env.isSmall,
43201
43540
  icon: "o-spreadsheet-Icon.CONDITIONAL_FORMAT",
43202
43541
  };
43203
43542
  const clearFormat = {
@@ -43581,8 +43920,7 @@ class ArrayFormulaHighlight extends SpreadsheetStore {
43581
43920
  }
43582
43921
  return [
43583
43922
  {
43584
- sheetId: position.sheetId,
43585
- zone,
43923
+ range: this.model.getters.getRangeFromZone(position.sheetId, zone),
43586
43924
  dashed: cell.value === CellErrorType.SpilledBlocked,
43587
43925
  color: "#17A2B8",
43588
43926
  noFill: true,
@@ -43667,6 +44005,7 @@ function useDragAndDropBeyondTheViewport(env) {
43667
44005
  let previousEvClientPosition;
43668
44006
  let startingX;
43669
44007
  let startingY;
44008
+ let scrollDirection = "all";
43670
44009
  const getters = env.model.getters;
43671
44010
  let cleanUpFns = [];
43672
44011
  const cleanUp = () => {
@@ -43692,55 +44031,59 @@ function useDragAndDropBeyondTheViewport(env) {
43692
44031
  let timeoutDelay = MAX_DELAY;
43693
44032
  const x = currentEv.clientX - position.left;
43694
44033
  let colIndex = getters.getColIndex(x);
43695
- const previousX = previousEvClientPosition.clientX - position.left;
43696
- const edgeScrollInfoX = getters.getEdgeScrollCol(x, previousX, startingX);
43697
- if (edgeScrollInfoX.canEdgeScroll) {
43698
- canEdgeScroll = true;
43699
- timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoX.delay);
43700
- let newTarget = colIndex;
43701
- switch (edgeScrollInfoX.direction) {
43702
- case "reset":
43703
- colIndex = newTarget = xSplit;
43704
- break;
43705
- case 1:
43706
- colIndex = right;
43707
- newTarget = left + 1;
43708
- break;
43709
- case -1:
43710
- colIndex = left - 1;
43711
- while (env.model.getters.isColHidden(sheetId, colIndex)) {
43712
- colIndex--;
43713
- }
43714
- newTarget = colIndex;
43715
- break;
44034
+ if (scrollDirection !== "vertical") {
44035
+ const previousX = previousEvClientPosition.clientX - position.left;
44036
+ const edgeScrollInfoX = getters.getEdgeScrollCol(x, previousX, startingX);
44037
+ if (edgeScrollInfoX.canEdgeScroll) {
44038
+ canEdgeScroll = true;
44039
+ timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoX.delay);
44040
+ let newTarget = colIndex;
44041
+ switch (edgeScrollInfoX.direction) {
44042
+ case "reset":
44043
+ colIndex = newTarget = xSplit;
44044
+ break;
44045
+ case 1:
44046
+ colIndex = right;
44047
+ newTarget = left + 1;
44048
+ break;
44049
+ case -1:
44050
+ colIndex = left - 1;
44051
+ while (env.model.getters.isColHidden(sheetId, colIndex)) {
44052
+ colIndex--;
44053
+ }
44054
+ newTarget = colIndex;
44055
+ break;
44056
+ }
44057
+ scrollX = getters.getColDimensions(sheetId, newTarget).start - offsetCorrectionX;
43716
44058
  }
43717
- scrollX = getters.getColDimensions(sheetId, newTarget).start - offsetCorrectionX;
43718
44059
  }
43719
44060
  const y = currentEv.clientY - position.top;
43720
44061
  let rowIndex = getters.getRowIndex(y);
43721
- const previousY = previousEvClientPosition.clientY - position.top;
43722
- const edgeScrollInfoY = getters.getEdgeScrollRow(y, previousY, startingY);
43723
- if (edgeScrollInfoY.canEdgeScroll) {
43724
- canEdgeScroll = true;
43725
- timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoY.delay);
43726
- let newTarget = rowIndex;
43727
- switch (edgeScrollInfoY.direction) {
43728
- case "reset":
43729
- rowIndex = newTarget = ySplit;
43730
- break;
43731
- case 1:
43732
- rowIndex = bottom;
43733
- newTarget = top + 1;
43734
- break;
43735
- case -1:
43736
- rowIndex = top - 1;
43737
- while (env.model.getters.isRowHidden(sheetId, rowIndex)) {
43738
- rowIndex--;
43739
- }
43740
- newTarget = rowIndex;
43741
- break;
44062
+ if (scrollDirection !== "horizontal") {
44063
+ const previousY = previousEvClientPosition.clientY - position.top;
44064
+ const edgeScrollInfoY = getters.getEdgeScrollRow(y, previousY, startingY);
44065
+ if (edgeScrollInfoY.canEdgeScroll) {
44066
+ canEdgeScroll = true;
44067
+ timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoY.delay);
44068
+ let newTarget = rowIndex;
44069
+ switch (edgeScrollInfoY.direction) {
44070
+ case "reset":
44071
+ rowIndex = newTarget = ySplit;
44072
+ break;
44073
+ case 1:
44074
+ rowIndex = bottom;
44075
+ newTarget = top + 1;
44076
+ break;
44077
+ case -1:
44078
+ rowIndex = top - 1;
44079
+ while (env.model.getters.isRowHidden(sheetId, rowIndex)) {
44080
+ rowIndex--;
44081
+ }
44082
+ newTarget = rowIndex;
44083
+ break;
44084
+ }
44085
+ scrollY = env.model.getters.getRowDimensions(sheetId, newTarget).start - offsetCorrectionY;
43742
44086
  }
43743
- scrollY = env.model.getters.getRowDimensions(sheetId, newTarget).start - offsetCorrectionY;
43744
44087
  }
43745
44088
  if (!canEdgeScroll) {
43746
44089
  colIndex = adjustIndexWithinBounds(colIndex, x, getters.getNumberCols(sheetId) - 1);
@@ -43760,9 +44103,10 @@ function useDragAndDropBeyondTheViewport(env) {
43760
44103
  pointerUpCallback?.();
43761
44104
  cleanUp();
43762
44105
  };
43763
- const startFn = (initialPointerCoordinates, onPointerMove, onPointerUp) => {
44106
+ const startFn = (initialPointerCoordinates, onPointerMove, onPointerUp, startScrollDirection = "all") => {
43764
44107
  cleanUp();
43765
44108
  const position = gridOverlayPosition();
44109
+ scrollDirection = startScrollDirection;
43766
44110
  startingX = initialPointerCoordinates.clientX - position.left;
43767
44111
  startingY = initialPointerCoordinates.clientY - position.top;
43768
44112
  previousEvClientPosition = {
@@ -44240,7 +44584,7 @@ class GridComposer extends owl.Component {
44240
44584
  });
44241
44585
  }
44242
44586
  get shouldDisplayCellReference() {
44243
- return this.isCellReferenceVisible;
44587
+ return !this.env.isMobile() && this.isCellReferenceVisible;
44244
44588
  }
44245
44589
  get cellReference() {
44246
44590
  const { col, row, sheetId } = this.composerStore.currentEditedCell;
@@ -44281,11 +44625,12 @@ class GridComposer extends owl.Component {
44281
44625
  onInputContextMenu: this.props.onInputContextMenu,
44282
44626
  composerStore: this.composerStore,
44283
44627
  inputStyle: `max-height: ${maxHeight}px;`,
44628
+ inputMode: this.composerStore.editionMode === "inactive" ? "none" : undefined,
44284
44629
  };
44285
44630
  }
44286
44631
  get containerStyle() {
44287
- if (this.composerStore.editionMode === "inactive") {
44288
- return `z-index: -1000;`;
44632
+ if (this.composerStore.editionMode === "inactive" || this.env.isMobile()) {
44633
+ return `z-index: -1000; opacity: 0;`; // opacity 0 for safari on ios
44289
44634
  }
44290
44635
  const _isFormula = isFormula(this.composerStore.currentContent);
44291
44636
  const cell = this.env.model.getters.getActiveCell();
@@ -44314,11 +44659,13 @@ class GridComposer extends owl.Component {
44314
44659
  *
44315
44660
  * The +-1 are there to include cell borders in the composer sizing/positioning
44316
44661
  */
44662
+ const minHeight = Math.min(height + 1, maxHeight);
44663
+ const minWidth = Math.min(width + 1, maxWidth);
44317
44664
  return cssPropertiesToCss({
44318
44665
  left: `${left - 1}px`,
44319
44666
  top: `${top}px`,
44320
- "min-width": `${width + 1}px`,
44321
- "min-height": `${height + 1}px`,
44667
+ "min-width": `${minWidth}px`,
44668
+ "min-height": `${minHeight}px`,
44322
44669
  "max-width": `${maxWidth}px`,
44323
44670
  "max-height": `${maxHeight}px`,
44324
44671
  background,
@@ -44815,6 +45162,9 @@ class FiguresContainer extends owl.Component {
44815
45162
  if (!selectResult.isSuccessful) {
44816
45163
  return;
44817
45164
  }
45165
+ if (this.env.isMobile()) {
45166
+ return;
45167
+ }
44818
45168
  const sheetId = this.env.model.getters.getActiveSheetId();
44819
45169
  const initialMousePosition = { x: ev.clientX, y: ev.clientY };
44820
45170
  const initialScrollPosition = this.env.model.getters.getActiveSheetScrollInfo();
@@ -45107,78 +45457,6 @@ class GridAddRowsFooter extends owl.Component {
45107
45457
  }
45108
45458
  }
45109
45459
 
45110
- class GridCellIcon extends owl.Component {
45111
- static template = "o-spreadsheet-GridCellIcon";
45112
- static props = {
45113
- icon: Object,
45114
- verticalAlign: { type: String, optional: true },
45115
- slots: Object,
45116
- };
45117
- get iconStyle() {
45118
- const cellPosition = this.props.icon.position;
45119
- const merge = this.env.model.getters.getMerge(cellPosition);
45120
- const zone = merge || positionToZone(cellPosition);
45121
- const rect = this.env.model.getters.getVisibleRectWithoutHeaders(zone);
45122
- const x = this.getIconHorizontalPosition(rect, cellPosition);
45123
- const y = this.getIconVerticalPosition(rect, cellPosition);
45124
- return cssPropertiesToCss({
45125
- top: `${y}px`,
45126
- left: `${x}px`,
45127
- width: `${this.props.icon.size}px`,
45128
- height: `${this.props.icon.size}px`,
45129
- });
45130
- }
45131
- getIconVerticalPosition(rect, cellPosition) {
45132
- const start = rect.y;
45133
- const end = rect.y + rect.height;
45134
- const cell = this.env.model.getters.getCell(cellPosition);
45135
- const align = this.props.verticalAlign || cell?.style?.verticalAlign || DEFAULT_VERTICAL_ALIGN;
45136
- switch (align) {
45137
- case "bottom":
45138
- return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
45139
- case "top":
45140
- return start + GRID_ICON_MARGIN;
45141
- default:
45142
- const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
45143
- return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
45144
- }
45145
- }
45146
- getIconHorizontalPosition(rect, cellPosition) {
45147
- const start = rect.x;
45148
- const end = rect.x + rect.width;
45149
- const cell = this.env.model.getters.getCell(cellPosition);
45150
- const evaluatedCell = this.env.model.getters.getEvaluatedCell(cellPosition);
45151
- const align = this.props.icon.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
45152
- switch (align) {
45153
- case "right":
45154
- return end - this.props.icon.size - this.props.icon.margin;
45155
- case "left":
45156
- return start + this.props.icon.margin;
45157
- default:
45158
- const centeringOffset = Math.floor((end - start - this.props.icon.size) / 2);
45159
- return end - this.props.icon.size - centeringOffset;
45160
- }
45161
- }
45162
- isPositionVisible(position) {
45163
- const rect = this.env.model.getters.getVisibleRect(positionToZone(position));
45164
- return !(rect.width === 0 || rect.height === 0);
45165
- }
45166
- }
45167
-
45168
- class GridCellIconOverlay extends owl.Component {
45169
- static template = "o-spreadsheet-GridCellIconOverlay";
45170
- static props = {};
45171
- static components = { GridCellIcon };
45172
- get icons() {
45173
- const icons = [];
45174
- for (const position of this.env.model.getters.getVisibleCellPositions()) {
45175
- const cellIcons = this.env.model.getters.getCellIcons(position);
45176
- icons.push(...cellIcons.filter((icon) => icon.component));
45177
- }
45178
- return icons;
45179
- }
45180
- }
45181
-
45182
45460
  /**
45183
45461
  * Manages an event listener on a ref. Useful for hooks that want to manage
45184
45462
  * event listeners, especially more than one. Prefer using t-on directly in
@@ -45267,7 +45545,7 @@ class PaintFormatStore extends SpreadsheetStore {
45267
45545
  return [];
45268
45546
  }
45269
45547
  return data.zones.map((zone) => ({
45270
- zone,
45548
+ range: this.model.getters.getRangeFromZone(data.sheetId, zone),
45271
45549
  color: SELECTION_BORDER_COLOR,
45272
45550
  dashed: true,
45273
45551
  sheetId: data.sheetId,
@@ -45290,7 +45568,7 @@ class HoveredTableStore extends SpreadsheetStore {
45290
45568
  }
45291
45569
  }
45292
45570
  hover(position) {
45293
- if (position.col === this.col && position.row === this.row) {
45571
+ if (!this.getters.isDashboard() || (position.col === this.col && position.row === this.row)) {
45294
45572
  return "noStateChange";
45295
45573
  }
45296
45574
  this.col = position.col;
@@ -45324,6 +45602,14 @@ class HoveredTableStore extends SpreadsheetStore {
45324
45602
  }
45325
45603
  }
45326
45604
 
45605
+ class HoveredIconStore extends SpreadsheetStore {
45606
+ mutators = ["setHoveredIcon"];
45607
+ hoveredIcon = undefined;
45608
+ setHoveredIcon(icon) {
45609
+ this.hoveredIcon = icon;
45610
+ }
45611
+ }
45612
+
45327
45613
  const CURSOR_SVG = /*xml*/ `
45328
45614
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="16"><path d="M6.5.4c1.3-.8 2.9-.1 3.8 1.4l2.9 5.1c.2.4.9 1.6-.4 2.3l-1.6.9 1.8 3.1c.2.4.1 1-.2 1.2l-1.6 1c-.3.1-.9 0-1.1-.4l-1.8-3.1-1.6 1c-.6.4-1.7 0-2.2-.8L0 4.3"/><path fill="#fff" d="M9.1 2a1.4 1.1 60 0 0-1.7-.6L5.5 2.5l.9 1.6-1 .6-.9-1.6-.6.4 1.8 3.1-1.3.7-1.8-3.1-1 .6 3.8 6.6 6.8-3.98M3.9 8.8 10.82 5l.795 1.4-6.81 3.96"/></svg>
45329
45615
  `;
@@ -45395,10 +45681,11 @@ function useCellHovered(env, gridRef) {
45395
45681
  return pause();
45396
45682
  }
45397
45683
  }
45398
- useRefListener(gridRef, "pointermove", updateMousePosition);
45684
+ useRefListener(gridRef, "pointermove", (ev) => !env.isMobile() && updateMousePosition(ev));
45399
45685
  useRefListener(gridRef, "mouseleave", onMouseLeave);
45400
45686
  useRefListener(gridRef, "mouseenter", resume);
45401
45687
  useRefListener(gridRef, "pointerdown", recompute);
45688
+ useRefListener(gridRef, "pointerdown", (ev) => env.isMobile() && updateMousePosition(ev));
45402
45689
  owl.useExternalListener(window, "click", handleGlobalClick);
45403
45690
  function handleGlobalClick(e) {
45404
45691
  const target = e.target;
@@ -45427,11 +45714,11 @@ class GridOverlay extends owl.Component {
45427
45714
  onGridMoved: Function,
45428
45715
  gridOverlayDimensions: String,
45429
45716
  slots: { type: Object, optional: true },
45717
+ getGridSize: Function,
45430
45718
  };
45431
45719
  static components = {
45432
45720
  FiguresContainer,
45433
45721
  GridAddRowsFooter,
45434
- GridCellIconOverlay,
45435
45722
  };
45436
45723
  static defaultProps = {
45437
45724
  onCellDoubleClicked: () => { },
@@ -45443,15 +45730,17 @@ class GridOverlay extends owl.Component {
45443
45730
  gridOverlay = owl.useRef("gridOverlay");
45444
45731
  cellPopovers;
45445
45732
  paintFormatStore;
45733
+ hoveredIconStore;
45446
45734
  setup() {
45447
45735
  useCellHovered(this.env, this.gridOverlay);
45448
45736
  const resizeObserver = new ResizeObserver(() => {
45449
45737
  const boundingRect = this.gridOverlayEl.getBoundingClientRect();
45738
+ const { width, height } = this.props.getGridSize();
45450
45739
  this.props.onGridResized({
45451
45740
  x: boundingRect.left,
45452
45741
  y: boundingRect.top,
45453
- height: this.gridOverlayEl.clientHeight,
45454
- width: this.gridOverlayEl.clientWidth,
45742
+ height: height,
45743
+ width: width,
45455
45744
  });
45456
45745
  });
45457
45746
  owl.onMounted(() => {
@@ -45462,6 +45751,7 @@ class GridOverlay extends owl.Component {
45462
45751
  });
45463
45752
  this.cellPopovers = useStore(CellPopoverStore);
45464
45753
  this.paintFormatStore = useStore(PaintFormatStore);
45754
+ this.hoveredIconStore = useStore(HoveredIconStore);
45465
45755
  }
45466
45756
  get gridOverlayEl() {
45467
45757
  if (!this.gridOverlay.el) {
@@ -45470,26 +45760,59 @@ class GridOverlay extends owl.Component {
45470
45760
  return this.gridOverlay.el;
45471
45761
  }
45472
45762
  get style() {
45473
- return this.props.gridOverlayDimensions;
45763
+ return (this.props.gridOverlayDimensions +
45764
+ cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "default" }));
45474
45765
  }
45475
45766
  get isPaintingFormat() {
45476
45767
  return this.paintFormatStore.isActive;
45477
45768
  }
45478
- onMouseDown(ev) {
45479
- if (ev.button > 0) {
45769
+ onPointerMove(ev) {
45770
+ if (this.env.isMobile()) {
45771
+ return;
45772
+ }
45773
+ const icon = this.getInteractiveIconAtEvent(ev);
45774
+ const hoveredIcon = icon?.type ? { id: icon.type, position: icon.position } : undefined;
45775
+ if (!deepEquals(hoveredIcon, this.hoveredIconStore.hoveredIcon)) {
45776
+ this.hoveredIconStore.setHoveredIcon(hoveredIcon);
45777
+ }
45778
+ }
45779
+ onPointerDown(ev) {
45780
+ if (ev.button > 0 || this.env.isMobile()) {
45480
45781
  // not main button, probably a context menu
45481
45782
  return;
45482
45783
  }
45483
- if (ev.target === this.gridOverlay.el && this.cellPopovers.isOpen) {
45484
- this.cellPopovers.close();
45784
+ this.onCellClicked(ev);
45785
+ }
45786
+ onClick(ev) {
45787
+ if (ev.button > 0 || !this.env.isMobile()) {
45788
+ // not main button, probably a context menu
45789
+ return;
45485
45790
  }
45791
+ this.onCellClicked(ev);
45792
+ }
45793
+ onCellClicked(ev) {
45794
+ const openedPopover = this.cellPopovers.persistentCellPopover;
45486
45795
  const [col, row] = this.getCartesianCoordinates(ev);
45487
45796
  this.props.onCellClicked(col, row, {
45488
45797
  expandZone: ev.shiftKey,
45489
45798
  addZone: isCtrlKey(ev),
45490
45799
  }, ev);
45800
+ const clickedIcon = this.getInteractiveIconAtEvent(ev);
45801
+ if (clickedIcon?.onClick) {
45802
+ clickedIcon.onClick(clickedIcon.position, this.env);
45803
+ }
45804
+ if (ev.target === this.gridOverlay.el &&
45805
+ this.cellPopovers.isOpen &&
45806
+ deepEquals(openedPopover, this.cellPopovers.persistentCellPopover)) {
45807
+ // Only close the popover if props.click/icon.click didn't open a new one
45808
+ this.cellPopovers.close();
45809
+ return;
45810
+ }
45491
45811
  }
45492
45812
  onDoubleClick(ev) {
45813
+ if (this.getInteractiveIconAtEvent(ev)) {
45814
+ return;
45815
+ }
45493
45816
  const [col, row] = this.getCartesianCoordinates(ev);
45494
45817
  this.props.onCellDoubleClicked(col, row);
45495
45818
  }
@@ -45505,6 +45828,24 @@ class GridOverlay extends owl.Component {
45505
45828
  const rowIndex = this.env.model.getters.getRowIndex(y);
45506
45829
  return [colIndex, rowIndex];
45507
45830
  }
45831
+ getInteractiveIconAtEvent(ev) {
45832
+ const gridOverLayRect = getRefBoundingRect(this.gridOverlay);
45833
+ const gridOffset = this.env.model.getters.getGridOffset();
45834
+ const x = ev.clientX - gridOverLayRect.x + gridOffset.x;
45835
+ const y = ev.clientY - gridOverLayRect.y + gridOffset.y;
45836
+ const [col, row] = this.getCartesianCoordinates(ev);
45837
+ const sheetId = this.env.model.getters.getActiveSheetId();
45838
+ let position = { col, row, sheetId };
45839
+ const merge = this.env.model.getters.getMerge(position);
45840
+ if (merge) {
45841
+ position = { col: merge.left, row: merge.top, sheetId };
45842
+ }
45843
+ const icons = this.env.model.getters.getCellIcons(position);
45844
+ const icon = icons.find((icon) => {
45845
+ return isPointInsideRect(x, y, this.env.model.getters.getCellIconRect(icon));
45846
+ });
45847
+ return icon?.onClick ? icon : undefined;
45848
+ }
45508
45849
  }
45509
45850
 
45510
45851
  class GridPopover extends owl.Component {
@@ -45665,6 +46006,9 @@ class AbstractResizer extends owl.Component {
45665
46006
  this.state.waitingForMove = false;
45666
46007
  }
45667
46008
  onMouseMove(ev) {
46009
+ if (this.env.isMobile()) {
46010
+ return;
46011
+ }
45668
46012
  if (this.state.isResizing || this.state.isMoving || this.state.isSelecting) {
45669
46013
  return;
45670
46014
  }
@@ -45709,7 +46053,21 @@ class AbstractResizer extends owl.Component {
45709
46053
  };
45710
46054
  startDnd(onMouseMove, onMouseUp);
45711
46055
  }
46056
+ onClick(ev) {
46057
+ if (!this.env.isMobile()) {
46058
+ return;
46059
+ }
46060
+ if (ev.button > 0) {
46061
+ // not main button, probably a context menu
46062
+ return;
46063
+ }
46064
+ const index = this._getElementIndex(this._getEvOffset(ev));
46065
+ this._selectElement(index, false);
46066
+ }
45712
46067
  select(ev) {
46068
+ if (this.env.isMobile()) {
46069
+ return;
46070
+ }
45713
46071
  if (ev.button > 0) {
45714
46072
  // not main button, probably a context menu
45715
46073
  return;
@@ -45778,6 +46136,9 @@ class AbstractResizer extends owl.Component {
45778
46136
  this.dragNDropGrid.start(ev, mouseMoveMovement, mouseUpMovement);
45779
46137
  }
45780
46138
  startSelection(ev, index) {
46139
+ if (this.env.isMobile()) {
46140
+ return;
46141
+ }
45781
46142
  this.state.isSelecting = true;
45782
46143
  if (ev.shiftKey) {
45783
46144
  this._increaseSelection(index);
@@ -46223,11 +46584,13 @@ class GridRenderer {
46223
46584
  renderer;
46224
46585
  fingerprints;
46225
46586
  hoveredTables;
46587
+ hoveredIcon;
46226
46588
  constructor(get) {
46227
46589
  this.getters = get(ModelStore).getters;
46228
46590
  this.renderer = get(RendererStore);
46229
46591
  this.fingerprints = get(FormulaFingerprintStore);
46230
46592
  this.hoveredTables = get(HoveredTableStore);
46593
+ this.hoveredIcon = get(HoveredIconStore);
46231
46594
  this.renderer.register(this);
46232
46595
  }
46233
46596
  get renderingLayers() {
@@ -46464,7 +46827,7 @@ class GridRenderer {
46464
46827
  // compute vertical align start point parameter:
46465
46828
  const textLineHeight = computeTextFontSizeInPixels(style);
46466
46829
  const numberOfLines = box.content.textLines.length;
46467
- let y = this.computeTextYCoordinate(box, textLineHeight, numberOfLines);
46830
+ let y = this.getters.computeTextYCoordinate(box, textLineHeight, style.verticalAlign, numberOfLines);
46468
46831
  // use the horizontal and the vertical start points to:
46469
46832
  // fill text / fill strikethrough / fill underline
46470
46833
  for (const brokenLine of box.content.textLines) {
@@ -46481,7 +46844,12 @@ class GridRenderer {
46481
46844
  const { ctx } = renderingContext;
46482
46845
  for (const box of boxes) {
46483
46846
  for (const icon of Object.values(box.icons)) {
46484
- if (!icon || !icon.svg) {
46847
+ if (!icon) {
46848
+ continue;
46849
+ }
46850
+ const isHovered = deepEquals({ id: icon.type, position: icon.position }, this.hoveredIcon.hoveredIcon);
46851
+ const svg = isHovered ? icon.hoverSvg || icon.svg : icon.svg;
46852
+ if (!svg) {
46485
46853
  continue;
46486
46854
  }
46487
46855
  ctx.save();
@@ -46489,46 +46857,17 @@ class GridRenderer {
46489
46857
  ctx.rect(box.x, box.y, box.width, box.height);
46490
46858
  ctx.clip();
46491
46859
  const iconSize = icon.size;
46492
- const iconY = this.computeTextYCoordinate(box, iconSize);
46493
- const svg = icon.svg;
46494
- let x;
46495
- if (icon.horizontalAlign === "left") {
46496
- x = box.x + icon.margin;
46497
- }
46498
- else if (icon.horizontalAlign === "right") {
46499
- x = box.x + box.width - iconSize - icon.margin;
46500
- }
46501
- else {
46502
- x = box.x + (box.width - iconSize) / 2;
46503
- }
46504
- ctx.translate(x, iconY);
46860
+ const { x, y } = this.getters.getCellIconRect(icon);
46861
+ ctx.translate(x, y);
46505
46862
  ctx.scale(iconSize / svg.width, iconSize / svg.height);
46506
- ctx.fillStyle = svg.fillColor;
46507
- ctx.fill(new Path2D(svg.path));
46863
+ for (const path of svg.paths) {
46864
+ ctx.fillStyle = path.fillColor;
46865
+ ctx.fill(getPath2D(path.path));
46866
+ }
46508
46867
  ctx.restore();
46509
46868
  }
46510
46869
  }
46511
46870
  }
46512
- /** Computes the vertical start point from which a text line should be draw.
46513
- *
46514
- * Note that in case the cell does not have enough spaces to display its text lines,
46515
- * (wrapping cell case) then the vertical align should be at the top.
46516
- * */
46517
- computeTextYCoordinate(box, textLineHeight, numberOfLines = 1) {
46518
- const y = box.y + 1;
46519
- const textHeight = computeTextLinesHeight(textLineHeight, numberOfLines);
46520
- const hasEnoughSpaces = box.height > textHeight + MIN_CELL_TEXT_MARGIN * 2;
46521
- const verticalAlign = box.verticalAlign || DEFAULT_VERTICAL_ALIGN;
46522
- if (hasEnoughSpaces) {
46523
- if (verticalAlign === "middle") {
46524
- return y + (box.height - textHeight) / 2;
46525
- }
46526
- if (verticalAlign === "bottom") {
46527
- return y + box.height - textHeight - MIN_CELL_TEXT_MARGIN;
46528
- }
46529
- }
46530
- return y + MIN_CELL_TEXT_MARGIN;
46531
- }
46532
46871
  drawHeaders(renderingContext) {
46533
46872
  const { ctx, thinLineWidth } = renderingContext;
46534
46873
  const visibleCols = this.getters.getSheetViewVisibleCols();
@@ -46965,6 +47304,9 @@ function useTouchScroll(ref, updateScroll, canMoveUp) {
46965
47304
  const deltaX = lastX - clientX;
46966
47305
  const deltaY = lastY - clientY;
46967
47306
  const elapsedTime = currentTime - lastTime;
47307
+ if (!elapsedTime) {
47308
+ return;
47309
+ }
46968
47310
  velocityX = deltaX / elapsedTime;
46969
47311
  velocityY = deltaY / elapsedTime;
46970
47312
  lastX = clientX;
@@ -46985,6 +47327,11 @@ function useTouchScroll(ref, updateScroll, canMoveUp) {
46985
47327
  function onTouchEnd(ev) {
46986
47328
  isMouseDown = false;
46987
47329
  lastX = lastY = 0;
47330
+ if (resetTimeout) {
47331
+ clearTimeout(resetTimeout);
47332
+ }
47333
+ velocityX *= 1.2;
47334
+ velocityY *= 1.2;
46988
47335
  requestAnimationFrame(scroll);
46989
47336
  }
46990
47337
  function scroll() {
@@ -47069,12 +47416,16 @@ class Border extends owl.Component {
47069
47416
  }
47070
47417
  }
47071
47418
 
47419
+ const MOBILE_HANDLER_WIDTH = 40;
47072
47420
  css /* scss */ `
47073
47421
  .o-corner {
47074
47422
  position: absolute;
47075
- height: 8px;
47076
- width: 8px;
47423
+ }
47424
+
47425
+ .o-corner-button {
47077
47426
  border: 1px solid white;
47427
+ height: ${AUTOFILL_EDGE_LENGTH}px;
47428
+ width: ${AUTOFILL_EDGE_LENGTH}px;
47078
47429
  }
47079
47430
  .o-corner-nw,
47080
47431
  .o-corner-se {
@@ -47101,34 +47452,61 @@ class Corner extends owl.Component {
47101
47452
  isResizing: Boolean,
47102
47453
  onResizeHighlight: Function,
47103
47454
  };
47104
- isTop = this.props.orientation[0] === "n";
47105
- isLeft = this.props.orientation[1] === "w";
47106
- get style() {
47455
+ dirX;
47456
+ dirY;
47457
+ setup() {
47458
+ const { dirX, dirY } = orientationToDir(this.props.orientation);
47459
+ this.dirX = dirX;
47460
+ this.dirY = dirY;
47461
+ }
47462
+ get handlerStyle() {
47107
47463
  const z = this.props.zone;
47108
- const col = this.isLeft ? z.left : z.right;
47109
- const row = this.isTop ? z.top : z.bottom;
47110
47464
  const rect = this.env.model.getters.getVisibleRect({
47111
- left: col,
47112
- right: col,
47113
- top: row,
47114
- bottom: row,
47465
+ left: this.dirX === 1 ? z.right : z.left,
47466
+ right: this.dirX === -1 ? z.left : z.right,
47467
+ top: this.dirY === 1 ? z.bottom : z.top,
47468
+ bottom: this.dirY === -1 ? z.top : z.bottom,
47115
47469
  });
47116
47470
  // Don't show if not visible in the viewport
47117
47471
  if (rect.width * rect.height === 0) {
47118
- return `display:none`;
47472
+ return `display: none !important;`;
47473
+ }
47474
+ const leftValue = rect.x + rect.width / 2 + (this.dirX * rect.width) / 2;
47475
+ const topValue = rect.y + rect.height / 2 + (this.dirY * rect.height) / 2;
47476
+ const edgeLength = this.getHandlerEdgeLength();
47477
+ const css = {
47478
+ left: `${leftValue - edgeLength / 2}px`,
47479
+ top: `${topValue - edgeLength / 2}px`,
47480
+ height: `${edgeLength}px`,
47481
+ width: `${edgeLength}px`,
47482
+ };
47483
+ if (this.env.isMobile()) {
47484
+ css["border-radius"] = `${edgeLength / 2}px`;
47119
47485
  }
47120
- const leftValue = this.isLeft ? rect.x : rect.x + rect.width;
47121
- const topValue = this.isTop ? rect.y : rect.y + rect.height;
47122
- return cssPropertiesToCss({
47123
- left: `${leftValue - AUTOFILL_EDGE_LENGTH / 2}px`,
47124
- top: `${topValue - AUTOFILL_EDGE_LENGTH / 2}px`,
47486
+ return cssPropertiesToCss(css);
47487
+ }
47488
+ getHandlerEdgeLength() {
47489
+ return this.env.isMobile() ? MOBILE_HANDLER_WIDTH : AUTOFILL_EDGE_LENGTH;
47490
+ }
47491
+ get buttonLook() {
47492
+ const css = {
47125
47493
  "background-color": this.props.color,
47126
- });
47494
+ cursor: `${this.props.orientation}-resize`,
47495
+ };
47496
+ if (this.env.isMobile()) {
47497
+ css["border-radius"] = `${AUTOFILL_EDGE_LENGTH / 2}px`;
47498
+ }
47499
+ return cssPropertiesToCss(css);
47127
47500
  }
47128
47501
  onMouseDown(ev) {
47129
- this.props.onResizeHighlight(ev, this.isLeft, this.isTop);
47502
+ this.props.onResizeHighlight(ev, this.dirX, this.dirY);
47130
47503
  }
47131
47504
  }
47505
+ function orientationToDir(or) {
47506
+ const dirX = or.includes("w") ? -1 : or.includes("e") ? 1 : 0;
47507
+ const dirY = or.includes("n") ? -1 : or.includes("s") ? 1 : 0;
47508
+ return { dirX, dirY };
47509
+ }
47132
47510
 
47133
47511
  css /*SCSS*/ `
47134
47512
  .o-highlight {
@@ -47138,7 +47516,7 @@ css /*SCSS*/ `
47138
47516
  class Highlight extends owl.Component {
47139
47517
  static template = "o-spreadsheet-Highlight";
47140
47518
  static props = {
47141
- zone: Object,
47519
+ range: Object,
47142
47520
  color: String,
47143
47521
  };
47144
47522
  static components = {
@@ -47149,26 +47527,49 @@ class Highlight extends owl.Component {
47149
47527
  shiftingMode: "none",
47150
47528
  });
47151
47529
  dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
47152
- onResizeHighlight(ev, isLeft, isTop) {
47530
+ get cornerOrientations() {
47531
+ if (!this.env.isMobile()) {
47532
+ return ["nw", "ne", "sw", "se"];
47533
+ }
47534
+ const z = this.props.range.unboundedZone;
47535
+ if (z.bottom === undefined) {
47536
+ return ["w", "e"];
47537
+ }
47538
+ else if (z.right === undefined) {
47539
+ return ["n", "s"];
47540
+ }
47541
+ else {
47542
+ return ["nw", "se"];
47543
+ }
47544
+ }
47545
+ onResizeHighlight(ev, dirX, dirY) {
47153
47546
  const activeSheetId = this.env.model.getters.getActiveSheetId();
47154
47547
  this.highlightState.shiftingMode = "isResizing";
47155
- const z = this.props.zone;
47156
- const pivotCol = isLeft ? z.right : z.left;
47157
- const pivotRow = isTop ? z.bottom : z.top;
47158
- let lastCol = isLeft ? z.left : z.right;
47159
- let lastRow = isTop ? z.top : z.bottom;
47548
+ const z = this.props.range.zone;
47549
+ const pivotCol = dirX === 1 ? z.left : z.right;
47550
+ const pivotRow = dirY === 1 ? z.top : z.bottom;
47551
+ let lastCol = dirX === 1 ? z.right : z.left;
47552
+ let lastRow = dirY === 1 ? z.bottom : z.top;
47160
47553
  let currentZone = z;
47554
+ let scrollDirection = "all";
47555
+ if (this.env.isMobile()) {
47556
+ scrollDirection = dirX === 0 ? "vertical" : dirY === 0 ? "horizontal" : "all";
47557
+ }
47161
47558
  this.env.model.dispatch("START_CHANGE_HIGHLIGHT", { zone: currentZone });
47162
47559
  const mouseMove = (col, row) => {
47163
47560
  if (lastCol !== col || lastRow !== row) {
47164
- lastCol = clip(col === -1 ? lastCol : col, 0, this.env.model.getters.getNumberCols(activeSheetId) - 1);
47165
- lastRow = clip(row === -1 ? lastRow : row, 0, this.env.model.getters.getNumberRows(activeSheetId) - 1);
47166
- const newZone = {
47167
- left: Math.min(pivotCol, lastCol),
47168
- top: Math.min(pivotRow, lastRow),
47169
- right: Math.max(pivotCol, lastCol),
47170
- bottom: Math.max(pivotRow, lastRow),
47171
- };
47561
+ let { left, right, top, bottom } = currentZone;
47562
+ if (scrollDirection !== "horizontal") {
47563
+ lastRow = lastRow = clip(row === -1 ? lastRow : row, 0, this.env.model.getters.getNumberRows(activeSheetId) - 1);
47564
+ top = Math.min(pivotRow, lastRow);
47565
+ bottom = Math.max(pivotRow, lastRow);
47566
+ }
47567
+ if (scrollDirection !== "vertical") {
47568
+ lastCol = clip(col === -1 ? lastCol : col, 0, this.env.model.getters.getNumberCols(activeSheetId) - 1);
47569
+ left = Math.min(pivotCol, lastCol);
47570
+ right = Math.max(pivotCol, lastCol);
47571
+ }
47572
+ const newZone = { left, right, top, bottom };
47172
47573
  if (!isEqual(newZone, currentZone)) {
47173
47574
  this.env.model.selection.selectZone({
47174
47575
  cell: { col: newZone.left, row: newZone.top },
@@ -47181,11 +47582,11 @@ class Highlight extends owl.Component {
47181
47582
  const mouseUp = () => {
47182
47583
  this.highlightState.shiftingMode = "none";
47183
47584
  };
47184
- this.dragNDropGrid.start(ev, mouseMove, mouseUp);
47585
+ this.dragNDropGrid.start(ev, mouseMove, mouseUp, scrollDirection);
47185
47586
  }
47186
47587
  onMoveHighlight(ev) {
47187
47588
  this.highlightState.shiftingMode = "isMoving";
47188
- const z = this.props.zone;
47589
+ const z = this.props.range.zone;
47189
47590
  const position = gridOverlayPosition();
47190
47591
  const activeSheetId = this.env.model.getters.getActiveSheetId();
47191
47592
  const initCol = this.env.model.getters.getColIndex(ev.clientX - position.left);
@@ -47407,6 +47808,18 @@ class VerticalScrollBar extends owl.Component {
47407
47808
  }
47408
47809
  }
47409
47810
 
47811
+ class Selection extends owl.Component {
47812
+ static template = "o-spreadsheet-Selection";
47813
+ static props = {};
47814
+ static components = { Highlight };
47815
+ get highlightProps() {
47816
+ const sheetId = this.env.model.getters.getActiveSheetId();
47817
+ const zone = this.env.model.getters.getUnboundedZone(sheetId, this.env.model.getters.getSelectedZone());
47818
+ const range = this.env.model.getters.getRangeFromZone(sheetId, zone);
47819
+ return { range, color: SELECTION_BORDER_COLOR };
47820
+ }
47821
+ }
47822
+
47410
47823
  class Section extends owl.Component {
47411
47824
  static template = "o_spreadsheet.Section";
47412
47825
  static props = {
@@ -48352,11 +48765,13 @@ class ColorPickerWidget extends owl.Component {
48352
48765
 
48353
48766
  css /* scss */ `
48354
48767
  .o-font-size-editor {
48768
+ width: max-content !important;
48355
48769
  height: calc(100% - 4px);
48356
48770
  input.o-font-size {
48357
48771
  outline: none;
48358
48772
  height: 20px;
48359
- width: 23px;
48773
+ width: 31px;
48774
+ text-align: center;
48360
48775
  }
48361
48776
  }
48362
48777
  .o-text-options > div {
@@ -50500,8 +50915,7 @@ class ConditionalFormatPreview extends owl.Component {
50500
50915
  get highlights() {
50501
50916
  const sheetId = this.env.model.getters.getActiveSheetId();
50502
50917
  return this.props.conditionalFormat.ranges.map((range) => ({
50503
- sheetId,
50504
- zone: this.env.model.getters.getRangeFromSheetXC(sheetId, range).zone,
50918
+ range: this.env.model.getters.getRangeFromSheetXC(sheetId, range),
50505
50919
  color: HIGHLIGHT_COLOR,
50506
50920
  fillAlpha: 0.06,
50507
50921
  }));
@@ -51459,8 +51873,7 @@ class DataValidationPreview extends owl.Component {
51459
51873
  }
51460
51874
  get highlights() {
51461
51875
  return this.props.rule.ranges.map((range) => ({
51462
- sheetId: this.env.model.getters.getActiveSheetId(),
51463
- zone: range.zone,
51876
+ range,
51464
51877
  color: HIGHLIGHT_COLOR,
51465
51878
  fillAlpha: 0.06,
51466
51879
  }));
@@ -51789,13 +52202,15 @@ class FindAndReplaceStore extends SpreadsheetStore {
51789
52202
  if (this.selectedMatchIndex === null) {
51790
52203
  return;
51791
52204
  }
52205
+ this.preserveSelectedMatchIndex = true;
52206
+ this.shouldFinalizeUpdateSelection = true;
51792
52207
  this.model.dispatch("REPLACE_SEARCH", {
51793
52208
  searchString: this.toSearch,
51794
52209
  replaceWith: this.toReplace,
51795
52210
  matches: [this.searchMatches[this.selectedMatchIndex]],
51796
52211
  searchOptions: this.searchOptions,
51797
52212
  });
51798
- this.selectNextCell(Direction.next, { jumpToMatchSheet: true, updateSelection: true });
52213
+ this.preserveSelectedMatchIndex = false;
51799
52214
  }
51800
52215
  /**
51801
52216
  * Apply the replace function to all the matches one time.
@@ -51867,8 +52282,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
51867
52282
  const { width, height } = this.getters.getVisibleRect(zoneWithMerge);
51868
52283
  if (width > 0 && height > 0) {
51869
52284
  highlights.push({
51870
- sheetId,
51871
- zone: zoneWithMerge,
52285
+ range: this.model.getters.getRangeFromZone(sheetId, zoneWithMerge),
51872
52286
  color: FIND_AND_REPLACE_HIGHLIGHT_COLOR,
51873
52287
  noBorder: index !== this.selectedMatchIndex,
51874
52288
  thinLine: true,
@@ -51880,8 +52294,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
51880
52294
  const range = this.searchOptions.specificRange;
51881
52295
  if (range && range.sheetId === sheetId) {
51882
52296
  highlights.push({
51883
- sheetId,
51884
- zone: range.zone,
52297
+ range,
51885
52298
  color: FIND_AND_REPLACE_HIGHLIGHT_COLOR,
51886
52299
  noFill: true,
51887
52300
  thinLine: true,
@@ -52255,7 +52668,11 @@ function getPivotHighlights(getters, pivotId) {
52255
52668
  const sheetId = getters.getActiveSheetId();
52256
52669
  const pivotCellPositions = getVisiblePivotCellPositions(getters, pivotId);
52257
52670
  const mergedZones = mergeContiguousZones(pivotCellPositions.map(positionToZone));
52258
- return mergedZones.map((zone) => ({ sheetId, zone, noFill: true, color: HIGHLIGHT_COLOR }));
52671
+ return mergedZones.map((zone) => ({
52672
+ range: getters.getRangeFromZone(sheetId, zone),
52673
+ noFill: true,
52674
+ color: HIGHLIGHT_COLOR,
52675
+ }));
52259
52676
  }
52260
52677
  function getVisiblePivotCellPositions(getters, pivotId) {
52261
52678
  const positions = [];
@@ -52524,7 +52941,7 @@ class TextInput extends owl.Component {
52524
52941
 
52525
52942
  class CogWheelMenu extends owl.Component {
52526
52943
  static template = "o-spreadsheet-CogWheelMenu";
52527
- static components = { Menu };
52944
+ static components = { MenuPopover };
52528
52945
  static props = {
52529
52946
  items: Array,
52530
52947
  };
@@ -53432,28 +53849,45 @@ class SpreadsheetPivotTable {
53432
53849
  getNumberOfDataColumns() {
53433
53850
  return this.columns.at(-1)?.length || 0;
53434
53851
  }
53435
- getPivotCells(includeTotal = true, includeColumnHeaders = true) {
53436
- const key = JSON.stringify({ includeTotal, includeColumnHeaders });
53852
+ getSkippedRows(visibilityOptions) {
53853
+ const skippedRows = new Set();
53854
+ if (!visibilityOptions.displayColumnHeaders) {
53855
+ for (let i = 0; i < this.columns.length - 1; i++) {
53856
+ skippedRows.add(i);
53857
+ }
53858
+ }
53859
+ if (!visibilityOptions.displayMeasuresRow) {
53860
+ skippedRows.add(this.columns.length - 1);
53861
+ }
53862
+ return skippedRows;
53863
+ }
53864
+ getPivotCells(visibilityOptions = {
53865
+ displayColumnHeaders: true,
53866
+ displayTotals: true,
53867
+ displayMeasuresRow: true,
53868
+ }) {
53869
+ const key = JSON.stringify(visibilityOptions);
53437
53870
  if (!this.pivotCells[key]) {
53871
+ const { displayTotals } = visibilityOptions;
53438
53872
  const numberOfDataRows = this.rows.length;
53439
53873
  const numberOfDataColumns = this.getNumberOfDataColumns();
53440
53874
  let pivotHeight = this.columns.length + numberOfDataRows;
53441
53875
  let pivotWidth = 1 /*(row headers)*/ + numberOfDataColumns;
53442
- if (!includeTotal && numberOfDataRows !== 1) {
53876
+ if (!displayTotals && numberOfDataRows !== 1) {
53443
53877
  pivotHeight -= 1;
53444
53878
  }
53445
- if (!includeTotal && numberOfDataColumns !== this.measures.length) {
53879
+ if (!displayTotals && numberOfDataColumns !== this.measures.length) {
53446
53880
  pivotWidth -= this.measures.length;
53447
53881
  }
53448
53882
  const domainArray = [];
53449
- const startRow = includeColumnHeaders ? 0 : this.columns.length;
53883
+ const skippedRows = this.getSkippedRows(visibilityOptions);
53450
53884
  for (let col = 0; col < pivotWidth; col++) {
53451
53885
  domainArray.push([]);
53452
- for (let row = startRow; row < pivotHeight; row++) {
53453
- if (!includeTotal && row === pivotHeight) {
53886
+ for (let row = 0; row < pivotHeight; row++) {
53887
+ if (skippedRows.has(row)) {
53454
53888
  continue;
53455
53889
  }
53456
- domainArray[col].push(this.getPivotCell(col, row, includeTotal));
53890
+ domainArray[col].push(this.getPivotCell(col, row, displayTotals));
53457
53891
  }
53458
53892
  }
53459
53893
  this.pivotCells[key] = domainArray;
@@ -55094,6 +55528,7 @@ function createTableStyleContextMenuActions(env, styleId) {
55094
55528
  id: "editTableStyle",
55095
55529
  name: _t("Edit table style"),
55096
55530
  execute: (env) => env.openSidePanel("TableStyleEditorPanel", { styleId }),
55531
+ isEnabled: (env) => !env.isSmall,
55097
55532
  icon: "o-spreadsheet-Icon.EDIT",
55098
55533
  },
55099
55534
  {
@@ -55215,7 +55650,7 @@ css /* scss */ `
55215
55650
  `;
55216
55651
  class TableStylePreview extends owl.Component {
55217
55652
  static template = "o-spreadsheet-TableStylePreview";
55218
- static components = { Menu };
55653
+ static components = { MenuPopover };
55219
55654
  static props = {
55220
55655
  tableConfig: Object,
55221
55656
  tableStyle: Object,
@@ -55786,6 +56221,17 @@ sidePanelRegistry.add("PivotMeasureDisplayPanel", {
55786
56221
  },
55787
56222
  });
55788
56223
 
56224
+ class ScreenWidthStore {
56225
+ mutators = ["setSmallThreshhold"];
56226
+ _isSmallCallback = () => false;
56227
+ get isSmall() {
56228
+ return this._isSmallCallback();
56229
+ }
56230
+ setSmallThreshhold(isSmall) {
56231
+ this._isSmallCallback = isSmall;
56232
+ }
56233
+ }
56234
+
55789
56235
  const DEFAULT_SIDE_PANEL_SIZE = 350;
55790
56236
  const MIN_SHEET_VIEW_WIDTH = 150;
55791
56237
  class SidePanelStore extends SpreadsheetStore {
@@ -55793,6 +56239,7 @@ class SidePanelStore extends SpreadsheetStore {
55793
56239
  initialPanelProps = {};
55794
56240
  componentTag = "";
55795
56241
  panelSize = DEFAULT_SIDE_PANEL_SIZE;
56242
+ screenWidthStore = this.get(ScreenWidthStore);
55796
56243
  get isOpen() {
55797
56244
  if (!this.componentTag) {
55798
56245
  return false;
@@ -55814,6 +56261,9 @@ class SidePanelStore extends SpreadsheetStore {
55814
56261
  return undefined;
55815
56262
  }
55816
56263
  open(componentTag, panelProps = {}) {
56264
+ if (this.screenWidthStore.isSmall) {
56265
+ return;
56266
+ }
55817
56267
  const state = this.computeState(componentTag, panelProps);
55818
56268
  if (!state.isOpen) {
55819
56269
  return;
@@ -55928,8 +56378,7 @@ class TableResizer extends owl.Component {
55928
56378
  return [];
55929
56379
  return [
55930
56380
  {
55931
- zone: this.state.highlightZone,
55932
- sheetId: this.props.table.range.sheetId,
56381
+ range: this.env.model.getters.getRangeFromZone(this.props.table.range.sheetId, this.state.highlightZone),
55933
56382
  color: COLOR,
55934
56383
  noFill: true,
55935
56384
  },
@@ -55951,13 +56400,14 @@ class Grid extends owl.Component {
55951
56400
  static template = "o-spreadsheet-Grid";
55952
56401
  static props = {
55953
56402
  exposeFocus: Function,
56403
+ getGridSize: Function,
55954
56404
  };
55955
56405
  static components = {
55956
56406
  GridComposer,
55957
56407
  GridOverlay,
55958
56408
  GridPopover,
55959
56409
  HeadersOverlay,
55960
- Menu,
56410
+ MenuPopover,
55961
56411
  Autofill,
55962
56412
  ClientTag,
55963
56413
  Highlight,
@@ -55965,6 +56415,7 @@ class Grid extends owl.Component {
55965
56415
  VerticalScrollBar,
55966
56416
  HorizontalScrollBar,
55967
56417
  TableResizer,
56418
+ Selection,
55968
56419
  };
55969
56420
  HEADER_HEIGHT = HEADER_HEIGHT;
55970
56421
  HEADER_WIDTH = HEADER_WIDTH;
@@ -56247,8 +56698,8 @@ class Grid extends owl.Component {
56247
56698
  }
56248
56699
  onGridResized({ height, width }) {
56249
56700
  this.env.model.dispatch("RESIZE_SHEETVIEW", {
56250
- width: width,
56251
- height: height,
56701
+ width: width - HEADER_WIDTH,
56702
+ height: height - HEADER_HEIGHT,
56252
56703
  gridOffsetX: HEADER_WIDTH,
56253
56704
  gridOffsetY: HEADER_HEIGHT,
56254
56705
  });
@@ -56302,6 +56753,9 @@ class Grid extends owl.Component {
56302
56753
  else {
56303
56754
  this.env.model.selection.selectCell(col, row);
56304
56755
  }
56756
+ if (this.env.isMobile()) {
56757
+ return;
56758
+ }
56305
56759
  let prevCol = col;
56306
56760
  let prevRow = row;
56307
56761
  const onMouseMove = (col, row, ev) => {
@@ -56587,6 +57041,9 @@ class Grid extends owl.Component {
56587
57041
  const sheetId = this.env.model.getters.getActiveSheetId();
56588
57042
  return this.env.model.getters.getCoreTables(sheetId).filter(isStaticTable);
56589
57043
  }
57044
+ get displaySelectionHandler() {
57045
+ return this.env.isMobile() && this.composerFocusStore.activeComposer.editionMode === "inactive";
57046
+ }
56590
57047
  }
56591
57048
 
56592
57049
  /**
@@ -61621,7 +62078,9 @@ class TablePlugin extends CorePlugin {
61621
62078
  const ranges = cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData));
61622
62079
  const union = this.getters.getRangesUnion(ranges);
61623
62080
  const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
61624
- this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
62081
+ if (mergesInTarget.length) {
62082
+ this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
62083
+ }
61625
62084
  const id = this.consumeNextId();
61626
62085
  const config = cmd.config || DEFAULT_TABLE_CONFIG;
61627
62086
  const newTable = cmd.tableType === "dynamic"
@@ -61720,14 +62179,16 @@ class TablePlugin extends CorePlugin {
61720
62179
  const zoneToCheckIfEmpty = direction === "down"
61721
62180
  ? { ...zone, bottom: zone.bottom + 1, top: zone.bottom + 1 }
61722
62181
  : { ...zone, right: zone.right + 1, left: zone.right + 1 };
61723
- for (const position of positions(zoneToCheckIfEmpty)) {
61724
- const cellPosition = { sheetId, ...position };
61725
- // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content
61726
- const cellContent = this.getters.getCell(cellPosition)?.content;
61727
- if (cellContent ||
61728
- this.getters.isInMerge(cellPosition) ||
61729
- this.getTablesOverlappingZones(sheetId, [positionToZone(position)]).length) {
61730
- return "none";
62182
+ for (let row = zoneToCheckIfEmpty.top; row <= zoneToCheckIfEmpty.bottom; row++) {
62183
+ for (let col = zoneToCheckIfEmpty.left; col <= zoneToCheckIfEmpty.right; col++) {
62184
+ const cellPosition = { sheetId, col, row };
62185
+ // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content
62186
+ const cellContent = this.getters.getCell(cellPosition)?.content;
62187
+ if (cellContent ||
62188
+ this.getters.isInMerge(cellPosition) ||
62189
+ this.getTablesOverlappingZones(sheetId, [positionToZone(cellPosition)]).length) {
62190
+ return "none";
62191
+ }
61731
62192
  }
61732
62193
  }
61733
62194
  return direction;
@@ -62513,7 +62974,7 @@ class PivotCorePlugin extends CorePlugin {
62513
62974
  break;
62514
62975
  }
62515
62976
  case "UPDATE_PIVOT": {
62516
- this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
62977
+ this.history.update("pivots", cmd.pivotId, "definition", this.repairSortedColumn(deepCopy(cmd.pivot)));
62517
62978
  this.compileCalculatedMeasures(cmd.pivot.measures);
62518
62979
  break;
62519
62980
  }
@@ -62584,7 +63045,10 @@ class PivotCorePlugin extends CorePlugin {
62584
63045
  // Private
62585
63046
  // -------------------------------------------------------------------------
62586
63047
  addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {
62587
- this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
63048
+ this.history.update("pivots", pivotId, {
63049
+ definition: this.repairSortedColumn(deepCopy(pivot)),
63050
+ formulaId,
63051
+ });
62588
63052
  this.compileCalculatedMeasures(pivot.measures);
62589
63053
  this.history.update("formulaIds", formulaId, pivotId);
62590
63054
  this.history.update("nextFormulaId", this.nextFormulaId + 1);
@@ -62673,6 +63137,7 @@ class PivotCorePlugin extends CorePlugin {
62673
63137
  }
62674
63138
  }
62675
63139
  checkSortedColumnInMeasures(definition) {
63140
+ definition = this.repairSortedColumn(definition);
62676
63141
  const measures = definition.measures.map((measure) => measure.id);
62677
63142
  if (definition.sortedColumn && !measures.includes(definition.sortedColumn.measure)) {
62678
63143
  return "InvalidDefinition" /* CommandResult.InvalidDefinition */;
@@ -62686,6 +63151,26 @@ class PivotCorePlugin extends CorePlugin {
62686
63151
  }
62687
63152
  return "Success" /* CommandResult.Success */;
62688
63153
  }
63154
+ repairSortedColumn(definition) {
63155
+ if (definition.sortedColumn) {
63156
+ // Fix for an upgrade issue: the sortedColumn measure was not updated
63157
+ // from using fieldName to using id. If the sortedColumn measure matches
63158
+ // a measure fieldName in the definition, update it to use the measure's id instead
63159
+ // of its fieldName.
63160
+ // TODO: add an upgrade step to fix this in master and remove this code
63161
+ const sortedMeasure = definition.measures.find((measure) => measure.fieldName === definition.sortedColumn?.measure);
63162
+ if (sortedMeasure) {
63163
+ return {
63164
+ ...definition,
63165
+ sortedColumn: {
63166
+ ...definition.sortedColumn,
63167
+ measure: sortedMeasure.id,
63168
+ },
63169
+ };
63170
+ }
63171
+ }
63172
+ return definition;
63173
+ }
62689
63174
  // ---------------------------------------------------------------------
62690
63175
  // Import/Export
62691
63176
  // ---------------------------------------------------------------------
@@ -65553,186 +66038,6 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65553
66038
  }
65554
66039
  }
65555
66040
 
65556
- const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
65557
- css /* scss */ `
65558
- .o-dv-checkbox {
65559
- margin: ${MARGIN}px;
65560
- /* required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
65561
- position: absolute;
65562
- }
65563
- `;
65564
- class DataValidationCheckbox extends owl.Component {
65565
- static template = "o-spreadsheet-DataValidationCheckbox";
65566
- static components = {
65567
- Checkbox,
65568
- };
65569
- static props = {
65570
- cellPosition: Object,
65571
- };
65572
- onCheckboxChange(value) {
65573
- const { sheetId, col, row } = this.props.cellPosition;
65574
- const cellContent = value ? "TRUE" : "FALSE";
65575
- this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
65576
- }
65577
- get checkBoxValue() {
65578
- return !!this.env.model.getters.getEvaluatedCell(this.props.cellPosition).value;
65579
- }
65580
- get isDisabled() {
65581
- const cell = this.env.model.getters.getCell(this.props.cellPosition);
65582
- return this.env.model.getters.isReadonly() || !!cell?.isFormula;
65583
- }
65584
- }
65585
-
65586
- const ICON_WIDTH = 13;
65587
- css /* scss */ `
65588
- .o-dv-list-icon {
65589
- color: ${TEXT_BODY_MUTED};
65590
- border-radius: 1px;
65591
- height: ${GRID_ICON_EDGE_LENGTH}px;
65592
- width: ${GRID_ICON_EDGE_LENGTH}px;
65593
-
65594
- &:hover {
65595
- color: #ffffff;
65596
- background-color: ${TEXT_BODY_MUTED};
65597
- }
65598
-
65599
- svg {
65600
- width: ${ICON_WIDTH}px;
65601
- height: ${ICON_WIDTH}px;
65602
- }
65603
- }
65604
- `;
65605
- class DataValidationListIcon extends owl.Component {
65606
- static template = "o-spreadsheet-DataValidationListIcon";
65607
- static props = {
65608
- cellPosition: Object,
65609
- };
65610
- onClick() {
65611
- const { col, row } = this.props.cellPosition;
65612
- this.env.model.selection.selectCell(col, row);
65613
- this.env.startCellEdition();
65614
- }
65615
- }
65616
-
65617
- css /* scss */ `
65618
- .o-filter-icon {
65619
- color: ${FILTERS_COLOR};
65620
- display: flex;
65621
- align-items: center;
65622
- justify-content: center;
65623
- width: ${GRID_ICON_EDGE_LENGTH}px;
65624
- height: ${GRID_ICON_EDGE_LENGTH}px;
65625
-
65626
- &:hover {
65627
- background: ${FILTERS_COLOR};
65628
- color: #fff;
65629
- }
65630
-
65631
- &.o-high-contrast {
65632
- color: #defade;
65633
- }
65634
- &.o-high-contrast:hover {
65635
- color: ${FILTERS_COLOR};
65636
- background: #fff;
65637
- }
65638
- }
65639
- .o-filter-icon:hover {
65640
- background: ${FILTERS_COLOR};
65641
- color: #fff;
65642
- }
65643
- `;
65644
- class FilterIcon extends owl.Component {
65645
- static template = "o-spreadsheet-FilterIcon";
65646
- static props = {
65647
- cellPosition: Object,
65648
- };
65649
- cellPopovers;
65650
- setup() {
65651
- this.cellPopovers = useStore(CellPopoverStore);
65652
- }
65653
- onClick() {
65654
- const position = this.props.cellPosition;
65655
- const activePopover = this.cellPopovers.persistentCellPopover;
65656
- const { col, row } = position;
65657
- if (activePopover.isOpen &&
65658
- activePopover.col === col &&
65659
- activePopover.row === row &&
65660
- activePopover.type === "FilterMenu") {
65661
- this.cellPopovers.close();
65662
- return;
65663
- }
65664
- this.cellPopovers.open({ col, row }, "FilterMenu");
65665
- }
65666
- get isFilterActive() {
65667
- return this.env.model.getters.isFilterActive(this.props.cellPosition);
65668
- }
65669
- get iconClass() {
65670
- const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
65671
- const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
65672
- return luminance < 0.45 ? "o-high-contrast" : "";
65673
- }
65674
- }
65675
-
65676
- css /* scss */ `
65677
- .o-spreadsheet {
65678
- .o-pivot-collapse-icon {
65679
- cursor: pointer;
65680
- width: 11px;
65681
- height: 11px;
65682
- border: 1px solid #777;
65683
- background-color: #eee;
65684
- margin: 3px 0 3px 6px;
65685
-
65686
- .o-icon {
65687
- width: 5px;
65688
- height: 5px;
65689
- }
65690
- }
65691
- }
65692
- `;
65693
- class PivotCollapseIcon extends owl.Component {
65694
- static template = "o-spreadsheet-PivotCollapseIcon";
65695
- static props = {
65696
- cellPosition: Object,
65697
- };
65698
- onClick() {
65699
- const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
65700
- const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
65701
- if (!pivotId || pivotCell.type !== "HEADER") {
65702
- return;
65703
- }
65704
- const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
65705
- const collapsedDomains = definition.collapsedDomains?.[pivotCell.dimension]
65706
- ? [...definition.collapsedDomains[pivotCell.dimension]]
65707
- : [];
65708
- const index = collapsedDomains.findIndex((domain) => deepEquals(domain, pivotCell.domain));
65709
- if (index !== -1) {
65710
- collapsedDomains.splice(index, 1);
65711
- }
65712
- else {
65713
- collapsedDomains.push(pivotCell.domain);
65714
- }
65715
- const newDomains = definition.collapsedDomains
65716
- ? { ...definition.collapsedDomains }
65717
- : { COL: [], ROW: [] };
65718
- newDomains[pivotCell.dimension] = collapsedDomains;
65719
- this.env.model.dispatch("UPDATE_PIVOT", {
65720
- pivotId,
65721
- pivot: { ...definition, collapsedDomains: newDomains },
65722
- });
65723
- }
65724
- get isCollapsed() {
65725
- const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
65726
- const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
65727
- if (!pivotId || pivotCell.type !== "HEADER") {
65728
- return false;
65729
- }
65730
- const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
65731
- const domains = definition.collapsedDomains?.[pivotCell.dimension] ?? [];
65732
- return domains?.some((domain) => deepEquals(domain, pivotCell.domain));
65733
- }
65734
- }
65735
-
65736
66041
  /**
65737
66042
  * Registry to draw icons on cells
65738
66043
  */
@@ -65740,14 +66045,25 @@ const iconsOnCellRegistry = new Registry();
65740
66045
  iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
65741
66046
  const hasIcon = getters.isCellValidCheckbox(position);
65742
66047
  if (hasIcon) {
66048
+ const value = !!getters.getEvaluatedCell(position).value;
65743
66049
  return {
65744
- svg: undefined,
66050
+ svg: value ? CHECKBOX_CHECKED : CHECKBOX_UNCHECKED,
66051
+ hoverSvg: value ? CHECKBOX_CHECKED : CHECKBOX_UNCHECKED_HOVERED,
65745
66052
  priority: 2,
65746
66053
  horizontalAlign: "center",
65747
66054
  size: GRID_ICON_EDGE_LENGTH,
65748
66055
  margin: GRID_ICON_MARGIN,
65749
- component: DataValidationCheckbox,
65750
66056
  position,
66057
+ type: "data_validation_checkbox",
66058
+ onClick: (position, env) => {
66059
+ const cell = env.model.getters.getCell(position);
66060
+ const isDisabled = env.model.getters.isReadonly() || !!cell?.isFormula;
66061
+ if (isDisabled) {
66062
+ return;
66063
+ }
66064
+ const cellContent = value ? "FALSE" : "TRUE";
66065
+ env.model.dispatch("UPDATE_CELL", { ...position, content: cellContent });
66066
+ },
65751
66067
  };
65752
66068
  }
65753
66069
  return undefined;
@@ -65756,13 +66072,19 @@ iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
65756
66072
  const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
65757
66073
  if (hasIcon) {
65758
66074
  return {
65759
- svg: undefined,
66075
+ svg: CARET_DOWN,
66076
+ hoverSvg: HOVERED_CARET_DOWN,
65760
66077
  priority: 2,
65761
66078
  horizontalAlign: "right",
65762
66079
  size: GRID_ICON_EDGE_LENGTH,
65763
66080
  margin: GRID_ICON_MARGIN,
65764
- component: DataValidationListIcon,
65765
66081
  position,
66082
+ onClick: (position, env) => {
66083
+ const { col, row } = position;
66084
+ env.model.selection.selectCell(col, row);
66085
+ env.startCellEdition();
66086
+ },
66087
+ type: "data_validation_list_icon",
65766
66088
  };
65767
66089
  }
65768
66090
  return undefined;
@@ -65770,14 +66092,30 @@ iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
65770
66092
  iconsOnCellRegistry.add("filter_icon", (getters, position) => {
65771
66093
  const hasIcon = getters.isFilterHeader(position);
65772
66094
  if (hasIcon) {
66095
+ const isFilterActive = getters.isFilterActive(position);
66096
+ const cellStyle = getters.getCellComputedStyle(position);
66097
+ const isHighContrast = relativeLuminance(cellStyle.fillColor || "#fff") < 0.45;
65773
66098
  return {
65774
- svg: undefined,
66099
+ type: "filter_icon",
66100
+ svg: getDataFilterIcon(isFilterActive, isHighContrast, false),
66101
+ hoverSvg: getDataFilterIcon(isFilterActive, isHighContrast, true),
65775
66102
  priority: 3,
65776
66103
  horizontalAlign: "right",
65777
66104
  size: GRID_ICON_EDGE_LENGTH,
65778
66105
  margin: GRID_ICON_MARGIN,
65779
- component: FilterIcon,
65780
66106
  position,
66107
+ onClick: (position, env) => {
66108
+ const cellPopovers = env.getStore(CellPopoverStore);
66109
+ const activePopover = cellPopovers.persistentCellPopover;
66110
+ if (activePopover.isOpen &&
66111
+ activePopover.col === position.col &&
66112
+ activePopover.row === position.row &&
66113
+ activePopover.type === "FilterMenu") {
66114
+ cellPopovers.close();
66115
+ return;
66116
+ }
66117
+ cellPopovers.open(position, "FilterMenu");
66118
+ },
65781
66119
  };
65782
66120
  }
65783
66121
  return undefined;
@@ -65787,6 +66125,7 @@ iconsOnCellRegistry.add("conditional_formatting", (getters, position) => {
65787
66125
  if (icon) {
65788
66126
  const style = getters.getCellStyle(position);
65789
66127
  return {
66128
+ type: "conditional_formatting",
65790
66129
  svg: ICONS[icon].svg,
65791
66130
  priority: 1,
65792
66131
  horizontalAlign: "left",
@@ -65807,23 +66146,55 @@ iconsOnCellRegistry.add("pivot_collapse", (getters, position) => {
65807
66146
  const definition = getters.getPivotCoreDefinition(pivotId);
65808
66147
  const isDashboard = getters.isDashboard();
65809
66148
  const fields = pivotCell.dimension === "COL" ? definition.columns : definition.rows;
65810
- const component = !isDashboard && pivotCell.domain.length !== fields.length ? PivotCollapseIcon : undefined;
66149
+ const hasIcon = !isDashboard && pivotCell.domain.length !== fields.length;
66150
+ const domains = definition.collapsedDomains?.[pivotCell.dimension] ?? [];
66151
+ const isCollapsed = domains.some((domain) => deepEquals(domain, pivotCell.domain));
66152
+ const indent = pivotCell.dimension === "ROW" ? (pivotCell.domain.length - 1) * PIVOT_INDENT : 0;
65811
66153
  return {
66154
+ type: "pivot_collapse",
65812
66155
  priority: 4,
65813
66156
  horizontalAlign: "left",
65814
- size: !!component || (!isDashboard && pivotCell.dimension === "ROW" && definition.rows.length > 1)
65815
- ? GRID_ICON_EDGE_LENGTH
66157
+ size: hasIcon || (!isDashboard && pivotCell.dimension === "ROW" && definition.rows.length > 1)
66158
+ ? PIVOT_COLLAPSE_ICON_SIZE
65816
66159
  : 0,
65817
- margin: pivotCell.dimension === "ROW" ? (pivotCell.domain.length - 1) * PIVOT_INDENT : 0,
65818
- component,
66160
+ margin: hasIcon ? GRID_ICON_MARGIN * 2 + indent : indent,
66161
+ svg: hasIcon ? getPivotIconSvg(isCollapsed, false) : undefined,
66162
+ hoverSvg: hasIcon ? getPivotIconSvg(isCollapsed, true) : undefined,
65819
66163
  position,
66164
+ onClick: togglePivotCollapse,
65820
66165
  };
65821
66166
  }
65822
66167
  return undefined;
65823
66168
  });
66169
+ function togglePivotCollapse(position, env) {
66170
+ const pivotCell = env.model.getters.getPivotCellFromPosition(position);
66171
+ const pivotId = env.model.getters.getPivotIdFromPosition(position);
66172
+ if (!pivotId || pivotCell.type !== "HEADER") {
66173
+ return;
66174
+ }
66175
+ const definition = env.model.getters.getPivotCoreDefinition(pivotId);
66176
+ const collapsedDomains = definition.collapsedDomains?.[pivotCell.dimension]
66177
+ ? [...definition.collapsedDomains[pivotCell.dimension]]
66178
+ : [];
66179
+ const index = collapsedDomains.findIndex((domain) => deepEquals(domain, pivotCell.domain));
66180
+ if (index !== -1) {
66181
+ collapsedDomains.splice(index, 1);
66182
+ }
66183
+ else {
66184
+ collapsedDomains.push(pivotCell.domain);
66185
+ }
66186
+ const newDomains = definition.collapsedDomains
66187
+ ? { ...definition.collapsedDomains }
66188
+ : { COL: [], ROW: [] };
66189
+ newDomains[pivotCell.dimension] = collapsedDomains;
66190
+ env.model.dispatch("UPDATE_PIVOT", {
66191
+ pivotId,
66192
+ pivot: { ...definition, collapsedDomains: newDomains },
66193
+ });
66194
+ }
65824
66195
 
65825
66196
  class CellIconPlugin extends CoreViewPlugin {
65826
- static getters = ["doesCellHaveGridIcon", "getCellIcons"];
66197
+ static getters = ["doesCellHaveGridIcon", "getCellIcons", "getCellIconRect"];
65827
66198
  cellIconsCache = {};
65828
66199
  handle(cmd) {
65829
66200
  if (cmd.type !== "SET_VIEWPORT_OFFSET") {
@@ -65843,6 +66214,29 @@ class CellIconPlugin extends CoreViewPlugin {
65843
66214
  }
65844
66215
  return this.cellIconsCache[position.sheetId][position.col][position.row];
65845
66216
  }
66217
+ getCellIconRect(icon) {
66218
+ const cellPosition = icon.position;
66219
+ const merge = this.getters.getMerge(cellPosition);
66220
+ const zone = merge || positionToZone(cellPosition);
66221
+ const cellRect = this.getters.getRect(zone);
66222
+ const cell = this.getters.getCell(cellPosition);
66223
+ const x = this.getIconHorizontalPosition(cellRect, icon.horizontalAlign, icon);
66224
+ const y = this.getters.computeTextYCoordinate(cellRect, icon.size, cell?.style?.verticalAlign);
66225
+ return { x: x, y: y, width: icon.size, height: icon.size };
66226
+ }
66227
+ getIconHorizontalPosition(rect, align, icon) {
66228
+ const start = rect.x;
66229
+ const end = rect.x + rect.width;
66230
+ switch (align) {
66231
+ case "right":
66232
+ return end - icon.margin - icon.size;
66233
+ case "left":
66234
+ return start + icon.margin;
66235
+ default:
66236
+ const centeringOffset = Math.floor((end - start - icon.size) / 2);
66237
+ return end - icon.size - centeringOffset;
66238
+ }
66239
+ }
65846
66240
  computeCellIcons(position) {
65847
66241
  const icons = { left: undefined, right: undefined, center: undefined };
65848
66242
  const callbacks = iconsOnCellRegistry.getAll();
@@ -66947,10 +67341,15 @@ class PivotUIPlugin extends CoreViewPlugin {
66947
67341
  const includeTotal = toScalar(args[2]);
66948
67342
  const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal);
66949
67343
  const includeColumnHeaders = toScalar(args[3]);
67344
+ const includeMeasures = toScalar(args[5]);
67345
+ const shouldIncludeMeasures = includeMeasures === undefined ? true : toBoolean(includeMeasures);
66950
67346
  const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
66951
- const pivotCells = pivot
66952
- .getCollapsedTableStructure()
66953
- .getPivotCells(shouldIncludeTotal, shouldIncludeColumnHeaders);
67347
+ const visibilityOptions = {
67348
+ displayColumnHeaders: shouldIncludeColumnHeaders,
67349
+ displayTotals: shouldIncludeTotal,
67350
+ displayMeasuresRow: shouldIncludeMeasures,
67351
+ };
67352
+ const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(visibilityOptions);
66954
67353
  const pivotCol = position.col - mainPosition.col;
66955
67354
  const pivotRow = position.row - mainPosition.row;
66956
67355
  return pivotCells[pivotCol][pivotRow];
@@ -69172,8 +69571,7 @@ class CollaborativePlugin extends UIPlugin {
69172
69571
  "isFullySynchronized",
69173
69572
  ];
69174
69573
  static layers = ["Selection"];
69175
- availableColors = new AlternatingColorGenerator(12);
69176
- colors = {};
69574
+ colors = new AlternatingColorMap(12);
69177
69575
  session;
69178
69576
  constructor(config) {
69179
69577
  super(config);
@@ -69191,7 +69589,7 @@ class CollaborativePlugin extends UIPlugin {
69191
69589
  }
69192
69590
  getConnectedClients() {
69193
69591
  return [...this.session.getConnectedClients()].map((client) => {
69194
- return { ...client, color: this.colors[client.id] };
69592
+ return { ...client, color: this.colors.get(client.id) };
69195
69593
  });
69196
69594
  }
69197
69595
  isFullySynchronized() {
@@ -69220,10 +69618,7 @@ class CollaborativePlugin extends UIPlugin {
69220
69618
  client.position &&
69221
69619
  client.position.sheetId === sheetId &&
69222
69620
  this.isPositionValid(client.position)) {
69223
- if (!this.colors[client.id]) {
69224
- this.colors[client.id] = this.availableColors.next();
69225
- }
69226
- clients.push({ ...client, color: this.colors[client.id], position: client.position });
69621
+ clients.push({ ...client, position: client.position });
69227
69622
  }
69228
69623
  }
69229
69624
  return clients;
@@ -70069,6 +70464,7 @@ class SheetUIPlugin extends UIPlugin {
70069
70464
  "getCellText",
70070
70465
  "getCellMultiLineText",
70071
70466
  "getContiguousZone",
70467
+ "computeTextYCoordinate",
70072
70468
  ];
70073
70469
  ctx = document.createElement("canvas").getContext("2d");
70074
70470
  // ---------------------------------------------------------------------------
@@ -70171,6 +70567,25 @@ class SheetUIPlugin extends UIPlugin {
70171
70567
  });
70172
70568
  return splitTextToWidth(this.ctx, text, style, args.wrapText ? args.maxWidth : undefined);
70173
70569
  }
70570
+ /** Computes the vertical start point from which a text line should be draw in a cell.
70571
+ *
70572
+ * Note that in case the cell does not have enough spaces to display its text lines,
70573
+ * (wrapping cell case) then the vertical align should be at the top.
70574
+ * */
70575
+ computeTextYCoordinate(cellRect, textLineHeight, verticalAlign = DEFAULT_VERTICAL_ALIGN, numberOfLines = 1) {
70576
+ const y = cellRect.y + 1; // +1 to skip the cell grid line at the top
70577
+ const textHeight = computeTextLinesHeight(textLineHeight, numberOfLines);
70578
+ const hasEnoughSpaces = cellRect.height > textHeight + MIN_CELL_TEXT_MARGIN * 2;
70579
+ if (hasEnoughSpaces) {
70580
+ if (verticalAlign === "middle") {
70581
+ return Math.ceil(y + (cellRect.height - textHeight) / 2);
70582
+ }
70583
+ if (verticalAlign === "bottom") {
70584
+ return y + cellRect.height - textHeight - MIN_CELL_TEXT_MARGIN;
70585
+ }
70586
+ }
70587
+ return y + MIN_CELL_TEXT_MARGIN;
70588
+ }
70174
70589
  /**
70175
70590
  * Expands the given zone until bordered by empty cells or reached the sheet boundaries.
70176
70591
  */
@@ -72077,9 +72492,10 @@ class FilterEvaluationPlugin extends UIPlugin {
72077
72492
  const filteredValues = filterValue.hiddenValues?.map(toLowerCase);
72078
72493
  if (!filteredValues)
72079
72494
  continue;
72495
+ const filteredValuesSet = new Set(filteredValues);
72080
72496
  for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
72081
72497
  const value = this.getCellValueAsString(sheetId, filter.col, row);
72082
- if (filteredValues.includes(value)) {
72498
+ if (filteredValuesSet.has(value)) {
72083
72499
  hiddenRows.add(row);
72084
72500
  }
72085
72501
  }
@@ -72196,6 +72612,7 @@ class GridSelectionPlugin extends UIPlugin {
72196
72612
  "getElementsFromSelection",
72197
72613
  "tryGetActiveSheetId",
72198
72614
  "isGridSelectionActive",
72615
+ "getSelectecUnboundedZone",
72199
72616
  ];
72200
72617
  gridSelection = {
72201
72618
  anchor: {
@@ -72207,12 +72624,14 @@ class GridSelectionPlugin extends UIPlugin {
72207
72624
  selectedFigureId = null;
72208
72625
  sheetsData = {};
72209
72626
  moveClient;
72627
+ isUnbounded;
72210
72628
  // This flag is used to avoid to historize the ACTIVE_SHEET command when it's
72211
72629
  // the main command.
72212
72630
  activeSheet = null;
72213
72631
  constructor(config) {
72214
72632
  super(config);
72215
72633
  this.moveClient = config.moveClient;
72634
+ this.isUnbounded = false;
72216
72635
  }
72217
72636
  // ---------------------------------------------------------------------------
72218
72637
  // Command Handling
@@ -72238,6 +72657,7 @@ class GridSelectionPlugin extends UIPlugin {
72238
72657
  handleEvent(event) {
72239
72658
  const anchor = event.anchor;
72240
72659
  let zones = [];
72660
+ this.isUnbounded = event.options?.unbounded || false;
72241
72661
  switch (event.mode) {
72242
72662
  case "overrideSelection":
72243
72663
  zones = [anchor.zone];
@@ -72427,6 +72847,12 @@ class GridSelectionPlugin extends UIPlugin {
72427
72847
  getSelectedZone() {
72428
72848
  return deepCopy(this.gridSelection.anchor.zone);
72429
72849
  }
72850
+ getSelectecUnboundedZone() {
72851
+ const zone = this.isUnbounded
72852
+ ? this.getters.getUnboundedZone(this.activeSheet.id, this.gridSelection.anchor.zone)
72853
+ : this.gridSelection.anchor.zone;
72854
+ return deepCopy(zone);
72855
+ }
72430
72856
  getSelection() {
72431
72857
  return deepCopy(this.gridSelection);
72432
72858
  }
@@ -72620,11 +73046,6 @@ class GridSelectionPlugin extends UIPlugin {
72620
73046
  },
72621
73047
  ];
72622
73048
  const sheetId = this.getActiveSheetId();
72623
- const handler = new CellClipboardHandler(this.getters, this.dispatch);
72624
- const data = handler.copy(getClipboardDataPositions(sheetId, target));
72625
- if (!data) {
72626
- return;
72627
- }
72628
73049
  const base = isBasedBefore ? cmd.base : cmd.base + 1;
72629
73050
  const pasteTarget = [
72630
73051
  {
@@ -72634,7 +73055,14 @@ class GridSelectionPlugin extends UIPlugin {
72634
73055
  bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
72635
73056
  },
72636
73057
  ];
72637
- handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });
73058
+ for (const Handler of clipboardHandlersRegistries.cellHandlers.getAll()) {
73059
+ const handler = new Handler(this.getters, this.dispatch);
73060
+ const data = handler.copy(getClipboardDataPositions(sheetId, target));
73061
+ if (!data) {
73062
+ continue;
73063
+ }
73064
+ handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });
73065
+ }
72638
73066
  const selection = pasteTarget[0];
72639
73067
  const col = selection.left;
72640
73068
  const row = selection.top;
@@ -73180,6 +73608,7 @@ class SheetViewPlugin extends UIPlugin {
73180
73608
  "getRect",
73181
73609
  "getFigureUI",
73182
73610
  "getPositionAnchorOffset",
73611
+ "getGridOffset",
73183
73612
  ];
73184
73613
  viewports = {};
73185
73614
  /**
@@ -73369,6 +73798,9 @@ class SheetViewPlugin extends UIPlugin {
73369
73798
  height: this.sheetViewHeight,
73370
73799
  };
73371
73800
  }
73801
+ getGridOffset() {
73802
+ return { x: this.gridOffsetX, y: this.gridOffsetY };
73803
+ }
73372
73804
  /** type as pane, not viewport but basically pane extends viewport */
73373
73805
  getActiveMainViewport() {
73374
73806
  const sheetId = this.getters.getActiveSheetId();
@@ -74745,8 +75177,9 @@ topbarMenuRegistry
74745
75177
  })
74746
75178
  .addChild("settings", ["file"], {
74747
75179
  name: _t("Settings"),
74748
- sequence: 100,
75180
+ sequence: 200,
74749
75181
  execute: (env) => env.openSidePanel("Settings"),
75182
+ isEnabled: (env) => !env.isSmall,
74750
75183
  icon: "o-spreadsheet-Icon.COG",
74751
75184
  })
74752
75185
  // ---------------------------------------------------------------------
@@ -75164,6 +75597,7 @@ topbarMenuRegistry
75164
75597
  execute: (env) => {
75165
75598
  env.openSidePanel("DataValidation");
75166
75599
  },
75600
+ isEnabled: (env) => !env.isSmall,
75167
75601
  icon: "o-spreadsheet-Icon.DATA_VALIDATION",
75168
75602
  sequence: 30,
75169
75603
  separator: true,
@@ -75187,6 +75621,7 @@ topbarMenuRegistry
75187
75621
  sequence: sequence + index,
75188
75622
  isReadonlyAllowed: true,
75189
75623
  execute: (env) => env.openSidePanel("PivotSidePanel", { pivotId }),
75624
+ isEnabled: (env) => !env.isSmall,
75190
75625
  onStartHover: (env) => env.getStore(HighlightStore).register(highlightProvider),
75191
75626
  onStopHover: (env) => env.getStore(HighlightStore).unRegister(highlightProvider),
75192
75627
  icon: "o-spreadsheet-Icon.PIVOT",
@@ -75458,7 +75893,7 @@ css /* scss */ `
75458
75893
  .o-sheet {
75459
75894
  padding: 0 15px;
75460
75895
  padding-right: 10px;
75461
- height: ${BOTTOMBAR_HEIGHT}px;
75896
+ height: ${DESKTOP_BOTTOMBAR_HEIGHT}px;
75462
75897
  border-left: 1px solid #c1c1c1;
75463
75898
  border-right: 1px solid #c1c1c1;
75464
75899
  margin-left: -1px;
@@ -75502,6 +75937,10 @@ css /* scss */ `
75502
75937
  width: calc(100% - 1px);
75503
75938
  }
75504
75939
  }
75940
+
75941
+ .o-spreadshet-mobile .o-sheet {
75942
+ height: ${MOBILE_BOTTOMBAR_HEIGHT}px;
75943
+ }
75505
75944
  `;
75506
75945
  class BottomBarSheet extends owl.Component {
75507
75946
  static template = "o-spreadsheet-BottomBarSheet";
@@ -75556,7 +75995,16 @@ class BottomBarSheet extends owl.Component {
75556
75995
  this.stopEdition();
75557
75996
  }
75558
75997
  }
75998
+ onClick() {
75999
+ if (!this.env.isMobile()) {
76000
+ return;
76001
+ }
76002
+ this.activateSheet();
76003
+ }
75559
76004
  onMouseDown(ev) {
76005
+ if (this.env.isMobile()) {
76006
+ return;
76007
+ }
75560
76008
  this.activateSheet();
75561
76009
  this.props.onMouseDown(ev);
75562
76010
  }
@@ -75900,8 +76348,8 @@ css /* scss */ `
75900
76348
  }
75901
76349
  }
75902
76350
 
75903
- .mobile.o-spreadsheet-bottom-bar {
75904
- padding-left: 1rem;
76351
+ .o-spreadsheet-mobile .o-spreadsheet-bottom-bar {
76352
+ padding-left: 0;
75905
76353
 
75906
76354
  .add-sheet-container {
75907
76355
  order: 2;
@@ -75918,10 +76366,8 @@ css /* scss */ `
75918
76366
  `;
75919
76367
  class BottomBar extends owl.Component {
75920
76368
  static template = "o-spreadsheet-BottomBar";
75921
- static props = {
75922
- onClick: Function,
75923
- };
75924
- static components = { Menu, Ripple, BottomBarSheet, BottomBarStatistic };
76369
+ static props = { onClick: Function };
76370
+ static components = { MenuPopover, Ripple, BottomBarSheet, BottomBarStatistic };
75925
76371
  bottomBarRef = owl.useRef("bottomBar");
75926
76372
  sheetListRef = owl.useRef("sheetList");
75927
76373
  dragAndDrop = useDragAndDropListItems();
@@ -76058,6 +76504,9 @@ class BottomBar extends owl.Component {
76058
76504
  if (event.button !== 0 || this.env.model.getters.isReadonly())
76059
76505
  return;
76060
76506
  this.closeMenu();
76507
+ if (this.env.isMobile()) {
76508
+ return;
76509
+ }
76061
76510
  const visibleSheets = this.getVisibleSheets();
76062
76511
  const sheetRects = this.getSheetItemRects();
76063
76512
  const sheets = visibleSheets.map((sheet, index) => ({
@@ -76177,7 +76626,7 @@ css /* scss */ `
76177
76626
  `;
76178
76627
  class SpreadsheetDashboard extends owl.Component {
76179
76628
  static template = "o-spreadsheet-SpreadsheetDashboard";
76180
- static props = {};
76629
+ static props = { getGridSize: Function };
76181
76630
  static components = {
76182
76631
  GridOverlay,
76183
76632
  GridPopover,
@@ -76464,7 +76913,7 @@ class HeaderGroupContainer extends owl.Component {
76464
76913
  dimension: String,
76465
76914
  layers: Array,
76466
76915
  };
76467
- static components = { RowGroup, ColGroup, Menu };
76916
+ static components = { RowGroup, ColGroup, MenuPopover };
76468
76917
  menu = owl.useState({ isOpen: false, anchorRect: null, menuItems: [] });
76469
76918
  getLayerOffset(layerIndex) {
76470
76919
  return layerIndex * GROUP_LAYER_WIDTH;
@@ -76680,6 +77129,158 @@ class SidePanel extends owl.Component {
76680
77129
  }
76681
77130
  }
76682
77131
 
77132
+ class RibbonMenu extends owl.Component {
77133
+ static template = "o-spreadsheet-RibbonMenu";
77134
+ static props = {
77135
+ onClose: Function,
77136
+ };
77137
+ static components = { Menu };
77138
+ rootItems = topbarMenuRegistry.getMenuItems();
77139
+ menuRef = owl.useRef("menu");
77140
+ state = owl.useState({
77141
+ menuItems: this.rootItems,
77142
+ title: _t("Menu Bar"),
77143
+ parentState: undefined,
77144
+ });
77145
+ setup() {
77146
+ owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
77147
+ }
77148
+ onExternalClick(ev) {
77149
+ if (!this.menuRef.el?.contains(ev.target)) {
77150
+ this.props.onClose();
77151
+ }
77152
+ }
77153
+ onClickMenu(menu) {
77154
+ const children = menu.children(this.env);
77155
+ if (children.length) {
77156
+ this.state.parentState = { ...this.state };
77157
+ this.state.menuItems = children;
77158
+ this.state.title = menu.name(this.env);
77159
+ }
77160
+ else {
77161
+ this.state.menuItems = this.rootItems;
77162
+ this.state.title = undefined;
77163
+ this.state.parentState = undefined;
77164
+ menu.execute?.(this.env);
77165
+ this.props.onClose();
77166
+ }
77167
+ }
77168
+ get menuProps() {
77169
+ return {
77170
+ menuItems: this.state.menuItems,
77171
+ onClose: this.props.onClose,
77172
+ onClickMenu: this.onClickMenu.bind(this),
77173
+ };
77174
+ }
77175
+ get style() {
77176
+ return cssPropertiesToCss({
77177
+ height: `${this.props.height}px`,
77178
+ });
77179
+ }
77180
+ onClickBack() {
77181
+ if (!this.state.parentState) {
77182
+ this.props.onClose();
77183
+ return;
77184
+ }
77185
+ this.state.menuItems = this.state.parentState.menuItems;
77186
+ this.state.title = this.state.parentState.title;
77187
+ this.state.parentState = this.state.parentState.parentState;
77188
+ }
77189
+ get backTitle() {
77190
+ return this.state.parentState ? _t("Go to previous menu") : _t("Close menu bar");
77191
+ }
77192
+ }
77193
+
77194
+ css `
77195
+ .o-small-composer {
77196
+ z-index: ${ComponentsImportance.TopBarComposer};
77197
+ }
77198
+ `;
77199
+ class SmallBottomBar extends owl.Component {
77200
+ static components = { Composer, BottomBar, Ripple, RibbonMenu };
77201
+ static template = "o-spreadsheet-SmallBottomBar";
77202
+ static props = {
77203
+ onClick: Function,
77204
+ };
77205
+ composerFocusStore;
77206
+ composerStore;
77207
+ composerInterface;
77208
+ composerRef = owl.useRef("bottombarComposer");
77209
+ menuState = owl.useState({
77210
+ isOpen: false,
77211
+ });
77212
+ setup() {
77213
+ this.composerFocusStore = useStore(ComposerFocusStore);
77214
+ const composerStore = useStore(CellComposerStore);
77215
+ this.composerStore = composerStore;
77216
+ this.composerInterface = {
77217
+ id: "bottombarComposer",
77218
+ get editionMode() {
77219
+ return composerStore.editionMode;
77220
+ },
77221
+ startEdition: this.composerStore.startEdition,
77222
+ setCurrentContent: this.composerStore.setCurrentContent,
77223
+ stopEdition: this.composerStore.stopEdition,
77224
+ };
77225
+ owl.useEffect(() => {
77226
+ if (
77227
+ // we hide the grid composer on mobile so we need to autofocus this composer
77228
+ this.env.isMobile() &&
77229
+ !this.menuState.isOpen &&
77230
+ this.composerStore.editionMode !== "inactive" &&
77231
+ this.composerFocusStore.activeComposer !== this.composerInterface) {
77232
+ this.composerFocusStore.focusComposer(this.composerInterface, {
77233
+ focusMode: "contentFocus",
77234
+ });
77235
+ }
77236
+ });
77237
+ }
77238
+ get focus() {
77239
+ return this.composerFocusStore.activeComposer === this.composerInterface
77240
+ ? this.composerFocusStore.focusMode
77241
+ : "inactive";
77242
+ }
77243
+ get rect() {
77244
+ return this.composerRef.el
77245
+ ? getBoundingRectAsPOJO(this.composerRef.el)
77246
+ : { x: 0, y: 0, width: 0, height: 0 };
77247
+ }
77248
+ get composerProps() {
77249
+ const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
77250
+ return {
77251
+ rect: { ...this.rect },
77252
+ delimitation: {
77253
+ width,
77254
+ height,
77255
+ },
77256
+ focus: this.focus,
77257
+ composerStore: this.composerStore,
77258
+ onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {
77259
+ focusMode: "contentFocus",
77260
+ }),
77261
+ isDefaultFocus: false,
77262
+ inputStyle: cssPropertiesToCss({
77263
+ height: this.focus === "inactive" ? "26px" : "fit-content",
77264
+ "max-height": `130px`,
77265
+ }),
77266
+ showAssistant: !isIOS(), // Hide assistant on iOS as it breaks visually
77267
+ };
77268
+ }
77269
+ get symbols() {
77270
+ return ["=", "(", ")", ":", "-", "/", "*", ",", "+", "$", "."];
77271
+ }
77272
+ insertSymbol(symbol) {
77273
+ this.composerStore.replaceComposerCursorSelection(symbol);
77274
+ this.composerFocusStore.focusComposer(this.composerInterface, {
77275
+ focusMode: "contentFocus",
77276
+ });
77277
+ }
77278
+ toggleRibbon() {
77279
+ this.composerStore.cancelEdition();
77280
+ this.menuState.isOpen = !this.menuState.isOpen;
77281
+ }
77282
+ }
77283
+
76683
77284
  const COMPOSER_MAX_HEIGHT = 100;
76684
77285
  /* svg free of use from https://uxwing.com/formula-fx-icon/ */
76685
77286
  const FX_SVG = /*xml*/ `
@@ -76689,7 +77290,7 @@ const FX_SVG = /*xml*/ `
76689
77290
  `;
76690
77291
  css /* scss */ `
76691
77292
  .o-topbar-composer-container {
76692
- height: ${TOPBAR_TOOLBAR_HEIGHT}px;
77293
+ height: ${DESKTOP_TOPBAR_TOOLBAR_HEIGHT}px;
76693
77294
  }
76694
77295
 
76695
77296
  .o-topbar-composer {
@@ -76744,7 +77345,7 @@ class TopBarComposer extends owl.Component {
76744
77345
  "max-height": `${COMPOSER_MAX_HEIGHT}px`,
76745
77346
  "line-height": "24px",
76746
77347
  };
76747
- style.height = this.focus === "inactive" ? `${TOPBAR_TOOLBAR_HEIGHT}px` : "fit-content";
77348
+ style.height = this.focus === "inactive" ? `${DESKTOP_TOPBAR_TOOLBAR_HEIGHT}px` : "fit-content";
76748
77349
  return cssPropertiesToCss(style);
76749
77350
  }
76750
77351
  get containerStyle() {
@@ -77247,7 +77848,7 @@ class TopBarFontSizeEditor extends owl.Component {
77247
77848
 
77248
77849
  class NumberFormatsTool extends owl.Component {
77249
77850
  static template = "o-spreadsheet-NumberFormatsTool";
77250
- static components = { Menu, ActionButton };
77851
+ static components = { MenuPopover, ActionButton };
77251
77852
  static props = { class: String };
77252
77853
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
77253
77854
  topBarToolStore;
@@ -77297,7 +77898,7 @@ topBarToolBarRegistry
77297
77898
  .addChild("edit", {
77298
77899
  component: PaintFormatButton,
77299
77900
  props: {
77300
- class: "o-hoverable-button o-toolbar-button",
77901
+ class: "o-hoverable-button o-toolbar-button o-mobile-disabled",
77301
77902
  },
77302
77903
  sequence: 3,
77303
77904
  })
@@ -77456,7 +78057,7 @@ topBarToolBarRegistry
77456
78057
  .add("misc")
77457
78058
  .addChild("misc", {
77458
78059
  component: TableDropdownButton,
77459
- props: { class: "o-toolbar-button o-hoverable-button o-menu-item-button" },
78060
+ props: { class: "o-toolbar-button o-hoverable-button o-menu-item-button o-mobile-disabled" },
77460
78061
  sequence: 1,
77461
78062
  })
77462
78063
  .addChild("misc", {
@@ -77476,6 +78077,7 @@ css /* scss */ `
77476
78077
  border-right: 1px solid ${SEPARATOR_COLOR};
77477
78078
  width: 0;
77478
78079
  margin: 0 6px;
78080
+ height: 30px;
77479
78081
  }
77480
78082
 
77481
78083
  .o-toolbar-button {
@@ -77484,7 +78086,7 @@ css /* scss */ `
77484
78086
 
77485
78087
  .o-spreadsheet-topbar {
77486
78088
  line-height: 1.2;
77487
- font-size: 13px;
78089
+ font-size: 14px;
77488
78090
  font-weight: 500;
77489
78091
  background-color: #fff;
77490
78092
 
@@ -77508,7 +78110,7 @@ css /* scss */ `
77508
78110
 
77509
78111
  .irregularity-map {
77510
78112
  border-top: 1px solid ${SEPARATOR_COLOR};
77511
- height: ${TOPBAR_TOOLBAR_HEIGHT}px;
78113
+ height: ${DESKTOP_TOPBAR_TOOLBAR_HEIGHT}px;
77512
78114
 
77513
78115
  .alert-info {
77514
78116
  border-left: 3px solid ${ALERT_INFO_BORDER};
@@ -77521,7 +78123,7 @@ css /* scss */ `
77521
78123
 
77522
78124
  /* Toolbar */
77523
78125
  .o-topbar-toolbar {
77524
- height: ${TOPBAR_TOOLBAR_HEIGHT}px;
78126
+ height: ${DESKTOP_TOPBAR_TOOLBAR_HEIGHT}px;
77525
78127
 
77526
78128
  .o-readonly-toolbar {
77527
78129
  background-color: ${BACKGROUND_HEADER_COLOR};
@@ -77535,6 +78137,24 @@ css /* scss */ `
77535
78137
  }
77536
78138
  }
77537
78139
  }
78140
+
78141
+ .o-spreadsheet-mobile {
78142
+ .o-topbar-toolbar {
78143
+ height: ${MOBILE_TOPBAR_TOOLBAR_HEIGHT}px;
78144
+ }
78145
+ .o-topbar-divider {
78146
+ border-width: 2px;
78147
+ border-radius: 4px;
78148
+ }
78149
+
78150
+ .o-toolbar-button {
78151
+ height: 35px;
78152
+ width: 31px;
78153
+ .o-toolbar-button.o-mobile-disabled * {
78154
+ color: ${DISABLED_TEXT_COLOR};
78155
+ cursor: not-allowed;
78156
+ }
78157
+ }
77538
78158
  `;
77539
78159
  class TopBar extends owl.Component {
77540
78160
  static template = "o-spreadsheet-TopBar";
@@ -77543,7 +78163,7 @@ class TopBar extends owl.Component {
77543
78163
  dropdownMaxHeight: Number,
77544
78164
  };
77545
78165
  static components = {
77546
- Menu,
78166
+ MenuPopover,
77547
78167
  TopBarComposer,
77548
78168
  Popover,
77549
78169
  };
@@ -77623,7 +78243,7 @@ class TopBar extends owl.Component {
77623
78243
  // TODO : manage click events better. We need this piece of code
77624
78244
  // otherwise the event opening the menu would close it on the same frame.
77625
78245
  // And we cannot stop the event propagation because it's used in an
77626
- // external listener of the Menu component to close the context menu when
78246
+ // external listener of the MenuPopover component to close the context menu when
77627
78247
  // clicking on the top bar
77628
78248
  if (this.openedEl === ev.target) {
77629
78249
  return;
@@ -78049,6 +78669,11 @@ css /* scss */ `
78049
78669
  color: ${TEXT_BODY};
78050
78670
  }
78051
78671
  }
78672
+
78673
+ .o-spreadsheet-topbar-wrapper,
78674
+ .o-spreadsheet-bottombar-wrapper {
78675
+ z-index: ${ComponentsImportance.ScrollBar + 1};
78676
+ }
78052
78677
  `;
78053
78678
  class Spreadsheet extends owl.Component {
78054
78679
  static template = "o-spreadsheet-Spreadsheet";
@@ -78062,6 +78687,7 @@ class Spreadsheet extends owl.Component {
78062
78687
  TopBar,
78063
78688
  Grid,
78064
78689
  BottomBar,
78690
+ SmallBottomBar,
78065
78691
  SidePanel,
78066
78692
  SpreadsheetDashboard,
78067
78693
  HeaderGroupContainer,
@@ -78085,12 +78711,25 @@ class Spreadsheet extends owl.Component {
78085
78711
  else {
78086
78712
  properties["grid-template-rows"] = `min-content auto min-content`;
78087
78713
  }
78088
- properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
78714
+ const columnWidth = this.sidePanel.isOpen ? `${this.sidePanel.panelSize}px` : "auto";
78715
+ properties["grid-template-columns"] = `auto ${columnWidth}`;
78089
78716
  return cssPropertiesToCss(properties);
78090
78717
  }
78091
78718
  setup() {
78719
+ if (!("isSmall" in this.env)) {
78720
+ const screenSize = useScreenWidth();
78721
+ owl.useSubEnv({
78722
+ get isSmall() {
78723
+ return screenSize.isSmall;
78724
+ },
78725
+ });
78726
+ }
78092
78727
  const stores = useStoreProvider();
78093
78728
  stores.inject(ModelStore, this.model);
78729
+ const env = this.env;
78730
+ stores.get(ScreenWidthStore).setSmallThreshhold(() => {
78731
+ return env.isSmall;
78732
+ });
78094
78733
  this.notificationStore = useStore(NotificationStore);
78095
78734
  this.composerFocusStore = useStore(ComposerFocusStore);
78096
78735
  this.sidePanel = useStore(SidePanelStore);
@@ -78108,15 +78747,8 @@ class Spreadsheet extends owl.Component {
78108
78747
  notifyUser: (notification) => this.notificationStore.notifyUser(notification),
78109
78748
  askConfirmation: (text, confirm, cancel) => this.notificationStore.askConfirmation(text, confirm, cancel),
78110
78749
  raiseError: (text, cb) => this.notificationStore.raiseError(text, cb),
78750
+ isMobile: isMobileOS,
78111
78751
  });
78112
- if (!("isSmall" in this.env)) {
78113
- const screenSize = useScreenWidth();
78114
- owl.useSubEnv({
78115
- get isSmall() {
78116
- return screenSize.isSmall;
78117
- },
78118
- });
78119
- }
78120
78752
  this.notificationStore.updateNotificationCallbacks({ ...this.props });
78121
78753
  owl.useEffect(() => {
78122
78754
  /**
@@ -78222,6 +78854,24 @@ class Spreadsheet extends owl.Component {
78222
78854
  const sheetId = this.env.model.getters.getActiveSheetId();
78223
78855
  return this.env.model.getters.getVisibleGroupLayers(sheetId, "COL");
78224
78856
  }
78857
+ getGridSize() {
78858
+ const topBarHeight = this.spreadsheetRef.el
78859
+ ?.querySelector(".o-spreadsheet-topbar-wrapper")
78860
+ ?.getBoundingClientRect().height || 0;
78861
+ const bottomBarHeight = this.spreadsheetRef.el
78862
+ ?.querySelector(".o-spreadsheet-bottombar-wrapper")
78863
+ ?.getBoundingClientRect().height || 0;
78864
+ const gridWidth = this.spreadsheetRef.el?.querySelector(".o-grid")?.getBoundingClientRect().width || 0;
78865
+ const gridHeight = (this.spreadsheetRef.el?.getBoundingClientRect().height || 0) -
78866
+ (this.spreadsheetRef.el?.querySelector(".o-column-groups")?.getBoundingClientRect().height ||
78867
+ 0) -
78868
+ topBarHeight -
78869
+ bottomBarHeight;
78870
+ return {
78871
+ width: Math.max(gridWidth - SCROLLBAR_WIDTH, 0),
78872
+ height: Math.max(gridHeight - SCROLLBAR_WIDTH, 0),
78873
+ };
78874
+ }
78225
78875
  }
78226
78876
 
78227
78877
  function inverseCommand(cmd) {
@@ -82484,7 +83134,7 @@ const SPREADSHEET_DIMENSIONS = {
82484
83134
  MIN_COL_WIDTH,
82485
83135
  HEADER_HEIGHT,
82486
83136
  HEADER_WIDTH,
82487
- BOTTOMBAR_HEIGHT,
83137
+ DESKTOP_BOTTOMBAR_HEIGHT,
82488
83138
  DEFAULT_CELL_WIDTH,
82489
83139
  DEFAULT_CELL_HEIGHT,
82490
83140
  SCROLLBAR_WIDTH,
@@ -82630,7 +83280,7 @@ const components = {
82630
83280
  FunnelChartDesignPanel,
82631
83281
  ChartTypePicker,
82632
83282
  FigureComponent,
82633
- Menu,
83283
+ MenuPopover,
82634
83284
  Popover,
82635
83285
  SelectionInput,
82636
83286
  ValidationMessages,
@@ -82695,6 +83345,7 @@ exports.AbstractCellClipboardHandler = AbstractCellClipboardHandler;
82695
83345
  exports.AbstractChart = AbstractChart;
82696
83346
  exports.AbstractFigureClipboardHandler = AbstractFigureClipboardHandler;
82697
83347
  exports.CellErrorType = CellErrorType;
83348
+ exports.ClientDisconnectedError = ClientDisconnectedError;
82698
83349
  exports.CorePlugin = CorePlugin;
82699
83350
  exports.CoreViewPlugin = CoreViewPlugin;
82700
83351
  exports.DispatchResult = DispatchResult;
@@ -82741,6 +83392,6 @@ exports.tokenColors = tokenColors;
82741
83392
  exports.tokenize = tokenize;
82742
83393
 
82743
83394
 
82744
- __info__.version = "18.4.0-alpha.6";
82745
- __info__.date = "2025-05-30T08:44:33.216Z";
82746
- __info__.hash = "eecf7e4";
83395
+ __info__.version = "18.4.0-alpha.8";
83396
+ __info__.date = "2025-06-12T09:53:48.133Z";
83397
+ __info__.hash = "9b7a8d0";