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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.3.0-alpha.1
6
- * @date 2025-02-25T06:00:14.885Z
7
- * @hash be4d957
5
+ * @version 18.3.0-alpha.3
6
+ * @date 2025-03-07T10:41:05.411Z
7
+ * @hash f59f5f6
8
8
  */
9
9
 
10
10
  (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
  },
@@ -38464,6 +38901,9 @@ stores.inject(MyMetaStore, storeInstance);
38464
38901
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
38465
38902
  dataSets: this.dataSets,
38466
38903
  });
38904
+ if (this.state.datasetDispatchResult.isSuccessful) {
38905
+ this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
38906
+ }
38467
38907
  }
38468
38908
  getDataSeriesRanges() {
38469
38909
  return this.dataSets;
@@ -39010,8 +39450,12 @@ stores.inject(MyMetaStore, storeInstance);
39010
39450
  currentFontSize: Number,
39011
39451
  onFontSizeChanged: Function,
39012
39452
  onToggle: { type: Function, optional: true },
39453
+ onFocusInput: { type: Function, optional: true },
39013
39454
  class: String,
39014
39455
  };
39456
+ static defaultProps = {
39457
+ onFocusInput: () => { },
39458
+ };
39015
39459
  static components = { Popover };
39016
39460
  fontSizes = FONT_SIZES;
39017
39461
  dropdown = owl.useState({ isOpen: false });
@@ -39704,6 +40148,36 @@ stores.inject(MyMetaStore, storeInstance);
39704
40148
  }
39705
40149
  }
39706
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
+
39707
40181
  class GaugeChartConfigPanel extends owl.Component {
39708
40182
  static template = "o-spreadsheet-GaugeChartConfigPanel";
39709
40183
  static components = { ChartErrorSection, ChartDataSeries };
@@ -39880,8 +40354,16 @@ stores.inject(MyMetaStore, storeInstance);
39880
40354
  }
39881
40355
  let startNode = this.findChildAtCharacterIndex(start);
39882
40356
  let endNode = this.findChildAtCharacterIndex(end);
39883
- range.setStart(startNode.node, startNode.offset);
39884
- range.setEnd(endNode.node, endNode.offset);
40357
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
40358
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
40359
+ if (start <= end) {
40360
+ range.setStart(startNode.node, startNode.offset);
40361
+ range.setEnd(endNode.node, endNode.offset);
40362
+ }
40363
+ else {
40364
+ range.setStart(endNode.node, endNode.offset);
40365
+ range.setEnd(startNode.node, startNode.offset);
40366
+ }
39885
40367
  }
39886
40368
  }
