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