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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.3.0-alpha.1
6
- * @date 2025-02-25T06:00:14.885Z
7
- * @hash be4d957
5
+ * @version 18.3.0-alpha.3
6
+ * @date 2025-03-07T10:41:05.411Z
7
+ * @hash f59f5f6
8
8
  */
9
9
 
10
10
  'use strict';
@@ -351,7 +351,6 @@ var ComponentsImportance;
351
351
  ComponentsImportance[ComponentsImportance["ScrollBar"] = 15] = "ScrollBar";
352
352
  ComponentsImportance[ComponentsImportance["GridPopover"] = 19] = "GridPopover";
353
353
  ComponentsImportance[ComponentsImportance["GridComposer"] = 20] = "GridComposer";
354
- ComponentsImportance[ComponentsImportance["Dropdown"] = 21] = "Dropdown";
355
354
  ComponentsImportance[ComponentsImportance["IconPicker"] = 25] = "IconPicker";
356
355
  ComponentsImportance[ComponentsImportance["TopBarComposer"] = 30] = "TopBarComposer";
357
356
  ComponentsImportance[ComponentsImportance["Popover"] = 35] = "Popover";
@@ -861,7 +860,7 @@ function insertItemsAtIndex(array, items, index) {
861
860
  }
862
861
  function replaceItemAtIndex(array, newItem, index) {
863
862
  const newArray = [...array];
864
- newArray.splice(index, 1, newItem);
863
+ newArray[index] = newItem;
865
864
  return newArray;
866
865
  }