39887
40369
  /**
@@ -40183,8 +40665,7 @@ stores.inject(MyMetaStore, storeInstance);
40183
40665
  }
40184
40666
 
40185
40667
  .o-composer-assistant {
40186
- position: absolute;
40187
- margin: 1px 4px;
40668
+ margin-top: 1px;
40188
40669
 
40189
40670
  .o-semi-bold {
40190
40671
  /* FIXME: to remove in favor of Bootstrap
@@ -40235,10 +40716,11 @@ stores.inject(MyMetaStore, storeInstance);
40235
40716
  });
40236
40717
  compositionActive = false;
40237
40718
  spreadsheetRect = useSpreadsheetRect();
40238
- get assistantStyle() {
40719
+ get assistantStyleProperties() {
40239
40720
  const composerRect = this.composerRef.el.getBoundingClientRect();
40240
40721
  const assistantStyle = {};
40241
- 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`;
40242
40724
  const proposals = this.autoCompleteState.provider?.proposals;
40243
40725
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40244
40726
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40262,13 +40744,29 @@ stores.inject(MyMetaStore, storeInstance);
40262
40744
  }
40263
40745
  }
40264
40746
  else {
40265
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40747
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40266
40748
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40267
40749
  this.spreadsheetRect.width) {
40268
40750
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40269
40751
  }
40270
40752
  }
40271
- 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
+ });
40272
40770
  }
40273
40771
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40274
40772
  shouldProcessInputEvents = false;
@@ -41453,6 +41951,10 @@ stores.inject(MyMetaStore, storeInstance);
41453
41951
  .add("geo", {
41454
41952
  configuration: GeoChartConfigPanel,
41455
41953
  design: GeoChartDesignPanel,
41954
+ })
41955
+ .add("funnel", {
41956
+ configuration: GenericChartConfigPanel,
41957
+ design: FunnelChartDesignPanel,
41456
41958
  });
41457
41959
 
41458
41960
  css /* scss */ `
@@ -44795,6 +45297,10 @@ stores.inject(MyMetaStore, storeInstance);
44795
45297
  type: String,
44796
45298
  optional: true,
44797
45299
  },
45300
+ autofocus: {
45301
+ type: Boolean,
45302
+ optional: true,
45303
+ },
44798
45304
  };
44799
45305
  inputRef = owl.useRef("input");
44800
45306
  setup() {
@@ -44803,6 +45309,9 @@ stores.inject(MyMetaStore, storeInstance);
44803
45309
  this.save();
44804
45310
  }
44805
45311
  }, { capture: true });
45312
+ if (this.props.autofocus) {
45313
+ useAutofocus({ refName: "input" });
45314
+ }
44806
45315
  }
44807
45316
  onKeyDown(ev) {
44808
45317
  switch (ev.key) {
@@ -46742,9 +47251,7 @@ stores.inject(MyMetaStore, storeInstance);
46742
47251
  pivot: this.draft,
46743
47252
  });
46744
47253
  this.draft = null;
46745
- if (!this.alreadyNotified &&
46746
- !this.isDynamicPivotInViewport() &&
46747
- this.isStaticPivotInViewport()) {
47254
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46748
47255
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46749
47256
  const pivotExample = `=PIVOT(${formulaId})`;
46750
47257
  this.alreadyNotified = true;
@@ -46800,26 +47307,33 @@ stores.inject(MyMetaStore, storeInstance);
46800
47307
  this.applyUpdate();
46801
47308
  }
46802
47309
  }
46803
- isDynamicPivotInViewport() {
46804
- for (const position of this.getters.getVisibleCellPositions()) {
46805
- const isDynamicPivot = this.getters.isSpillPivotFormula(position);
46806
- if (isDynamicPivot) {
46807
- return true;
46808
- }
46809
- }
46810
- return false;
46811
- }
46812
- 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);
46813
47317
  for (const position of this.getters.getVisibleCellPositions()) {
46814
47318
  const cell = this.getters.getCell(position);
46815
47319
  if (cell?.isFormula) {
46816
47320
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46817
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46818
- 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
+ }
46819
47331
  }
46820
47332
  }
46821
47333
  }
46822
- return false;
47334
+ // we return true if there are only static pivots visible inserted the viewport,
47335
+ // otherwise false
47336
+ return staticPivotCount > 0;
46823
47337
  }
46824
47338
  addDefaultDateTimeGranularity(fields, definition) {
46825
47339
  const { columns, rows } = definition;
@@ -46995,6 +47509,7 @@ stores.inject(MyMetaStore, storeInstance);
46995
47509
  columns: {},
46996
47510
  });
46997
47511
  setup() {
47512
+ owl.onMounted(() => this.updateColumns());
46998
47513
  owl.onWillUpdateProps(() => this.updateColumns());
46999
47514
  }
47000
47515
  toggleHasHeader() {
@@ -52295,13 +52810,13 @@ stores.inject(MyMetaStore, storeInstance);
52295
52810
  : this.onComposerContentFocused();
52296
52811
  },
52297
52812
  Delete: () => {
52298
- this.env.model.dispatch("DELETE_CONTENT", {
52813
+ this.env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
52299
52814
  sheetId: this.env.model.getters.getActiveSheetId(),
52300
52815
  target: this.env.model.getters.getSelectedZones(),
52301
52816
  });
52302
52817
  },
52303
52818
  Backspace: () => {
52304
- this.env.model.dispatch("DELETE_CONTENT", {
52819
+ this.env.model.dispatch("DELETE_UNFILTERED_CONTENT", {
52305
52820
  sheetId: this.env.model.getters.getActiveSheetId(),
52306
52821
  target: this.env.model.getters.getSelectedZones(),
52307
52822
  });
@@ -52644,11 +53159,8 @@ stores.inject(MyMetaStore, storeInstance);
52644
53159
  else {
52645
53160
  this.env.model.dispatch("COPY");
52646
53161
  }
52647
- const content = this.env.model.getters.getClipboardContent();
52648
- const clipboardData = ev.clipboardData;
52649
- for (const type in content) {
52650
- clipboardData?.setData(type, content[type]);
52651
- }
53162
+ const osContent = await this.env.model.getters.getClipboardTextAndImageContent();
53163
+ await this.env.clipboard.write(osContent);
52652
53164
  ev.preventDefault();
52653
53165
  }
52654
53166
  async paste(ev) {
@@ -52660,21 +53172,27 @@ stores.inject(MyMetaStore, storeInstance);
52660
53172
  if (!clipboardData) {
52661
53173
  return;
52662
53174
  }
53175
+ const image = [...clipboardData?.files]?.find((file) => AllowedImageMimeTypes.includes(file.type));
52663
53176
  const osClipboard = {
52664
53177
  content: {
52665
53178
  [ClipboardMIMEType.PlainText]: clipboardData?.getData(ClipboardMIMEType.PlainText),
52666
53179
  [ClipboardMIMEType.Html]: clipboardData?.getData(ClipboardMIMEType.Html),
52667
53180
  },
52668
53181
  };
53182
+ if (image) {
53183
+ // TODO: support import of multiple images
53184
+ osClipboard.content[image.type] = image;
53185
+ }
52669
53186
  const target = this.env.model.getters.getSelectedZones();
52670
53187
  const isCutOperation = this.env.model.getters.isCutOperation();
52671
- const clipboardContent = parseOSClipboardContent(osClipboard.content);
52672
- const clipboardId = clipboardContent.data?.clipboardId;
52673
- 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) {
52674
53192
  interactivePaste(this.env, target);
52675
53193
  }
52676
53194
  else {
52677
- interactivePasteFromOS(this.env, target, clipboardContent);
53195
+ await interactivePasteFromOS(this.env, target, osClipboardContent);
52678
53196
  }
52679
53197
  if (isCutOperation) {
52680
53198
  await this.env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
@@ -54320,25 +54838,57 @@ stores.inject(MyMetaStore, storeInstance);
54320
54838
  "getAdaptedCfRanges",
54321
54839
  ];
54322
54840
  cfRules = {};
54323
- loopThroughRangesOfSheet(sheetId, applyChange) {
54324
- for (const rule of this.cfRules[sheetId]) {
54325
- if (rule.rule.type === "DataBarRule" && rule.rule.rangeValues) {
54326
- const change = applyChange(rule.rule.rangeValues);
54327
- switch (change.changeType) {
54328
- case "REMOVE":
54329
- this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54330
- //@ts-expect-error
54331
- "rangeValues", undefined);
54332
- break;
54333
- case "RESIZE":
54334
- case "MOVE":
54335
- 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++) {
54336
54863
  this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "rule",
54337
54864
  //@ts-expect-error
54338
- "rangeValues", change.range);
54339
- 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
+ }
54340
54886
  }
54341
54887
  }
54888
+ }
54889
+ }
54890
+ adaptCFRanges(sheetId, applyChange) {
54891
+ for (const rule of this.cfRules[sheetId]) {
54342
54892
  for (const range of rule.ranges) {
54343
54893
  const change = applyChange(range);
54344
54894
  switch (change.changeType) {
@@ -54362,14 +54912,11 @@ stores.inject(MyMetaStore, storeInstance);
54362
54912
  }
54363
54913
  }
54364
54914
  adaptRanges(applyChange, sheetId) {
54365
- if (sheetId) {
54366
- this.loopThroughRangesOfSheet(sheetId, applyChange);
54367
- }
54368
- else {
54369
- for (const sheetId of Object.keys(this.cfRules)) {
54370
- this.loopThroughRangesOfSheet(sheetId, applyChange);
54371
- }
54915
+ const sheetIds = sheetId ? [sheetId] : Object.keys(this.cfRules);
54916
+ for (const sheetId of sheetIds) {
54917
+ this.adaptCFRanges(sheetId, applyChange);
54372
54918
  }
54919
+ this.adaptCFFormulas(applyChange);
54373
54920
  }
54374
54921
  // ---------------------------------------------------------------------------
54375
54922
  // Command Handling
@@ -54763,10 +55310,23 @@ stores.inject(MyMetaStore, storeInstance);
54763
55310
  adaptRanges(applyChange, sheetId) {
54764
55311
  const sheetIds = sheetId ? [sheetId] : Object.keys(this.rules);
54765
55312
  for (const sheetId of sheetIds) {
54766
- 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
+ }
54767
55327
  }
54768
55328
  }
54769
- loopThroughRangesOfSheet(sheetId, applyChange) {
55329
+ adaptDVRanges(sheetId, applyChange) {
54770
55330
  const rules = this.rules[sheetId];
54771
55331
  for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
54772
55332
  const rule = this.rules[sheetId][ruleIndex];
@@ -60673,6 +61233,7 @@ stores.inject(MyMetaStore, storeInstance);
60673
61233
  exportForExcel(data) {
60674
61234
  for (const sheet of data.sheets) {
60675
61235
  sheet.cellValues = {};
61236
+ sheet.formulaSpillRanges = {};
60676
61237
  }
60677
61238
  for (const position of this.evaluator.getEvaluatedPositions()) {
60678
61239
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60684,8 +61245,9 @@ stores.inject(MyMetaStore, storeInstance);
60684
61245
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60685
61246
  const formulaCell = this.getCorrespondingFormulaCell(position);
60686
61247
  if (formulaCell) {
61248
+ const cell = this.getters.getCell(position);
60687
61249
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60688
- isFormula = isExported;
61250
+ isFormula = isExported && cell?.content === formulaCell.content;
60689
61251
  // If the cell contains a non-exported formula and that is evaluates to
60690
61252
  // nothing* ,we don't export it.
60691
61253
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60708,7 +61270,11 @@ stores.inject(MyMetaStore, storeInstance);
60708
61270
  content = !isExported ? newContent : exportedCellData;
60709
61271
  }
60710
61272
  exportedSheetData.cells[xc] = content;
60711
- 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
+ }
60712
61278
  }
60713
61279
  }
60714
61280
  /**
@@ -61003,7 +61569,7 @@ stores.inject(MyMetaStore, storeInstance);
61003
61569
  }
61004
61570
  const type = this.getters.getChartType(figureId);
61005
61571
  const runtime = this.getters.getChartRuntime(figureId);
61006
- const img = chartToImage(runtime, figure, type);
61572
+ const img = chartToImageUrl(runtime, figure, type);
61007
61573
  if (img) {
61008
61574
  sheet.images.push({
61009
61575
  ...figure,
@@ -62936,7 +63502,7 @@ stores.inject(MyMetaStore, storeInstance);
62936
63502
  getRule(cell, cells) {
62937
63503
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62938
63504
  const rule = rules.find((rule) => rule.condition(cell, cells));
62939
- return rule && rule.generateRule(cell, cells);
63505
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62940
63506
  }
62941
63507
  /**
62942
63508
  * Create the generator to be able to autofill the next cells.
@@ -65091,6 +65657,17 @@ stores.inject(MyMetaStore, storeInstance);
65091
65657
  sheetId: cmd.sheetId,
65092
65658
  });
65093
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;
65094
65671
  }
65095
65672
  }
65096
65673
  // ---------------------------------------------------------------------------
@@ -65730,6 +66307,7 @@ stores.inject(MyMetaStore, storeInstance);
65730
66307
  repeatLocalCommandTransformRegistry.add("SORT_CELLS", repeatSortCellsCommand);
65731
66308
  repeatLocalCommandTransformRegistry.add("SUM_SELECTION", genericRepeat);
65732
66309
  repeatLocalCommandTransformRegistry.add("SET_DECIMAL", genericRepeat);
66310
+ repeatLocalCommandTransformRegistry.add("DELETE_UNFILTERED_CONTENT", genericRepeat);
65733
66311
  function genericRepeat(getters, command) {
65734
66312
  let transformedCommand = deepCopy(command);
65735
66313
  for (const repeatTransform of genericRepeatsTransforms) {
@@ -66168,6 +66746,7 @@ stores.inject(MyMetaStore, storeInstance);
66168
66746
  }
66169
66747
  }
66170
66748
 
66749
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
66171
66750
  /**
66172
66751
  * Clipboard Plugin
66173
66752
  *
@@ -66177,7 +66756,7 @@ stores.inject(MyMetaStore, storeInstance);
66177
66756
  class ClipboardPlugin extends UIPlugin {
66178
66757
  static layers = ["Clipboard"];
66179
66758
  static getters = [
66180
- "getClipboardContent",
66759
+ "getClipboardTextAndImageContent",
66181
66760
  "getClipboardId",
66182
66761
  "getClipboardTextContent",
66183
66762
  "isCutOperation",
@@ -66187,6 +66766,13 @@ stores.inject(MyMetaStore, storeInstance);
66187
66766
  copiedData;
66188
66767
  _isCutOperation = false;
66189
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
+ }
66190
66776
  // ---------------------------------------------------------------------------
66191
66777
  // Command Handling
66192
66778
  // ---------------------------------------------------------------------------
@@ -66249,9 +66835,29 @@ stores.inject(MyMetaStore, storeInstance);
66249
66835
  break;
66250
66836
  case "PASTE_FROM_OS_CLIPBOARD": {
66251
66837
  this._isCutOperation = false;
66252
- this.copiedData =
66253
- cmd.clipboardContent.data ||
66254
- 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
+ }
66255
66861
  const pasteOption = cmd.pasteOption;
66256
66862
  this.paste(cmd.target, this.copiedData, {
66257
66863
  pasteOption,
@@ -66259,6 +66865,7 @@ stores.inject(MyMetaStore, storeInstance);
66259
66865
  isCutOperation: false,
66260
66866
  });
66261
66867
  this.status = "invisible";
66868
+ this.copiedData = undefined;
66262
66869
  break;
66263
66870
  }
66264
66871
  case "PASTE": {
@@ -66551,11 +67158,17 @@ stores.inject(MyMetaStore, storeInstance);
66551
67158
  getClipboardId() {
66552
67159
  return this.clipboardId;
66553
67160
  }
66554
- getClipboardContent() {
66555
- return {
67161
+ async getClipboardTextAndImageContent() {
67162
+ const file = await this.getImageContent();
67163
+ const mime = file?.type;
67164
+ const content = {
66556
67165
  [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),
66557
- [ClipboardMIMEType.Html]: this.getHTMLContent(),
67166
+ [ClipboardMIMEType.Html]: await this.getHTMLContent(),
66558
67167
  };
67168
+ if (mime && file) {
67169
+ content[mime] = file;
67170
+ }
67171
+ return content;
66559
67172
  }
66560
67173
  getSheetData() {
66561
67174
  const data = {
@@ -66584,11 +67197,24 @@ stores.inject(MyMetaStore, storeInstance);
66584
67197
  })
66585
67198
  .join("\n") || "\t");
66586
67199
  }
66587
- getHTMLContent() {
67200
+ async getHTMLContent() {
66588
67201
  let innerHTML = "";
66589
67202
  const cells = this.copiedData?.cells;
66590
67203
  if (!cells) {
66591
- 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
+ }
66592
67218
  }
66593
67219
  else if (cells.length === 1 && cells[0].length === 1) {
66594
67220
  innerHTML = `${this.getters.getCellText(cells[0][0].position)}`;
@@ -66616,6 +67242,62 @@ stores.inject(MyMetaStore, storeInstance);
66616
67242
  const serializedData = JSON.stringify(this.getSheetData());
66617
67243
  return `<div data-osheet-clipboard='${xmlEscape(serializedData)}'>${innerHTML}</div>`;
66618
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
+ }
66619
67301
  isCutOperation() {
66620
67302
  return this._isCutOperation ?? false;
66621
67303
  }
@@ -68690,12 +69372,17 @@ stores.inject(MyMetaStore, storeInstance);
68690
69372
  this.fileStore = fileStore;
68691
69373
  }
68692
69374
  async requestImage() {
68693
- 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) {
68694
69381
  const path = await this.fileStore.upload(file);
68695
69382
  const size = await this.getImageOriginalSize(path);
68696
69383
  return { path, size, mimetype: file.type };
68697
69384
  }
68698
- getImageFromUser() {
69385
+ userImageUpload() {
68699
69386
  return new Promise((resolve, reject) => {
68700
69387
  const input = document.createElement("input");
68701
69388
  input.setAttribute("type", "file");
@@ -68714,12 +69401,12 @@ stores.inject(MyMetaStore, storeInstance);
68714
69401
  getImageOriginalSize(path) {
68715
69402
  return new Promise((resolve, reject) => {
68716
69403
  const image = new Image();
68717
- image.src = path;
68718
69404
  image.addEventListener("load", () => {
68719
69405
  const size = { width: image.width, height: image.height };
68720
69406
  resolve(size);
68721
69407
  });
68722
69408
  image.addEventListener("error", reject);
69409
+ image.src = path;
68723
69410
  });
68724
69411
  }
68725
69412
  }
@@ -70097,6 +70784,126 @@ stores.inject(MyMetaStore, storeInstance);
70097
70784
  }
70098
70785
  }
70099
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
+
70100
70907
  css /* scss */ `
