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