867
866
  function trimContent(content) {
@@ -3403,6 +3402,7 @@ var ClipboardMIMEType;
3403
3402
  (function (ClipboardMIMEType) {
3404
3403
  ClipboardMIMEType["PlainText"] = "text/plain";
3405
3404
  ClipboardMIMEType["Html"] = "text/html";
3405
+ ClipboardMIMEType["Image"] = "image";
3406
3406
  })(ClipboardMIMEType || (ClipboardMIMEType = {}));
3407
3407
 
3408
3408
  function isSheetDependent(cmd) {
@@ -6105,8 +6105,9 @@ function spreadRange(getters, dataSets) {
6105
6105
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6106
6106
  if (zone.right) {
6107
6107
  for (let j = zone.left; j <= zone.right; ++j) {
6108
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6108
6109
  postProcessedRanges.push({
6109
- ...dataSet,
6110
+ ...datasetOptions,
6110
6111
  dataRange: `${sheetPrefix}${zoneToXc({
6111
6112
  left: j,
6112
6113
  right: j,
@@ -6118,8 +6119,9 @@ function spreadRange(getters, dataSets) {
6118
6119
  }
6119
6120
  else {
6120
6121
  for (let j = zone.top; j <= zone.bottom; ++j) {
6122
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6121
6123
  postProcessedRanges.push({
6122
- ...dataSet,
6124
+ ...datasetOptions,
6123
6125
  dataRange: `${sheetPrefix}${zoneToXc({
6124
6126
  left: zone.left,
6125
6127
  right: zone.right,
@@ -6558,6 +6560,17 @@ class UuidGenerator {
6558
6560
  }
6559
6561
  }
6560
6562
 
6563
+ const AllowedImageMimeTypes = [
6564
+ "image/avif",
6565
+ "image/bmp",
6566
+ "image/gif",
6567
+ "image/vnd.microsoft.icon",
6568
+ "image/jpeg",
6569
+ "image/png",
6570
+ "image/tiff",
6571
+ "image/webp",
6572
+ ];
6573
+
6561
6574
  function getClipboardDataPositions(sheetId, zones) {
6562
6575
  const lefts = new Set(zones.map((z) => z.left));
6563
6576
  const rights = new Set(zones.map((z) => z.right));
@@ -6604,21 +6617,28 @@ function getPasteZones(target, content) {
6604
6617
  const width = content[0].length, height = content.length;
6605
6618
  return target.map((t) => splitZoneForPaste(t, width, height)).flat();
6606
6619
  }
6607
- function parseOSClipboardContent(content) {
6608
- if (!content[ClipboardMIMEType.Html]) {
6609
- return {
6610
- text: content[ClipboardMIMEType.PlainText],
6611
- };
6620
+ function parseOSClipboardContent(content, clipboardId) {
6621
+ let spreadsheetContent = undefined;
6622
+ if (content[ClipboardMIMEType.Html]) {
6623
+ const htmlDocument = new DOMParser().parseFromString(content[ClipboardMIMEType.Html], "text/html");
6624
+ const oSheetClipboardData = htmlDocument
6625
+ .querySelector("div")
6626
+ ?.getAttribute("data-osheet-clipboard");
6627
+ spreadsheetContent = oSheetClipboardData && JSON.parse(oSheetClipboardData);
6628
+ }
6629
+ let imageBlob = undefined;
6630
+ for (const type of AllowedImageMimeTypes) {
6631
+ if (content[type]) {
6632
+ imageBlob = content[type];
6633
+ break;
6634
+ }
6612
6635
  }
6613
- const htmlDocument = new DOMParser().parseFromString(content[ClipboardMIMEType.Html], "text/html");
6614
- const oSheetClipboardData = htmlDocument
6615
- .querySelector("div")
6616
- ?.getAttribute("data-osheet-clipboard");
6617
- const spreadsheetContent = oSheetClipboardData && JSON.parse(oSheetClipboardData);
6618
- return {
6636
+ const osClipboardContent = {
6619
6637
  text: content[ClipboardMIMEType.PlainText],
6620
6638
  data: spreadsheetContent,
6639
+ imageBlob,
6621
6640
  };
6641
+ return osClipboardContent;
6622
6642
  }
6623
6643
 
6624
6644
  class ClipboardHandler {
@@ -9681,6 +9701,91 @@ function getElementMargins(el) {
9681
9701
  };
9682
9702
  }
9683
9703
 
9704
+ class FunnelChartController extends window.Chart?.BarController {
9705
+ static id = "funnel";
9706
+ static defaults = { ...window.Chart?.BarController.defaults, dataElementType: "funnel" };
9707
+ /** Called at each chart render to update the elements of the chart (FunnelChartElement) with the updated data */
9708
+ updateElements(rects, start, count, mode) {
9709
+ super.updateElements(rects, start, count, mode);
9710
+ for (let i = start; i < start + count; i++) {
9711
+ const rect = rects[i];
9712
+ // Add the width of the next element to the element's props
9713
+ this.updateElement(rect, i, { nextElementWidth: rects[i + 1]?.width || 0 }, mode);
9714
+ }
9715
+ }
9716
+ }
9717
+ /**
9718
+ * Similar to a bar chart element, but it's a trapezoid rather than a rectangle. The top is of width
9719
+ * `width`, and the bottom is of width `nextElementWidth`.
9720
+ */
9721
+ class FunnelChartElement extends window.Chart?.BarElement {
9722
+ static id = "funnel";
9723
+ /** Overwrite this to draw a trapezoid rather then a rectangle */
9724
+ draw(ctx) {
9725
+ ctx.save();
9726
+ const { x, y, width, height, nextElementWidth, base, options } = this.getProps([
9727
+ "x",
9728
+ "y",
9729
+ "width",
9730
+ "height",
9731
+ "nextElementWidth",
9732
+ "base",
9733
+ "options",
9734
+ ]);
9735
+ const offset = (width - nextElementWidth) / 2;
9736
+ const startX = Math.min(x, base);
9737
+ const startY = y - height / 2;
9738
+ ctx.fillStyle = options.backgroundColor;
9739
+ ctx.beginPath();
9740
+ ctx.moveTo(startX, startY);
9741
+ ctx.lineTo(startX + width, startY);
9742
+ ctx.lineTo(startX + width - offset, startY + height);
9743
+ ctx.lineTo(startX + offset, startY + height);
9744
+ ctx.closePath();
9745
+ ctx.fill();
9746
+ if (options.borderWidth) {
9747
+ ctx.strokeStyle = options.borderColor;
9748
+ ctx.lineWidth = options.borderWidth;
9749
+ ctx.stroke();
9750
+ }
9751
+ ctx.restore();
9752
+ }
9753
+ /** Check if the mouse is inside the trapezoid */
9754
+ inRange(mouseX, mouseY, useFinalPosition) {
9755
+ const { x, y, width, height, nextElementWidth, base } = this.getProps(["x", "y", "width", "height", "nextElementWidth", "base"], useFinalPosition);
9756
+ const startX = Math.min(x, base);
9757
+ const startY = y - height / 2;
9758
+ if (mouseY < startY || mouseY > startY + height) {
9759
+ return false;
9760
+ }
9761
+ const offset = (width - nextElementWidth) / 2;
9762
+ const left = startX + (offset * (mouseY - startY)) / height;
9763
+ const right = startX + width - (offset * (mouseY - startY)) / height;
9764
+ if (mouseX < left || mouseX > right) {
9765
+ return false;
9766
+ }
9767
+ return true;
9768
+ }
9769
+ }
9770
+ /**
9771
+ * Position the tooltip inside the trapezoid.
9772
+ * The default position for tooltips of bar elements is at the end of rectangle, which is not ideal for trapezoids.
9773
+ */
9774
+ const funnelTooltipPositioner = function (elements) {
9775
+ if (!elements.length) {
9776
+ return { x: 0, y: 0 };
9777
+ }
9778
+ const element = elements[0].element;
9779
+ const { x, y, base, width, height } = element.getProps(["x", "y", "width", "height", "base"]);
9780
+ const startX = Math.min(x, base);
9781
+ const startY = y - height / 2;
9782
+ return {
9783
+ x: startX + (width * 2) / 3,
9784
+ y: startY + height / 2,
9785
+ };
9786
+ };
9787
+ window.Chart.Tooltip.positioners.funnelTooltipPositioner = funnelTooltipPositioner;
9788
+
9684
9789
  const TREND_LINE_XAXIS_ID = "x1";
9685
9790
  /**
9686
9791
  * This file contains helpers that are common to different charts (mainly
@@ -10061,6 +10166,9 @@ const chartShowValuesPlugin = {
10061
10166
  ? drawHorizontalBarChartValues(chart, options, ctx)
10062
10167
  : drawLineOrBarOrRadarChartValues(chart, options, ctx);
10063
10168
  break;
10169
+ case "funnel":
10170
+ drawHorizontalBarChartValues(chart, options, ctx);
10171
+ break;
10064
10172
  }
10065
10173
  ctx.restore();
10066
10174
  },
@@ -10227,6 +10335,7 @@ function getNextNonEmptyBar(bars, startIndex) {
10227
10335
 
10228
10336
  window.Chart?.register(waterfallLinesPlugin);
10229
10337
  window.Chart?.register(chartShowValuesPlugin);
10338
+ window.Chart?.register(FunnelChartController, FunnelChartElement);
10230
10339
  css /* scss */ `
10231
10340
  .o-spreadsheet {
10232
10341
  .o-chart-custom-tooltip {
@@ -22331,7 +22440,7 @@ autofillRulesRegistry
22331
22440
  condition: (cell) => !cell.isFormula &&
22332
22441
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22333
22442
  alphaNumericValueRegExp.test(cell.content),
22334
- generateRule: (cell, cells) => {
22443
+ generateRule: (cell, cells, direction) => {
22335
22444
  const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22336
22445
  const prefix = cell.content.match(stringPrefixRegExp)[0];
22337
22446
  const numberPostfixLength = cell.content.length - prefix.length;
@@ -22339,7 +22448,10 @@ autofillRulesRegistry
22339
22448
  alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22340
22449
  .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22341
22450
  .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22342
- const increment = calculateIncrementBasedOnGroup(group);
22451
+ let increment = calculateIncrementBasedOnGroup(group);
22452
+ if (["up", "left"].includes(direction) && group.length === 1) {
22453
+ increment = -increment;
22454
+ }
22343
22455
  return {
22344
22456
  type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22345
22457
  prefix,
@@ -22402,10 +22514,13 @@ autofillRulesRegistry
22402
22514
  .add("increment_number", {
22403
22515
  condition: (cell) => !cell.isFormula &&
22404
22516
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22405
- generateRule: (cell, cells) => {
22517
+ generateRule: (cell, cells, direction) => {
22406
22518
  const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22407
22519
  !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22408
- const increment = calculateIncrementBasedOnGroup(group);
22520
+ let increment = calculateIncrementBasedOnGroup(group);
22521
+ if (["up", "left"].includes(direction) && group.length === 1) {
22522
+ increment = -increment;
22523
+ }
22409
22524
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22410
22525
  return {
22411
22526
  type: "INCREMENT_MODIFIER",
@@ -22832,7 +22947,7 @@ const CHART_COMMON_OPTIONS = {
22832
22947
  },
22833
22948
  animation: false,
22834
22949
  };
22835
- function chartToImage(runtime, figure, type) {
22950
+ function chartToImageUrl(runtime, figure, type) {
22836
22951
  // wrap the canvas in a div with a fixed size because chart.js would
22837
22952
  // fill the whole page otherwise
22838
22953
  const div = document.createElement("div");
@@ -22842,31 +22957,59 @@ function chartToImage(runtime, figure, type) {
22842
22957
  div.append(canvas);
22843
22958
  canvas.setAttribute("width", figure.width.toString());
22844
22959
  canvas.setAttribute("height", figure.height.toString());
22960
+ let imageContent;
22845
22961
  // we have to add the canvas to the DOM otherwise it won't be rendered
22846
22962
  document.body.append(div);
22847
22963
  if ("chartJsConfig" in runtime) {
22848
22964
  const config = deepCopy(runtime.chartJsConfig);
22849
22965
  config.plugins = [backgroundColorChartJSPlugin];
22850
22966
  const chart = new window.Chart(canvas, config);
22851
- const imgContent = chart.toBase64Image();
22967
+ imageContent = chart.toBase64Image();
22852
22968
  chart.destroy();
22853
- div.remove();
22854
- return imgContent;
22855
22969
  }
22856
22970
  else if (type === "scorecard") {
22857
22971
  const design = getScorecardConfiguration(figure, runtime);
22858
22972
  drawScoreChart(design, canvas);
22859
- const imgContent = canvas.toDataURL();
22860
- div.remove();
22861
- return imgContent;
22973
+ imageContent = canvas.toDataURL();
22862
22974
  }
22863
22975
  else if (type === "gauge") {
22864
22976
  drawGaugeChart(canvas, runtime);
22865
- const imgContent = canvas.toDataURL();
22866
- div.remove();
22867
- return imgContent;
22977
+ imageContent = canvas.toDataURL();
22868
22978
  }
22869
- return undefined;
22979
+ div.remove();
22980
+ return imageContent;
22981
+ }
22982
+ async function chartToImageFile(runtime, figure, type) {
22983
+ // wrap the canvas in a div with a fixed size because chart.js would
22984
+ // fill the whole page otherwise
22985
+ const div = document.createElement("div");
22986
+ div.style.width = `${figure.width}px`;
22987
+ div.style.height = `${figure.height}px`;
22988
+ const canvas = document.createElement("canvas");
22989
+ div.append(canvas);
22990
+ canvas.setAttribute("width", figure.width.toString());
22991
+ canvas.setAttribute("height", figure.height.toString());
22992
+ // we have to add the canvas to the DOM otherwise it won't be rendered
22993
+ document.body.append(div);
22994
+ let chartBlob = null;
22995
+ if ("chartJsConfig" in runtime) {
22996
+ const config = deepCopy(runtime.chartJsConfig);
22997
+ config.plugins = [backgroundColorChartJSPlugin];
22998
+ const chart = new window.Chart(canvas, config);
22999
+ chartBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
23000
+ chart.destroy();
23001
+ }
23002
+ else if (type === "scorecard") {
23003
+ const design = getScorecardConfiguration(figure, runtime);
23004
+ drawScoreChart(design, canvas);
23005
+ chartBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
23006
+ }
23007
+ else if (type === "gauge") {
23008
+ drawGaugeChart(canvas, runtime);
23009
+ chartBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
23010
+ }
23011
+ div.remove();
23012
+ return chartBlob ? new File([chartBlob], "chart.png", { type: "image/png" }) : undefined;
22870
23013
  }
22871
23014
  /**
22872
23015
  * Custom chart.js plugin to set the background color of the canvas
@@ -22949,6 +23092,7 @@ const CONTENT_TYPES = {
22949
23092
  macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
22950
23093
  excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
22951
23094
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
23095
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22952
23096
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22953
23097
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22954
23098
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22961,6 +23105,7 @@ const CONTENT_TYPES = {
22961
23105
  const XLSX_RELATION_TYPE = {
22962
23106
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22963
23107
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
23108
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22964
23109
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22965
23110
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22966
23111
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22970,6 +23115,7 @@ const XLSX_RELATION_TYPE = {
22970
23115
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22971
23116
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22972
23117
  };
23118
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22973
23119
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22974
23120
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22975
23121
  /**
@@ -25391,29 +25537,34 @@ function convertPivotTableConfig(pivotTable) {
25391
25537
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25392
25538
  */
25393
25539
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25394
- for (let sheet of convertedSheets) {
25395
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25540
+ for (let tableSheet of convertedSheets) {
25541
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25396
25542
  for (let table of tables) {
25397
25543
  const tabRef = table.name + "[";
25398
- for (let position of positions(toZone(table.ref))) {
25399
- const xc = toXC(position.col, position.row);
25400
- let cellContent = sheet.cells[xc];
25401
- if (cellContent?.startsWith("=")) {
25402
- let refIndex;
25403
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25404
- let reference = cellContent.slice(refIndex + tabRef.length);
25405
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25406
- let endIndex = reference.indexOf("]");
25407
- if (reference.startsWith(`[`)) {
25408
- endIndex = reference.indexOf("]", endIndex + 1);
25409
- endIndex = reference.indexOf("]", endIndex + 1);
25544
+ for (let sheet of convertedSheets) {
25545
+ for (let xc in sheet.cells) {
25546
+ const cell = sheet.cells[xc];
25547
+ let cellContent = sheet.cells[xc];
25548
+ if (cell && cellContent && cellContent.startsWith("=")) {
25549
+ let refIndex;
25550
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25551
+ let endIndex = refIndex + tabRef.length;
25552
+ let openBrackets = 1;
25553
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25554
+ if (cellContent[endIndex] === "[") {
25555
+ openBrackets++;
25556
+ }
25557
+ else if (cellContent[endIndex] === "]") {
25558
+ openBrackets--;
25559
+ }
25560
+ endIndex++;
25561
+ }
25562
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25563
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25564
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25565
+ cellContent =
25566
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25410
25567
  }
25411
- reference = reference.slice(0, endIndex);
25412
- const convertedRef = convertTableReference(reference, table, xc);
25413
- cellContent =
25414
- cellContent.slice(0, refIndex) +
25415
- convertedRef +
25416
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25417
25568
  }
25418
25569
  sheet.cells[xc] = cellContent;
25419
25570
  }
@@ -25422,11 +25573,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25422
25573
  }
25423
25574
  }
25424
25575
  /**
25425
- * Convert table-specific references in formulas into standard references.
25576
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25577
+ * and of keywords determining the rows of the table to reference.
25426
25578
  *
25427
25579
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25428
25580
  * - tableName[colName] : reference to the whole column "colName"
25581
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25429
25582
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25583
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25584
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25585
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25586
+ *
25430
25587
  *
25431
25588
  * The available keywords are :
25432
25589
  * - #All : all the column (including totals)
@@ -25434,58 +25591,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25434
25591
  * - #Headers : only the header of the column
25435
25592
  * - #Totals : only the totals of the column
25436
25593
  * - #This Row : only the element in the same row as the cell
25594
+ *
25595
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25437
25596
  */
25438
- function convertTableReference(expr, table, cellXc) {
25439
- const refElements = expr.split(",");
25597
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25598
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25599
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25600
+ const parts = expr.split(",").map((part) => part.trim());
25440
25601
  const tableZone = toZone(table.ref);
25441
- const refZone = { ...tableZone };
25442
- let isReferencedZoneValid = true;
25443
- // Single column reference
25444
- if (refElements.length === 1) {
25445
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25446
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25447
- if (table.headerRowCount) {
25448
- refZone.top += table.headerRowCount;
25449
- }
25450
- if (table.totalsRowCount) {
25451
- refZone.bottom -= 1;
25602
+ const colIndexes = [];
25603
+ const rowIndexes = [];
25604
+ const foundKeywords = [];
25605
+ for (const part of parts) {
25606
+ if (removeBrackets(part).startsWith("#")) {
25607
+ const keyWord = removeBrackets(part);
25608
+ foundKeywords.push(keyWord);
25609
+ switch (keyWord) {
25610
+ case "#All":
25611
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25612
+ break;
25613
+ case "#Data":
25614
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25615
+ const bottom = table.totalsRowCount
25616
+ ? tableZone.bottom - table.totalsRowCount
25617
+ : tableZone.bottom;
25618
+ rowIndexes.push(top, bottom);
25619
+ break;
25620
+ case "#This Row":
25621
+ rowIndexes.push(toCartesian(cellXc).row);
25622
+ break;
25623
+ case "#Headers":
25624
+ if (!table.headerRowCount) {
25625
+ return CellErrorType.InvalidReference;
25626
+ }
25627
+ rowIndexes.push(tableZone.top);
25628
+ break;
25629
+ case "#Totals":
25630
+ if (!table.totalsRowCount) {
25631
+ return CellErrorType.InvalidReference;
25632
+ }
25633
+ rowIndexes.push(tableZone.bottom);
25634
+ break;
25635
+ }
25452
25636
  }
25453
- }
25454
- // Other references
25455
- else {
25456
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25457
- case "#All":
25458
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25459
- refZone.bottom = tableZone.bottom;
25460
- break;
25461
- case "#Data":
25462
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25463
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25464
- break;
25465
- case "#This Row":
25466
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25467
- break;
25468
- case "#Headers":
25469
- refZone.top = refZone.bottom = tableZone.top;
25470
- if (!table.headerRowCount) {
25471
- isReferencedZoneValid = false;
25472
- }
25473
- break;
25474
- case "#Totals":
25475
- refZone.top = refZone.bottom = tableZone.bottom;
25476
- if (!table.totalsRowCount) {
25477
- isReferencedZoneValid = false;
25637
+ else {
25638
+ const columns = part
25639
+ .split(":")
25640
+ .map((part) => part.trim())
25641
+ .map(removeBrackets);
25642
+ if (colIndexes.length) {
25643
+ return CellErrorType.InvalidReference;
25644
+ }
25645
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25646
+ if (colRelativeIndex === -1) {
25647
+ return CellErrorType.InvalidReference;
25648
+ }
25649
+ colIndexes.push(colRelativeIndex + tableZone.left);
25650
+ if (columns[1]) {
25651
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25652
+ if (colRelativeIndex2 === -1) {
25653
+ return CellErrorType.InvalidReference;
25478
25654
  }
25479
- break;
25655
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25656
+ }
25480
25657
  }
25481
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25482
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25483
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25484
25658
  }
25485
- if (!isReferencedZoneValid) {
25659
+ if (!areKeywordsCompatible(foundKeywords)) {
25486
25660
  return CellErrorType.InvalidReference;
25487
25661
  }
25488
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25662
+ if (rowIndexes.length === 0) {
25663
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25664
+ const bottom = table.totalsRowCount
25665
+ ? tableZone.bottom - table.totalsRowCount
25666
+ : tableZone.bottom;
25667
+ rowIndexes.push(top, bottom);
25668
+ }
25669
+ if (colIndexes.length === 0) {
25670
+ colIndexes.push(tableZone.left, tableZone.right);
25671
+ }
25672
+ const refZone = {
25673
+ top: Math.min(...rowIndexes),
25674
+ left: Math.min(...colIndexes),
25675
+ bottom: Math.max(...rowIndexes),
25676
+ right: Math.max(...colIndexes),
25677
+ };
25678
+ return sheetPrefix + zoneToXc(refZone);
25679
+ }
25680
+ function removeBrackets(str) {
25681
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25682
+ }
25683
+ function areKeywordsCompatible(keywords) {
25684
+ if (keywords.length < 2) {
25685
+ return true;
25686
+ }
25687
+ else if (keywords.length > 2) {
25688
+ return false;
25689
+ }
25690
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25691
+ return true;
25692
+ }
25693
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25694
+ return true;
25695
+ }
25696
+ return false;
25489
25697
  }
25490
25698
 
25491
25699
  // -------------------------------------
@@ -27936,28 +28144,46 @@ function interactivePaste(env, target, pasteOption) {
27936
28144
  const result = env.model.dispatch("PASTE", { target, pasteOption });
27937
28145
  handlePasteResult(env, result);
27938
28146
  }
27939
- function interactivePasteFromOS(env, target, clipboardContent, pasteOption) {
28147
+ async function interactivePasteFromOS(env, target, parsedClipboardContent, pasteOption) {
27940
28148
  let result;
27941
28149
  // We do not trust the clipboard content to be accurate and comprehensive.
27942
28150
  // Therefore, to ensure reliability, we handle unexpected errors that may
27943
28151
  // arise from content that would not be suitable for the current version.
27944
28152
  try {
28153
+ const clipboarContent = parsedClipboardContent;
28154
+ if (parsedClipboardContent.imageBlob) {
28155
+ try {
28156
+ const imageData = await env.imageProvider?.uploadFile(parsedClipboardContent.imageBlob);
28157
+ clipboarContent.imageData = imageData;
28158
+ }
28159
+ catch (e) {
28160
+ const msg = _t("An error occurred while uploading the image. %s", e.message);
28161
+ console.error(e);
28162
+ env.raiseError(msg);
28163
+ }
28164
+ delete parsedClipboardContent.imageBlob;
28165
+ }
27945
28166
  result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
27946
28167
  target,
27947
- clipboardContent,
28168
+ clipboardContent: parsedClipboardContent,
27948
28169
  pasteOption,
27949
28170
  });
27950
28171
  }
27951
28172
  catch (error) {
27952
- const parsedSpreadsheetContent = clipboardContent.data;
28173
+ const parsedSpreadsheetContent = parsedClipboardContent.data;
27953
28174
  if (parsedSpreadsheetContent?.version !== CURRENT_VERSION) {
27954
28175
  env.raiseError(_t("An unexpected error occurred while pasting content.\
27955
28176
  This is probably due to a spreadsheet version mismatch."));
27956
28177
  }
28178
+ else {
28179
+ env.raiseError(_t("An unexpected error occurred while pasting content.\
28180
+ Additional information can be found in the browser console."));
28181
+ console.error(error);
28182
+ }
27957
28183
  result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
27958
28184
  target,
27959
28185
  clipboardContent: {
27960
- text: clipboardContent.text,
28186
+ text: parsedClipboardContent.text,
27961
28187
  },
27962
28188
  pasteOption,
27963
28189
  });
@@ -28656,8 +28882,8 @@ function isLuxonTimeAdapterInstalled() {
28656
28882
  if (!window.Chart) {
28657
28883
  return false;
28658
28884
  }
28659
- // @ts-ignore
28660
28885
  const adapter = new window.Chart._adapters._date({});
28886
+ // @ts-ignore
28661
28887
  const isInstalled = adapter._id === "luxon";
28662
28888
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28663
28889
  missingTimeAdapterAlreadyWarned = true;
@@ -29086,6 +29312,31 @@ function getGeoChartDatasets(definition, args) {
29086
29312
  }
29087
29313
  return [dataset];
29088
29314
  }
29315
+ function getFunnelChartDatasets(definition, args) {
29316
+ const dataSetsValues = args.dataSetsValues[0];
29317
+ const labels = args.labels;
29318
+ if (!dataSetsValues) {
29319
+ return [];
29320
+ }
29321
+ let { label: datasetLabel, data } = dataSetsValues;
29322
+ datasetLabel = definition.dataSets?.[0].label || datasetLabel;
29323
+ const dataset = {
29324
+ label: datasetLabel,
29325
+ data: data.map((value) => (value <= 0 ? [0, 0] : [-value, value])),
29326
+ backgroundColor: getFunnelLabelColors(labels, definition.funnelColors),
29327
+ yAxisID: "y",
29328
+ xAxisID: "x",
29329
+ barPercentage: 1,
29330
+ categoryPercentage: 1,
29331
+ borderColor: definition.background || BACKGROUND_CHART_COLOR,
29332
+ borderWidth: 3,
29333
+ };
29334
+ return [dataset];
29335
+ }
29336
+ function getFunnelLabelColors(labels, colors) {
29337
+ const colorGenerator = new ColorGenerator(labels.length, colors);
29338
+ return labels.map(() => colorGenerator.next());
29339
+ }
29089
29340
  function getTrendingLineDataSet(dataset, config, data) {
29090
29341
  const defaultBorderColor = colorToRGBA(dataset.backgroundColor);
29091
29342
  defaultBorderColor.a = 1;
@@ -29519,6 +29770,38 @@ function getGeoChartScales(definition, args) {
29519
29770
  },
29520
29771
  };
29521
29772
  }
29773
+ function getFunnelChartScales(definition, args) {
29774
+ const dataSet = args.dataSetsValues[0];
29775
+ return {
29776
+ x: {
29777
+ display: false,
29778
+ },
29779
+ y: {
29780
+ grid: { offset: false }, // bar charts grid is offset by default
29781
+ ticks: {
29782
+ callback: function (tickValue) {
29783
+ return truncateLabel(this.getLabelForValue(tickValue));
29784
+ },
29785
+ },
29786
+ border: { display: false },
29787
+ },
29788
+ percentages: {
29789
+ position: "right",
29790
+ border: { display: false },
29791
+ ticks: {
29792
+ callback: function (tickValue, index, ticks) {
29793
+ const value = dataSet.data?.[index];
29794
+ const baseValue = dataSet.data?.[0];
29795
+ if (!baseValue || value === undefined) {
29796
+ return "";
29797
+ }
29798
+ return formatValue(value / baseValue, { format: "0%", locale: args.locale });
29799
+ },
29800
+ },
29801
+ grid: { display: false },
29802
+ },
29803
+ };
29804
+ }
29522
29805
  function getGeoChartProjection(projection) {
29523
29806
  if (projection === "conicConformal") {
29524
29807
  return window.ChartGeo.geoConicConformal().rotate([100, 0]); // Centered on the US
@@ -29882,6 +30165,23 @@ function getGeoChartTooltip(definition, args) {
29882
30165
  },
29883
30166
  };
29884
30167
  }
30168
+ function getFunnelChartTooltip(definition, args) {
30169
+ return {
30170
+ enabled: false,
30171
+ external: customTooltipHandler,
30172
+ position: "funnelTooltipPositioner",
30173
+ callbacks: {
30174
+ title: () => "",
30175
+ beforeLabel: (tooltipItem) => tooltipItem.label,
30176
+ label: function (tooltipItem) {
30177
+ const yLabel = tooltipItem.parsed.x;
30178
+ const axisId = tooltipItem.dataset.xAxisID;
30179
+ const yLabelStr = formatChartDatasetValue(args.axisFormats, args.locale)(yLabel, axisId);
30180
+ return yLabelStr;
30181
+ },
30182
+ },
30183
+ };
30184
+ }
29885
30185
  function calculatePercentage(dataset, dataIndex) {
29886
30186
  const numericData = dataset.filter((value) => typeof value === "number");
29887
30187
  const total = numericData.reduce((sum, value) => sum + value, 0);
@@ -29952,6 +30252,10 @@ var CHART_RUNTIME_HELPERS = /*#__PURE__*/Object.freeze({
29952
30252
  getComboChartDatasets: getComboChartDatasets,
29953
30253
  getComboChartLegend: getComboChartLegend,
29954
30254
  getData: getData,
30255
+ getFunnelChartDatasets: getFunnelChartDatasets,
30256
+ getFunnelChartScales: getFunnelChartScales,
30257
+ getFunnelChartTooltip: getFunnelChartTooltip,
30258
+ getFunnelLabelColors: getFunnelLabelColors,
29955
30259
  getGeoChartData: getGeoChartData,
29956
30260
  getGeoChartDatasets: getGeoChartDatasets,
29957
30261
  getGeoChartScales: getGeoChartScales,
@@ -30290,6 +30594,146 @@ function createComboChartRuntime(chart, getters) {
30290
30594
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
30291
30595
  }
30292
30596
 
30597
+ class FunnelChart extends AbstractChart {
30598
+ dataSets;
30599
+ labelRange;
30600
+ background;
30601
+ legendPosition;
30602
+ aggregated;
30603
+ type = "funnel";
30604
+ dataSetsHaveTitle;
30605
+ dataSetDesign;
30606
+ axesDesign;
30607
+ horizontal = true;
30608
+ showValues;
30609
+ funnelColors;
30610
+ constructor(definition, sheetId, getters) {
30611
+ super(definition, sheetId, getters);
30612
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
30613
+ this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
30614
+ this.background = definition.background;
30615
+ this.legendPosition = definition.legendPosition;
30616
+ this.aggregated = definition.aggregated;
30617
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
30618
+ this.dataSetDesign = definition.dataSets;
30619
+ this.axesDesign = definition.axesDesign;
30620
+ this.showValues = definition.showValues;
30621
+ this.horizontal = true;
30622
+ this.funnelColors = definition.funnelColors;
30623
+ }
30624
+ static transformDefinition(definition, executed) {
30625
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
30626
+ }
30627
+ static validateChartDefinition(validator, definition) {
30628
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
30629
+ }
30630
+ static getDefinitionFromContextCreation(context) {
30631
+ return {
30632
+ background: context.background,
30633
+ dataSets: context.range ?? [],
30634
+ dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
30635
+ aggregated: context.aggregated ?? false,
30636
+ legendPosition: "none",
30637
+ title: context.title || { text: "" },
30638
+ type: "funnel",
30639
+ labelRange: context.auxiliaryRange || undefined,
30640
+ showValues: context.showValues,
30641
+ axesDesign: context.axesDesign,
30642
+ funnelColors: context.funnelColors,
30643
+ horizontal: true,
30644
+ };
30645
+ }
30646
+ getContextCreation() {
30647
+ const range = [];
30648
+ for (const [i, dataSet] of this.dataSets.entries()) {
30649
+ range.push({
30650
+ ...this.dataSetDesign?.[i],
30651
+ dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
30652
+ });
30653
+ }
30654
+ return {
30655
+ ...this,
30656
+ range,
30657
+ auxiliaryRange: this.labelRange
30658
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
30659
+ : undefined,
30660
+ };
30661
+ }
30662
+ duplicateInDuplicatedSheet(newSheetId) {
30663
+ const dataSets = duplicateDataSetsInDuplicatedSheet(this.sheetId, newSheetId, this.dataSets);
30664
+ const labelRange = duplicateLabelRangeInDuplicatedSheet(this.sheetId, newSheetId, this.labelRange);
30665
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, newSheetId);
30666
+ return new FunnelChart(definition, newSheetId, this.getters);
30667
+ }
30668
+ copyInSheetId(sheetId) {
30669
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
30670
+ return new FunnelChart(definition, sheetId, this.getters);
30671
+ }
30672
+ getDefinition() {
30673
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
30674
+ }
30675
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
30676
+ const ranges = [];
30677
+ for (const [i, dataSet] of dataSets.entries()) {
30678
+ ranges.push({
30679
+ ...this.dataSetDesign?.[i],
30680
+ dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
30681
+ });
30682
+ }
30683
+ return {
30684
+ type: "funnel",
30685
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
30686
+ background: this.background,
30687
+ dataSets: ranges,
30688
+ legendPosition: this.legendPosition,
30689
+ labelRange: labelRange
30690
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
30691
+ : undefined,
30692
+ title: this.title,
30693
+ aggregated: this.aggregated,
30694
+ horizontal: this.horizontal,
30695
+ axesDesign: this.axesDesign,
30696
+ showValues: this.showValues,
30697
+ funnelColors: this.funnelColors,
30698
+ };
30699
+ }
30700
+ getDefinitionForExcel() {
30701
+ return undefined;
30702
+ }
30703
+ updateRanges(applyChange) {
30704
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
30705
+ if (!isStale) {
30706
+ return this;
30707
+ }
30708
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
30709
+ return new FunnelChart(definition, this.sheetId, this.getters);
30710
+ }
30711
+ }
30712
+ function createFunnelChartRuntime(chart, getters) {
30713
+ const definition = chart.getDefinition();
30714
+ const chartData = getBarChartData(definition, chart.dataSets, chart.labelRange, getters);
30715
+ const config = {
30716
+ type: "funnel",
30717
+ data: {
30718
+ labels: chartData.labels,
30719
+ datasets: getFunnelChartDatasets(definition, chartData),
30720
+ },
30721
+ options: {
30722
+ ...CHART_COMMON_OPTIONS,
30723
+ indexAxis: "y",
30724
+ layout: getChartLayout(),
30725
+ scales: getFunnelChartScales(definition, chartData),
30726
+ plugins: {
30727
+ title: getChartTitle(definition),
30728
+ legend: { display: false },
30729
+ tooltip: getFunnelChartTooltip(definition, chartData),
30730
+ chartShowValuesPlugin: getChartShowValues(definition, chartData),
30731
+ },
30732
+ },
30733
+ };
30734
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
30735
+ }
30736
+
30293
30737
  function isDataRangeValid(definition) {
30294
30738
  return definition.dataRange && !rangeReference.test(definition.dataRange)
30295
30739
  ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
@@ -31696,6 +32140,15 @@ chartRegistry.add("geo", {
31696
32140
  getChartDefinitionFromContextCreation: GeoChart.getDefinitionFromContextCreation,
31697
32141
  sequence: 90,
31698
32142
  });
32143
+ chartRegistry.add("funnel", {
32144
+ match: (type) => type === "funnel",
32145
+ createChart: (definition, sheetId, getters) => new FunnelChart(definition, sheetId, getters),
32146
+ getChartRuntime: createFunnelChartRuntime,
32147
+ validateChartDefinition: FunnelChart.validateChartDefinition,
32148
+ transformDefinition: FunnelChart.transformDefinition,
32149
+ getChartDefinitionFromContextCreation: FunnelChart.getDefinitionFromContextCreation,
32150
+ sequence: 100,
32151
+ });
31699
32152
  const chartComponentRegistry = new Registry();
31700
32153
  chartComponentRegistry.add("line", ChartJsComponent);
31701
32154
  chartComponentRegistry.add("bar", ChartJsComponent);
@@ -31708,6 +32161,7 @@ chartComponentRegistry.add("waterfall", ChartJsComponent);
31708
32161
  chartComponentRegistry.add("pyramid", ChartJsComponent);
31709
32162
  chartComponentRegistry.add("radar", ChartJsComponent);
31710
32163
  chartComponentRegistry.add("geo", ChartJsComponent);
32164
+ chartComponentRegistry.add("funnel", ChartJsComponent);
31711
32165
  const chartCategories = {
31712
32166
  line: _t("Line"),
31713
32167
  column: _t("Column"),
@@ -31874,6 +32328,13 @@ chartSubtypeRegistry
31874
32328
  chartType: "geo",
31875
32329
  category: "misc",
31876
32330
  preview: "o-spreadsheet-ChartPreview.GEO_CHART",
32331
+ })
32332
+ .add("funnel", {
32333
+ displayName: _t("Funnel"),
32334
+ chartSubtype: "funnel",
32335
+ chartType: "funnel",
32336
+ category: "misc",
32337
+ preview: "o-spreadsheet-ChartPreview.FUNNEL_CHART",
31877
32338
  });
31878
32339
 
31879
32340
  /**
@@ -31934,6 +32395,391 @@ class ImageFigure extends owl.Component {
31934
32395
  }
31935
32396
  }
31936
32397
 
32398
+ const macRegex = /Mac/i;
32399
+ const MODIFIER_KEYS = ["Shift", "Control", "Alt", "Meta"];
32400
+ /**
32401
+ * Return true if the event was triggered from
32402
+ * a child element.
32403
+ */
32404
+ function isChildEvent(parent, ev) {
32405
+ if (!parent)
32406
+ return false;
32407
+ return !!ev.target && parent.contains(ev.target);
32408
+ }
32409
+ function gridOverlayPosition() {
32410
+ const spreadsheetElement = document.querySelector(".o-grid-overlay");
32411
+ if (spreadsheetElement) {
32412
+ const { top, left } = spreadsheetElement?.getBoundingClientRect();
32413
+ return { top, left };
32414
+ }
32415
+ throw new Error("Can't find spreadsheet position");
32416
+ }
32417
+ function getBoundingRectAsPOJO(el) {
32418
+ const rect = el.getBoundingClientRect();
32419
+ return {
32420
+ x: rect.x,
32421
+ y: rect.y,
32422
+ width: rect.width,
32423
+ height: rect.height,
32424
+ };
32425
+ }
32426
+ /**
32427
+ * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
32428
+ */
32429
+ function* iterateChildren(el) {
32430
+ yield el;
32431
+ if (el.hasChildNodes()) {
32432
+ for (let child of el.childNodes) {
32433
+ yield* iterateChildren(child);
32434
+ }
32435
+ }
32436
+ }
32437
+ function getOpenedMenus() {
32438
+ return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
32439
+ }
32440
+ function getCurrentSelection(el) {
32441
+ let { startElement, endElement, startSelectionOffset, endSelectionOffset } = getStartAndEndSelection(el);
32442
+ let startSizeBefore = findSelectionIndex(el, startElement, startSelectionOffset);
32443
+ let endSizeBefore = findSelectionIndex(el, endElement, endSelectionOffset);
32444
+ return {
32445
+ start: startSizeBefore,
32446
+ end: endSizeBefore,
32447
+ };
32448
+ }
32449
+ function getStartAndEndSelection(el) {
32450
+ const selection = document.getSelection();
32451
+ return {
32452
+ startElement: selection.anchorNode || el,
32453
+ startSelectionOffset: selection.anchorOffset,
32454
+ endElement: selection.focusNode || el,
32455
+ endSelectionOffset: selection.focusOffset,
32456
+ };
32457
+ }
32458
+ /**
32459
+ * Computes the text 'index' inside this.el based on the currently selected node and its offset.
32460
+ * The selected node is either a Text node or an Element node.
32461
+ *
32462
+ * case 1 -Text node:
32463
+ * the offset is the number of characters from the start of the node. We have to add this offset to the
32464
+ * content length of all previous nodes.
32465
+ *
32466
+ * case 2 - Element node:
32467
+ * the offset is the number of child nodes before the selected node. We have to add the content length of
32468
+ * all the nodes prior to the selected node as well as the content of the child node before the offset.
32469
+ *
32470
+ * See the MDN documentation for more details.
32471
+ * https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset
32472
+ * https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset
32473
+ *
32474
+ */
32475
+ function findSelectionIndex(el, nodeToFind, nodeOffset) {
32476
+ let usedCharacters = 0;
32477
+ let it = iterateChildren(el);
32478
+ let current = it.next();
32479
+ let isFirstParagraph = true;
32480
+ while (!current.done && current.value !== nodeToFind) {
32481
+ if (!current.value.hasChildNodes()) {
32482
+ if (current.value.textContent) {
32483
+ usedCharacters += current.value.textContent.length;
32484
+ }
32485
+ }
32486
+ // One new paragraph = one new line character, except for the first paragraph
32487
+ if (current.value.nodeName === "P" ||
32488
+ (current.value.nodeName === "DIV" && current.value !== el) // On paste, the HTML may contain <div> instead of <p>
32489
+ ) {
32490
+ if (isFirstParagraph) {
32491
+ isFirstParagraph = false;
32492
+ }
32493
+ else {
32494
+ usedCharacters++;
32495
+ }
32496
+ }
32497
+ current = it.next();
32498
+ }
32499
+ if (current.value !== nodeToFind) {
32500
+ /** This situation can happen if the code is called while the selection is not currently on the element.
32501
+ * In this case, we return 0 because we don't know the size of the text before the selection.
32502
+ *
32503
+ * A known occurrence is triggered since the introduction of commit d4663158 (PR #2038).
32504
+ */
32505
+ return 0;
32506
+ }
32507
+ else {
32508
+ if (!current.value.hasChildNodes()) {
32509
+ usedCharacters += nodeOffset;
32510
+ }
32511
+ else {
32512
+ const children = [...current.value.childNodes].slice(0, nodeOffset);
32513
+ usedCharacters += children.reduce((acc, child, index) => {
32514
+ if (child.textContent !== null) {
32515
+ // need to account for paragraph nodes that implicitly add a new line
32516
+ // except for the last paragraph
32517
+ let chars = child.textContent.length;
32518
+ if (child.nodeName === "P" && index !== children.length - 1) {
32519
+ chars++;
32520
+ }
32521
+ return acc + chars;
32522
+ }
32523
+ else {
32524
+ return acc;
32525
+ }
32526
+ }, 0);
32527
+ }
32528
+ }
32529
+ if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
32530
+ usedCharacters++;
32531
+ }
32532
+ return usedCharacters;
32533
+ }
32534
+ const letterRegex = /^[a-zA-Z]$/;
32535
+ /**
32536
+ * Transform a keyboard event into a shortcut string that represent this event. The letters keys will be uppercased.
32537
+ *
32538
+ * @argument ev - The keyboard event to transform
32539
+ * @argument mode - Use either ev.key of ev.code to get the string shortcut
32540
+ *
32541
+ * @example
32542
+ * event : { ctrlKey: true, key: "a" } => "Ctrl+A"
32543
+ * event : { shift: true, alt: true, key: "Home" } => "Alt+Shift+Home"
32544
+ */
32545
+ function keyboardEventToShortcutString(ev, mode = "key") {
32546
+ let keyDownString = "";
32547
+ if (!MODIFIER_KEYS.includes(ev.key)) {
32548
+ if (isCtrlKey(ev))
32549
+ keyDownString += "Ctrl+";
32550
+ if (ev.altKey)
32551
+ keyDownString += "Alt+";
32552
+ if (ev.shiftKey)
32553
+ keyDownString += "Shift+";
32554
+ }
32555
+ const key = mode === "key" ? ev.key : ev.code;
32556
+ keyDownString += letterRegex.test(key) ? key.toUpperCase() : key;
32557
+ return keyDownString;
32558
+ }
32559
+ function isMacOS() {
32560
+ return Boolean(macRegex.test(navigator.userAgent));
32561
+ }
32562
+ /**
32563
+ * @param {KeyboardEvent | MouseEvent} ev
32564
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
32565
+ * On Mac, this is the "meta" or "command" key.
32566
+ */
32567
+ function isCtrlKey(ev) {
32568
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
32569
+ }
32570
+ /**
32571
+ * @param {MouseEvent} ev - The mouse event.
32572
+ * @returns {boolean} Returns true if the event was triggered by a middle-click
32573
+ * or a Ctrl + Click (Cmd + Click on Mac).
32574
+ */
32575
+ function isMiddleClickOrCtrlClick(ev) {
32576
+ return ev.button === 1 || (isCtrlKey(ev) && ev.button === 0);
32577
+ }
32578
+ async function convertImageToPng(imageUrl) {
32579
+ return new Promise((resolve, reject) => {
32580
+ const image = new Image();
32581
+ image.addEventListener("load", () => {
32582
+ const canvas = document.createElement("canvas");
32583
+ canvas.width = image.width;
32584
+ canvas.height = image.height;
32585
+ const ctx = canvas.getContext("2d");
32586
+ ctx?.drawImage(image, 0, 0);
32587
+ canvas.toBlob(resolve, "image/png");
32588
+ });
32589
+ image.addEventListener("error", reject);
32590
+ image.src = imageUrl;
32591
+ });
32592
+ }
32593
+ function downloadFile(dataUrl, fileName) {
32594
+ const a = document.createElement("a");
32595
+ a.href = dataUrl;
32596
+ a.download = fileName;
32597
+ document.body.appendChild(a);
32598
+ a.click();
32599
+ document.body.removeChild(a);
32600
+ }
32601
+
32602
+ /**
32603
+ * Create a function used to create a Chart based on the definition
32604
+ */
32605
+ function chartFactory(getters) {
32606
+ const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
32607
+ function createChart(id, definition, sheetId) {
32608
+ const builder = builders.find((builder) => builder.match(definition.type));
32609
+ if (!builder) {
32610
+ throw new Error(`No builder for this chart: ${definition.type}`);
32611
+ }
32612
+ return builder.createChart(definition, sheetId, getters);
32613
+ }
32614
+ return createChart;
32615
+ }
32616
+ /**
32617
+ * Create a function used to create a Chart Runtime based on the chart class
32618
+ * instance
32619
+ */
32620
+ function chartRuntimeFactory(getters) {
32621
+ const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
32622
+ function createRuntimeChart(chart) {
32623
+ const builder = builders.find((builder) => builder.match(chart.type));
32624
+ if (!builder) {
32625
+ throw new Error("No runtime builder for this chart.");
32626
+ }
32627
+ return builder.getChartRuntime(chart, getters);
32628
+ }
32629
+ return createRuntimeChart;
32630
+ }
32631
+ /**
32632
+ * Validate the chart definition given in arguments
32633
+ */
32634
+ function validateChartDefinition(validator, definition) {
32635
+ const validators = chartRegistry.getAll().find((validator) => validator.match(definition.type));
32636
+ if (!validators) {
32637
+ throw new Error("Unknown chart type.");
32638
+ }
32639
+ return validators.validateChartDefinition(validator, definition);
32640
+ }
32641
+ /**
32642
+ * Get a new chart definition transformed with the executed command. This
32643
+ * functions will be called during operational transform process
32644
+ */
32645
+ function transformDefinition(definition, executed) {
32646
+ const transformation = chartRegistry.getAll().find((factory) => factory.match(definition.type));
32647
+ if (!transformation) {
32648
+ throw new Error("Unknown chart type.");
32649
+ }
32650
+ return transformation.transformDefinition(definition, executed);
32651
+ }
32652
+ /**
32653
+ * Return a "smart" chart definition in the given zone. The definition is "smart" because it will
32654
+ * use the best type of chart to display the data of the zone.
32655
+ *
32656
+ * It will also try to find labels and datasets in the range, and try to find title for the datasets.
32657
+ *
32658
+ * The type of chart will be :
32659
+ * - If the zone is a single non-empty cell, returns a scorecard
32660
+ * - If the all the labels are numbers/date, returns a line chart
32661
+ * - Else returns a bar chart
32662
+ */
32663
+ function getSmartChartDefinition(zone, getters) {
32664
+ const sheetId = getters.getActiveSheetId();
32665
+ let dataSetZone = zone;
32666
+ const singleColumn = zoneToDimension(zone).numberOfCols === 1;
32667
+ if (!singleColumn) {
32668
+ dataSetZone = { ...zone, left: zone.left + 1 };
32669
+ }
32670
+ const dataRange = zoneToXc(getters.getUnboundedZone(sheetId, dataSetZone));
32671
+ const dataSets = [{ dataRange, yAxisId: "y" }];
32672
+ const topLeftCell = getters.getCell({ sheetId, col: zone.left, row: zone.top });
32673
+ if (getZoneArea(zone) === 1 && topLeftCell?.content) {
32674
+ return {
32675
+ type: "scorecard",
32676
+ title: {},
32677
+ background: topLeftCell.style?.fillColor || undefined,
32678
+ keyValue: zoneToXc(zone),
32679
+ baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
32680
+ baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
32681
+ baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
32682
+ };
32683
+ }
32684
+ const cellsInFirstRow = getters.getEvaluatedCellsInZone(sheetId, {
32685
+ ...dataSetZone,
32686
+ bottom: dataSetZone.top,
32687
+ });
32688
+ const dataSetsHaveTitle = !!cellsInFirstRow.find((cell) => cell.type !== CellValueType.empty && cell.type !== CellValueType.number);
32689
+ let labelRangeXc;
32690
+ if (!singleColumn) {
32691
+ labelRangeXc = zoneToXc(getters.getUnboundedZone(sheetId, { ...zone, right: zone.left }));
32692
+ }
32693
+ // Only display legend for several datasets.
32694
+ const newLegendPos = dataSetZone.right === dataSetZone.left ? "none" : "top";
32695
+ const lineChartDefinition = {
32696
+ title: {},
32697
+ dataSets,
32698
+ labelsAsText: false,
32699
+ stacked: false,
32700
+ aggregated: false,
32701
+ cumulative: false,
32702
+ labelRange: labelRangeXc,
32703
+ type: "line",
32704
+ dataSetsHaveTitle,
32705
+ legendPosition: newLegendPos,
32706
+ };
32707
+ const chart = new LineChart(lineChartDefinition, sheetId, getters);
32708
+ if (canChartParseLabels(lineChartDefinition, chart.dataSets, chart.labelRange, getters)) {
32709
+ return lineChartDefinition;
32710
+ }
32711
+ const _dataSets = createDataSets(getters, dataSets, sheetId, dataSetsHaveTitle);
32712
+ if (singleColumn &&
32713
+ getData(getters, _dataSets[0]).every((e) => typeof e === "string" && !isEvaluationError(e))) {
32714
+ return {
32715
+ title: {},
32716
+ dataSets: [{ dataRange }],
32717
+ aggregated: true,
32718
+ labelRange: dataRange,
32719
+ type: "pie",
32720
+ legendPosition: "top",
32721
+ dataSetsHaveTitle: false,
32722
+ };
32723
+ }
32724
+ return {
32725
+ title: {},
32726
+ dataSets,
32727
+ labelRange: labelRangeXc,
32728
+ type: "bar",
32729
+ stacked: false,
32730
+ aggregated: false,
32731
+ dataSetsHaveTitle,
32732
+ legendPosition: newLegendPos,
32733
+ };
32734
+ }
32735
+
32736
+ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
32737
+ __proto__: null,
32738
+ AbstractChart: AbstractChart,
32739
+ BarChart: BarChart,
32740
+ CHART_AXIS_CHOICES: CHART_AXIS_CHOICES,
32741
+ CHART_COMMON_OPTIONS: CHART_COMMON_OPTIONS,
32742
+ GaugeChart: GaugeChart,
32743
+ LineChart: LineChart,
32744
+ PieChart: PieChart,
32745
+ ScorecardChart: ScorecardChart$1,
32746
+ TREND_LINE_XAXIS_ID: TREND_LINE_XAXIS_ID,
32747
+ WaterfallChart: WaterfallChart,
32748
+ adaptChartRange: adaptChartRange,
32749
+ chartFactory: chartFactory,
32750
+ chartFontColor: chartFontColor,
32751
+ chartMutedFontColor: chartMutedFontColor,
32752
+ chartRuntimeFactory: chartRuntimeFactory,
32753
+ chartToImageFile: chartToImageFile,
32754
+ chartToImageUrl: chartToImageUrl,
32755
+ checkDataset: checkDataset,
32756
+ checkLabelRange: checkLabelRange,
32757
+ createBarChartRuntime: createBarChartRuntime,
32758
+ createDataSets: createDataSets,
32759
+ createGaugeChartRuntime: createGaugeChartRuntime,
32760
+ createLineChartRuntime: createLineChartRuntime,
32761
+ createPieChartRuntime: createPieChartRuntime,
32762
+ createScorecardChartRuntime: createScorecardChartRuntime,
32763
+ createWaterfallChartRuntime: createWaterfallChartRuntime,
32764
+ drawScoreChart: drawScoreChart,
32765
+ duplicateDataSetsInDuplicatedSheet: duplicateDataSetsInDuplicatedSheet,
32766
+ duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
32767
+ formatChartDatasetValue: formatChartDatasetValue,
32768
+ formatTickValue: formatTickValue,
32769
+ getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
32770
+ getDefinedAxis: getDefinedAxis,
32771
+ getPieColors: getPieColors,
32772
+ getSmartChartDefinition: getSmartChartDefinition,
32773
+ shouldRemoveFirstLabel: shouldRemoveFirstLabel,
32774
+ toExcelDataset: toExcelDataset,
32775
+ toExcelLabelRange: toExcelLabelRange,
32776
+ transformChartDefinitionWithDataSetsWithZone: transformChartDefinitionWithDataSetsWithZone,
32777
+ transformDefinition: transformDefinition,
32778
+ truncateLabel: truncateLabel,
32779
+ updateChartRangesWithDataSets: updateChartRangesWithDataSets,
32780
+ validateChartDefinition: validateChartDefinition
32781
+ });
32782
+
31937
32783
  function centerFigurePosition(getters, size) {
31938
32784
  const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();
31939
32785
  const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
@@ -31991,6 +32837,39 @@ function getChartMenu(figureId, onFigureDeleted, env) {
31991
32837
  },
31992
32838
  getCopyMenuItem(figureId, env),
31993
32839
  getCutMenuItem(figureId, env),
32840
+ {
32841
+ id: "copy_as_image",
32842
+ name: _t("Copy as image"),
32843
+ icon: "o-spreadsheet-Icon.COPY_AS_IMAGE",
32844
+ sequence: 4,
32845
+ execute: async () => {
32846
+ const figureSheetId = env.model.getters.getFigureSheetId(figureId);
32847
+ const figure = env.model.getters.getFigure(figureSheetId, figureId);
32848
+ const chartType = env.model.getters.getChartType(figureId);
32849
+ const runtime = env.model.getters.getChartRuntime(figureId);
32850
+ const imageUrl = chartToImageUrl(runtime, figure, chartType);
32851
+ const innerHTML = `<img src="${xmlEscape(imageUrl)}" />`;
32852
+ const blob = await chartToImageFile(runtime, figure, chartType);
32853
+ env.clipboard.write({
32854
+ "text/html": innerHTML,
32855
+ "image/png": blob,
32856
+ });
32857
+ },
32858
+ },
32859
+ {
32860
+ id: "download",
32861
+ name: _t("Download"),
32862
+ icon: "o-spreadsheet-Icon.DOWNLOAD",
32863
+ sequence: 6,
32864
+ execute: async () => {
32865
+ const figureSheetId = env.model.getters.getFigureSheetId(figureId);
32866
+ const figure = env.model.getters.getFigure(figureSheetId, figureId);
32867
+ const chartType = env.model.getters.getChartType(figureId);
32868
+ const runtime = env.model.getters.getChartRuntime(figureId);
32869
+ const url = chartToImageUrl(runtime, figure, chartType);
32870
+ downloadFile(url, "chart");
32871
+ },
32872
+ },
31994
32873
  getDeleteMenuItem(figureId, onFigureDeleted, env),
31995
32874
  ];
31996
32875
  return createActions(menuItemSpecs);
@@ -32021,6 +32900,17 @@ function getImageMenuRegistry(figureId, onFigureDeleted, env) {
32021
32900
  },
32022
32901
  icon: "o-spreadsheet-Icon.REFRESH",
32023
32902
  },
32903
+ {
32904
+ id: "download",
32905
+ name: _t("Download"),
32906
+ sequence: 6,
32907
+ execute: async () => {
32908
+ env.model.dispatch("SELECT_FIGURE", { id: figureId });
32909
+ const path = env.model.getters.getImagePath(figureId);
32910
+ downloadFile(path, "image");
32911
+ },
32912
+ icon: "o-spreadsheet-Icon.DOWNLOAD",
32913
+ },
32024
32914
  getDeleteMenuItem(figureId, onFigureDeleted, env),
32025
32915
  ];
32026
32916
  return createActions(menuItemSpecs);
@@ -32034,7 +32924,8 @@ function getCopyMenuItem(figureId, env) {
32034
32924
  execute: async () => {
32035
32925
  env.model.dispatch("SELECT_FIGURE", { id: figureId });
32036
32926
  env.model.dispatch("COPY");
32037
- await env.clipboard.write(env.model.getters.getClipboardContent());
32927
+ const osClipboardContent = await env.model.getters.getClipboardTextAndImageContent();
32928
+ await env.clipboard.write(osClipboardContent);
32038
32929
  },
32039
32930
  icon: "o-spreadsheet-Icon.CLIPBOARD",
32040
32931
  };
@@ -32048,7 +32939,7 @@ function getCutMenuItem(figureId, env) {
32048
32939
  execute: async () => {
32049
32940
  env.model.dispatch("SELECT_FIGURE", { id: figureId });
32050
32941
  env.model.dispatch("CUT");
32051
- await env.clipboard.write(env.model.getters.getClipboardContent());
32942
+ await env.clipboard.write(await env.model.getters.getClipboardTextAndImageContent());
32052
32943
  },
32053
32944
  icon: "o-spreadsheet-Icon.CUT",
32054
32945
  };
@@ -32479,6 +33370,7 @@ class Popover extends owl.Component {
32479
33370
  onPopoverHidden: { type: Function, optional: true },
32480
33371
  onPopoverMoved: { type: Function, optional: true },
32481
33372
  zIndex: { type: Number, optional: true },
33373
+ class: { type: String, optional: true },
32482
33374
  slots: Object,
32483
33375
  };
32484
33376
  static defaultProps = {
@@ -32511,10 +33403,6 @@ class Popover extends owl.Component {
32511
33403
  this.currentDisplayValue = newDisplay;
32512
33404
  if (!anchor)
32513
33405
  return;
32514
- el.style.top = "";
32515
- el.style.left = "";
32516
- el.style["max-height"] = "";
32517
- el.style["max-width"] = "";
32518
33406
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32519
33407
  let elDims = {
32520
33408
  width: el.getBoundingClientRect().width,
@@ -33059,187 +33947,6 @@ const FilterMenuPopoverBuilder = {
33059
33947
  },
33060
33948
  };
33061
33949
 
33062
- const macRegex = /Mac/i;
33063
- const MODIFIER_KEYS = ["Shift", "Control", "Alt", "Meta"];
33064
- /**
33065
- * Return true if the event was triggered from
33066
- * a child element.
33067
- */
33068
- function isChildEvent(parent, ev) {
33069
- if (!parent)
33070
- return false;
33071
- return !!ev.target && parent.contains(ev.target);
33072
- }
33073
- function gridOverlayPosition() {
33074
- const spreadsheetElement = document.querySelector(".o-grid-overlay");
33075
- if (spreadsheetElement) {
33076
- const { top, left } = spreadsheetElement?.getBoundingClientRect();
33077
- return { top, left };
33078
- }
33079
- throw new Error("Can't find spreadsheet position");
33080
- }
33081
- function getBoundingRectAsPOJO(el) {
33082
- const rect = el.getBoundingClientRect();
33083
- return {
33084
- x: rect.x,
33085
- y: rect.y,
33086
- width: rect.width,
33087
- height: rect.height,
33088
- };
33089
- }
33090
- /**
33091
- * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
33092
- */
33093
- function* iterateChildren(el) {
33094
- yield el;
33095
- if (el.hasChildNodes()) {
33096
- for (let child of el.childNodes) {
33097
- yield* iterateChildren(child);
33098
- }
33099
- }
33100
- }
33101
- function getOpenedMenus() {
33102
- return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
33103
- }
33104
- function getCurrentSelection(el) {
33105
- let { startElement, endElement, startSelectionOffset, endSelectionOffset } = getStartAndEndSelection(el);
33106
- let startSizeBefore = findSelectionIndex(el, startElement, startSelectionOffset);
33107
- let endSizeBefore = findSelectionIndex(el, endElement, endSelectionOffset);
33108
- return {
33109
- start: startSizeBefore,
33110
- end: endSizeBefore,
33111
- };
33112
- }
33113
- function getStartAndEndSelection(el) {
33114
- const selection = document.getSelection();
33115
- return {
33116
- startElement: selection.anchorNode || el,
33117
- startSelectionOffset: selection.anchorOffset,
33118
- endElement: selection.focusNode || el,
33119
- endSelectionOffset: selection.focusOffset,
33120
- };
33121
- }
33122
- /**
33123
- * Computes the text 'index' inside this.el based on the currently selected node and its offset.
33124
- * The selected node is either a Text node or an Element node.
33125
- *
33126
- * case 1 -Text node:
33127
- * the offset is the number of characters from the start of the node. We have to add this offset to the
33128
- * content length of all previous nodes.
33129
- *
33130
- * case 2 - Element node:
33131
- * the offset is the number of child nodes before the selected node. We have to add the content length of
33132
- * all the nodes prior to the selected node as well as the content of the child node before the offset.
33133
- *
33134
- * See the MDN documentation for more details.
33135
- * https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset
33136
- * https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset
33137
- *
33138
- */
33139
- function findSelectionIndex(el, nodeToFind, nodeOffset) {
33140
- let usedCharacters = 0;
33141
- let it = iterateChildren(el);
33142
- let current = it.next();
33143
- let isFirstParagraph = true;
33144
- while (!current.done && current.value !== nodeToFind) {
33145
- if (!current.value.hasChildNodes()) {
33146
- if (current.value.textContent) {
33147
- usedCharacters += current.value.textContent.length;
33148
- }
33149
- }
33150
- // One new paragraph = one new line character, except for the first paragraph
33151
- if (current.value.nodeName === "P" ||
33152
- (current.value.nodeName === "DIV" && current.value !== el) // On paste, the HTML may contain <div> instead of <p>
33153
- ) {
33154
- if (isFirstParagraph) {
33155
- isFirstParagraph = false;
33156
- }
33157
- else {
33158
- usedCharacters++;
33159
- }
33160
- }
33161
- current = it.next();
33162
- }
33163
- if (current.value !== nodeToFind) {
33164
- /** This situation can happen if the code is called while the selection is not currently on the element.
33165
- * In this case, we return 0 because we don't know the size of the text before the selection.
33166
- *
33167
- * A known occurrence is triggered since the introduction of commit d4663158 (PR #2038).
33168
- */
33169
- return 0;
33170
- }
33171
- else {
33172
- if (!current.value.hasChildNodes()) {
33173
- usedCharacters += nodeOffset;
33174
- }
33175
- else {
33176
- const children = [...current.value.childNodes].slice(0, nodeOffset);
33177
- usedCharacters += children.reduce((acc, child, index) => {
33178
- if (child.textContent !== null) {
33179
- // need to account for paragraph nodes that implicitly add a new line
33180
- // except for the last paragraph
33181
- let chars = child.textContent.length;
33182
- if (child.nodeName === "P" && index !== children.length - 1) {
33183
- chars++;
33184
- }
33185
- return acc + chars;
33186
- }
33187
- else {
33188
- return acc;
33189
- }
33190
- }, 0);
33191
- }
33192
- }
33193
- if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
33194
- usedCharacters++;
33195
- }
33196
- return usedCharacters;
33197
- }
33198
- const letterRegex = /^[a-zA-Z]$/;
33199
- /**
33200
- * Transform a keyboard event into a shortcut string that represent this event. The letters keys will be uppercased.
33201
- *
33202
- * @argument ev - The keyboard event to transform
33203
- * @argument mode - Use either ev.key of ev.code to get the string shortcut
33204
- *
33205
- * @example
33206
- * event : { ctrlKey: true, key: "a" } => "Ctrl+A"
33207
- * event : { shift: true, alt: true, key: "Home" } => "Alt+Shift+Home"
33208
- */
33209
- function keyboardEventToShortcutString(ev, mode = "key") {
33210
- let keyDownString = "";
33211
- if (!MODIFIER_KEYS.includes(ev.key)) {
33212
- if (isCtrlKey(ev))
33213
- keyDownString += "Ctrl+";
33214
- if (ev.altKey)
33215
- keyDownString += "Alt+";
33216
- if (ev.shiftKey)
33217
- keyDownString += "Shift+";
33218
- }
33219
- const key = mode === "key" ? ev.key : ev.code;
33220
- keyDownString += letterRegex.test(key) ? key.toUpperCase() : key;
33221
- return keyDownString;
33222
- }
33223
- function isMacOS() {
33224
- return Boolean(macRegex.test(navigator.userAgent));
33225
- }
33226
- /**
33227
- * @param {KeyboardEvent | MouseEvent} ev
33228
- * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
33229
- * On Mac, this is the "meta" or "command" key.
33230
- */
33231
- function isCtrlKey(ev) {
33232
- return isMacOS() ? ev.metaKey : ev.ctrlKey;
33233
- }
33234
- /**
33235
- * @param {MouseEvent} ev - The mouse event.
33236
- * @returns {boolean} Returns true if the event was triggered by a middle-click
33237
- * or a Ctrl + Click (Cmd + Click on Mac).
33238
- */
33239
- function isMiddleClickOrCtrlClick(ev) {
33240
- return ev.button === 1 || (isCtrlKey(ev) && ev.button === 0);
33241
- }
33242
-
33243
33950
  const LINK_TOOLTIP_HEIGHT = 32;
33244
33951
  const LINK_TOOLTIP_WIDTH = 220;
33245
33952
  css /* scss */ `
@@ -33909,186 +34616,6 @@ cellPopoverRegistry
33909
34616
  .add("LinkEditor", LinkEditorPopoverBuilder)
33910
34617
  .add("FilterMenu", FilterMenuPopoverBuilder);
33911
34618
 
33912
- /**
33913
- * Create a function used to create a Chart based on the definition
33914
- */
33915
- function chartFactory(getters) {
33916
- const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
33917
- function createChart(id, definition, sheetId) {
33918
- const builder = builders.find((builder) => builder.match(definition.type));
33919
- if (!builder) {
33920
- throw new Error(`No builder for this chart: ${definition.type}`);
33921
- }
33922
- return builder.createChart(definition, sheetId, getters);
33923
- }
33924
- return createChart;
33925
- }
33926
- /**
33927
- * Create a function used to create a Chart Runtime based on the chart class
33928
- * instance
33929
- */
33930
- function chartRuntimeFactory(getters) {
33931
- const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
33932
- function createRuntimeChart(chart) {
33933
- const builder = builders.find((builder) => builder.match(chart.type));
33934
- if (!builder) {
33935
- throw new Error("No runtime builder for this chart.");
33936
- }
33937
- return builder.getChartRuntime(chart, getters);
33938
- }
33939
- return createRuntimeChart;
33940
- }
33941
- /**
33942
- * Validate the chart definition given in arguments
33943
- */
33944
- function validateChartDefinition(validator, definition) {
33945
- const validators = chartRegistry.getAll().find((validator) => validator.match(definition.type));
33946
- if (!validators) {
33947
- throw new Error("Unknown chart type.");
33948
- }
33949
- return validators.validateChartDefinition(validator, definition);
33950
- }
33951
- /**
33952
- * Get a new chart definition transformed with the executed command. This
33953
- * functions will be called during operational transform process
33954
- */
33955
- function transformDefinition(definition, executed) {
33956
- const transformation = chartRegistry.getAll().find((factory) => factory.match(definition.type));
33957
- if (!transformation) {
33958
- throw new Error("Unknown chart type.");
33959
- }
33960
- return transformation.transformDefinition(definition, executed);
33961
- }
33962
- /**
33963
- * Return a "smart" chart definition in the given zone. The definition is "smart" because it will
33964
- * use the best type of chart to display the data of the zone.
33965
- *
33966
- * It will also try to find labels and datasets in the range, and try to find title for the datasets.
33967
- *
33968
- * The type of chart will be :
33969
- * - If the zone is a single non-empty cell, returns a scorecard
33970
- * - If the all the labels are numbers/date, returns a line chart
33971
- * - Else returns a bar chart
33972
- */
33973
- function getSmartChartDefinition(zone, getters) {
33974
- const sheetId = getters.getActiveSheetId();
33975
- let dataSetZone = zone;
33976
- const singleColumn = zoneToDimension(zone).numberOfCols === 1;
33977
- if (!singleColumn) {
33978
- dataSetZone = { ...zone, left: zone.left + 1 };
33979
- }
33980
- const dataRange = zoneToXc(getters.getUnboundedZone(sheetId, dataSetZone));
33981
- const dataSets = [{ dataRange, yAxisId: "y" }];
33982
- const topLeftCell = getters.getCell({ sheetId, col: zone.left, row: zone.top });
33983
- if (getZoneArea(zone) === 1 && topLeftCell?.content) {
33984
- return {
33985
- type: "scorecard",
33986
- title: {},
33987
- background: topLeftCell.style?.fillColor || undefined,
33988
- keyValue: zoneToXc(zone),
33989
- baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
33990
- baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
33991
- baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
33992
- };
33993
- }
33994
- const cellsInFirstRow = getters.getEvaluatedCellsInZone(sheetId, {
33995
- ...dataSetZone,
33996
- bottom: dataSetZone.top,
33997
- });
33998
- const dataSetsHaveTitle = !!cellsInFirstRow.find((cell) => cell.type !== CellValueType.empty && cell.type !== CellValueType.number);
33999
- let labelRangeXc;
34000
- if (!singleColumn) {
34001
- labelRangeXc = zoneToXc(getters.getUnboundedZone(sheetId, { ...zone, right: zone.left }));
34002
- }
34003
- // Only display legend for several datasets.
34004
- const newLegendPos = dataSetZone.right === dataSetZone.left ? "none" : "top";
34005
- const lineChartDefinition = {
34006
- title: {},
34007
- dataSets,
34008
- labelsAsText: false,
34009
- stacked: false,
34010
- aggregated: false,
34011
- cumulative: false,
34012
- labelRange: labelRangeXc,
34013
- type: "line",
34014
- dataSetsHaveTitle,
34015
- legendPosition: newLegendPos,
34016
- };
34017
- const chart = new LineChart(lineChartDefinition, sheetId, getters);
34018
- if (canChartParseLabels(lineChartDefinition, chart.dataSets, chart.labelRange, getters)) {
34019
- return lineChartDefinition;
34020
- }
34021
- const _dataSets = createDataSets(getters, dataSets, sheetId, dataSetsHaveTitle);
34022
- if (singleColumn &&
34023
- getData(getters, _dataSets[0]).every((e) => typeof e === "string" && !isEvaluationError(e))) {
34024
- return {
34025
- title: {},
34026
- dataSets: [{ dataRange }],
34027
- aggregated: true,
34028
- labelRange: dataRange,
34029
- type: "pie",
34030
- legendPosition: "top",
34031
- dataSetsHaveTitle: false,
34032
- };
34033
- }
34034
- return {
34035
- title: {},
34036
- dataSets,
34037
- labelRange: labelRangeXc,
34038
- type: "bar",
34039
- stacked: false,
34040
- aggregated: false,
34041
- dataSetsHaveTitle,
34042
- legendPosition: newLegendPos,
34043
- };
34044
- }
34045
-
34046
- var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34047
- __proto__: null,
34048
- AbstractChart: AbstractChart,
34049
- BarChart: BarChart,
34050
- CHART_AXIS_CHOICES: CHART_AXIS_CHOICES,
34051
- CHART_COMMON_OPTIONS: CHART_COMMON_OPTIONS,
34052
- GaugeChart: GaugeChart,
34053
- LineChart: LineChart,
34054
- PieChart: PieChart,
34055
- ScorecardChart: ScorecardChart$1,
34056
- TREND_LINE_XAXIS_ID: TREND_LINE_XAXIS_ID,
34057
- WaterfallChart: WaterfallChart,
34058
- adaptChartRange: adaptChartRange,
34059
- chartFactory: chartFactory,
34060
- chartFontColor: chartFontColor,
34061
- chartMutedFontColor: chartMutedFontColor,
34062
- chartRuntimeFactory: chartRuntimeFactory,
34063
- chartToImage: chartToImage,
34064
- checkDataset: checkDataset,
34065
- checkLabelRange: checkLabelRange,
34066
- createBarChartRuntime: createBarChartRuntime,
34067
- createDataSets: createDataSets,
34068
- createGaugeChartRuntime: createGaugeChartRuntime,
34069
- createLineChartRuntime: createLineChartRuntime,
34070
- createPieChartRuntime: createPieChartRuntime,
34071
- createScorecardChartRuntime: createScorecardChartRuntime,
34072
- createWaterfallChartRuntime: createWaterfallChartRuntime,
34073
- drawScoreChart: drawScoreChart,
34074
- duplicateDataSetsInDuplicatedSheet: duplicateDataSetsInDuplicatedSheet,
34075
- duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34076
- formatChartDatasetValue: formatChartDatasetValue,
34077
- formatTickValue: formatTickValue,
34078
- getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34079
- getDefinedAxis: getDefinedAxis,
34080
- getPieColors: getPieColors,
34081
- getSmartChartDefinition: getSmartChartDefinition,
34082
- shouldRemoveFirstLabel: shouldRemoveFirstLabel,
34083
- toExcelDataset: toExcelDataset,
34084
- toExcelLabelRange: toExcelLabelRange,
34085
- transformChartDefinitionWithDataSetsWithZone: transformChartDefinitionWithDataSetsWithZone,
34086
- transformDefinition: transformDefinition,
34087
- truncateLabel: truncateLabel,
34088
- updateChartRangesWithDataSets: updateChartRangesWithDataSets,
34089
- validateChartDefinition: validateChartDefinition
34090
- });
34091
-
34092
34619
  /**
34093
34620
  * Create a table on the selected zone, with UI warnings to the user if the creation fails.
34094
34621
  * If a single cell is selected, expand the selection to non-empty adjacent cells to create a table.
@@ -34143,11 +34670,12 @@ async function paste$1(env, pasteOption) {
34143
34670
  const osClipboard = await env.clipboard.read();
34144
34671
  switch (osClipboard.status) {
34145
34672
  case "ok":
34146
- const clipboardContent = parseOSClipboardContent(osClipboard.content);
34147
- const clipboardId = clipboardContent.data?.clipboardId;
34673
+ const clipboardId = env.model.getters.getClipboardId();
34674
+ const osClipboardContent = parseOSClipboardContent(osClipboard.content);
34675
+ const osClipboardId = osClipboardContent.data?.clipboardId;
34148
34676
  const target = env.model.getters.getSelectedZones();
34149
- if (env.model.getters.getClipboardId() !== clipboardId) {
34150
- interactivePasteFromOS(env, target, clipboardContent, pasteOption);
34677
+ if (clipboardId !== osClipboardId) {
34678
+ await interactivePasteFromOS(env, target, osClipboardContent, pasteOption);
34151
34679
  }
34152
34680
  else {
34153
34681
  interactivePaste(env, target, pasteOption);
@@ -34507,7 +35035,7 @@ async function requestImage(env) {
34507
35035
  }
34508
35036
  catch {
34509
35037
  env.raiseError(_t("An unexpected error occurred during the image transfer"));
34510
- return undefined;
35038
+ return;
34511
35039
  }
34512
35040
  }
34513
35041
  const CREATE_IMAGE = async (env) => {
@@ -34516,7 +35044,7 @@ const CREATE_IMAGE = async (env) => {
34516
35044
  const figureId = env.model.uuidGenerator.smallUuid();
34517
35045
  const image = await requestImage(env);
34518
35046
  if (!image) {
34519
- throw new Error("No image provider was given to the environment");
35047
+ return;
34520
35048
  }
34521
35049
  const size = getMaxFigureSize(env.model.getters, image.size);
34522
35050
  const position = centerFigurePosition(env.model.getters, size);
@@ -34658,7 +35186,7 @@ const copy = {
34658
35186
  isReadonlyAllowed: true,
34659
35187
  execute: async (env) => {
34660
35188
  env.model.dispatch("COPY");
34661
- await env.clipboard.write(env.model.getters.getClipboardContent());
35189
+ await env.clipboard.write(await env.model.getters.getClipboardTextAndImageContent());
34662
35190
  },
34663
35191
  icon: "o-spreadsheet-Icon.CLIPBOARD",
34664
35192
  };
@@ -34667,7 +35195,7 @@ const cut = {
34667
35195
  description: "Ctrl+X",
34668
35196
  execute: async (env) => {
34669
35197
  interactiveCut(env);
34670
- await env.clipboard.write(env.model.getters.getClipboardContent());
35198
+ await env.clipboard.write(await env.model.getters.getClipboardTextAndImageContent());
34671
35199
  },
34672
35200
  icon: "o-spreadsheet-Icon.CUT",
34673
35201
  };
@@ -34704,7 +35232,7 @@ const findAndReplace = {
34704
35232
  };
34705
35233
  const deleteValues = {
34706
35234
  name: _t("Delete values"),
34707
- execute: (env) => env.model.dispatch("DELETE_CONTENT", {
35235
+ execute: (env) => env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
34708
35236
  sheetId: env.model.getters.getActiveSheetId(),
34709
35237
  target: env.model.getters.getSelectedZones(),
34710
35238
  }),
@@ -34806,32 +35334,6 @@ function toggleMerge(env) {
34806
35334
  }
34807
35335
  }
34808
35336
 
34809
- var ACTION_EDIT = /*#__PURE__*/Object.freeze({
34810
- __proto__: null,
34811
- clearCols: clearCols,
34812
- clearRows: clearRows,
34813
- copy: copy,
34814
- cut: cut,
34815
- deleteCellShiftLeft: deleteCellShiftLeft,
34816
- deleteCellShiftUp: deleteCellShiftUp,
34817
- deleteCells: deleteCells,
34818
- deleteCol: deleteCol,
34819
- deleteCols: deleteCols,
34820
- deleteRow: deleteRow,
34821
- deleteRows: deleteRows,
34822
- deleteTable: deleteTable,
34823
- deleteValues: deleteValues,
34824
- editTable: editTable,
34825
- findAndReplace: findAndReplace,
34826
- mergeCells: mergeCells,
34827
- paste: paste,
34828
- pasteSpecial: pasteSpecial,
34829
- pasteSpecialFormat: pasteSpecialFormat,
34830
- pasteSpecialValue: pasteSpecialValue,
34831
- redo: redo,
34832
- undo: undo
34833
- });
34834
-
34835
35337
  const insertRow = {
34836
35338
  name: (env) => {
34837
35339
  const number = getRowsNumber(env);
@@ -35475,21 +35977,6 @@ const reinsertStaticPivotMenu = {
35475
35977
  isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid()),
35476
35978
  };
35477
35979
 
35478
- var ACTION_DATA = /*#__PURE__*/Object.freeze({
35479
- __proto__: null,
35480
- createRemoveFilter: createRemoveFilter,
35481
- createRemoveFilterTool: createRemoveFilterTool,
35482
- dataCleanup: dataCleanup,
35483
- reinsertDynamicPivotMenu: reinsertDynamicPivotMenu,
35484
- reinsertStaticPivotMenu: reinsertStaticPivotMenu,
35485
- removeDuplicates: removeDuplicates,
35486
- sortAscending: sortAscending,
35487
- sortDescending: sortDescending,
35488
- sortRange: sortRange,
35489
- splitToColumns: splitToColumns,
35490
- trimWhitespace: trimWhitespace
35491
- });
35492
-
35493
35980
  /**
35494
35981
  * Create a format action specification for a given format.
35495
35982
  * The format can be dynamically computed from the environment.
@@ -35632,7 +36119,7 @@ const formatNumberShortMonth = createFormatActionSpec({
35632
36119
  format: "mmm yyyy",
35633
36120
  descriptionValue: EXAMPLE_DATE,
35634
36121
  });
35635
- const incraseDecimalPlaces = {
36122
+ const increaseDecimalPlaces = {
35636
36123
  name: _t("Increase decimal places"),
35637
36124
  icon: "o-spreadsheet-Icon.INCREASE_DECIMAL",
35638
36125
  execute: (env) => env.model.dispatch("SET_DECIMAL", {
@@ -35641,7 +36128,7 @@ const incraseDecimalPlaces = {
35641
36128
  step: 1,
35642
36129
  }),
35643
36130
  };
35644
- const decraseDecimalPlaces = {
36131
+ const decreaseDecimalPlaces = {
35645
36132
  name: _t("Decrease decimal places"),
35646
36133
  icon: "o-spreadsheet-Icon.DECRASE_DECIMAL",
35647
36134
  execute: (env) => env.model.dispatch("SET_DECIMAL", {
@@ -35759,14 +36246,14 @@ const formatWrappingClip = {
35759
36246
  isActive: (env) => getWrappingMode(env) === "clip",
35760
36247
  icon: "o-spreadsheet-Icon.WRAPPING_CLIP",
35761
36248
  };
35762
- const textColor = {
36249
+ ({
35763
36250
  name: _t("Text Color"),
35764
36251
  icon: "o-spreadsheet-Icon.TEXT_COLOR",
35765
- };
35766
- const fillColor = {
36252
+ });
36253
+ ({
35767
36254
  name: _t("Fill Color"),
35768
36255
  icon: "o-spreadsheet-Icon.FILL_COLOR",
35769
- };
36256
+ });
35770
36257
  const formatCF = {
35771
36258
  name: _t("Conditional formatting"),
35772
36259
  execute: OPEN_CF_SIDEPANEL_ACTION,
@@ -35868,60 +36355,6 @@ function getWrapModeIcon(env) {
35868
36355
  }
35869
36356
  }
35870
36357
 
35871
- var ACTION_FORMAT = /*#__PURE__*/Object.freeze({
35872
- __proto__: null,
35873
- EXAMPLE_DATE: EXAMPLE_DATE,
35874
- clearFormat: clearFormat,
35875
- createFormatActionSpec: createFormatActionSpec,
35876
- decraseDecimalPlaces: decraseDecimalPlaces,
35877
- fillColor: fillColor,
35878
- formatAlignment: formatAlignment,
35879
- formatAlignmentBottom: formatAlignmentBottom,
35880
- formatAlignmentCenter: formatAlignmentCenter,
35881
- formatAlignmentHorizontal: formatAlignmentHorizontal,
35882
- formatAlignmentLeft: formatAlignmentLeft,
35883
- formatAlignmentMiddle: formatAlignmentMiddle,
35884
- formatAlignmentRight: formatAlignmentRight,
35885
- formatAlignmentTop: formatAlignmentTop,
35886
- formatAlignmentVertical: formatAlignmentVertical,
35887
- formatBold: formatBold,
35888
- formatCF: formatCF,
35889
- formatCustomCurrency: formatCustomCurrency,
35890
- formatFontSize: formatFontSize,
35891
- formatItalic: formatItalic,
35892
- formatNumberAccounting: formatNumberAccounting,
35893
- formatNumberAutomatic: formatNumberAutomatic,
35894
- formatNumberCurrency: formatNumberCurrency,
35895
- formatNumberCurrencyRounded: formatNumberCurrencyRounded,
35896
- formatNumberDate: formatNumberDate,
35897
- formatNumberDateTime: formatNumberDateTime,
35898
- formatNumberDayAndFullMonth: formatNumberDayAndFullMonth,
35899
- formatNumberDayAndShortMonth: formatNumberDayAndShortMonth,
35900
- formatNumberDuration: formatNumberDuration,
35901
- formatNumberFullDateTime: formatNumberFullDateTime,
35902
- formatNumberFullMonth: formatNumberFullMonth,
35903
- formatNumberFullQuarter: formatNumberFullQuarter,
35904
- formatNumberFullWeekDayAndMonth: formatNumberFullWeekDayAndMonth,
35905
- formatNumberNumber: formatNumberNumber,
35906
- formatNumberPercent: formatNumberPercent,
35907
- formatNumberPlainText: formatNumberPlainText,
35908
- formatNumberQuarter: formatNumberQuarter,
35909
- formatNumberShortMonth: formatNumberShortMonth,
35910
- formatNumberShortWeekDay: formatNumberShortWeekDay,
35911
- formatNumberTime: formatNumberTime,
35912
- formatPercent: formatPercent,
35913
- formatStrikethrough: formatStrikethrough,
35914
- formatUnderline: formatUnderline,
35915
- formatWrapping: formatWrapping,
35916
- formatWrappingClip: formatWrappingClip,
35917
- formatWrappingIcon: formatWrappingIcon,
35918
- formatWrappingOverflow: formatWrappingOverflow,
35919
- formatWrappingWrap: formatWrappingWrap,
35920
- incraseDecimalPlaces: incraseDecimalPlaces,
35921
- moreFormats: moreFormats,
35922
- textColor: textColor
35923
- });
35924
-
35925
36358
  function interactiveFreezeColumnsRows(env, dimension, base) {
35926
36359
  const sheetId = env.model.getters.getActiveSheetId();
35927
36360
  const cmd = dimension === "COL" ? "FREEZE_COLUMNS" : "FREEZE_ROWS";
@@ -37875,6 +38308,11 @@ class SelectionInputStore extends SpreadsheetStore {
37875
38308
  }
37876
38309
  updateColors(colors) {
37877
38310
  this.colors = colors;
38311
+ const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
38312
+ this.ranges = this.ranges.map((range) => ({
38313
+ ...range,
38314
+ color: colorGenerator.next(),
38315
+ }));
37878
38316
  }
37879
38317
  confirm() {
37880
38318
  for (const range of this.selectionInputs) {
@@ -37909,12 +38347,11 @@ class SelectionInputStore extends SpreadsheetStore {
37909
38347
  * e.g. ["A1", "Sheet2!B3", "E12"]
37910
38348
  */
37911
38349
  get selectionInputs() {
37912
- const generator = new ColorGenerator(this.ranges.length, this.colors);
37913
38350
  return this.ranges.map((input, index) => Object.assign({}, input, {
37914
38351
  color: this.hasMainFocus &&
37915
38352
  this.focusedRangeIndex !== null &&
37916
38353
  this.getters.isRangeValid(input.xc)
37917
- ? generator.next()
38354
+ ? input.color
37918
38355
  : null,
37919
38356
  isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
37920
38357
  isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
@@ -38184,10 +38621,10 @@ class SelectionInput extends owl.Component {
38184
38621
  if (originalIndex === finalIndex) {
38185
38622
  return;
38186
38623
  }
38187
- const draggedItems = [...draggableIds];
38188
- draggedItems.splice(originalIndex, 1);
38189
- draggedItems.splice(finalIndex, 0, rangeId);
38190
- this.props.onSelectionReordered?.(this.store.selectionInputs.map((range) => draggedItems.indexOf(range.id)));
38624
+ const indexes = range(0, draggableIds.length);
38625
+ indexes.splice(originalIndex, 1);
38626
+ indexes.splice(finalIndex, 0, originalIndex);
38627
+ this.props.onSelectionReordered?.(indexes);
38191
38628
  this.props.onSelectionConfirmed?.();
38192
38629
  this.store.confirm();
38193
38630
  },
@@ -38465,6 +38902,9 @@ class GenericChartConfigPanel extends owl.Component {
38465
38902
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
38466
38903
  dataSets: this.dataSets,
38467
38904
  });
38905
+ if (this.state.datasetDispatchResult.isSuccessful) {
38906
+ this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
38907
+ }
38468
38908
  }
38469
38909
  getDataSeriesRanges() {
38470
38910
  return this.dataSets;
@@ -39011,8 +39451,12 @@ class FontSizeEditor extends owl.Component {
39011
39451
  currentFontSize: Number,
39012
39452
  onFontSizeChanged: Function,
39013
39453
  onToggle: { type: Function, optional: true },
39454
+ onFocusInput: { type: Function, optional: true },
39014
39455
  class: String,
39015
39456
  };
39457
+ static defaultProps = {
39458
+ onFocusInput: () => { },
39459
+ };
39016
39460
  static components = { Popover };
39017
39461
  fontSizes = FONT_SIZES;
39018
39462
  dropdown = owl.useState({ isOpen: false });
@@ -39705,6 +40149,36 @@ class ComboChartDesignPanel extends ChartWithAxisDesignPanel {
39705
40149
  }
39706
40150
  }
39707
40151
 
40152
+ class FunnelChartDesignPanel extends owl.Component {
40153
+ static template = "o-spreadsheet-FunnelChartDesignPanel";
40154
+ static components = {
40155
+ GeneralDesignEditor,
40156
+ SidePanelCollapsible,
40157
+ RoundColorPicker,
40158
+ Section,
40159
+ Checkbox,
40160
+ };
40161
+ static props = {
40162
+ figureId: String,
40163
+ definition: Object,
40164
+ updateChart: Function,
40165
+ canUpdateChart: Function,
40166
+ };
40167
+ getFunnelColorItems() {
40168
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);
40169
+ const labels = (runtime.chartJsConfig.data.labels || []);
40170
+ const colors = getFunnelLabelColors(labels, this.props.definition.funnelColors);
40171
+ return labels.map((label, index) => ({
40172
+ label: label || _t("Value %s", index + 1),
40173
+ color: colors[index],
40174
+ }));
40175
+ }
40176
+ updateFunnelItemColor(index, color) {
40177
+ const funnelColors = replaceItemAtIndex(this.props.definition.funnelColors || [], color, index);
40178
+ this.props.updateChart(this.props.figureId, { funnelColors });
40179
+ }
40180
+ }
40181
+
39708
40182
  class GaugeChartConfigPanel extends owl.Component {
39709
40183
  static template = "o-spreadsheet-GaugeChartConfigPanel";
39710
40184
  static components = { ChartErrorSection, ChartDataSeries };
@@ -39881,8 +40355,16 @@ class ContentEditableHelper {
39881
40355
  }
39882
40356
  let startNode = this.findChildAtCharacterIndex(start);
39883
40357
  let endNode = this.findChildAtCharacterIndex(end);
39884
- range.setStart(startNode.node, startNode.offset);
39885
- range.setEnd(endNode.node, endNode.offset);
40358
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
40359
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
40360
+ if (start <= end) {
40361
+ range.setStart(startNode.node, startNode.offset);
40362
+ range.setEnd(endNode.node, endNode.offset);
40363
+ }
40364
+ else {
40365
+ range.setStart(endNode.node, endNode.offset);
40366
+ range.setEnd(startNode.node, startNode.offset);
40367
+ }
39886
40368
  }
39887
40369
  }
39888
40370
  /**
@@ -40184,8 +40666,7 @@ css /* scss */ `
40184
40666
  }
40185
40667
 
40186
40668
  .o-composer-assistant {
40187
- position: absolute;
40188
- margin: 1px 4px;
40669
+ margin-top: 1px;
40189
40670
 
40190
40671
  .o-semi-bold {
40191
40672
  /* FIXME: to remove in favor of Bootstrap
@@ -40236,10 +40717,11 @@ class Composer extends owl.Component {
40236
40717
  });
40237
40718
  compositionActive = false;
40238
40719
  spreadsheetRect = useSpreadsheetRect();
40239
- get assistantStyle() {
40720
+ get assistantStyleProperties() {
40240
40721
  const composerRect = this.composerRef.el.getBoundingClientRect();
40241
40722
  const assistantStyle = {};
40242
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40723
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40724
+ assistantStyle["min-width"] = `${minWidth}px`;
40243
40725
  const proposals = this.autoCompleteState.provider?.proposals;
40244
40726
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40245
40727
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40263,13 +40745,29 @@ class Composer extends owl.Component {
40263
40745
  }
40264
40746
  }
40265
40747
  else {
40266
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40748
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40267
40749
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40268
40750
  this.spreadsheetRect.width) {
40269
40751
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40270
40752
  }
40271
40753
  }
40272
- return cssPropertiesToCss(assistantStyle);
40754
+ return assistantStyle;
40755
+ }
40756
+ get assistantStyle() {
40757
+ const allProperties = this.assistantStyleProperties;
40758
+ return cssPropertiesToCss({
40759
+ "max-height": allProperties["max-height"],
40760
+ width: allProperties["width"],
40761
+ "min-width": allProperties["min-width"],
40762
+ });
40763
+ }
40764
+ get assistantContainerStyle() {
40765
+ const allProperties = this.assistantStyleProperties;
40766
+ return cssPropertiesToCss({
40767
+ top: allProperties["top"],
40768
+ right: allProperties["right"],
40769
+ transform: allProperties["transform"],
40770
+ });
40273
40771
  }
40274
40772
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40275
40773
  shouldProcessInputEvents = false;
@@ -41454,6 +41952,10 @@ chartSidePanelComponentRegistry
41454
41952
  .add("geo", {
41455
41953
  configuration: GeoChartConfigPanel,
41456
41954
  design: GeoChartDesignPanel,
41955
+ })
41956
+ .add("funnel", {
41957
+ configuration: GenericChartConfigPanel,
41958
+ design: FunnelChartDesignPanel,
41457
41959
  });
41458
41960
 
41459
41961
  css /* scss */ `
@@ -44796,6 +45298,10 @@ class TextInput extends owl.Component {
44796
45298
  type: String,
44797
45299
  optional: true,
44798
45300
  },
45301
+ autofocus: {
45302
+ type: Boolean,
45303
+ optional: true,
45304
+ },
44799
45305
  };
44800
45306
  inputRef = owl.useRef("input");
44801
45307
  setup() {
@@ -44804,6 +45310,9 @@ class TextInput extends owl.Component {
44804
45310
  this.save();
44805
45311
  }
44806
45312
  }, { capture: true });
45313
+ if (this.props.autofocus) {
45314
+ useAutofocus({ refName: "input" });
45315
+ }
44807
45316
  }
44808
45317
  onKeyDown(ev) {
44809
45318
  switch (ev.key) {
@@ -46743,9 +47252,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46743
47252
  pivot: this.draft,
46744
47253
  });
46745
47254
  this.draft = null;
46746
- if (!this.alreadyNotified &&
46747
- !this.isDynamicPivotInViewport() &&
46748
- this.isStaticPivotInViewport()) {
47255
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46749
47256
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46750
47257
  const pivotExample = `=PIVOT(${formulaId})`;
46751
47258
  this.alreadyNotified = true;
@@ -46801,26 +47308,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
46801
47308
  this.applyUpdate();
46802
47309
  }
46803
47310
  }
46804
- isDynamicPivotInViewport() {
46805
- for (const position of this.getters.getVisibleCellPositions()) {
46806
- const isDynamicPivot = this.getters.isSpillPivotFormula(position);
46807
- if (isDynamicPivot) {
46808
- return true;
46809
- }
46810
- }
46811
- return false;
46812
- }
46813
- isStaticPivotInViewport() {
47311
+ /**
47312
+ * @returns true if the updated pivot is visible in the viewport only as a
47313
+ * static pivot and not as a dynamic pivot
47314
+ */
47315
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
47316
+ let staticPivotCount = 0;
47317
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46814
47318
  for (const position of this.getters.getVisibleCellPositions()) {
46815
47319
  const cell = this.getters.getCell(position);
46816
47320
  if (cell?.isFormula) {
46817
47321
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46818
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46819
- return true;
47322
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
47323
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
47324
+ if (pivotFunction.functionName === "PIVOT") {
47325
+ // if we have at least one dynamic pivot visible inserted the viewport
47326
+ // we return false
47327
+ return false;
47328
+ }
47329
+ else {
47330
+ staticPivotCount++;
47331
+ }
46820
47332
  }
46821
47333
  }
46822
47334
  }
46823
- return false;
47335
+ // we return true if there are only static pivots visible inserted the viewport,
47336
+ // otherwise false
47337
+ return staticPivotCount > 0;
46824
47338
  }
46825
47339
  addDefaultDateTimeGranularity(fields, definition) {
46826
47340
  const { columns, rows } = definition;
@@ -46996,6 +47510,7 @@ class RemoveDuplicatesPanel extends owl.Component {
46996
47510
  columns: {},
46997
47511
  });
46998
47512
  setup() {
47513
+ owl.onMounted(() => this.updateColumns());
46999
47514
  owl.onWillUpdateProps(() => this.updateColumns());
47000
47515
  }
47001
47516
  toggleHasHeader() {
@@ -52296,13 +52811,13 @@ class Grid extends owl.Component {
52296
52811
  : this.onComposerContentFocused();
52297
52812
  },
52298
52813
  Delete: () => {
52299
- this.env.model.dispatch("DELETE_CONTENT", {
52814
+ this.env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
52300
52815
  sheetId: this.env.model.getters.getActiveSheetId(),
52301
52816
  target: this.env.model.getters.getSelectedZones(),
52302
52817
  });
52303
52818
  },
52304
52819
  Backspace: () => {
52305
- this.env.model.dispatch("DELETE_CONTENT", {
52820
+ this.env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
52306
52821
  sheetId: this.env.model.getters.getActiveSheetId(),
52307
52822
  target: this.env.model.getters.getSelectedZones(),
52308
52823
  });
@@ -52645,11 +53160,8 @@ class Grid extends owl.Component {
52645
53160
  else {
52646
53161
  this.env.model.dispatch("COPY");
52647
53162
  }
52648
- const content = this.env.model.getters.getClipboardContent();
52649
- const clipboardData = ev.clipboardData;
52650
- for (const type in content) {
52651
- clipboardData?.setData(type, content[type]);
52652
- }
53163
+ const osContent = await this.env.model.getters.getClipboardTextAndImageContent();
53164
+ await this.env.clipboard.write(osContent);
52653
53165
  ev.preventDefault();
52654
53166
  }
52655
53167
  async paste(ev) {
@@ -52661,21 +53173,27 @@ class Grid extends owl.Component {
52661
53173
  if (!clipboardData) {
52662
53174
  return;
52663
53175
  }
53176
+ const image = [...clipboardData?.files]?.find((file) => AllowedImageMimeTypes.includes(file.type));
52664
53177
  const osClipboard = {
52665
53178
  content: {
52666
53179
  [ClipboardMIMEType.PlainText]: clipboardData?.getData(ClipboardMIMEType.PlainText),
52667
53180
  [ClipboardMIMEType.Html]: clipboardData?.getData(ClipboardMIMEType.Html),
52668
53181
  },
52669
53182
  };
53183
+ if (image) {
53184
+ // TODO: support import of multiple images
53185
+ osClipboard.content[image.type] = image;
53186
+ }
52670
53187
  const target = this.env.model.getters.getSelectedZones();
52671
53188
  const isCutOperation = this.env.model.getters.isCutOperation();
52672
- const clipboardContent = parseOSClipboardContent(osClipboard.content);
52673
- const clipboardId = clipboardContent.data?.clipboardId;
52674
- if (this.env.model.getters.getClipboardId() === clipboardId) {
53189
+ const clipboardId = this.env.model.getters.getClipboardId();
53190
+ const osClipboardContent = parseOSClipboardContent(osClipboard.content);
53191
+ const osClipboardId = osClipboardContent.data?.clipboardId;
53192
+ if (clipboardId === osClipboardId) {
52675
53193
  interactivePaste(this.env, target);
52676
53194
  }
52677
53195
  else {
52678
- interactivePasteFromOS(this.env, target, clipboardContent);
53196
+ await interactivePasteFromOS(this.env, target, osClipboardContent);
52679
53197
  }
52680
53198
  if (isCutOperation) {
52681
53199
  await this.env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
@@ -54321,25 +54839,57 @@ class ConditionalFormatPlugin extends CorePlugin {
54321
54839
  "getAdaptedCfRanges",
54322
54840
  ];
54323
54841
  cfRules = {};
54324
- loopThroughRangesOfSheet(sheetId, applyChange) {
54325
- for (const rule of this.cfRules[sheetId]) {
54326
- if (rule.rule.type === "DataBarRule" && rule.rule.rangeValues) {
54327
- const change = applyChange(rule.rule.rangeValues);
54328
- switch (change.changeType) {
54329
- case "REMOVE":
54330
- this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54331
- //@ts-expect-error
54332
- "rangeValues", undefined);
54333
- break;
54334
- case "RESIZE":
54335
- case "MOVE":
54336
- case "CHANGE":
54842
+ adaptCFFormulas(applyChange) {
54843
+ for (const sheetId in this.cfRules) {
54844
+ for (const rule of this.cfRules[sheetId]) {
54845
+ if (rule.rule.type === "DataBarRule" && rule.rule.rangeValues) {
54846
+ const change = applyChange(rule.rule.rangeValues);
54847
+ switch (change.changeType) {
54848
+ case "REMOVE":
54849
+ this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54850
+ //@ts-expect-error
54851
+ "rangeValues", undefined);
54852
+ break;
54853
+ case "RESIZE":
54854
+ case "MOVE":
54855
+ case "CHANGE":
54856
+ this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54857
+ //@ts-expect-error
54858
+ "rangeValues", change.range);
54859
+ break;
54860
+ }
54861
+ }
54862
+ else if (rule.rule.type === "CellIsRule") {
54863
+ for (let i = 0; i < rule.rule.values.length; i++) {
54337
54864
  this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54338
54865
  //@ts-expect-error
54339
- "rangeValues", change.range);
54340
- break;
54866
+ "values", i, this.getters.adaptFormulaStringDependencies(sheetId, rule.rule.values[i], applyChange));
54867
+ }
54868
+ }
54869
+ else if (rule.rule.type === "IconSetRule") {
54870
+ for (const inflectionPoint of ["lowerInflectionPoint", "upperInflectionPoint"]) {
54871
+ if (rule.rule[inflectionPoint].type === "formula") {
54872
+ this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54873
+ //@ts-expect-error
54874
+ inflectionPoint, "value", this.getters.adaptFormulaStringDependencies(sheetId, rule.rule[inflectionPoint].value, applyChange));
54875
+ }
54876
+ }
54877
+ }
54878
+ else if (rule.rule.type === "ColorScaleRule") {
54879
+ for (const value of ["minimum", "maximum", "midpoint"]) {
54880
+ const ruleValue = rule.rule[value];
54881
+ if (ruleValue?.type === "formula" && ruleValue?.value) {
54882
+ this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54883
+ //@ts-expect-error
54884
+ value, "value", this.getters.adaptFormulaStringDependencies(sheetId, ruleValue.value, applyChange));
54885
+ }
54886
+ }
54341
54887
  }
54342
54888
  }
54889
+ }
54890
+ }
54891
+ adaptCFRanges(sheetId, applyChange) {
54892
+ for (const rule of this.cfRules[sheetId]) {
54343
54893
  for (const range of rule.ranges) {
54344
54894
  const change = applyChange(range);
54345
54895
  switch (change.changeType) {
@@ -54363,14 +54913,11 @@ class ConditionalFormatPlugin extends CorePlugin {
54363
54913
  }
54364
54914
  }
54365
54915
  adaptRanges(applyChange, sheetId) {
54366
- if (sheetId) {
54367
- this.loopThroughRangesOfSheet(sheetId, applyChange);
54368
- }
54369
- else {
54370
- for (const sheetId of Object.keys(this.cfRules)) {
54371
- this.loopThroughRangesOfSheet(sheetId, applyChange);
54372
- }
54916
+ const sheetIds = sheetId ? [sheetId] : Object.keys(this.cfRules);
54917
+ for (const sheetId of sheetIds) {
54918
+ this.adaptCFRanges(sheetId, applyChange);
54373
54919
  }
54920
+ this.adaptCFFormulas(applyChange);
54374
54921
  }
54375
54922
  // ---------------------------------------------------------------------------
54376
54923
  // Command Handling
@@ -54764,10 +55311,23 @@ class DataValidationPlugin extends CorePlugin {
54764
55311
  adaptRanges(applyChange, sheetId) {
54765
55312
  const sheetIds = sheetId ? [sheetId] : Object.keys(this.rules);
54766
55313
  for (const sheetId of sheetIds) {
54767
- this.loopThroughRangesOfSheet(sheetId, applyChange);
55314
+ this.adaptDVRanges(sheetId, applyChange);
55315
+ }
55316
+ this.adaptDVFormulas(applyChange);
55317
+ }
55318
+ adaptDVFormulas(applyChange) {
55319
+ for (const sheetId in this.rules) {
55320
+ const rules = this.rules[sheetId];
55321
+ for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
55322
+ const rule = this.rules[sheetId][ruleIndex];
55323
+ for (let valueIndex = 0; valueIndex < rule.criterion.values.length; valueIndex++) {
55324
+ const value = this.getters.adaptFormulaStringDependencies(sheetId, rule.criterion.values[valueIndex], applyChange);
55325
+ this.history.update("rules", sheetId, ruleIndex, "criterion", "values", valueIndex, value);
55326
+ }
55327
+ }
54768
55328
  }
54769
55329
  }
54770
- loopThroughRangesOfSheet(sheetId, applyChange) {
55330
+ adaptDVRanges(sheetId, applyChange) {
54771
55331
  const rules = this.rules[sheetId];
54772
55332
  for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
54773
55333
  const rule = this.rules[sheetId][ruleIndex];
@@ -60674,6 +61234,7 @@ class EvaluationPlugin extends CoreViewPlugin {
60674
61234
  exportForExcel(data) {
60675
61235
  for (const sheet of data.sheets) {
60676
61236
  sheet.cellValues = {};
61237
+ sheet.formulaSpillRanges = {};
60677
61238
  }
60678
61239
  for (const position of this.evaluator.getEvaluatedPositions()) {
60679
61240
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60685,8 +61246,9 @@ class EvaluationPlugin extends CoreViewPlugin {
60685
61246
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60686
61247
  const formulaCell = this.getCorrespondingFormulaCell(position);
60687
61248
  if (formulaCell) {
61249
+ const cell = this.getters.getCell(position);
60688
61250
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60689
- isFormula = isExported;
61251
+ isFormula = isExported && cell?.content === formulaCell.content;
60690
61252
  // If the cell contains a non-exported formula and that is evaluates to
60691
61253
  // nothing* ,we don't export it.
60692
61254
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60709,7 +61271,11 @@ class EvaluationPlugin extends CoreViewPlugin {
60709
61271
  content = !isExported ? newContent : exportedCellData;
60710
61272
  }
60711
61273
  exportedSheetData.cells[xc] = content;
60712
- exportedSheetData.cellValues[xc] = value;
61274
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
61275
+ const spillZone = this.getSpreadZone(position);
61276
+ if (spillZone) {
61277
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
61278
+ }
60713
61279
  }
60714
61280
  }
60715
61281
  /**
@@ -61004,7 +61570,7 @@ class EvaluationChartPlugin extends CoreViewPlugin {
61004
61570
  }
61005
61571
  const type = this.getters.getChartType(figureId);
61006
61572
  const runtime = this.getters.getChartRuntime(figureId);
61007
- const img = chartToImage(runtime, figure, type);
61573
+ const img = chartToImageUrl(runtime, figure, type);
61008
61574
  if (img) {
61009
61575
  sheet.images.push({
61010
61576
  ...figure,
@@ -62937,7 +63503,7 @@ class AutofillPlugin extends UIPlugin {
62937
63503
  getRule(cell, cells) {
62938
63504
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62939
63505
  const rule = rules.find((rule) => rule.condition(cell, cells));
62940
- return rule && rule.generateRule(cell, cells);
63506
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62941
63507
  }
62942
63508
  /**
62943
63509
  * Create the generator to be able to autofill the next cells.
@@ -65092,6 +65658,17 @@ class SheetUIPlugin extends UIPlugin {
65092
65658
  sheetId: cmd.sheetId,
65093
65659
  });
65094
65660
  break;
65661
+ case "DELETE_UNFILTERED_CONTENT":
65662
+ const newTarget = [];
65663
+ for (const target of cmd.target) {
65664
+ const nonFilteredRows = range(target.top, target.bottom + 1).filter((row) => !this.getters.isRowFiltered(cmd.sheetId, row));
65665
+ const consecutiveRows = groupConsecutive(nonFilteredRows);
65666
+ for (const group of consecutiveRows) {
65667
+ newTarget.push({ ...target, top: group[0], bottom: group[group.length - 1] });
65668
+ }
65669
+ }
65670
+ this.dispatch("DELETE_CONTENT", { sheetId: cmd.sheetId, target: newTarget });
65671
+ break;
65095
65672
  }
65096
65673
  }
65097
65674
  // ---------------------------------------------------------------------------
@@ -65731,6 +66308,7 @@ repeatLocalCommandTransformRegistry.add("AUTORESIZE_ROWS", repeatAutoResizeComma
65731
66308
  repeatLocalCommandTransformRegistry.add("SORT_CELLS", repeatSortCellsCommand);
65732
66309
  repeatLocalCommandTransformRegistry.add("SUM_SELECTION", genericRepeat);
65733
66310
  repeatLocalCommandTransformRegistry.add("SET_DECIMAL", genericRepeat);
66311
+ repeatLocalCommandTransformRegistry.add("DELETE_UNFILTERED_CONTENT", genericRepeat);
65734
66312
  function genericRepeat(getters, command) {
65735
66313
  let transformedCommand = deepCopy(command);
65736
66314
  for (const repeatTransform of genericRepeatsTransforms) {
@@ -66169,6 +66747,7 @@ class TableResizeUI extends UIPlugin {
66169
66747
  }
66170
66748
  }
66171
66749
 
66750
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
66172
66751
  /**
66173
66752
  * Clipboard Plugin
66174
66753
  *
@@ -66178,7 +66757,7 @@ class TableResizeUI extends UIPlugin {
66178
66757
  class ClipboardPlugin extends UIPlugin {
66179
66758
  static layers = ["Clipboard"];
66180
66759
  static getters = [
66181
- "getClipboardContent",
66760
+ "getClipboardTextAndImageContent",
66182
66761
  "getClipboardId",
66183
66762
  "getClipboardTextContent",
66184
66763
  "isCutOperation",
@@ -66188,6 +66767,13 @@ class ClipboardPlugin extends UIPlugin {
66188
66767
  copiedData;
66189
66768
  _isCutOperation = false;
66190
66769
  clipboardId = new UuidGenerator().uuidv4();
66770
+ fileStore;
66771
+ uuidGenerator;
66772
+ constructor(config) {
66773
+ super(config);
66774
+ this.fileStore = config.external.fileStore;
66775
+ this.uuidGenerator = new UuidGenerator();
66776
+ }
66191
66777
  // ---------------------------------------------------------------------------
66192
66778
  // Command Handling
66193
66779
  // ---------------------------------------------------------------------------
@@ -66250,9 +66836,29 @@ class ClipboardPlugin extends UIPlugin {
66250
66836
  break;
66251
66837
  case "PASTE_FROM_OS_CLIPBOARD": {
66252
66838
  this._isCutOperation = false;
66253
- this.copiedData =
66254
- cmd.clipboardContent.data ||
66255
- this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
66839
+ const htmlData = cmd.clipboardContent.data;
66840
+ // TODO: support multiple image import
66841
+ if (cmd.clipboardContent.imageData) {
66842
+ const sheetId = this.getters.getActiveSheetId();
66843
+ const figureId = this.uuidGenerator.uuidv4();
66844
+ const definition = cmd.clipboardContent.imageData;
66845
+ // compute position based on current selection
66846
+ const { x, y } = this.getters.getVisibleRectWithoutHeaders(cmd.target[0]);
66847
+ const size = getMaxFigureSize(this.getters, definition.size);
66848
+ this.dispatch("CREATE_IMAGE", {
66849
+ definition,
66850
+ size,
66851
+ position: { x, y },
66852
+ sheetId,
66853
+ figureId,
66854
+ });
66855
+ }
66856
+ if (htmlData) {
66857
+ this.copiedData = htmlData;
66858
+ }
66859
+ else {
66860
+ this.copiedData = this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
66861
+ }
66256
66862
  const pasteOption = cmd.pasteOption;
66257
66863
  this.paste(cmd.target, this.copiedData, {
66258
66864
  pasteOption,
@@ -66260,6 +66866,7 @@ class ClipboardPlugin extends UIPlugin {
66260
66866
  isCutOperation: false,
66261
66867
  });
66262
66868
  this.status = "invisible";
66869
+ this.copiedData = undefined;
66263
66870
  break;
66264
66871
  }
66265
66872
  case "PASTE": {
@@ -66552,11 +67159,17 @@ class ClipboardPlugin extends UIPlugin {
66552
67159
  getClipboardId() {
66553
67160
  return this.clipboardId;
66554
67161
  }
66555
- getClipboardContent() {
66556
- return {
67162
+ async getClipboardTextAndImageContent() {
67163
+ const file = await this.getImageContent();
67164
+ const mime = file?.type;
67165
+ const content = {
66557
67166
  [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),
66558
- [ClipboardMIMEType.Html]: this.getHTMLContent(),
67167
+ [ClipboardMIMEType.Html]: await this.getHTMLContent(),
66559
67168
  };
67169
+ if (mime && file) {
67170
+ content[mime] = file;
67171
+ }
67172
+ return content;
66560
67173
  }
66561
67174
  getSheetData() {
66562
67175
  const data = {
@@ -66585,11 +67198,24 @@ class ClipboardPlugin extends UIPlugin {
66585
67198
  })
66586
67199
  .join("\n") || "\t");
66587
67200
  }
66588
- getHTMLContent() {
67201
+ async getHTMLContent() {
66589
67202
  let innerHTML = "";
66590
67203
  const cells = this.copiedData?.cells;
66591
67204
  if (!cells) {
66592
- innerHTML = "\t";
67205
+ if (this.copiedData?.figureId) {
67206
+ const figureId = this.copiedData.figureId;
67207
+ const figureSheetId = this.getters.getFigureSheetId(figureId);
67208
+ const figure = this.getters.getFigure(figureSheetId, figureId);
67209
+ if (figure.tag == "image") {
67210
+ innerHTML = await this.craftImageHTML(figureId);
67211
+ }
67212
+ else {
67213
+ innerHTML = "\t";
67214
+ }
67215
+ }
67216
+ else {
67217
+ innerHTML = "\t";
67218
+ }
66593
67219
  }
66594
67220
  else if (cells.length === 1 && cells[0].length === 1) {
66595
67221
  innerHTML = `${this.getters.getCellText(cells[0][0].position)}`;
@@ -66617,6 +67243,62 @@ class ClipboardPlugin extends UIPlugin {
66617
67243
  const serializedData = JSON.stringify(this.getSheetData());
66618
67244
  return `<div data-osheet-clipboard='${xmlEscape(serializedData)}'>${innerHTML}</div>`;
66619
67245
  }
67246
+ readFileAsDataURL(blob) {
67247
+ return new Promise((resolve) => {
67248
+ const reader = new FileReader();
67249
+ reader.onload = () => resolve(reader.result);
67250
+ reader.readAsDataURL(blob);
67251
+ });
67252
+ }
67253
+ async craftImageHTML(figureId) {
67254
+ if (!this.fileStore) {
67255
+ return "\t";
67256
+ }
67257
+ const imageUrl = this.getters.getImage(figureId).path;
67258
+ const file = (await this.fileStore?.getFile(imageUrl)) || null;
67259
+ if (file) {
67260
+ const imageUrl = (await this.readFileAsDataURL(file));
67261
+ return `<img src="${xmlEscape(imageUrl)}" />`;
67262
+ }
67263
+ else {
67264
+ return "\t";
67265
+ }
67266
+ }
67267
+ async getImageContent() {
67268
+ const figureId = this.copiedData?.figureId;
67269
+ if (!figureId) {
67270
+ return;
67271
+ }
67272
+ const figureSheetId = this.getters.getFigureSheetId(figureId);
67273
+ const figure = this.getters.getFigure(figureSheetId, figureId);
67274
+ let file;
67275
+ if (figure.tag == "image") {
67276
+ if (!this.fileStore) {
67277
+ return;
67278
+ }
67279
+ const imageUrl = this.getters.getImage(figureId).path;
67280
+ file = await this.fileStore?.getFile(imageUrl);
67281
+ // we can only write on image/png format in the clipboard
67282
+ // So we convert the image to png if it's not already
67283
+ if (file.type !== "image/png") {
67284
+ if (file.size > MAX_FILE_SIZE) {
67285
+ this.ui.notifyUI({
67286
+ text: _t(`The file you are trying to copy is too large (>%sMB).\nIt will not be added to your OS clipboard.\nYou can download it directly instead.`, Math.round(MAX_FILE_SIZE / (1024 * 1024))),
67287
+ sticky: false,
67288
+ type: "warning",
67289
+ });
67290
+ return undefined;
67291
+ }
67292
+ file = await convertImageToPng(imageUrl);
67293
+ }
67294
+ }
67295
+ if (!file) {
67296
+ return undefined;
67297
+ }
67298
+ else {
67299
+ return file instanceof File ? file : new File([file], "image.png", { type: "image/png" });
67300
+ }
67301
+ }
66620
67302
  isCutOperation() {
66621
67303
  return this._isCutOperation ?? false;
66622
67304
  }
@@ -68691,12 +69373,17 @@ class ImageProvider {
68691
69373
  this.fileStore = fileStore;
68692
69374
  }
68693
69375
  async requestImage() {
68694
- const file = await this.getImageFromUser();
69376
+ const file = await this.userImageUpload();
69377
+ const path = await this.fileStore.upload(file);
69378
+ const size = await this.getImageOriginalSize(path);
69379
+ return { path, size, mimetype: file.type };
69380
+ }
69381
+ async uploadFile(file) {
68695
69382
  const path = await this.fileStore.upload(file);
68696
69383
  const size = await this.getImageOriginalSize(path);
68697
69384
  return { path, size, mimetype: file.type };
68698
69385
  }
68699
- getImageFromUser() {
69386
+ userImageUpload() {
68700
69387
  return new Promise((resolve, reject) => {
68701
69388
  const input = document.createElement("input");
68702
69389
  input.setAttribute("type", "file");
@@ -68715,12 +69402,12 @@ class ImageProvider {
68715
69402
  getImageOriginalSize(path) {
68716
69403
  return new Promise((resolve, reject) => {
68717
69404
  const image = new Image();
68718
- image.src = path;
68719
69405
  image.addEventListener("load", () => {
68720
69406
  const size = { width: image.width, height: image.height };
68721
69407
  resolve(size);
68722
69408
  });
68723
69409
  image.addEventListener("error", reject);
69410
+ image.src = path;
68724
69411
  });
68725
69412
  }
68726
69413
  }
@@ -70098,6 +70785,126 @@ class SidePanel extends owl.Component {
70098
70785
  }
70099
70786
  }
70100
70787
 
70788
+ const COMPOSER_MAX_HEIGHT = 100;
70789
+ /* svg free of use from https://uxwing.com/formula-fx-icon/ */
70790
+ const FX_SVG = /*xml*/ `
70791
+ <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 121.8 122.9' width='16' height='16' focusable='false'>
70792
+ <path d='m28 34-4 5v2h10l-6 40c-4 22-6 28-7 30-2 2-3 3-5 3-3 0-7-2-9-4H4c-2 2-4 4-4 7s4 6 8 6 9-2 15-8c8-7 13-17 18-39l7-35 13-1 3-6H49c4-23 7-27 11-27 2 0 5 2 8 6h4c1-1 4-4 4-7 0-2-3-6-9-6-5 0-13 4-20 10-6 7-9 14-11 24h-8zm41 16c4-5 7-7 8-7s2 1 5 9l3 12c-7 11-12 17-16 17l-3-1-2-1c-3 0-6 3-6 7s3 7 7 7c6 0 12-6 22-23l3 10c3 9 6 13 10 13 5 0 11-4 18-15l-3-4c-4 6-7 8-8 8-2 0-4-3-6-10l-5-15 8-10 6-4 3 1 3 2c2 0 6-3 6-7s-2-7-6-7c-6 0-11 5-21 20l-2-6c-3-9-5-14-9-14-5 0-12 6-18 15l3 3z' fill='#BDBDBD'/>
70793
+ </svg>
70794
+ `;
70795
+ css /* scss */ `
70796
+ .o-topbar-composer-container {
70797
+ height: ${TOPBAR_TOOLBAR_HEIGHT}px;
70798
+ }
70799
+
70800
+ .o-topbar-composer {
70801
+ height: fit-content;
70802
+ margin-top: -1px;
70803
+ margin-bottom: -1px;
70804
+ border: 1px solid;
70805
+ font-family: ${DEFAULT_FONT};
70806
+
70807
+ .o-composer:empty:not(:focus):not(.active)::before {
70808
+ content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}");
70809
+ position: relative;
70810
+ top: 20%;
70811
+ }
70812
+ }
70813
+
70814
+ .user-select-text {
70815
+ user-select: text;
70816
+ }
70817
+ `;
70818
+ class TopBarComposer extends owl.Component {
70819
+ static template = "o-spreadsheet-TopBarComposer";
70820
+ static props = {};
70821
+ static components = { Composer };
70822
+ composerFocusStore;
70823
+ composerStore;
70824
+ composerInterface;
70825
+ setup() {
70826
+ this.composerFocusStore = useStore(ComposerFocusStore);
70827
+ const composerStore = useStore(CellComposerStore);
70828
+ this.composerStore = composerStore;
70829
+ this.composerInterface = {
70830
+ id: "topbarComposer",
70831
+ get editionMode() {
70832
+ return composerStore.editionMode;
70833
+ },
70834
+ startEdition: this.composerStore.startEdition,
70835
+ setCurrentContent: this.composerStore.setCurrentContent,
70836
+ stopEdition: this.composerStore.stopEdition,
70837
+ };
70838
+ }
70839
+ get focus() {
70840
+ return this.composerFocusStore.activeComposer === this.composerInterface
70841
+ ? this.composerFocusStore.focusMode
70842
+ : "inactive";
70843
+ }
70844
+ get composerStyle() {
70845
+ const style = {
70846
+ padding: "5px 0px 5px 8px",
70847
+ "max-height": `${COMPOSER_MAX_HEIGHT}px`,
70848
+ "line-height": "24px",
70849
+ };
70850
+ style.height = this.focus === "inactive" ? `${TOPBAR_TOOLBAR_HEIGHT}px` : "fit-content";
70851
+ return cssPropertiesToCss(style);
70852
+ }
70853
+ get containerStyle() {
70854
+ if (this.focus === "inactive") {
70855
+ return cssPropertiesToCss({
70856
+ "border-color": SEPARATOR_COLOR,
70857
+ "border-right": "none",
70858
+ });
70859
+ }
70860
+ return cssPropertiesToCss({
70861
+ "border-color": SELECTION_BORDER_COLOR,
70862
+ "z-index": String(ComponentsImportance.TopBarComposer),
70863
+ });
70864
+ }
70865
+ onFocus(selection) {
70866
+ this.composerFocusStore.focusComposer(this.composerInterface, { selection });
70867
+ }
70868
+ }
70869
+
70870
+ /**
70871
+ * This store is used to manage the dropdown that is currently
70872
+ * opened after clicking an item on the toolbar.
70873
+ * It can only have one displayed at a time.
70874
+ *
70875
+ */
70876
+ class TopBarToolStore {
70877
+ mutators = ["closeDropdowns", "openDropdown"];
70878
+ _currentDropdown = null;
70879
+ closeDropdowns() {
70880
+ this._currentDropdown = null;
70881
+ }
70882
+ openDropdown(dropdownComponent) {
70883
+ this._currentDropdown = dropdownComponent;
70884
+ }
70885
+ get currentDropdown() {
70886
+ return this._currentDropdown;
70887
+ }
70888
+ }
70889
+
70890
+ class ToolBarRegistry {
70891
+ content = {};
70892
+ add(key) {
70893
+ this.content[key] = [];
70894
+ return this;
70895
+ }
70896
+ addChild(key, value) {
70897
+ this.content[key].push(value);
70898
+ return this;
70899
+ }
70900
+ getEntries(id) {
70901
+ return this.content[id].sort((a, b) => a.sequence - b.sequence);
70902
+ }
70903
+ getCategories() {
70904
+ return Object.keys(this.content);
70905
+ }
70906
+ }
70907
+
70101
70908
  css /* scss */ `
70102
70909
  .o-menu-item-button {
70103
70910
  display: flex;
@@ -70165,6 +70972,25 @@ class ActionButton extends owl.Component {
70165
70972
  }
70166
70973
  }
70167
70974
 
70975
+ function useToolBarDropdownStore() {
70976
+ const component = owl.useComponent();
70977
+ const topbarStore = useStore(TopBarToolStore);
70978
+ owl.onWillUnmount(() => {
70979
+ if (component === topbarStore.currentDropdown) {
70980
+ topbarStore.closeDropdowns();
70981
+ }
70982
+ });
70983
+ return {
70984
+ closeDropdowns: () => topbarStore.closeDropdowns(),
70985
+ openDropdown: () => {
70986
+ topbarStore.openDropdown(component);
70987
+ },
70988
+ get isActive() {
70989
+ return topbarStore.currentDropdown === component;
70990
+ },
70991
+ };
70992
+ }
70993
+
70168
70994
  /**
70169
70995
  * List the available borders positions and the corresponding icons.
70170
70996
  * The structure of this array is defined to match the order/lines we want
@@ -70334,13 +71160,12 @@ class BorderEditor extends owl.Component {
70334
71160
  class BorderEditorWidget extends owl.Component {
70335
71161
  static template = "o-spreadsheet-BorderEditorWidget";
70336
71162
  static props = {
70337
- toggleBorderEditor: Function,
70338
- showBorderEditor: Boolean,
70339
71163
  disabled: { type: Boolean, optional: true },
70340
71164
  dropdownMaxHeight: { type: Number, optional: true },
70341
71165
  class: { type: String, optional: true },
70342
71166
  };
70343
71167
  static components = { BorderEditor };
71168
+ topBarToolStore;
70344
71169
  borderEditorButtonRef = owl.useRef("borderEditorButton");
70345
71170
  state = owl.useState({
70346
71171
  currentColor: DEFAULT_BORDER_DESC.color,
@@ -70348,12 +71173,16 @@ class BorderEditorWidget extends owl.Component {
70348
71173
  currentPosition: undefined,
70349
71174
  });
70350
71175
  setup() {
70351
- owl.onWillUpdateProps((newProps) => {
70352
- if (!newProps.showBorderEditor) {
71176
+ this.topBarToolStore = useToolBarDropdownStore();
71177
+ owl.onWillUpdateProps(() => {
71178
+ if (!this.isActive) {
70353
71179
  this.state.currentPosition = undefined;
70354
71180
  }
70355
71181
  });
70356
71182
  }
71183
+ get dropdownMaxHeight() {
71184
+ return this.env.model.getters.getSheetViewDimension().height;
71185
+ }
70357
71186
  get borderEditorAnchorRect() {
70358
71187
  const button = this.borderEditorButtonRef.el;
70359
71188
  const buttonRect = button.getBoundingClientRect();
@@ -70376,6 +71205,17 @@ class BorderEditorWidget extends owl.Component {
70376
71205
  this.state.currentStyle = style;
70377
71206
  this.updateBorder();
70378
71207
  }
71208
+ get isActive() {
71209
+ return this.topBarToolStore.isActive;
71210
+ }
71211
+ toggleBorderEditor() {
71212
+ if (this.isActive) {
71213
+ this.topBarToolStore.closeDropdowns();
71214
+ }
71215
+ else {
71216
+ this.topBarToolStore.openDropdown();
71217
+ }
71218
+ }
70379
71219
  updateBorder() {
70380
71220
  if (this.state.currentPosition === undefined) {
70381
71221
  return;
@@ -70392,93 +71232,42 @@ class BorderEditorWidget extends owl.Component {
70392
71232
  }
70393
71233
  }
70394
71234
 
70395
- const COMPOSER_MAX_HEIGHT = 100;
70396
- /* svg free of use from https://uxwing.com/formula-fx-icon/ */
70397
- const FX_SVG = /*xml*/ `
70398
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 121.8 122.9' width='16' height='16' focusable='false'>
70399
- <path d='m28 34-4 5v2h10l-6 40c-4 22-6 28-7 30-2 2-3 3-5 3-3 0-7-2-9-4H4c-2 2-4 4-4 7s4 6 8 6 9-2 15-8c8-7 13-17 18-39l7-35 13-1 3-6H49c4-23 7-27 11-27 2 0 5 2 8 6h4c1-1 4-4 4-7 0-2-3-6-9-6-5 0-13 4-20 10-6 7-9 14-11 24h-8zm41 16c4-5 7-7 8-7s2 1 5 9l3 12c-7 11-12 17-16 17l-3-1-2-1c-3 0-6 3-6 7s3 7 7 7c6 0 12-6 22-23l3 10c3 9 6 13 10 13 5 0 11-4 18-15l-3-4c-4 6-7 8-8 8-2 0-4-3-6-10l-5-15 8-10 6-4 3 1 3 2c2 0 6-3 6-7s-2-7-6-7c-6 0-11 5-21 20l-2-6c-3-9-5-14-9-14-5 0-12 6-18 15l3 3z' fill='#BDBDBD'/>
70400
- </svg>
70401
- `;
70402
- css /* scss */ `
70403
- .o-topbar-composer-container {
70404
- height: ${TOPBAR_TOOLBAR_HEIGHT}px;
70405
- }
70406
-
70407
- .o-topbar-composer {
70408
- height: fit-content;
70409
- margin-top: -1px;
70410
- margin-bottom: -1px;
70411
- border: 1px solid;
70412
- font-family: ${DEFAULT_FONT};
70413
-
70414
- .o-composer:empty:not(:focus):not(.active)::before {
70415
- content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}");
70416
- position: relative;
70417
- top: 20%;
70418
- }
70419
- }
70420
-
70421
- .user-select-text {
70422
- user-select: text;
70423
- }
70424
- `;
70425
- class TopBarComposer extends owl.Component {
70426
- static template = "o-spreadsheet-TopBarComposer";
70427
- static props = {};
70428
- static components = { Composer };
70429
- composerFocusStore;
70430
- composerStore;
70431
- composerInterface;
71235
+ class PaintFormatButton extends owl.Component {
71236
+ static template = "o-spreadsheet-PaintFormatButton";
71237
+ static props = {
71238
+ class: { type: String, optional: true },
71239
+ };
71240
+ paintFormatStore;
70432
71241
  setup() {
70433
- this.composerFocusStore = useStore(ComposerFocusStore);
70434
- const composerStore = useStore(CellComposerStore);
70435
- this.composerStore = composerStore;
70436
- this.composerInterface = {
70437
- id: "topbarComposer",
70438
- get editionMode() {
70439
- return composerStore.editionMode;
70440
- },
70441
- startEdition: this.composerStore.startEdition,
70442
- setCurrentContent: this.composerStore.setCurrentContent,
70443
- stopEdition: this.composerStore.stopEdition,
70444
- };
71242
+ this.paintFormatStore = useStore(PaintFormatStore);
70445
71243
  }
70446
- get focus() {
70447
- return this.composerFocusStore.activeComposer === this.composerInterface
70448
- ? this.composerFocusStore.focusMode
70449
- : "inactive";
71244
+ get isActive() {
71245
+ return this.paintFormatStore.isActive;
70450
71246
  }
70451
- get composerStyle() {
70452
- const style = {
70453
- padding: "5px 0px 5px 8px",
70454
- "max-height": `${COMPOSER_MAX_HEIGHT}px`,
70455
- "line-height": "24px",
70456
- };
70457
- style.height = this.focus === "inactive" ? `${TOPBAR_TOOLBAR_HEIGHT}px` : "fit-content";
70458
- return cssPropertiesToCss(style);
71247
+ onDblClick() {
71248
+ this.paintFormatStore.activate({ persistent: true });
70459
71249
  }
70460
- get containerStyle() {
70461
- if (this.focus === "inactive") {
70462
- return cssPropertiesToCss({
70463
- "border-color": SEPARATOR_COLOR,
70464
- "border-right": "none",
70465
- });
71250
+ togglePaintFormat() {
71251
+ if (this.isActive) {
71252
+ this.paintFormatStore.cancel();
71253
+ }
71254
+ else {
71255
+ this.paintFormatStore.activate({ persistent: false });
70466
71256
  }
70467
- return cssPropertiesToCss({
70468
- "border-color": SELECTION_BORDER_COLOR,
70469
- "z-index": String(ComponentsImportance.TopBarComposer),
70470
- });
70471
- }
70472
- onFocus(selection) {
70473
- this.composerFocusStore.focusComposer(this.composerInterface, { selection });
70474
71257
  }
70475
71258
  }
70476
71259
 
70477
71260
  class TableDropdownButton extends owl.Component {
70478
71261
  static template = "o-spreadsheet-TableDropdownButton";
70479
71262
  static components = { TableStylesPopover, ActionButton };
70480
- static props = {};
71263
+ static props = {
71264
+ class: { type: String, optional: true },
71265
+ };
71266
+ topBarToolStore;
70481
71267
  state = owl.useState({ popoverProps: undefined });
71268
+ setup() {
71269
+ this.topBarToolStore = useToolBarDropdownStore();
71270
+ }
70482
71271
  onStylePicked(styleId) {
70483
71272
  const sheetId = this.env.model.getters.getActiveSheetId();
70484
71273
  const tableConfig = { ...this.tableConfig, styleId };
@@ -70494,12 +71283,13 @@ class TableDropdownButton extends owl.Component {
70494
71283
  return;
70495
71284
  }
70496
71285
  if (this.env.model.getters.getFirstTableInSelection()) {
71286
+ this.topBarToolStore.closeDropdowns();
70497
71287
  this.env.toggleSidePanel("TableSidePanel", {});
70498
71288
  return;
70499
71289
  }
70500
- // Open the popover
70501
71290
  const target = ev.currentTarget;
70502
- const { bottom, left } = target.getBoundingClientRect();
71291
+ const { left, bottom } = target.getBoundingClientRect();
71292
+ this.topBarToolStore.openDropdown();
70503
71293
  this.state.popoverProps = {
70504
71294
  anchorRect: { x: left, y: bottom, width: 0, height: 0 },
70505
71295
  positioning: "BottomLeft",
@@ -70511,8 +71301,8 @@ class TableDropdownButton extends owl.Component {
70511
71301
  }
70512
71302
  get action() {
70513
71303
  return {
70514
- name: (env) => this.env.model.getters.getFirstTableInSelection() ? _t("Edit table") : _t("Insert table"),
70515
- icon: (env) => this.env.model.getters.getFirstTableInSelection()
71304
+ name: (env) => env.model.getters.getFirstTableInSelection() ? _t("Edit table") : _t("Insert table"),
71305
+ icon: (env) => env.model.getters.getFirstTableInSelection()
70516
71306
  ? "o-spreadsheet-Icon.EDIT_TABLE"
70517
71307
  : "o-spreadsheet-Icon.PAINT_TABLE",
70518
71308
  };
@@ -70522,35 +71312,347 @@ class TableDropdownButton extends owl.Component {
70522
71312
  }
70523
71313
  }
70524
71314
 
70525
- class PaintFormatButton extends owl.Component {
70526
- static template = "o-spreadsheet-PaintFormatButton";
71315
+ class TopBarColorEditor extends owl.Component {
71316
+ static components = { ColorPickerWidget };
71317
+ static props = { class: String, style: String, icon: String, title: String };
71318
+ static template = "o-spreadsheet-ColorEditor";
71319
+ topBarToolStore;
71320
+ state = owl.useState({
71321
+ isOpen: false,
71322
+ });
71323
+ setup() {
71324
+ this.topBarToolStore = useToolBarDropdownStore();
71325
+ }
71326
+ get currentColor() {
71327
+ return (this.env.model.getters.getCurrentStyle()[this.props.style] ||
71328
+ (this.props.style === "textColor" ? "#000000" : "#ffffff"));
71329
+ }
71330
+ setColor(color) {
71331
+ setStyle(this.env, { [this.props.style]: color });
71332
+ this.state.isOpen = false;
71333
+ }
71334
+ get dropdownMaxHeight() {
71335
+ return this.env.model.getters.getSheetViewDimension().height;
71336
+ }
71337
+ get isMenuOpen() {
71338
+ return this.topBarToolStore.isActive;
71339
+ }
71340
+ onClick() {
71341
+ if (!this.isMenuOpen) {
71342
+ this.topBarToolStore.openDropdown();
71343
+ }
71344
+ else {
71345
+ this.topBarToolStore.closeDropdowns();
71346
+ }
71347
+ }
71348
+ }
71349
+
71350
+ class DropdownAction extends owl.Component {
71351
+ static template = "o-spreadsheet-DropdownAction";
71352
+ static components = { ActionButton, Popover };
70527
71353
  static props = {
70528
- class: { type: String, optional: true },
71354
+ parentAction: Object,
71355
+ childActions: Array,
71356
+ class: String,
71357
+ childClass: String,
70529
71358
  };
70530
- paintFormatStore;
71359
+ topBarToolStore;
71360
+ actionRef = owl.useRef("actionRef");
70531
71361
  setup() {
70532
- this.paintFormatStore = useStore(PaintFormatStore);
71362
+ this.topBarToolStore = useToolBarDropdownStore();
71363
+ }
71364
+ toggleDropdown() {
71365
+ if (this.isActive) {
71366
+ this.topBarToolStore.closeDropdowns();
71367
+ }
71368
+ else {
71369
+ this.topBarToolStore.openDropdown();
71370
+ }
70533
71371
  }
70534
71372
  get isActive() {
70535
- return this.paintFormatStore.isActive;
71373
+ return this.topBarToolStore.isActive;
70536
71374
  }
70537
- onDblClick() {
70538
- this.paintFormatStore.activate({ persistent: true });
71375
+ get popoverProps() {
71376
+ const rect = this.actionRef.el
71377
+ ? this.actionRef.el.getBoundingClientRect()
71378
+ : { x: 0, y: 0, width: 0, height: 0 };
71379
+ return {
71380
+ anchorRect: rect,
71381
+ positioning: "BottomLeft",
71382
+ verticalOffset: 0,
71383
+ class: "rounded",
71384
+ };
70539
71385
  }
70540
- togglePaintFormat() {
71386
+ }
71387
+
71388
+ class TopBarFontSizeEditor extends owl.Component {
71389
+ static components = { FontSizeEditor };
71390
+ static props = { class: String };
71391
+ static template = "o-spreadsheet-TopBarFontSizeEditor";
71392
+ topBarToolStore;
71393
+ setup() {
71394
+ this.topBarToolStore = useToolBarDropdownStore();
71395
+ }
71396
+ get currentFontSize() {
71397
+ return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
71398
+ }
71399
+ setFontSize(fontSize) {
71400
+ setStyle(this.env, { fontSize });
71401
+ }
71402
+ onToggle() {
70541
71403
  if (this.isActive) {
70542
- this.paintFormatStore.cancel();
71404
+ this.topBarToolStore.closeDropdowns();
70543
71405
  }
70544
71406
  else {
70545
- this.paintFormatStore.activate({ persistent: false });
71407
+ this.topBarToolStore.openDropdown();
71408
+ }
71409
+ }
71410
+ onFocusInput() {
71411
+ this.topBarToolStore.openDropdown();
71412
+ }
71413
+ get isActive() {
71414
+ return this.topBarToolStore.isActive;
71415
+ }
71416
+ }
71417
+
71418
+ class NumberFormatsTool extends owl.Component {
71419
+ static template = "o-spreadsheet-NumberFormatsTool";
71420
+ static components = { Menu, ActionButton };
71421
+ static props = { class: String };
71422
+ formatNumberMenuItemSpec = formatNumberMenuItemSpec;
71423
+ topBarToolStore;
71424
+ buttonRef = owl.useRef("buttonRef");
71425
+ state = owl.useState({
71426
+ position: { x: 0, y: 0 },
71427
+ menuItems: [],
71428
+ });
71429
+ setup() {
71430
+ this.topBarToolStore = useToolBarDropdownStore();
71431
+ }
71432
+ toggleMenu() {
71433
+ if (this.isActive) {
71434
+ this.topBarToolStore.closeDropdowns();
71435
+ }
71436
+ else {
71437
+ const menu = createAction(this.formatNumberMenuItemSpec);
71438
+ this.state.menuItems = menu.children(this.env).sort((a, b) => a.sequence - b.sequence);
71439
+ const { x, y, height } = this.buttonRef.el.getBoundingClientRect();
71440
+ this.state.position = { x, y: y + height };
71441
+ this.topBarToolStore.openDropdown();
70546
71442
  }
70547
71443
  }
71444
+ get isActive() {
71445
+ return this.topBarToolStore.isActive;
71446
+ }
70548
71447
  }
70549
71448
 
71449
+ const topBarToolBarRegistry = new ToolBarRegistry();
71450
+ topBarToolBarRegistry
71451
+ .add("edit")
71452
+ .addChild("edit", {
71453
+ component: ActionButton,
71454
+ props: {
71455
+ action: undo,
71456
+ class: "o-hoverable-button o-toolbar-button",
71457
+ },
71458
+ sequence: 1,
71459
+ })
71460
+ .addChild("edit", {
71461
+ component: ActionButton,
71462
+ props: {
71463
+ action: redo,
71464
+ class: "o-hoverable-button o-toolbar-button",
71465
+ },
71466
+ sequence: 2,
71467
+ })
71468
+ .addChild("edit", {
71469
+ component: PaintFormatButton,
71470
+ props: {
71471
+ class: "o-hoverable-button o-toolbar-button",
71472
+ },
71473
+ sequence: 3,
71474
+ })
71475
+ .addChild("edit", {
71476
+ component: ActionButton,
71477
+ props: {
71478
+ action: clearFormat,
71479
+ class: "o-hoverable-button o-toolbar-button",
71480
+ },
71481
+ sequence: 4,
71482
+ })
71483
+ .add("numberFormat")
71484
+ .addChild("numberFormat", {
71485
+ component: ActionButton,
71486
+ props: {
71487
+ action: formatPercent,
71488
+ class: "o-hoverable-button o-toolbar-button",
71489
+ },
71490
+ sequence: 1,
71491
+ })
71492
+ .addChild("numberFormat", {
71493
+ component: ActionButton,
71494
+ props: {
71495
+ action: decreaseDecimalPlaces,
71496
+ class: "o-hoverable-button o-toolbar-button",
71497
+ },
71498
+ sequence: 2,
71499
+ })
71500
+ .addChild("numberFormat", {
71501
+ component: ActionButton,
71502
+ props: {
71503
+ action: increaseDecimalPlaces,
71504
+ class: "o-hoverable-button o-toolbar-button",
71505
+ },
71506
+ sequence: 3,
71507
+ })
71508
+ .addChild("numberFormat", {
71509
+ component: NumberFormatsTool,
71510
+ props: {
71511
+ class: "o-menu-item-button o-hoverable-button o-toolbar-button",
71512
+ },
71513
+ sequence: 4,
71514
+ })
71515
+ .add("fontSize")
71516
+ .addChild("fontSize", {
71517
+ component: TopBarFontSizeEditor,
71518
+ props: {
71519
+ class: "o-hoverable-button o-toolbar-button",
71520
+ },
71521
+ sequence: 3,
71522
+ })
71523
+ .add("textStyle")
71524
+ .addChild("textStyle", {
71525
+ component: ActionButton,
71526
+ props: {
71527
+ action: formatBold,
71528
+ class: "o-hoverable-button o-toolbar-button",
71529
+ },
71530
+ sequence: 1,
71531
+ })
71532
+ .addChild("textStyle", {
71533
+ component: ActionButton,
71534
+ props: {
71535
+ action: formatItalic,
71536
+ class: "o-hoverable-button o-toolbar-button",
71537
+ },
71538
+ sequence: 2,
71539
+ })
71540
+ .addChild("textStyle", {
71541
+ component: ActionButton,
71542
+ props: {
71543
+ action: formatStrikethrough,
71544
+ class: "o-hoverable-button o-toolbar-button",
71545
+ },
71546
+ sequence: 3,
71547
+ })
71548
+ .addChild("textStyle", {
71549
+ component: TopBarColorEditor,
71550
+ props: {
71551
+ class: "o-hoverable-button o-menu-item-button o-toolbar-button",
71552
+ style: "textColor",
71553
+ icon: "o-spreadsheet-Icon.TEXT_COLOR",
71554
+ title: _t("Text Color"),
71555
+ },
71556
+ sequence: 4,
71557
+ })
71558
+ .add("cellStyle")
71559
+ .addChild("cellStyle", {
71560
+ component: TopBarColorEditor,
71561
+ props: {
71562
+ class: "o-hoverable-button o-menu-item-button o-toolbar-button",
71563
+ style: "fillColor",
71564
+ icon: "o-spreadsheet-Icon.FILL_COLOR",
71565
+ title: _t("Fill Color"),
71566
+ },
71567
+ sequence: 1,
71568
+ })
71569
+ .addChild("cellStyle", {
71570
+ component: BorderEditorWidget,
71571
+ props: {
71572
+ class: "o-hoverable-button o-menu-item-button o-toolbar-button",
71573
+ },
71574
+ sequence: 2,
71575
+ })
71576
+ .addChild("cellStyle", {
71577
+ component: ActionButton,
71578
+ props: {
71579
+ action: mergeCells,
71580
+ class: "o-hoverable-button o-menu-item-button o-toolbar-button",
71581
+ },
71582
+ sequence: 3,
71583
+ })
71584
+ .add("alignment")
71585
+ .addChild("alignment", {
71586
+ component: DropdownAction,
71587
+ props: {
71588
+ parentAction: formatAlignmentHorizontal,
71589
+ childActions: [
71590
+ formatAlignmentLeft,
71591
+ formatAlignmentCenter,
71592
+ formatAlignmentRight,
71593
+ ],
71594
+ class: "o-hoverable-button o-toolbar-button",
71595
+ childClass: "o-hoverable-button",
71596
+ },
71597
+ sequence: 1,
71598
+ })
71599
+ .addChild("alignment", {
71600
+ component: DropdownAction,
71601
+ props: {
71602
+ parentAction: formatAlignmentVertical,
71603
+ childActions: [
71604
+ formatAlignmentTop,
71605
+ formatAlignmentMiddle,
71606
+ formatAlignmentBottom,
71607
+ ],
71608
+ class: "o-hoverable-button o-menu-item-button o-toolbar-button",
71609
+ childClass: "o-hoverable-button",
71610
+ },
71611
+ sequence: 2,
71612
+ })
71613
+ .addChild("alignment", {
71614
+ component: DropdownAction,
71615
+ props: {
71616
+ parentAction: formatWrapping,
71617
+ childActions: [
71618
+ formatWrappingOverflow,
71619
+ formatWrappingWrap,
71620
+ formatWrappingClip,
71621
+ ],
71622
+ class: "o-hoverable-button o-menu-item-button o-toolbar-button",
71623
+ childClass: "o-hoverable-button",
71624
+ },
71625
+ sequence: 3,
71626
+ })
71627
+ .add("misc")
71628
+ .addChild("misc", {
71629
+ component: TableDropdownButton,
71630
+ props: { class: "o-toolbar-button o-hoverable-button o-menu-item-button" },
71631
+ sequence: 1,
71632
+ })
71633
+ .addChild("misc", {
71634
+ component: ActionButton,
71635
+ props: {
71636
+ action: createRemoveFilterTool,
71637
+ class: "o-hoverable-button o-menu-item-button o-toolbar-button",
71638
+ },
71639
+ sequence: 2,
71640
+ });
71641
+
70550
71642
  // -----------------------------------------------------------------------------
70551
71643
  // TopBar
70552
71644
  // -----------------------------------------------------------------------------
70553
71645
  css /* scss */ `
71646
+ .o-topbar-divider {
71647
+ border-right: 1px solid ${SEPARATOR_COLOR};
71648
+ width: 0;
71649
+ margin: 0 6px;
71650
+ }
71651
+
71652
+ .o-toolbar-button {
71653
+ height: 30px;
71654
+ }
71655
+
70554
71656
  .o-spreadsheet-topbar {
70555
71657
  line-height: 1.2;
70556
71658
  font-size: 13px;
@@ -70600,47 +71702,7 @@ css /* scss */ `
70600
71702
 
70601
71703
  /* Toolbar */
70602
71704
  .o-toolbar-tools {
70603
- display: flex;
70604
- flex-shrink: 0;
70605
- margin: 0px 6px 0px 16px;
70606
71705
  cursor: default;
70607
-
70608
- .o-divider {
70609
- display: inline-block;
70610
- border-right: 1px solid ${SEPARATOR_COLOR};
70611
- width: 0;
70612
- margin: 0 6px;
70613
- }
70614
-
70615
- .o-dropdown {
70616
- position: relative;
70617
- display: flex;
70618
- align-items: center;
70619
-
70620
- > span {
70621
- height: 30px;
70622
- }
70623
-
70624
- .o-dropdown-content {
70625
- position: absolute;
70626
- top: 100%;
70627
- left: 0;
70628
- overflow-y: auto;
70629
- overflow-x: hidden;
70630
- padding: 2px;
70631
- z-index: ${ComponentsImportance.Dropdown};
70632
- box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
70633
- background-color: white;
70634
-
70635
- .o-dropdown-line {
70636
- display: flex;
70637
-
70638
- > span {
70639
- padding: 4px;
70640
- }
70641
- }
70642
- }
70643
- }
70644
71706
  }
70645
71707
  }
70646
71708
  }
@@ -70652,38 +71714,73 @@ class TopBar extends owl.Component {
70652
71714
  dropdownMaxHeight: Number,
70653
71715
  };
70654
71716
  static components = {
70655
- ColorPickerWidget,
70656
- ColorPicker,
70657
71717
  Menu,
70658
71718
  TopBarComposer,
70659
- FontSizeEditor,
70660
- ActionButton,
70661
- PaintFormatButton,
70662
- BorderEditorWidget,
70663
- TableDropdownButton,
71719
+ Popover,
70664
71720
  };
71721
+ toolsCategories = topBarToolBarRegistry.getCategories();
70665
71722
  state = owl.useState({
70666
71723
  menuState: { isOpen: false, position: null, menuItems: [] },
70667
- activeTool: "",
70668
- fillColor: "#ffffff",
70669
- textColor: "#000000",
71724
+ invisibleToolsCategories: [],
71725
+ toolsPopoverState: { isOpen: false },
70670
71726
  });
70671
71727
  isSelectingMenu = false;
70672
71728
  openedEl = null;
70673
71729
  menus = [];
70674
- EDIT = ACTION_EDIT;
70675
- FORMAT = ACTION_FORMAT;
70676
- DATA = ACTION_DATA;
71730
+ toolbarMenuRegistry = topBarToolBarRegistry;
70677
71731
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
70678
71732
  isntToolbarMenu = false;
70679
71733
  composerFocusStore;
70680
71734
  fingerprints;
71735
+ topBarToolStore;
71736
+ toolBarContainerRef = owl.useRef("toolBarContainer");
71737
+ toolbarRef = owl.useRef("toolBar");
71738
+ moreToolsContainerRef = owl.useRef("moreToolsContainer");
71739
+ moreToolsButtonRef = owl.useRef("moreToolsButton");
71740
+ spreadsheetRect = useSpreadsheetRect();
70681
71741
  setup() {
70682
71742
  this.composerFocusStore = useStore(ComposerFocusStore);
70683
71743
  this.fingerprints = useStore(FormulaFingerprintStore);
71744
+ this.topBarToolStore = useStore(TopBarToolStore);
70684
71745
  owl.useExternalListener(window, "click", this.onExternalClick);
70685
71746
  owl.onWillStart(() => this.updateCellState());
70686
71747
  owl.onWillUpdateProps(() => this.updateCellState());
71748
+ owl.useEffect(() => {
71749
+ this.state.toolsPopoverState.isOpen = false;
71750
+ this.setVisibilityToolsGroups();
71751
+ }, () => [this.spreadsheetRect.width]);
71752
+ }
71753
+ setVisibilityToolsGroups() {
71754
+ if (this.env.model.getters.isReadonly()) {
71755
+ return;
71756
+ }
71757
+ const hiddenCategories = [];
71758
+ const { x: toolsX } = this.toolbarRef.el.getBoundingClientRect();
71759
+ const { x } = this.toolBarContainerRef.el.getBoundingClientRect();
71760
+ // Compute the with of the button that will toggle the hidden tools
71761
+ this.moreToolsContainerRef.el?.classList.remove("d-none");
71762
+ const moreToolsWidth = this.moreToolsButtonRef.el?.getBoundingClientRect().width || 0;
71763
+ // The actual width in which we can place our tools so that they are visible.
71764
+ // Every tool container passed that width will be hidden.
71765
+ // We remove 16px to the width to account for a scrollbar that might appear.
71766
+ // Otherwise, we could end up in a loop of computation
71767
+ const usableWidth = Math.round(this.spreadsheetRect.width) - moreToolsWidth - (toolsX - x) - 16;
71768
+ const toolElements = document.querySelectorAll(".tool-container");
71769
+ let currentWidth = 0;
71770
+ for (let index = 0; index < toolElements.length; index++) {
71771
+ const element = toolElements[index];
71772
+ element.classList.remove("d-none");
71773
+ const { width: toolWidth } = element.getBoundingClientRect();
71774
+ currentWidth += toolWidth;
71775
+ if (currentWidth > usableWidth) {
71776
+ element.classList.add("d-none");
71777
+ hiddenCategories.push(this.toolsCategories[index]);
71778
+ }
71779
+ }
71780
+ this.state.invisibleToolsCategories = hiddenCategories;
71781
+ if (!hiddenCategories.length) {
71782
+ this.moreToolsContainerRef.el?.classList.add("d-none");
71783
+ }
70687
71784
  }
70688
71785
  get topbarComponents() {
70689
71786
  return topbarComponentRegistry
@@ -70713,12 +71810,6 @@ class TopBar extends owl.Component {
70713
71810
  this.openMenu(menu, ev);
70714
71811
  }
70715
71812
  }
70716
- toggleDropdownTool(tool, ev) {
70717
- const isOpen = this.state.activeTool === tool;
70718
- this.closeMenus();
70719
- this.state.activeTool = isOpen ? "" : tool;
70720
- this.openedEl = isOpen ? null : ev.target;
70721
- }
70722
71813
  toggleContextMenu(menu, ev) {
70723
71814
  if (this.state.menuState.isOpen && this.isntToolbarMenu) {
70724
71815
  this.closeMenus();
@@ -70728,19 +71819,10 @@ class TopBar extends owl.Component {
70728
71819
  this.isntToolbarMenu = true;
70729
71820
  }
70730
71821
  }
70731
- toggleToolbarContextMenu(menuSpec, ev) {
70732
- if (this.state.menuState.isOpen && !this.isntToolbarMenu) {
70733
- this.closeMenus();
70734
- }
70735
- else {
70736
- const menu = createAction(menuSpec);
70737
- this.openMenu(menu, ev);
70738
- this.isntToolbarMenu = false;
70739
- }
70740
- }
70741
71822
  openMenu(menu, ev) {
71823
+ this.topBarToolStore.closeDropdowns();
71824
+ this.state.toolsPopoverState.isOpen = false;
70742
71825
  const { left, top, height } = ev.currentTarget.getBoundingClientRect();
70743
- this.state.activeTool = "";
70744
71826
  this.state.menuState.isOpen = true;
70745
71827
  this.state.menuState.position = { x: left, y: top + height };
70746
71828
  this.state.menuState.menuItems = menu
@@ -70752,16 +71834,14 @@ class TopBar extends owl.Component {
70752
71834
  this.composerFocusStore.activeComposer.stopEdition();
70753
71835
  }
70754
71836
  closeMenus() {
70755
- this.state.activeTool = "";
71837
+ this.topBarToolStore.closeDropdowns();
71838
+ this.state.toolsPopoverState.isOpen = false;
70756
71839
  this.state.menuState.isOpen = false;
70757
71840
  this.state.menuState.parentMenu = undefined;
70758
71841
  this.isSelectingMenu = false;
70759
71842
  this.openedEl = null;
70760
71843
  }
70761
71844
  updateCellState() {
70762
- const style = this.env.model.getters.getCurrentStyle();
70763
- this.state.fillColor = style.fillColor || "#ffffff";
70764
- this.state.textColor = style.textColor || "#000000";
70765
71845
  this.menus = topbarMenuRegistry.getMenuItems();
70766
71846
  }
70767
71847
  getMenuName(menu) {
@@ -70774,6 +71854,26 @@ class TopBar extends owl.Component {
70774
71854
  setFontSize(fontSize) {
70775
71855
  setStyle(this.env, { fontSize });
70776
71856
  }
71857
+ toggleMoreTools() {
71858
+ this.topBarToolStore.closeDropdowns();
71859
+ this.state.toolsPopoverState.isOpen = !this.state.toolsPopoverState.isOpen;
71860
+ }
71861
+ get toolsPopoverProps() {
71862
+ const rect = this.moreToolsButtonRef.el
71863
+ ? getBoundingRectAsPOJO(this.moreToolsButtonRef.el)
71864
+ : { x: 0, y: 0, width: 0, height: 0 };
71865
+ return {
71866
+ anchorRect: rect,
71867
+ positioning: "BottomLeft",
71868
+ verticalOffset: 0,
71869
+ class: "rounded",
71870
+ maxWidth: 300,
71871
+ };
71872
+ }
71873
+ showDivider(categoryIndex) {
71874
+ return (categoryIndex < this.toolsCategories.length - 1 ||
71875
+ this.state.invisibleToolsCategories.length > 0);
71876
+ }
70777
71877
  }
70778
71878
 
70779
71879
  function instantiateClipboard() {
@@ -70796,6 +71896,7 @@ class WebClipboardWrapper {
70796
71896
  * Therefore, we try to catch any errors and fallback on writing only standard
70797
71897
  * mimetypes to prevent the whole copy action from crashing.
70798
71898
  */
71899
+ console.log("Failed to write on the clipboard, falling back to plain/html text. Error %s", e);
70799
71900
  try {
70800
71901
  await this.clipboard?.write([
70801
71902
  new ClipboardItem({
@@ -70827,14 +71928,20 @@ class WebClipboardWrapper {
70827
71928
  if (this.clipboard?.read) {
70828
71929
  try {
70829
71930
  const clipboardItems = await this.clipboard.read();
70830
- const clipboardContent = {};
71931
+ const osClipboardContent = {};
70831
71932
  for (const item of clipboardItems) {
70832
71933
  for (const type of item.types) {
70833
71934
  const blob = await item.getType(type);
70834
- clipboardContent[type] = await blob.text();
71935
+ if (AllowedImageMimeTypes.includes(type)) {
71936
+ osClipboardContent[type] = blob;
71937
+ }
71938
+ else {
71939
+ const text = await blob.text();
71940
+ osClipboardContent[type] = text;
71941
+ }
70835
71942
  }
70836
71943
  }
70837
- return { status: "ok", content: clipboardContent };
71944
+ return { status: "ok", content: osClipboardContent };
70838
71945
  }
70839
71946
  catch (e) {
70840
71947
  const status = permissionResult?.state === "denied" ? "permissionDenied" : "notImplemented";
@@ -70851,13 +71958,17 @@ class WebClipboardWrapper {
70851
71958
  }
70852
71959
  }
70853
71960
  getClipboardItems(content) {
70854
- const clipboardItemData = {
70855
- [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),
70856
- [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),
70857
- };
71961
+ const clipboardItemData = {};
71962
+ for (const type of Object.keys(content)) {
71963
+ clipboardItemData[type] = this.getBlob(content, type);
71964
+ }
70858
71965
  return [new ClipboardItem(clipboardItemData)];
70859
71966
  }
70860
71967
  getBlob(clipboardContent, type) {
71968
+ const content = clipboardContent[type];
71969
+ if (content instanceof Blob || content instanceof File) {
71970
+ return content;
71971
+ }
70861
71972
  return new Blob([clipboardContent[type] || ""], {
70862
71973
  type,
70863
71974
  });
@@ -71136,7 +72247,7 @@ class Spreadsheet extends owl.Component {
71136
72247
  properties["grid-template-rows"] = `auto`;
71137
72248
  }
71138
72249
  else {
71139
- properties["grid-template-rows"] = `max-content auto ${BOTTOMBAR_HEIGHT + 1}px`;
72250
+ properties["grid-template-rows"] = `min-content auto min-content`;
71140
72251
  }
71141
72252
  properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
71142
72253
  return cssPropertiesToCss(properties);
@@ -71247,8 +72358,7 @@ class Spreadsheet extends owl.Component {
71247
72358
  this._focusGrid();
71248
72359
  }
71249
72360
  get gridHeight() {
71250
- const { height } = this.env.model.getters.getSheetViewDimension();
71251
- return height;
72361
+ return this.env.model.getters.getSheetViewDimension().height;
71252
72362
  }
71253
72363
  get gridContainerStyle() {
71254
72364
  const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
@@ -73419,7 +74529,7 @@ function numberRef(reference) {
73419
74529
  `;
73420
74530
  }
73421
74531
 
73422
- function addFormula(formula, value) {
74532
+ function addFormula(formula, value, formulaSpillRange) {
73423
74533
  if (!formula) {
73424
74534
  return { attrs: [], node: escapeXml `` };
73425
74535
  }
@@ -73427,10 +74537,17 @@ function addFormula(formula, value) {
73427
74537
  if (type === undefined) {
73428
74538
  return { attrs: [], node: escapeXml `` };
73429
74539
  }
73430
- const attrs = [["t", type]];
74540
+ const attrs = [
74541
+ ["cm", "1"],
74542
+ ["t", type],
74543
+ ];
73431
74544
  const XlsxFormula = adaptFormulaToExcel(formula);
73432
74545
  const exportedValue = adaptFormulaValueToExcel(value);
73433
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
74546
+ // We treat all formulas as array formulas (a simple formula
74547
+ // is an array formula that spills on only one cell) to avoid
74548
+ // trying to detect spilling sub-formulas which is not a trivial task.
74549
+ let node;
74550
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73434
74551
  return { attrs, node };
73435
74552
  }
73436
74553
  function addContent(content, sharedStrings, forceString = false) {
@@ -74420,7 +75537,7 @@ function addRows(construct, data, sheet) {
74420
75537
  let cellNode = escapeXml ``;
74421
75538
  // Either formula or static value inside the cell
74422
75539
  if (content?.startsWith("=") && value !== undefined) {
74423
- const res = addFormula(content, value);
75540
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74424
75541
  if (!res) {
74425
75542
  continue;
74426
75543
  }
@@ -74706,6 +75823,30 @@ function createWorksheets(data, construct) {
74706
75823
  `;
74707
75824
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74708
75825
  }
75826
+ const sheetMetadataXml = escapeXml /*xml*/ `
75827
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
75828
+ <metadataTypes count="1">
75829
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
75830
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
75831
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
75832
+ </metadataTypes>
75833
+ <futureMetadata name="XLDAPR" count="1">
75834
+ <bk>
75835
+ <extLst>
75836
+ <ext uri="{${ARRAY_FORMULA_URI}}">
75837
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
75838
+ </ext>
75839
+ </extLst>
75840
+ </bk>
75841
+ </futureMetadata>
75842
+ <cellMetadata count="1">
75843
+ <bk>
75844
+ <rc t="1" v="0" />
75845
+ </bk>
75846
+ </cellMetadata>
75847
+ </metadata>
75848
+ `;
75849
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74709
75850
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74710
75851
  type: XLSX_RELATION_TYPE.sharedStrings,
74711
75852
  target: "sharedStrings.xml",
@@ -74714,6 +75855,10 @@ function createWorksheets(data, construct) {
74714
75855
  type: XLSX_RELATION_TYPE.styles,
74715
75856
  target: "styles.xml",
74716
75857
  });
75858
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
75859
+ type: XLSX_RELATION_TYPE.metadata,
75860
+ target: "metadata.xml",
75861
+ });
74717
75862
  return files;
74718
75863
  }
74719
75864
  /**
@@ -75675,6 +76820,6 @@ exports.tokenColors = tokenColors;
75675
76820
  exports.tokenize = tokenize;
75676
76821
 
75677
76822
 
75678
- __info__.version = "18.3.0-alpha.1";
75679
- __info__.date = "2025-02-25T06:00:14.885Z";
75680
- __info__.hash = "be4d957";
76823
+ __info__.version = "18.3.0-alpha.3";
76824
+ __info__.date = "2025-03-07T10:41:05.411Z";
76825
+ __info__.hash = "f59f5f6";