70101
70908
  .o-menu-item-button {
70102
70909
  display: flex;
@@ -70164,6 +70971,25 @@ stores.inject(MyMetaStore, storeInstance);
70164
70971
  }
70165
70972
  }
70166
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
+
70167
70993
  /**
70168
70994
  * List the available borders positions and the corresponding icons.
70169
70995
  * The structure of this array is defined to match the order/lines we want
@@ -70333,13 +71159,12 @@ stores.inject(MyMetaStore, storeInstance);
70333
71159
  class BorderEditorWidget extends owl.Component {
70334
71160
  static template = "o-spreadsheet-BorderEditorWidget";
70335
71161
  static props = {
70336
- toggleBorderEditor: Function,
70337
- showBorderEditor: Boolean,
70338
71162
  disabled: { type: Boolean, optional: true },
70339
71163
  dropdownMaxHeight: { type: Number, optional: true },
70340
71164
  class: { type: String, optional: true },
70341
71165
  };
70342
71166
  static components = { BorderEditor };
71167
+ topBarToolStore;
70343
71168
  borderEditorButtonRef = owl.useRef("borderEditorButton");
70344
71169
  state = owl.useState({
70345
71170
  currentColor: DEFAULT_BORDER_DESC.color,
@@ -70347,12 +71172,16 @@ stores.inject(MyMetaStore, storeInstance);
70347
71172
  currentPosition: undefined,
70348
71173
  });
70349
71174
  setup() {
70350
- owl.onWillUpdateProps((newProps) => {
70351
- if (!newProps.showBorderEditor) {
71175
+ this.topBarToolStore = useToolBarDropdownStore();
71176
+ owl.onWillUpdateProps(() => {
71177
+ if (!this.isActive) {
70352
71178
  this.state.currentPosition = undefined;
70353
71179
  }
70354
71180
  });
70355
71181
  }
71182
+ get dropdownMaxHeight() {
71183
+ return this.env.model.getters.getSheetViewDimension().height;
71184
+ }
70356
71185
  get borderEditorAnchorRect() {
70357
71186
  const button = this.borderEditorButtonRef.el;
70358
71187
  const buttonRect = button.getBoundingClientRect();
@@ -70375,6 +71204,17 @@ stores.inject(MyMetaStore, storeInstance);
70375
71204
  this.state.currentStyle = style;
70376
71205
  this.updateBorder();
70377
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
+ }
70378
71218
  updateBorder() {
70379
71219
  if (this.state.currentPosition === undefined) {
70380
71220
  return;
@@ -70391,93 +71231,42 @@ stores.inject(MyMetaStore, storeInstance);
70391
71231
  }
70392
71232
  }
70393
71233
 
70394
- const COMPOSER_MAX_HEIGHT = 100;
70395
- /* svg free of use from https://uxwing.com/formula-fx-icon/ */
70396
- const FX_SVG = /*xml*/ `
70397
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 121.8 122.9' width='16' height='16' focusable='false'>
70398
- <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'/>
70399
- </svg>
70400
- `;
70401
- css /* scss */ `
70402
- .o-topbar-composer-container {
70403
- height: ${TOPBAR_TOOLBAR_HEIGHT}px;
70404
- }
70405
-
70406
- .o-topbar-composer {
70407
- height: fit-content;
70408
- margin-top: -1px;
70409
- margin-bottom: -1px;
70410
- border: 1px solid;
70411
- font-family: ${DEFAULT_FONT};
70412
-
70413
- .o-composer:empty:not(:focus):not(.active)::before {
70414
- content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}");
70415
- position: relative;
70416
- top: 20%;
70417
- }
70418
- }
70419
-
70420
- .user-select-text {
70421
- user-select: text;
70422
- }
70423
- `;
70424
- class TopBarComposer extends owl.Component {
70425
- static template = "o-spreadsheet-TopBarComposer";
70426
- static props = {};
70427
- static components = { Composer };
70428
- composerFocusStore;
70429
- composerStore;
70430
- 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;
70431
71240
  setup() {
70432
- this.composerFocusStore = useStore(ComposerFocusStore);
70433
- const composerStore = useStore(CellComposerStore);
70434
- this.composerStore = composerStore;
70435
- this.composerInterface = {
70436
- id: "topbarComposer",
70437
- get editionMode() {
70438
- return composerStore.editionMode;
70439
- },
70440
- startEdition: this.composerStore.startEdition,
70441
- setCurrentContent: this.composerStore.setCurrentContent,
70442
- stopEdition: this.composerStore.stopEdition,
70443
- };
71241
+ this.paintFormatStore = useStore(PaintFormatStore);
70444
71242
  }
70445
- get focus() {
70446
- return this.composerFocusStore.activeComposer === this.composerInterface
70447
- ? this.composerFocusStore.focusMode
70448
- : "inactive";
71243
+ get isActive() {
71244
+ return this.paintFormatStore.isActive;
70449
71245
  }
70450
- get composerStyle() {
70451
- const style = {
70452
- padding: "5px 0px 5px 8px",
70453
- "max-height": `${COMPOSER_MAX_HEIGHT}px`,
70454
- "line-height": "24px",
70455
- };
70456
- style.height = this.focus === "inactive" ? `${TOPBAR_TOOLBAR_HEIGHT}px` : "fit-content";
70457
- return cssPropertiesToCss(style);
71246
+ onDblClick() {
71247
+ this.paintFormatStore.activate({ persistent: true });
70458
71248
  }
70459
- get containerStyle() {
70460
- if (this.focus === "inactive") {
70461
- return cssPropertiesToCss({
70462
- "border-color": SEPARATOR_COLOR,
70463
- "border-right": "none",
70464
- });
71249
+ togglePaintFormat() {
71250
+ if (this.isActive) {
71251
+ this.paintFormatStore.cancel();
71252
+ }
71253
+ else {
71254
+ this.paintFormatStore.activate({ persistent: false });
70465
71255
  }
70466
- return cssPropertiesToCss({
70467
- "border-color": SELECTION_BORDER_COLOR,
70468
- "z-index": String(ComponentsImportance.TopBarComposer),
70469
- });
70470
- }
70471
- onFocus(selection) {
70472
- this.composerFocusStore.focusComposer(this.composerInterface, { selection });
70473
71256
  }
70474
71257
  }
70475
71258
 
70476
71259
  class TableDropdownButton extends owl.Component {
70477
71260
  static template = "o-spreadsheet-TableDropdownButton";
70478
71261
  static components = { TableStylesPopover, ActionButton };
70479
- static props = {};
71262
+ static props = {
71263
+ class: { type: String, optional: true },
71264
+ };
71265
+ topBarToolStore;
70480
71266
  state = owl.useState({ popoverProps: undefined });
71267
+ setup() {
71268
+ this.topBarToolStore = useToolBarDropdownStore();
71269
+ }
70481
71270
  onStylePicked(styleId) {
70482
71271
  const sheetId = this.env.model.getters.getActiveSheetId();
70483
71272
  const tableConfig = { ...this.tableConfig, styleId };
@@ -70493,12 +71282,13 @@ stores.inject(MyMetaStore, storeInstance);
70493
71282
  return;
70494
71283
  }
70495
71284
  if (this.env.model.getters.getFirstTableInSelection()) {
71285
+ this.topBarToolStore.closeDropdowns();
70496
71286
  this.env.toggleSidePanel("TableSidePanel", {});
70497
71287
  return;
70498
71288
  }
70499
- // Open the popover
70500
71289
  const target = ev.currentTarget;
70501
- const { bottom, left } = target.getBoundingClientRect();
71290
+ const { left, bottom } = target.getBoundingClientRect();
71291
+ this.topBarToolStore.openDropdown();
70502
71292
  this.state.popoverProps = {
70503
71293
  anchorRect: { x: left, y: bottom, width: 0, height: 0 },
70504
71294
  positioning: "BottomLeft",
@@ -70510,8 +71300,8 @@ stores.inject(MyMetaStore, storeInstance);
70510
71300
  }
70511
71301
  get action() {
70512
71302
  return {
70513
- name: (env) => this.env.model.getters.getFirstTableInSelection() ? _t("Edit table") : _t("Insert table"),
70514
- 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()
70515
71305
  ? "o-spreadsheet-Icon.EDIT_TABLE"
70516
71306
  : "o-spreadsheet-Icon.PAINT_TABLE",
70517
71307
  };
@@ -70521,35 +71311,347 @@ stores.inject(MyMetaStore, storeInstance);
70521
71311
  }
70522
71312
  }
70523
71313
 
70524
- class PaintFormatButton extends owl.Component {
70525
- 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 };
70526
71352
  static props = {
70527
- class: { type: String, optional: true },
71353
+ parentAction: Object,
71354
+ childActions: Array,
71355
+ class: String,
71356
+ childClass: String,
70528
71357
  };
70529
- paintFormatStore;
71358
+ topBarToolStore;
71359
+ actionRef = owl.useRef("actionRef");
70530
71360
  setup() {
70531
- 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
+ }
70532
71370
  }
70533
71371
  get isActive() {
70534
- return this.paintFormatStore.isActive;
71372
+ return this.topBarToolStore.isActive;
70535
71373
  }
70536
- onDblClick() {
70537
- 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
+ };
70538
71384
  }
70539
- 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() {
70540
71402
  if (this.isActive) {
70541
- this.paintFormatStore.cancel();
71403
+ this.topBarToolStore.closeDropdowns();
70542
71404
  }
70543
71405
  else {
70544
- 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();
70545
71441
  }
70546
71442
  }
71443
+ get isActive() {
71444
+ return this.topBarToolStore.isActive;
71445
+ }
70547
71446
  }
70548
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
+
70549
71641
  // -----------------------------------------------------------------------------
70550
71642
  // TopBar
70551
71643
  // -----------------------------------------------------------------------------
70552
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
+
70553
71655
  .o-spreadsheet-topbar {
70554
71656
  line-height: 1.2;
70555
71657
  font-size: 13px;
@@ -70599,47 +71701,7 @@ stores.inject(MyMetaStore, storeInstance);
70599
71701
 
70600
71702
  /* Toolbar */
70601
71703
  .o-toolbar-tools {
70602
- display: flex;
70603
- flex-shrink: 0;
70604
- margin: 0px 6px 0px 16px;
70605
71704
  cursor: default;
70606
-
70607
- .o-divider {
70608
- display: inline-block;
70609
- border-right: 1px solid ${SEPARATOR_COLOR};
70610
- width: 0;
70611
- margin: 0 6px;
70612
- }
70613
-
70614
- .o-dropdown {
70615
- position: relative;
70616
- display: flex;
70617
- align-items: center;
70618
-
70619
- > span {
70620
- height: 30px;
70621
- }
70622
-
70623
- .o-dropdown-content {
70624
- position: absolute;
70625
- top: 100%;
70626
- left: 0;
70627
- overflow-y: auto;
70628
- overflow-x: hidden;
70629
- padding: 2px;
70630
- z-index: ${ComponentsImportance.Dropdown};
70631
- box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
70632
- background-color: white;
70633
-
70634
- .o-dropdown-line {
70635
- display: flex;
70636
-
70637
- > span {
70638
- padding: 4px;
70639
- }
70640
- }
70641
- }
70642
- }
70643
71705
  }
70644
71706
  }
70645
71707
  }
@@ -70651,38 +71713,73 @@ stores.inject(MyMetaStore, storeInstance);
70651
71713
  dropdownMaxHeight: Number,
70652
71714
  };
70653
71715
  static components = {
70654
- ColorPickerWidget,
70655
- ColorPicker,
70656
71716
  Menu,
70657
71717
  TopBarComposer,
70658
- FontSizeEditor,
70659
- ActionButton,
70660
- PaintFormatButton,
70661
- BorderEditorWidget,
70662
- TableDropdownButton,
71718
+ Popover,
70663
71719
  };
71720
+ toolsCategories = topBarToolBarRegistry.getCategories();
70664
71721
  state = owl.useState({
70665
71722
  menuState: { isOpen: false, position: null, menuItems: [] },
70666
- activeTool: "",
70667
- fillColor: "#ffffff",
70668
- textColor: "#000000",
71723
+ invisibleToolsCategories: [],
71724
+ toolsPopoverState: { isOpen: false },
70669
71725
  });
70670
71726
  isSelectingMenu = false;
70671
71727
  openedEl = null;
70672
71728
  menus = [];
70673
- EDIT = ACTION_EDIT;
70674
- FORMAT = ACTION_FORMAT;
70675
- DATA = ACTION_DATA;
71729
+ toolbarMenuRegistry = topBarToolBarRegistry;
70676
71730
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
70677
71731
  isntToolbarMenu = false;
70678
71732
  composerFocusStore;
70679
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();
70680
71740
  setup() {
70681
71741
  this.composerFocusStore = useStore(ComposerFocusStore);
70682
71742
  this.fingerprints = useStore(FormulaFingerprintStore);
71743
+ this.topBarToolStore = useStore(TopBarToolStore);
70683
71744
  owl.useExternalListener(window, "click", this.onExternalClick);
70684
71745
  owl.onWillStart(() => this.updateCellState());
70685
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
+ }
70686
71783
  }
70687
71784
  get topbarComponents() {
70688
71785
  return topbarComponentRegistry
@@ -70712,12 +71809,6 @@ stores.inject(MyMetaStore, storeInstance);
70712
71809
  this.openMenu(menu, ev);
70713
71810
  }
70714
71811
  }
70715
- toggleDropdownTool(tool, ev) {
70716
- const isOpen = this.state.activeTool === tool;
70717
- this.closeMenus();
70718
- this.state.activeTool = isOpen ? "" : tool;
70719
- this.openedEl = isOpen ? null : ev.target;
70720
- }
70721
71812
  toggleContextMenu(menu, ev) {
70722
71813
  if (this.state.menuState.isOpen && this.isntToolbarMenu) {
70723
71814
  this.closeMenus();
@@ -70727,19 +71818,10 @@ stores.inject(MyMetaStore, storeInstance);
70727
71818
  this.isntToolbarMenu = true;
70728
71819
  }
70729
71820
  }
70730
- toggleToolbarContextMenu(menuSpec, ev) {
70731
- if (this.state.menuState.isOpen && !this.isntToolbarMenu) {
70732
- this.closeMenus();
70733
- }
70734
- else {
70735
- const menu = createAction(menuSpec);
70736
- this.openMenu(menu, ev);
70737
- this.isntToolbarMenu = false;
70738
- }
70739
- }
70740
71821
  openMenu(menu, ev) {
71822
+ this.topBarToolStore.closeDropdowns();
71823
+ this.state.toolsPopoverState.isOpen = false;
70741
71824
  const { left, top, height } = ev.currentTarget.getBoundingClientRect();
70742
- this.state.activeTool = "";
70743
71825
  this.state.menuState.isOpen = true;
70744
71826
  this.state.menuState.position = { x: left, y: top + height };
70745
71827
  this.state.menuState.menuItems = menu
@@ -70751,16 +71833,14 @@ stores.inject(MyMetaStore, storeInstance);
70751
71833
  this.composerFocusStore.activeComposer.stopEdition();
70752
71834
  }
70753
71835
  closeMenus() {
70754
- this.state.activeTool = "";
71836
+ this.topBarToolStore.closeDropdowns();
71837
+ this.state.toolsPopoverState.isOpen = false;
70755
71838
  this.state.menuState.isOpen = false;
70756
71839
  this.state.menuState.parentMenu = undefined;
70757
71840
  this.isSelectingMenu = false;
70758
71841
  this.openedEl = null;
70759
71842
  }
70760
71843
  updateCellState() {
70761
- const style = this.env.model.getters.getCurrentStyle();
70762
- this.state.fillColor = style.fillColor || "#ffffff";
70763
- this.state.textColor = style.textColor || "#000000";
70764
71844
  this.menus = topbarMenuRegistry.getMenuItems();
70765
71845
  }
70766
71846
  getMenuName(menu) {
@@ -70773,6 +71853,26 @@ stores.inject(MyMetaStore, storeInstance);
70773
71853
  setFontSize(fontSize) {
70774
71854
  setStyle(this.env, { fontSize });
70775
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
+ }
70776
71876
  }
70777
71877
 
70778
71878
  function instantiateClipboard() {
@@ -70795,6 +71895,7 @@ stores.inject(MyMetaStore, storeInstance);
70795
71895
  * Therefore, we try to catch any errors and fallback on writing only standard
70796
71896
  * mimetypes to prevent the whole copy action from crashing.
70797
71897
  */
71898
+ console.log("Failed to write on the clipboard, falling back to plain/html text. Error %s", e);
70798
71899
  try {
70799
71900
  await this.clipboard?.write([
70800
71901
  new ClipboardItem({
@@ -70826,14 +71927,20 @@ stores.inject(MyMetaStore, storeInstance);
70826
71927
  if (this.clipboard?.read) {
70827
71928
  try {
70828
71929
  const clipboardItems = await this.clipboard.read();
70829
- const clipboardContent = {};
71930
+ const osClipboardContent = {};
70830
71931
  for (const item of clipboardItems) {
70831
71932
  for (const type of item.types) {
70832
71933
  const blob = await item.getType(type);
70833
- 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
+ }
70834
71941
  }
70835
71942
  }
70836
- return { status: "ok", content: clipboardContent };
71943
+ return { status: "ok", content: osClipboardContent };
70837
71944
  }
70838
71945
  catch (e) {
70839
71946
  const status = permissionResult?.state === "denied" ? "permissionDenied" : "notImplemented";
@@ -70850,13 +71957,17 @@ stores.inject(MyMetaStore, storeInstance);
70850
71957
  }
70851
71958
  }
70852
71959
  getClipboardItems(content) {
70853
- const clipboardItemData = {
70854
- [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),
70855
- [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),
70856
- };
71960
+ const clipboardItemData = {};
71961
+ for (const type of Object.keys(content)) {
71962
+ clipboardItemData[type] = this.getBlob(content, type);
71963
+ }
70857
71964
  return [new ClipboardItem(clipboardItemData)];
70858
71965
  }
70859
71966
  getBlob(clipboardContent, type) {
71967
+ const content = clipboardContent[type];
71968
+ if (content instanceof Blob || content instanceof File) {
71969
+ return content;
71970
+ }
70860
71971
  return new Blob([clipboardContent[type] || ""], {
70861
71972
  type,
70862
71973
  });
@@ -71135,7 +72246,7 @@ stores.inject(MyMetaStore, storeInstance);
71135
72246
  properties["grid-template-rows"] = `auto`;
71136
72247
  }
71137
72248
  else {
71138
- properties["grid-template-rows"] = `max-content auto ${BOTTOMBAR_HEIGHT + 1}px`;
72249
+ properties["grid-template-rows"] = `min-content auto min-content`;
71139
72250
  }
71140
72251
  properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
71141
72252
  return cssPropertiesToCss(properties);
@@ -71246,8 +72357,7 @@ stores.inject(MyMetaStore, storeInstance);
71246
72357
  this._focusGrid();
71247
72358
  }
71248
72359
  get gridHeight() {
71249
- const { height } = this.env.model.getters.getSheetViewDimension();
71250
- return height;
72360
+ return this.env.model.getters.getSheetViewDimension().height;
71251
72361
  }
71252
72362
  get gridContainerStyle() {
71253
72363
  const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
@@ -73418,7 +74528,7 @@ stores.inject(MyMetaStore, storeInstance);
73418
74528
  `;
73419
74529
  }
73420
74530
 
73421
- function addFormula(formula, value) {
74531
+ function addFormula(formula, value, formulaSpillRange) {
73422
74532
  if (!formula) {
73423
74533
  return { attrs: [], node: escapeXml `` };
73424
74534
  }
@@ -73426,10 +74536,17 @@ stores.inject(MyMetaStore, storeInstance);
73426
74536
  if (type === undefined) {
73427
74537
  return { attrs: [], node: escapeXml `` };
73428
74538
  }
73429
- const attrs = [["t", type]];
74539
+ const attrs = [
74540
+ ["cm", "1"],
74541
+ ["t", type],
74542
+ ];
73430
74543
  const XlsxFormula = adaptFormulaToExcel(formula);
73431
74544
  const exportedValue = adaptFormulaValueToExcel(value);
73432
- 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>`;
73433
74550
  return { attrs, node };
73434
74551
  }
73435
74552
  function addContent(content, sharedStrings, forceString = false) {
@@ -74419,7 +75536,7 @@ stores.inject(MyMetaStore, storeInstance);
74419
75536
  let cellNode = escapeXml ``;
74420
75537
  // Either formula or static value inside the cell
74421
75538
  if (content?.startsWith("=") && value !== undefined) {
74422
- const res = addFormula(content, value);
75539
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74423
75540
  if (!res) {
74424
75541
  continue;
74425
75542
  }
@@ -74705,6 +75822,30 @@ stores.inject(MyMetaStore, storeInstance);
74705
75822
  `;
74706
75823
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74707
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"));
74708
75849
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74709
75850
  type: XLSX_RELATION_TYPE.sharedStrings,
74710
75851
  target: "sharedStrings.xml",
@@ -74713,6 +75854,10 @@ stores.inject(MyMetaStore, storeInstance);
74713
75854
  type: XLSX_RELATION_TYPE.styles,
74714
75855
  target: "styles.xml",
74715
75856
  });
75857
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
75858
+ type: XLSX_RELATION_TYPE.metadata,
75859
+ target: "metadata.xml",
75860
+ });
74716
75861
  return files;
74717
75862
  }
74718
75863
  /**
@@ -75674,9 +76819,9 @@ stores.inject(MyMetaStore, storeInstance);
75674
76819
  exports.tokenize = tokenize;
75675
76820
 
75676
76821
 
75677
- __info__.version = "18.3.0-alpha.1";
75678
- __info__.date = "2025-02-25T06:00:14.885Z";
75679
- __info__.hash = "be4d957";
76822
+ __info__.version = "18.3.0-alpha.3";
76823
+ __info__.date = "2025-03-07T10:41:05.411Z";
76824
+ __info__.hash = "f59f5f6";
75680
76825
 
75681
76826
 
75682
76827
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);