@odoo/o-spreadsheet 18.4.0-alpha.3 → 18.4.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.4.0-alpha.3
6
- * @date 2025-05-13T17:54:54.061Z
7
- * @hash 70ad365
5
+ * @version 18.4.0-alpha.4
6
+ * @date 2025-05-20T05:57:45.452Z
7
+ * @hash 5c28bca
8
8
  */
9
9
 
10
10
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, useExternalListener, onWillUpdateProps, onWillStart, onWillPatch, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
@@ -184,7 +184,7 @@ const ALERT_INFO_BG = "#CDEDF1";
184
184
  const ALERT_INFO_BORDER = "#98DBE2";
185
185
  const ALERT_INFO_TEXT_COLOR = "#09414A";
186
186
  const BADGE_SELECTED_COLOR = "#E6F2F3";
187
- const CHART_PADDING$1 = 20;
187
+ const CHART_PADDING = 20;
188
188
  const CHART_PADDING_BOTTOM = 10;
189
189
  const CHART_PADDING_TOP = 15;
190
190
  const CHART_TITLE_FONT_SIZE = 16;
@@ -346,6 +346,8 @@ const DEFAULT_GAUGE_UPPER_COLOR = "#43C5B1";
346
346
  const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
347
347
  const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1";
348
348
  const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175";
349
+ const DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE = 32;
350
+ const DEFAULT_SCORECARD_BASELINE_FONT_SIZE = 16;
349
351
  const LINE_FILL_TRANSPARENCY = 0.4;
350
352
  const DEFAULT_WINDOW_SIZE = 2;
351
353
  // session
@@ -395,6 +397,7 @@ const PIVOT_TABLE_CONFIG = {
395
397
  styleId: "TableStyleMedium5",
396
398
  automaticAutofill: false,
397
399
  };
400
+ const PIVOT_INDENT = 15;
398
401
  const DEFAULT_CURRENCY = {
399
402
  symbol: "$",
400
403
  position: "before",
@@ -1636,18 +1639,53 @@ function lettersToNumber(letters) {
1636
1639
  let result = 0;
1637
1640
  const l = letters.length;
1638
1641
  for (let i = 0; i < l; i++) {
1639
- const charCode = letters.charCodeAt(i);
1640
- const colIndex = charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96;
1642
+ const colIndex = charToNumber(letters[i]);
1641
1643
  result = result * 26 + colIndex;
1642
1644
  }
1643
1645
  return result - 1;
1644
1646
  }
1647
+ function charToNumber(char) {
1648
+ const charCode = char.charCodeAt(0);
1649
+ return charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96;
1650
+ }
1645
1651
  function isCharALetter(char) {
1646
1652
  return (char >= "A" && char <= "Z") || (char >= "a" && char <= "z");
1647
1653
  }
1648
1654
  function isCharADigit(char) {
1649
1655
  return char >= "0" && char <= "9";
1650
1656
  }
1657
+ // we limit the max column to 3 letters and max row to 7 digits for performance reasons
1658
+ const MAX_COL = lettersToNumber("ZZZ");
1659
+ const MAX_ROW = 9999998;
1660
+ function consumeSpaces(chars) {
1661
+ while (chars.current === " ") {
1662
+ chars.advanceBy(1);
1663
+ }
1664
+ }
1665
+ function consumeLetters(chars) {
1666
+ if (chars.current === "$")
1667
+ chars.advanceBy(1);
1668
+ if (!chars.current || !isCharALetter(chars.current)) {
1669
+ return -1;
1670
+ }
1671
+ let colCoordinate = 0;
1672
+ while (chars.current && isCharALetter(chars.current)) {
1673
+ colCoordinate = colCoordinate * 26 + charToNumber(chars.shift());
1674
+ }
1675
+ return colCoordinate;
1676
+ }
1677
+ function consumeDigits(chars) {
1678
+ if (chars.current === "$")
1679
+ chars.advanceBy(1);
1680
+ if (!chars.current || !isCharADigit(chars.current)) {
1681
+ return -1;
1682
+ }
1683
+ let num = 0;
1684
+ while (chars.current && isCharADigit(chars.current)) {
1685
+ num = num * 10 + Number(chars.shift());
1686
+ }
1687
+ return num;
1688
+ }
1651
1689
  /**
1652
1690
  * Convert a "XC" coordinate to cartesian coordinates.
1653
1691
  *
@@ -1658,33 +1696,17 @@ function isCharADigit(char) {
1658
1696
  * Note: it also accepts lowercase coordinates, but not fixed references
1659
1697
  */
1660
1698
  function toCartesian(xc) {
1661
- xc = xc.trim();
1662
- let letterPart = "";
1663
- let numberPart = "";
1664
- let i = 0;
1665
- // Process letter part
1666
- if (xc[i] === "$")
1667
- i++;
1668
- while (i < xc.length && isCharALetter(xc[i])) {
1669
- letterPart += xc[i++];
1670
- }
1671
- if (letterPart.length === 0 || letterPart.length > 3) {
1672
- // limit to max 3 letters for performance reasons
1699
+ const chars = new TokenizingChars(xc);
1700
+ consumeSpaces(chars);
1701
+ const letterPart = consumeLetters(chars);
1702
+ if (letterPart === -1 || !chars.current) {
1673
1703
  throw new Error(`Invalid cell description: ${xc}`);
1674
1704
  }
1675
- // Process number part
1676
- if (xc[i] === "$")
1677
- i++;
1678
- while (i < xc.length && isCharADigit(xc[i])) {
1679
- numberPart += xc[i++];
1680
- }
1681
- if (i !== xc.length || numberPart.length === 0 || numberPart.length > 7) {
1682
- // limit to max 7 numbers for performance reasons
1683
- throw new Error(`Invalid cell description: ${xc}`);
1684
- }
1685
- const col = lettersToNumber(letterPart);
1686
- const row = Number(numberPart) - 1;
1687
- if (isNaN(row)) {
1705
+ const num = consumeDigits(chars);
1706
+ consumeSpaces(chars);
1707
+ const col = letterPart - 1;
1708
+ const row = num - 1;
1709
+ if (!chars.isOver() || col > MAX_COL || row > MAX_ROW) {
1688
1710
  throw new Error(`Invalid cell description: ${xc}`);
1689
1711
  }
1690
1712
  return { col, row };
@@ -5373,67 +5395,6 @@ function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
5373
5395
  return result;
5374
5396
  }
5375
5397
 
5376
- /** Reference of a cell (eg. A1, $B$5) */
5377
- const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
5378
- // Same as above, but matches the exact string (nothing before or after)
5379
- const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
5380
- /** Reference of a column header (eg. A, AB, $A) */
5381
- const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
5382
- /** Reference of a row header (eg. 1, $1) */
5383
- const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
5384
- /** Reference of a column (eg. A, $CA, Sheet1!B) */
5385
- const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
5386
- /** Reference of a row (eg. 1, 59, Sheet1!9) */
5387
- const rowReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([0-9]{1,7})$/, "i");
5388
- /** Reference of a normal range or a full row range (eg. A1:B1, 1:$5, $A2:5) */
5389
- const fullRowXc = /(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*:\s*(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*/i;
5390
- /** Reference of a normal range or a column row range (eg. A1:B1, A:$B, $A1:C) */
5391
- const fullColXc = /\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*:\s*\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*/i;
5392
- /** Reference of a cell or a range, it can be a bounded range, a full row or a full column */
5393
- const rangeReference = new RegExp(/^\s*('.+'!|[^']+!)?/.source +
5394
- "(" +
5395
- [cellReference.source, fullRowXc.source, fullColXc.source].join("|") +
5396
- ")" +
5397
- /$/.source, "i");
5398
- /**
5399
- * Return true if the given xc is the reference of a column (e.g. A or AC or Sheet1!A)
5400
- */
5401
- function isColReference(xc) {
5402
- return colReference.test(xc);
5403
- }
5404
- /**
5405
- * Return true if the given xc is the reference of a column (e.g. 1 or Sheet1!1)
5406
- */
5407
- function isRowReference(xc) {
5408
- return rowReference.test(xc);
5409
- }
5410
- function isColHeader(str) {
5411
- return colHeader.test(str);
5412
- }
5413
- function isRowHeader(str) {
5414
- return rowHeader.test(str);
5415
- }
5416
- /**
5417
- * Return true if the given xc is the reference of a single cell,
5418
- * without any specified sheet (e.g. A1)
5419
- */
5420
- function isSingleCellReference(xc) {
5421
- return singleCellReference.test(xc);
5422
- }
5423
- function splitReference(ref) {
5424
- if (!ref.includes("!")) {
5425
- return { xc: ref };
5426
- }
5427
- const parts = ref.split("!");
5428
- const xc = parts.pop();
5429
- const sheetName = getUnquotedSheetName(parts.join("!")) || undefined;
5430
- return { sheetName, xc };
5431
- }
5432
- /** Return a reference SheetName!xc from the given arguments */
5433
- function getFullReference(sheetName, xc) {
5434
- return sheetName !== undefined ? `${getCanonicalSymbolName(sheetName)}!${xc}` : xc;
5435
- }
5436
-
5437
5398
  /**
5438
5399
  * Convert from a cartesian reference to a Zone
5439
5400
  * The range boundaries will be kept in the same order as the
@@ -5451,63 +5412,55 @@ function getFullReference(sheetName, xc) {
5451
5412
  *
5452
5413
  */
5453
5414
  function toZoneWithoutBoundaryChanges(xc) {
5454
- if (xc.includes("!")) {
5455
- xc = xc.split("!").at(-1);
5456
- }
5457
- if (xc.includes("$")) {
5458
- xc = xc.replaceAll("$", "");
5459
- }
5460
- let firstRangePart = "";
5461
- let secondRangePart;
5462
- if (xc.includes(":")) {
5463
- [firstRangePart, secondRangePart] = xc.split(":");
5464
- firstRangePart = firstRangePart.trim();
5465
- secondRangePart = secondRangePart.trim();
5466
- }
5467
- else {
5468
- firstRangePart = xc.trim();
5469
- }
5415
+ const chars = new TokenizingChars(xc);
5416
+ consumeSpaces(chars);
5417
+ const sheetSeparatorIndex = xc.indexOf("!");
5418
+ if (sheetSeparatorIndex !== -1) {
5419
+ chars.advanceBy(sheetSeparatorIndex + 1);
5420
+ }
5421
+ const leftLetters = consumeLetters(chars);
5422
+ const leftNumbers = consumeDigits(chars);
5470
5423
  let top, bottom, left, right;
5471
5424
  let fullCol = false;
5472
5425
  let fullRow = false;
5473
5426
  let hasHeader = false;
5474
- if (isColReference(firstRangePart)) {
5475
- left = right = lettersToNumber(firstRangePart);
5427
+ if (leftNumbers === -1) {
5428
+ left = right = leftLetters - 1;
5476
5429
  top = bottom = 0;
5477
5430
  fullCol = true;
5478
5431
  }
5479
- else if (isRowReference(firstRangePart)) {
5480
- top = bottom = parseInt(firstRangePart, 10) - 1;
5432
+ else if (leftLetters === -1) {
5433
+ top = bottom = leftNumbers - 1;
5481
5434
  left = right = 0;
5482
5435
  fullRow = true;
5483
5436
  }
5484
5437
  else {
5485
- const c = toCartesian(firstRangePart);
5486
- left = right = c.col;
5487
- top = bottom = c.row;
5438
+ left = right = leftLetters - 1;
5439
+ top = bottom = leftNumbers - 1;
5488
5440
  hasHeader = true;
5489
5441
  }
5490
- if (secondRangePart) {
5491
- if (isColReference(secondRangePart)) {
5492
- right = lettersToNumber(secondRangePart);
5442
+ consumeSpaces(chars);
5443
+ if (chars.current === ":") {
5444
+ chars.advanceBy(1);
5445
+ consumeSpaces(chars);
5446
+ const rightLetters = consumeLetters(chars);
5447
+ const rightNumbers = consumeDigits(chars);
5448
+ if (rightNumbers === -1) {
5449
+ right = rightLetters - 1;
5493
5450
  fullCol = true;
5494
5451
  }
5495
- else if (isRowReference(secondRangePart)) {
5496
- bottom = parseInt(secondRangePart, 10) - 1;
5452
+ else if (rightLetters === -1) {
5453
+ bottom = rightNumbers - 1;
5497
5454
  fullRow = true;
5498
5455
  }
5499
5456
  else {
5500
- const c = toCartesian(secondRangePart);
5501
- right = c.col;
5502
- bottom = c.row;
5457
+ right = rightLetters - 1;
5458
+ bottom = rightNumbers - 1;
5503
5459
  top = fullCol ? bottom : top;
5504
5460
  left = fullRow ? right : left;
5505
5461
  hasHeader = true;
5506
5462
  }
5507
5463
  }
5508
- if (fullCol && fullRow) {
5509
- throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row");
5510
- }
5511
5464
  const zone = {
5512
5465
  top,
5513
5466
  left,
@@ -5536,7 +5489,16 @@ function toZoneWithoutBoundaryChanges(xc) {
5536
5489
  */
5537
5490
  function toUnboundedZone(xc) {
5538
5491
  const zone = toZoneWithoutBoundaryChanges(xc);
5539
- return reorderZone(zone);
5492
+ const orderedZone = reorderZone(zone);
5493
+ const bottom = orderedZone.bottom;
5494
+ const right = orderedZone.right;
5495
+ if ((bottom !== undefined && bottom > MAX_ROW) || (right !== undefined && right > MAX_COL)) {
5496
+ throw new Error(`Range string out of bounds: ${xc}`); // limit the size of the zone for performance
5497
+ }
5498
+ if (bottom === undefined && right === undefined) {
5499
+ throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row");
5500
+ }
5501
+ return orderedZone;
5540
5502
  }
5541
5503
  /**
5542
5504
  * Convert from a cartesian reference to a Zone.
@@ -6150,6 +6112,67 @@ function scrollDelay(value) {
6150
6112
  return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));
6151
6113
  }
6152
6114
 
6115
+ /** Reference of a cell (eg. A1, $B$5) */
6116
+ const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
6117
+ // Same as above, but matches the exact string (nothing before or after)
6118
+ const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
6119
+ /** Reference of a column header (eg. A, AB, $A) */
6120
+ const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
6121
+ /** Reference of a row header (eg. 1, $1) */
6122
+ const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
6123
+ /** Reference of a column (eg. A, $CA, Sheet1!B) */
6124
+ const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
6125
+ /** Reference of a row (eg. 1, 59, Sheet1!9) */
6126
+ const rowReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([0-9]{1,7})$/, "i");
6127
+ /** Reference of a normal range or a full row range (eg. A1:B1, 1:$5, $A2:5) */
6128
+ const fullRowXc = /(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*:\s*(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*/i;
6129
+ /** Reference of a normal range or a column row range (eg. A1:B1, A:$B, $A1:C) */
6130
+ const fullColXc = /\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*:\s*\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*/i;
6131
+ /** Reference of a cell or a range, it can be a bounded range, a full row or a full column */
6132
+ const rangeReference = new RegExp(/^\s*('.+'!|[^']+!)?/.source +
6133
+ "(" +
6134
+ [cellReference.source, fullRowXc.source, fullColXc.source].join("|") +
6135
+ ")" +
6136
+ /$/.source, "i");
6137
+ /**
6138
+ * Return true if the given xc is the reference of a column (e.g. A or AC or Sheet1!A)
6139
+ */
6140
+ function isColReference(xc) {
6141
+ return colReference.test(xc);
6142
+ }
6143
+ /**
6144
+ * Return true if the given xc is the reference of a column (e.g. 1 or Sheet1!1)
6145
+ */
6146
+ function isRowReference(xc) {
6147
+ return rowReference.test(xc);
6148
+ }
6149
+ function isColHeader(str) {
6150
+ return colHeader.test(str);
6151
+ }
6152
+ function isRowHeader(str) {
6153
+ return rowHeader.test(str);
6154
+ }
6155
+ /**
6156
+ * Return true if the given xc is the reference of a single cell,
6157
+ * without any specified sheet (e.g. A1)
6158
+ */
6159
+ function isSingleCellReference(xc) {
6160
+ return singleCellReference.test(xc);
6161
+ }
6162
+ function splitReference(ref) {
6163
+ if (!ref.includes("!")) {
6164
+ return { xc: ref };
6165
+ }
6166
+ const parts = ref.split("!");
6167
+ const xc = parts.pop();
6168
+ const sheetName = getUnquotedSheetName(parts.join("!")) || undefined;
6169
+ return { sheetName, xc };
6170
+ }
6171
+ /** Return a reference SheetName!xc from the given arguments */
6172
+ function getFullReference(sheetName, xc) {
6173
+ return sheetName !== undefined ? `${getCanonicalSymbolName(sheetName)}!${xc}` : xc;
6174
+ }
6175
+
6153
6176
  function createDefaultRows(rowNumber) {
6154
6177
  const rows = [];
6155
6178
  for (let i = 0; i < rowNumber; i++) {
@@ -6846,9 +6869,6 @@ function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precisio
6846
6869
  }
6847
6870
  return fontSize;
6848
6871
  }
6849
- function computeIconWidth(style) {
6850
- return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;
6851
- }
6852
6872
  /** Transform a string to lower case. If the string is undefined, return an empty string */
6853
6873
  function toLowerCase(str) {
6854
6874
  return str ? str.toLowerCase() : "";
@@ -8082,208 +8102,6 @@ function getMovingAverageValues(dataset, labels, windowSize = DEFAULT_WINDOW_SIZ
8082
8102
  return values;
8083
8103
  }
8084
8104
 
8085
- const PREVIOUS_VALUE = "(previous)";
8086
- const NEXT_VALUE = "(next)";
8087
- function getDomainOfParentRow(pivot, domain) {
8088
- const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
8089
- return [...colDomain, ...rowDomain.slice(0, rowDomain.length - 1)];
8090
- }
8091
- function getDomainOfParentCol(pivot, domain) {
8092
- const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
8093
- return [...colDomain.slice(0, colDomain.length - 1), ...rowDomain];
8094
- }
8095
- /**
8096
- * Split a pivot domain into the part related to the rows of the pivot, and the part related to the columns.
8097
- */
8098
- function domainToColRowDomain(pivot, domain) {
8099
- const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
8100
- const rowDomain = domain.filter((node) => rowFields.includes(node.field));
8101
- const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
8102
- const colDomain = domain.filter((node) => columnFields.includes(node.field));
8103
- return { colDomain, rowDomain };
8104
- }
8105
- function getDimensionDomain(pivot, dimension, domain) {
8106
- return dimension === "column"
8107
- ? domainToColRowDomain(pivot, domain).colDomain
8108
- : domainToColRowDomain(pivot, domain).rowDomain;
8109
- }
8110
- function getFieldValueInDomain(fieldNameWithGranularity, domain) {
8111
- const node = domain.find((n) => n.field === fieldNameWithGranularity);
8112
- return node?.value;
8113
- }
8114
- function isDomainIsInPivot(pivot, domain) {
8115
- const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
8116
- return (checkIfDomainInInTree(rowDomain, pivot.getTableStructure().getRowTree()) &&
8117
- checkIfDomainInInTree(colDomain, pivot.getTableStructure().getColTree()));
8118
- }
8119
- function checkIfDomainInInTree(domain, tree) {
8120
- return walkDomainTree(domain, tree) !== undefined;
8121
- }
8122
- /**
8123
- * Given a tree of the col/rows of a pivot, and a domain related to those col/rows, return the node of the tree
8124
- * corresponding to the domain.
8125
- *
8126
- * @param domain The domain to find in the tree
8127
- * @param tree The tree to search in7
8128
- * @param stopAtField If provided, the search will stop at the field with this name
8129
- */
8130
- function walkDomainTree(domain, tree, stopAtField) {
8131
- let currentTreeNode = tree;
8132
- for (const node of domain) {
8133
- const child = currentTreeNode.find((n) => n.value === node.value);
8134
- if (!child) {
8135
- return undefined;
8136
- }
8137
- if (child.field === stopAtField) {
8138
- return currentTreeNode;
8139
- }
8140
- currentTreeNode = child.children;
8141
- }
8142
- return currentTreeNode;
8143
- }
8144
- /**
8145
- * Get the domain parent of the given domain with the field `parentFieldName` as leaf of the domain.
8146
- *
8147
- * In practice, if the `parentFieldName` is a row in the pivot, the helper will return a domain with the same column
8148
- * domain, and with a row domain all groupBys children to `parentFieldName` removed.
8149
- */
8150
- function getFieldParentDomain(pivot, parentFieldName, domain) {
8151
- let { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
8152
- const dimension = getFieldDimensionType(pivot, parentFieldName);
8153
- if (dimension === "row") {
8154
- const index = rowDomain.findIndex((node) => node.field === parentFieldName);
8155
- if (index === -1) {
8156
- return domain;
8157
- }
8158
- rowDomain = rowDomain.slice(0, index + 1);
8159
- }
8160
- else {
8161
- const index = colDomain.findIndex((node) => node.field === parentFieldName);
8162
- if (index === -1) {
8163
- return domain;
8164
- }
8165
- colDomain = colDomain.slice(0, index + 1);
8166
- }
8167
- return [...rowDomain, ...colDomain];
8168
- }
8169
- /**
8170
- * Replace in the domain the value of the field `fieldNameWithGranularity` with the given `value`
8171
- */
8172
- function replaceFieldValueInDomain(domain, fieldNameWithGranularity, value) {
8173
- domain = deepCopy(domain);
8174
- const node = domain.find((n) => n.field === fieldNameWithGranularity);
8175
- if (!node) {
8176
- return domain;
8177
- }
8178
- node.value = value;
8179
- return domain;
8180
- }
8181
- function isFieldInDomain(nameWithGranularity, domain) {
8182
- return domain.some((node) => node.field === nameWithGranularity);
8183
- }
8184
- /**
8185
- * Check if the field is in the rows or columns of the pivot
8186
- */
8187
- function getFieldDimensionType(pivot, nameWithGranularity) {
8188
- const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
8189
- if (rowFields.includes(nameWithGranularity)) {
8190
- return "row";
8191
- }
8192
- const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
8193
- if (columnFields.includes(nameWithGranularity)) {
8194
- return "column";
8195
- }
8196
- throw new Error(`Field ${nameWithGranularity} not found in pivot`);
8197
- }
8198
- /**
8199
- * Replace in the given domain the value of the field `fieldNameWithGranularity` with the previous or next value.
8200
- */
8201
- function getPreviousOrNextValueDomain(pivot, domain, fieldNameWithGranularity, direction) {
8202
- const dimension = getFieldDimensionType(pivot, fieldNameWithGranularity);
8203
- const tree = dimension === "row"
8204
- ? pivot.getTableStructure().getRowTree()
8205
- : pivot.getTableStructure().getColTree();
8206
- const dimDomain = getDimensionDomain(pivot, dimension, domain);
8207
- const currentTreeNode = walkDomainTree(dimDomain, tree, fieldNameWithGranularity);
8208
- const values = currentTreeNode?.map((n) => n.value) ?? [];
8209
- const value = getFieldValueInDomain(fieldNameWithGranularity, domain);
8210
- if (value === undefined) {
8211
- return undefined;
8212
- }
8213
- const valueIndex = values.indexOf(value);
8214
- if (value === undefined || valueIndex === -1) {
8215
- return undefined;
8216
- }
8217
- const offset = direction === PREVIOUS_VALUE ? -1 : 1;
8218
- const newIndex = clip(valueIndex + offset, 0, values.length - 1);
8219
- return replaceFieldValueInDomain(domain, fieldNameWithGranularity, values[newIndex]);
8220
- }
8221
- function domainToString(domain) {
8222
- return domain ? domain.map(domainNodeToString).join(", ") : "";
8223
- }
8224
- function domainNodeToString(domainNode) {
8225
- return domainNode ? `${domainNode.field}=${domainNode.value}` : "";
8226
- }
8227
- /**
8228
- *
8229
- * For the ranking, the pivot cell values of a column (or row) at the same depth are grouped together before being sorted
8230
- * and ranked.
8231
- *
8232
- * The grouping of a pivot cell is done with both the value of the domain nodes that are parent of the field
8233
- * `fieldNameWithGranularity` and the value of the last node of the domain of the pivot cell, if it's not the field
8234
- * `fieldNameWithGranularity`.
8235
- *
8236
- * For example, let's take a pivot grouped by (Date:year, Stage, User, Product), where we want to rank by "Stage" field.
8237
- * The domain nodes parents of the "Stage" are [Date:year]. The pivot cell with domain:
8238
- * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
8239
- * - [Date:year=2021, Stage=Lead] is grouped with the cells [Date:year=2021, Stage=*, User=None, Product=None],
8240
- * and then ranked within the group
8241
- * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=*, User=Bob, Product=None],
8242
- * and then ranked within the group
8243
- * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells [Date:year=2021, Stage=*, User=*, Product=Table],
8244
- * and then ranked within the group
8245
- *
8246
- * If we rank the pivot on "User" instead, the parent domain becomes [Date:year, Sage] .The cell with domain:
8247
- * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
8248
- * - [Date:year=2021, Stage=Lead] is not ranked because it does not contain the "User" field
8249
- * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=Lead, User=Bob, Product=None],
8250
- * and then ranked within the group
8251
- * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells with [Date:year=2021, Stage=Lead, User=*, Product=Table],
8252
- * and then ranked within the group
8253
- *
8254
- */
8255
- function getRankingDomainKey(domain, fieldNameWithGranularity) {
8256
- const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
8257
- if (index === -1) {
8258
- return "";
8259
- }
8260
- const parent = domain.slice(0, index);
8261
- const lastNode = domain.at(-1);
8262
- return domainToString(lastNode.field === fieldNameWithGranularity ? parent : [...parent, lastNode]);
8263
- }
8264
- /**
8265
- * The running total domain is the domain without the field `fieldNameWithGranularity`, ie. we do the running total of
8266
- * all the pivot cells of the column that have any value for the field `fieldNameWithGranularity` and the same value for
8267
- * the other fields.
8268
- */
8269
- function getRunningTotalDomainKey(domain, fieldNameWithGranularity) {
8270
- const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
8271
- if (index === -1) {
8272
- return "";
8273
- }
8274
- return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
8275
- }
8276
- function sortPivotTree(tree, baseDomain, sortFn) {
8277
- const sortedTree = [...tree];
8278
- const domain = [...baseDomain];
8279
- sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
8280
- for (const node of tree) {
8281
- const children = sortPivotTree(node.children, [...domain, node], sortFn);
8282
- node.children = children;
8283
- }
8284
- return sortedTree;
8285
- }
8286
-
8287
8105
  const pivotTimeAdapterRegistry = new Registry();
8288
8106
  function pivotTimeAdapter(granularity) {
8289
8107
  return pivotTimeAdapterRegistry.get(granularity);
@@ -8794,23 +8612,11 @@ pivotToFunctionValueRegistry
8794
8612
  function getFieldDisplayName(field) {
8795
8613
  return field.displayName + (field.granularity ? ` (${ALL_PERIODS[field.granularity]})` : "");
8796
8614
  }
8797
- function addIndentAndAlignToPivotHeader(pivot, domain, functionResult) {
8798
- const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
8799
- if (rowDomain.length === 0 && colDomain.length === 0) {
8615
+ function addAlignFormatToPivotHeader(domain, functionResult) {
8616
+ if (domain.length === 0) {
8800
8617
  return functionResult;
8801
8618
  }
8802
- if (rowDomain.length === 0 && colDomain.length > 0) {
8803
- return {
8804
- ...functionResult,
8805
- format: (functionResult.format || "@") + "* ",
8806
- };
8807
- }
8808
- const indent = rowDomain.length - 1;
8809
- const format = functionResult.format || "@";
8810
- return {
8811
- ...functionResult,
8812
- format: `${" ".repeat(indent)}${format}* `,
8813
- };
8619
+ return { ...functionResult, format: (functionResult.format || "@") + "* " };
8814
8620
  }
8815
8621
  function isSortedColumnValid(sortedColumn, pivot) {
8816
8622
  try {
@@ -9841,6 +9647,13 @@ class DependencyContainer extends EventBus {
9841
9647
  resetStores() {
9842
9648
  this.dependencies.clear();
9843
9649
  }
9650
+ dispose() {
9651
+ for (const instance of this.dependencies.values()) {
9652
+ if ("dispose" in instance && typeof instance.dispose === "function") {
9653
+ instance.dispose();
9654
+ }
9655
+ }
9656
+ }
9844
9657
  }
9845
9658
  class StoreFactory {
9846
9659
  get;
@@ -9919,6 +9732,7 @@ function useStoreProvider() {
9919
9732
  return proxifyStoreMutation(store, () => container.trigger("store-updated"));
9920
9733
  },
9921
9734
  });
9735
+ onWillUnmount(() => container.dispose());
9922
9736
  return container;
9923
9737
  }
9924
9738
  /**
@@ -10272,6 +10086,19 @@ function unregisterChartJsExtensions() {
10272
10086
  }
10273
10087
  }
10274
10088
 
10089
+ class ChartAnimationStore extends SpreadsheetStore {
10090
+ mutators = ["disableAnimationForChart", "enableAnimationForChart"];
10091
+ animationPlayed = {};
10092
+ disableAnimationForChart(chartId, chartType) {
10093
+ this.animationPlayed[chartId] = chartType;
10094
+ return "noStateChange";
10095
+ }
10096
+ enableAnimationForChart(chartId) {
10097
+ this.animationPlayed[chartId] = undefined;
10098
+ return "noStateChange";
10099
+ }
10100
+ }
10101
+
10275
10102
  function getFunnelChartController() {
10276
10103
  return class FunnelChartController extends window.Chart.BarController {
10277
10104
  static id = "funnel";
@@ -11925,10 +11752,12 @@ class ChartJsComponent extends Component {
11925
11752
  static template = "o-spreadsheet-ChartJsComponent";
11926
11753
  static props = {
11927
11754
  figureUI: Object,
11755
+ isFullScreen: { type: Boolean, optional: true },
11928
11756
  };
11929
11757
  canvas = useRef("graphContainer");
11930
11758
  chart;
11931
11759
  currentRuntime;
11760
+ animationStore;
11932
11761
  currentDevicePixelRatio = window.devicePixelRatio;
11933
11762
  get background() {
11934
11763
  return this.chartRuntime.background;
@@ -11944,6 +11773,9 @@ class ChartJsComponent extends Component {
11944
11773
  return runtime;
11945
11774
  }
11946
11775
  setup() {
11776
+ if (this.env.model.getters.isDashboard()) {
11777
+ this.animationStore = useStore(ChartAnimationStore);
11778
+ }
11947
11779
  onMounted(() => {
11948
11780
  const runtime = this.chartRuntime;
11949
11781
  this.currentRuntime = runtime;
@@ -11970,11 +11802,25 @@ class ChartJsComponent extends Component {
11970
11802
  });
11971
11803
  }
11972
11804
  createChart(chartData) {
11805
+ if (this.env.model.getters.isDashboard() && this.animationStore) {
11806
+ const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
11807
+ if (chartType && this.animationStore.animationPlayed[this.animationFigureId] !== chartType) {
11808
+ chartData = this.enableAnimationInChartData(chartData);
11809
+ this.animationStore.disableAnimationForChart(this.animationFigureId, chartType);
11810
+ }
11811
+ }
11973
11812
  const canvas = this.canvas.el;
11974
11813
  const ctx = canvas.getContext("2d");
11975
11814
  this.chart = new window.Chart(ctx, chartData);
11976
11815
  }
11977
11816
  updateChartJs(chartData) {
11817
+ if (this.env.model.getters.isDashboard()) {
11818
+ const chartType = this.env.model.getters.getChart(this.props.figureUI.id)?.type;
11819
+ if (chartType && this.hasChartDataChanged() && this.animationStore) {
11820
+ chartData = this.enableAnimationInChartData(chartData);
11821
+ this.animationStore.disableAnimationForChart(this.animationFigureId, chartType);
11822
+ }
11823
+ }
11978
11824
  if (chartData.data && chartData.data.datasets) {
11979
11825
  this.chart.data = chartData.data;
11980
11826
  if (chartData.options?.plugins?.title) {
@@ -11987,6 +11833,20 @@ class ChartJsComponent extends Component {
11987
11833
  this.chart.config.options = chartData.options;
11988
11834
  this.chart.update();
11989
11835
  }
11836
+ hasChartDataChanged() {
11837
+ return !deepEquals(this.currentRuntime.chartJsConfig.data, this.chartRuntime.chartJsConfig.data);
11838
+ }
11839
+ enableAnimationInChartData(chartData) {
11840
+ return {
11841
+ ...chartData,
11842
+ options: { ...chartData.options, animation: { animateRotate: true } },
11843
+ };
11844
+ }
11845
+ get animationFigureId() {
11846
+ return this.props.isFullScreen
11847
+ ? this.props.figureUI.id + "-fullscreen"
11848
+ : this.props.figureUI.id;
11849
+ }
11990
11850
  }
11991
11851
 
11992
11852
  /**
@@ -12112,6 +11972,7 @@ const arrowDownPath = new window.Path2D("M8.6 4.8a.5.5 0 0 1 0 .75l-3.9 3.9a.5 .
12112
11972
  const arrowUpPath = new window.Path2D("M8.7 5.5a.5.5 0 0 0 0-.75l-3.8-4a.5.5 0 0 0-.75 0l-3.8 4a.5.5 0 0 0 0 .75l.4.4a.5.5 0 0 0 .75 0l2.3-2.4v5.8c0 .25.25.5.5.5h.6c.25 0 .5-.25.5-.5v-5.8l2.2 2.4a.5.5 0 0 0 .75 0z");
12113
11973
  let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
12114
11974
  keyValue;
11975
+ keyDescr;
12115
11976
  baseline;
12116
11977
  baselineMode;
12117
11978
  baselineDescr;
@@ -12125,6 +11986,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
12125
11986
  constructor(definition, sheetId, getters) {
12126
11987
  super(definition, sheetId, getters);
12127
11988
  this.keyValue = createValidRange(getters, sheetId, definition.keyValue);
11989
+ this.keyDescr = definition.keyDescr;
12128
11990
  this.baseline = createValidRange(getters, sheetId, definition.baseline);
12129
11991
  this.baselineMode = definition.baselineMode;
12130
11992
  this.baselineDescr = definition.baselineDescr;
@@ -12202,6 +12064,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
12202
12064
  keyValue: keyValue
12203
12065
  ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
12204
12066
  : undefined,
12067
+ keyDescr: this.keyDescr,
12205
12068
  humanize: this.humanize,
12206
12069
  };
12207
12070
  }
@@ -12225,7 +12088,7 @@ function drawScoreChart(structure, canvas) {
12225
12088
  canvas.width = dpr * structure.canvas.width;
12226
12089
  canvas.height = dpr * structure.canvas.height;
12227
12090
  ctx.scale(dpr, dpr);
12228
- const availableWidth = structure.canvas.width - CHART_PADDING$1 * 2;
12091
+ const availableWidth = structure.canvas.width - CHART_PADDING;
12229
12092
  ctx.fillStyle = structure.canvas.backgroundColor;
12230
12093
  ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);
12231
12094
  if (structure.title) {
@@ -12261,18 +12124,22 @@ function drawScoreChart(structure, canvas) {
12261
12124
  ctx.restore();
12262
12125
  }
12263
12126
  if (structure.baselineDescr) {
12264
- const descr = structure.baselineDescr[0];
12127
+ const descr = structure.baselineDescr;
12265
12128
  ctx.font = descr.style.font;
12266
12129
  ctx.fillStyle = descr.style.color;
12267
- for (const description of structure.baselineDescr) {
12268
- ctx.fillText(clipTextWithEllipsis(ctx, description.text, availableWidth - description.position.x), description.position.x, description.position.y);
12269
- }
12130
+ ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
12270
12131
  }
12271
12132
  if (structure.key) {
12272
12133
  ctx.font = structure.key.style.font;
12273
12134
  ctx.fillStyle = structure.key.style.color;
12274
12135
  drawDecoratedText(ctx, clipTextWithEllipsis(ctx, structure.key.text, availableWidth - structure.key.position.x), structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
12275
12136
  }
12137
+ if (structure.keyDescr) {
12138
+ const descr = structure.keyDescr;
12139
+ ctx.font = structure.keyDescr?.style.font ?? descr.style.font;
12140
+ ctx.fillStyle = descr.style.color;
12141
+ ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
12142
+ }
12276
12143
  if (structure.progressBar) {
12277
12144
  ctx.fillStyle = structure.progressBar.style.backgroundColor;
12278
12145
  ctx.beginPath();
@@ -12327,28 +12194,41 @@ function createScorecardChartRuntime(chart, getters) {
12327
12194
  text: chart.title.text ? _t(chart.title.text) : "",
12328
12195
  },
12329
12196
  keyValue: formattedKeyValue,
12197
+ keyDescr: chart.keyDescr?.text
12198
+ ? _t(chart.keyDescr.text) // descriptions are extracted from .json files and they are translated at runtime here
12199
+ : "",
12330
12200
  baselineDisplay,
12331
12201
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
12332
12202
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
12333
- baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr
12334
- ? _t(chart.baselineDescr) // descriptions are extracted from .json files and they are translated at runtime here
12203
+ baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr?.text
12204
+ ? _t(chart.baselineDescr.text) // descriptions are extracted from .json files and they are translated at runtime here
12335
12205
  : "",
12336
12206
  fontColor,
12337
12207
  background,
12338
- baselineStyle: chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
12339
- ? getters.getCellStyle({
12340
- sheetId: baseline.sheetId,
12341
- col: baseline.zone.left,
12342
- row: baseline.zone.top,
12343
- })
12344
- : undefined,
12345
- keyValueStyle: chart.keyValue
12346
- ? getters.getCellStyle({
12347
- sheetId: chart.keyValue.sheetId,
12348
- col: chart.keyValue.zone.left,
12349
- row: chart.keyValue.zone.top,
12350
- })
12351
- : undefined,
12208
+ baselineStyle: {
12209
+ ...(chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
12210
+ ? getters.getCellComputedStyle({
12211
+ sheetId: baseline.sheetId,
12212
+ col: baseline.zone.left,
12213
+ row: baseline.zone.top,
12214
+ })
12215
+ : undefined),
12216
+ fontSize: chart.baselineDescr?.fontSize,
12217
+ align: chart.baselineDescr?.align,
12218
+ },
12219
+ baselineDescrStyle: { textColor: chart.baselineDescr?.color, ...chart.baselineDescr },
12220
+ keyValueStyle: {
12221
+ ...(chart.keyValue
12222
+ ? getters.getCellComputedStyle({
12223
+ sheetId: chart.keyValue.sheetId,
12224
+ col: chart.keyValue.zone.left,
12225
+ row: chart.keyValue.zone.top,
12226
+ })
12227
+ : undefined),
12228
+ fontSize: chart.keyDescr?.fontSize,
12229
+ align: chart.keyDescr?.align,
12230
+ },
12231
+ keyValueDescrStyle: { textColor: chart.keyDescr?.color, ...chart.keyDescr },
12352
12232
  progressBar: chart.baselineMode === "progress"
12353
12233
  ? {
12354
12234
  value: baselineValue,
@@ -12359,11 +12239,7 @@ function createScorecardChartRuntime(chart, getters) {
12359
12239
  }
12360
12240
 
12361
12241
  /* Padding at the border of the chart */
12362
- const CHART_PADDING = 10;
12363
12242
  const BOTTOM_PADDING_RATIO = 0.05;
12364
- /* Maximum font sizes of each element */
12365
- const KEY_VALUE_FONT_SIZE = 32;
12366
- const BASELINE_MAX_FONT_SIZE = 16;
12367
12243
  function formatBaselineDescr(baselineDescr, baseline) {
12368
12244
  const _baselineDescr = baselineDescr || "";
12369
12245
  return baseline && _baselineDescr ? " " + _baselineDescr : _baselineDescr;
@@ -12413,7 +12289,7 @@ class ScorecardChartConfigBuilder {
12413
12289
  style: style.title,
12414
12290
  position: {
12415
12291
  x,
12416
- y: CHART_PADDING + titleHeight / 2,
12292
+ y: CHART_PADDING_BOTTOM + titleHeight / 2,
12417
12293
  },
12418
12294
  };
12419
12295
  }
@@ -12423,42 +12299,49 @@ class ScorecardChartConfigBuilder {
12423
12299
  baselineHeight = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).height;
12424
12300
  }
12425
12301
  const baselineDescrWidth = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).width;
12426
- structure.baseline = {
12427
- text: this.baseline,
12428
- style: style.baselineValue,
12429
- position: {
12430
- x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2,
12431
- y: this.keyValue
12432
- ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))
12433
- : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING,
12434
- },
12435
- };
12436
- const minimalBaselinePosition = baselineArrowSize + CHART_PADDING * 2;
12437
- if (structure.baseline.position.x < minimalBaselinePosition) {
12438
- structure.baseline.position.x = minimalBaselinePosition;
12439
- }
12440
- if (style.baselineArrow && !this.runtime.progressBar) {
12441
- structure.baselineArrow = {
12442
- direction: this.baselineArrow,
12443
- style: style.baselineArrow,
12302
+ let baselineX;
12303
+ switch (this.runtime.baselineStyle?.align) {
12304
+ case "right":
12305
+ baselineX = this.width - CHART_PADDING - baselineDescrWidth - baselineWidth;
12306
+ break;
12307
+ case "left":
12308
+ baselineX = CHART_PADDING + baselineArrowSize;
12309
+ break;
12310
+ default:
12311
+ baselineX = (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2;
12312
+ }
12313
+ if (this.baseline) {
12314
+ structure.baseline = {
12315
+ text: this.baseline,
12316
+ style: style.baselineValue,
12444
12317
  position: {
12445
- x: structure.baseline.position.x - baselineArrowSize,
12446
- y: structure.baseline.position.y - (baselineHeight + baselineArrowSize) / 2,
12318
+ x: baselineX,
12319
+ y: this.keyValue
12320
+ ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))
12321
+ : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING_BOTTOM,
12447
12322
  },
12448
12323
  };
12324
+ if (style.baselineArrow && !this.runtime.progressBar) {
12325
+ structure.baselineArrow = {
12326
+ direction: this.baselineArrow,
12327
+ style: style.baselineArrow,
12328
+ position: {
12329
+ x: structure.baseline.position.x - baselineArrowSize,
12330
+ y: structure.baseline.position.y - (baselineHeight + baselineArrowSize) / 2,
12331
+ },
12332
+ };
12333
+ }
12449
12334
  }
12450
- if (this.baselineDescr) {
12335
+ if (structure.baseline && this.baselineDescr) {
12451
12336
  const position = {
12452
12337
  x: structure.baseline.position.x + baselineWidth,
12453
12338
  y: structure.baseline.position.y,
12454
12339
  };
12455
- structure.baselineDescr = [
12456
- {
12457
- text: this.baselineDescr,
12458
- style: style.baselineDescr,
12459
- position,
12460
- },
12461
- ];
12340
+ structure.baselineDescr = {
12341
+ text: this.baselineDescr,
12342
+ style: style.baselineDescr,
12343
+ position,
12344
+ };
12462
12345
  }
12463
12346
  let progressBarHeight = 0;
12464
12347
  if (this.runtime.progressBar) {
@@ -12480,18 +12363,41 @@ class ScorecardChartConfigBuilder {
12480
12363
  };
12481
12364
  }
12482
12365
  const { width: keyWidth, height: keyHeight } = this.getFullTextDimensions(this.keyValue, style.keyValue.font);
12366
+ const keyDescrWidth = this.getTextDimensions(this.keyDescr, style.keyDescr.font).width;
12367
+ let keyX;
12368
+ switch (this.runtime.keyValueStyle?.align) {
12369
+ case "right":
12370
+ keyX = this.width - CHART_PADDING - keyDescrWidth - keyWidth;
12371
+ break;
12372
+ case "left":
12373
+ keyX = CHART_PADDING;
12374
+ break;
12375
+ default:
12376
+ keyX = (this.width - keyWidth - keyDescrWidth) / 2;
12377
+ }
12483
12378
  if (this.keyValue) {
12484
12379
  structure.key = {
12485
12380
  text: this.keyValue,
12486
12381
  style: style.keyValue,
12487
12382
  position: {
12488
- x: Math.max(CHART_PADDING, (this.width - keyWidth) / 2),
12383
+ x: Math.max(CHART_PADDING, keyX),
12489
12384
  y: this.height * (0.5 - BOTTOM_PADDING_RATIO * 2) +
12490
- CHART_PADDING / 2 +
12385
+ CHART_PADDING_BOTTOM / 2 +
12491
12386
  (titleHeight + keyHeight / 2) / 2,
12492
12387
  },
12493
12388
  };
12494
12389
  }
12390
+ if (structure.key && this.keyDescr) {
12391
+ const position = {
12392
+ x: structure.key.position.x + keyWidth,
12393
+ y: structure.key.position.y,
12394
+ };
12395
+ structure.keyDescr = {
12396
+ text: this.keyDescr,
12397
+ style: style.keyDescr,
12398
+ position,
12399
+ };
12400
+ }
12495
12401
  return structure;
12496
12402
  }
12497
12403
  get title() {
@@ -12500,6 +12406,9 @@ class ScorecardChartConfigBuilder {
12500
12406
  get keyValue() {
12501
12407
  return this.runtime.keyValue;
12502
12408
  }
12409
+ get keyDescr() {
12410
+ return formatBaselineDescr(this.runtime.keyDescr, this.keyValue);
12411
+ }
12503
12412
  get baseline() {
12504
12413
  return this.runtime.baselineDisplay;
12505
12414
  }
@@ -12532,7 +12441,9 @@ class ScorecardChartConfigBuilder {
12532
12441
  };
12533
12442
  }
12534
12443
  getTextStyles() {
12535
- let baselineValueFontSize = BASELINE_MAX_FONT_SIZE;
12444
+ const keyValueFontSize = this.runtime.keyValueStyle?.fontSize ?? DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE;
12445
+ const keyValueDescrFontSize = Math.floor(0.9 * keyValueFontSize);
12446
+ let baselineValueFontSize = this.runtime.baselineStyle?.fontSize ?? DEFAULT_SCORECARD_BASELINE_FONT_SIZE;
12536
12447
  const baselineDescrFontSize = Math.floor(0.9 * baselineValueFontSize);
12537
12448
  if (this.runtime.progressBar) {
12538
12449
  baselineValueFontSize /= 1.5;
@@ -12544,27 +12455,37 @@ class ScorecardChartConfigBuilder {
12544
12455
  },
12545
12456
  keyValue: {
12546
12457
  color: this.runtime.keyValueStyle?.textColor || this.runtime.fontColor,
12547
- font: getDefaultContextFont(KEY_VALUE_FONT_SIZE, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
12458
+ font: getDefaultContextFont(keyValueFontSize, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
12548
12459
  strikethrough: this.runtime.keyValueStyle?.strikethrough,
12549
12460
  underline: this.runtime.keyValueStyle?.underline,
12550
12461
  },
12462
+ keyDescr: {
12463
+ color: this.runtime.keyValueDescrStyle?.textColor || this.runtime.fontColor,
12464
+ font: getDefaultContextFont(keyValueDescrFontSize, this.runtime.keyValueDescrStyle?.bold, this.runtime.keyValueDescrStyle?.italic),
12465
+ strikethrough: this.runtime.keyValueDescrStyle?.strikethrough,
12466
+ underline: this.runtime.keyValueDescrStyle?.underline,
12467
+ },
12551
12468
  baselineValue: {
12552
12469
  font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
12553
12470
  strikethrough: this.runtime.baselineStyle?.strikethrough,
12554
12471
  underline: this.runtime.baselineStyle?.underline,
12555
- color: this.runtime.baselineStyle?.textColor ||
12556
- this.runtime.baselineColor ||
12472
+ color: this.runtime.baselineColor ||
12473
+ this.runtime.baselineStyle?.textColor ||
12557
12474
  this.secondaryFontColor,
12558
12475
  },
12559
12476
  baselineDescr: {
12560
- font: getDefaultContextFont(baselineDescrFontSize),
12561
- color: this.secondaryFontColor,
12477
+ font: getDefaultContextFont(baselineDescrFontSize, this.runtime.baselineDescrStyle?.bold, this.runtime.baselineDescrStyle?.italic),
12478
+ strikethrough: this.runtime.baselineDescrStyle?.strikethrough,
12479
+ underline: this.runtime.baselineDescrStyle?.underline,
12480
+ color: this.runtime.baselineDescrStyle?.textColor ?? this.secondaryFontColor,
12562
12481
  },
12563
12482
  baselineArrow: this.baselineArrow === "neutral" || this.runtime.progressBar
12564
12483
  ? undefined
12565
12484
  : {
12566
12485
  size: this.keyValue ? 0.8 * baselineValueFontSize : 0,
12567
- color: this.runtime.baselineColor || this.secondaryFontColor,
12486
+ color: this.runtime.baselineColor ||
12487
+ this.runtime.baselineStyle?.textColor ||
12488
+ this.secondaryFontColor,
12568
12489
  },
12569
12490
  };
12570
12491
  }
@@ -12958,14 +12879,14 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
12958
12879
  }
12959
12880
  switch (runtime.title.align) {
12960
12881
  case "right":
12961
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
12882
+ x = boundingRect.width - titleWidth - CHART_PADDING;
12962
12883
  break;
12963
12884
  case "center":
12964
12885
  x = (boundingRect.width - titleWidth) / 2;
12965
12886
  break;
12966
12887
  case "left":
12967
12888
  default:
12968
- x = CHART_PADDING$1;
12889
+ x = CHART_PADDING;
12969
12890
  break;
12970
12891
  }
12971
12892
  return {
@@ -14287,8 +14208,8 @@ function getTopPaddingForDashboard(definition, getters) {
14287
14208
  function getChartLayout(definition, args) {
14288
14209
  return {
14289
14210
  padding: {
14290
- left: CHART_PADDING$1,
14291
- right: CHART_PADDING$1,
14211
+ left: CHART_PADDING,
14212
+ right: CHART_PADDING,
14292
14213
  top: Math.max(CHART_PADDING_TOP, args.topPadding || 0),
14293
14214
  bottom: CHART_PADDING_BOTTOM,
14294
14215
  },
@@ -14839,11 +14760,11 @@ function getLegendMargin(definition) {
14839
14760
  case "right":
14840
14761
  const hasTitle = !!definition.title.text;
14841
14762
  const topMargin = hasTitle ? CHART_PADDING_TOP + 30 : CHART_PADDING_TOP;
14842
- return { top: topMargin, left: CHART_PADDING$1, right: CHART_PADDING$1 };
14763
+ return { top: topMargin, left: CHART_PADDING, right: CHART_PADDING };
14843
14764
  case "bottom":
14844
14765
  case "left":
14845
14766
  case "none":
14846
- return { left: CHART_PADDING$1, right: CHART_PADDING$1, bottom: CHART_PADDING_BOTTOM };
14767
+ return { left: CHART_PADDING, right: CHART_PADDING, bottom: CHART_PADDING_BOTTOM };
14847
14768
  }
14848
14769
  }
14849
14770
  function legendPositionToGeoLegendPosition(position) {
@@ -14911,7 +14832,7 @@ function getChartTitle(definition) {
14911
14832
  padding: {
14912
14833
  // Disable title top/left/right padding to use the chart padding instead.
14913
14834
  // The legend already has a top padding, so bottom padding is useless for the title there.
14914
- bottom: definition.legendPosition === "top" ? 0 : CHART_PADDING$1,
14835
+ bottom: definition.legendPosition === "top" ? 0 : CHART_PADDING,
14915
14836
  },
14916
14837
  };
14917
14838
  }
@@ -15474,6 +15395,10 @@ function createBarChartRuntime(chart, getters) {
15474
15395
 
15475
15396
  class GaugeChartComponent extends Component {
15476
15397
  static template = "o-spreadsheet-GaugeChartComponent";
15398
+ static props = {
15399
+ figureUI: Object,
15400
+ isFullScreen: { type: Boolean, optional: true },
15401
+ };
15477
15402
  canvas = useRef("chartContainer");
15478
15403
  get runtime() {
15479
15404
  return this.env.model.getters.getChartRuntime(this.props.figureUI.id);
@@ -15486,9 +15411,6 @@ class GaugeChartComponent extends Component {
15486
15411
  });
15487
15412
  }
15488
15413
  }
15489
- GaugeChartComponent.props = {
15490
- figureUI: Object,
15491
- };
15492
15414
 
15493
15415
  class ComboChart extends AbstractChart {
15494
15416
  dataSets;
@@ -18292,6 +18214,26 @@ function getDeleteMenuItem(figureId, onFigureDeleted, env) {
18292
18214
  };
18293
18215
  }
18294
18216
 
18217
+ class FullScreenChartStore extends SpreadsheetStore {
18218
+ mutators = ["toggleFullScreenChart"];
18219
+ fullScreenFigure = undefined;
18220
+ toggleFullScreenChart(figureId) {
18221
+ if (this.fullScreenFigure?.id === figureId) {
18222
+ this.fullScreenFigure = undefined;
18223
+ }
18224
+ else {
18225
+ this.makeFullScreen(figureId);
18226
+ }
18227
+ }
18228
+ makeFullScreen(figureId) {
18229
+ const sheetId = this.getters.getActiveSheetId();
18230
+ const figure = this.getters.getFigure(sheetId, figureId);
18231
+ if (figure) {
18232
+ this.fullScreenFigure = { ...figure, x: 0, y: 0, width: 0, height: 0 };
18233
+ }
18234
+ }
18235
+ }
18236
+
18295
18237
  /**
18296
18238
  * Repeatedly calls a callback function with a time delay between calls.
18297
18239
  */
@@ -18411,7 +18353,9 @@ function usePopoverContainer() {
18411
18353
  const spreadsheetRect = useSpreadsheetRect();
18412
18354
  function updateRect() {
18413
18355
  const env = component.env;
18414
- const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
18356
+ const newRect = "getPopoverContainerRect" in env && env.getPopoverContainerRect
18357
+ ? env.getPopoverContainerRect()
18358
+ : spreadsheetRect;
18415
18359
  container.x = newRect.x;
18416
18360
  container.y = newRect.y;
18417
18361
  container.width = newRect.width;
@@ -18936,9 +18880,11 @@ class ChartDashboardMenu extends Component {
18936
18880
  static components = { Menu };
18937
18881
  static props = { figureUI: Object };
18938
18882
  originalChartDefinition;
18883
+ fullScreenFigureStore;
18939
18884
  menuState = useState({ isOpen: false, anchorRect: null, menuItems: [] });
18940
18885
  setup() {
18941
18886
  super.setup();
18887
+ this.fullScreenFigureStore = useStore(FullScreenChartStore);
18942
18888
  this.originalChartDefinition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
18943
18889
  onWillUpdateProps(({ figureUI }) => {
18944
18890
  if (figureUI.id !== this.props.figureUI.id) {
@@ -18946,7 +18892,10 @@ class ChartDashboardMenu extends Component {
18946
18892
  }
18947
18893
  });
18948
18894
  }
18949
- getAvailableTypes() {
18895
+ getMenuItems() {
18896
+ return [this.fullScreenMenuItem, ...this.changeChartTypeMenuItems].filter(isDefined);
18897
+ }
18898
+ get changeChartTypeMenuItems() {
18950
18899
  const definition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
18951
18900
  if (!["line", "bar", "pie"].includes(definition.type)) {
18952
18901
  return [];
@@ -18954,8 +18903,11 @@ class ChartDashboardMenu extends Component {
18954
18903
  return ["column", "line", "pie"].map((type) => {
18955
18904
  const item = chartSubtypeRegistry.get(type);
18956
18905
  return {
18957
- ...item,
18958
- icon: this.getIconClasses(item.chartType),
18906
+ id: item.chartType,
18907
+ label: item.displayName,
18908
+ onClick: () => this.onTypeChange(item.chartType),
18909
+ isSelected: item.chartType === this.selectedChartType,
18910
+ iconClass: this.getIconClasses(item.chartType),
18959
18911
  };
18960
18912
  });
18961
18913
  }
@@ -19010,6 +18962,30 @@ class ChartDashboardMenu extends Component {
19010
18962
  this.menuState.anchorRect = { x: ev.clientX, y: ev.clientY, width: 0, height: 0 };
19011
18963
  this.menuState.menuItems = getChartMenuActions(this.props.figureUI.id, () => { }, this.env);
19012
18964
  }
18965
+ get fullScreenMenuItem() {
18966
+ const definition = this.env.model.getters.getChartDefinition(this.props.figureUI.id);
18967
+ if (definition.type === "scorecard") {
18968
+ return undefined;
18969
+ }
18970
+ if (this.props.figureUI.id === this.fullScreenFigureStore.fullScreenFigure?.id) {
18971
+ return {
18972
+ id: "fullScreenChart",
18973
+ label: _t("Exit Full Screen"),
18974
+ iconClass: "fa fa-compress",
18975
+ onClick: () => {
18976
+ this.fullScreenFigureStore.toggleFullScreenChart(this.props.figureUI.id);
18977
+ },
18978
+ };
18979
+ }
18980
+ return {
18981
+ id: "fullScreenChart",
18982
+ label: _t("Full Screen"),
18983
+ iconClass: "fa fa-expand",
18984
+ onClick: () => {
18985
+ this.fullScreenFigureStore.toggleFullScreenChart(this.props.figureUI.id);
18986
+ },
18987
+ };
18988
+ }
19013
18989
  }
19014
18990
 
19015
18991
  // -----------------------------------------------------------------------------
@@ -29028,7 +29004,7 @@ const PIVOT = {
29028
29004
  if (error) {
29029
29005
  return error;
29030
29006
  }
29031
- const table = pivot.getTableStructure();
29007
+ const table = pivot.getCollapsedTableStructure();
29032
29008
  const cells = table.getPivotCells(_includedTotal, _includeColumnHeaders);
29033
29009
  const headerRows = _includeColumnHeaders ? table.columns.length : 0;
29034
29010
  const pivotTitle = this.getters.getPivotDisplayName(pivotId);
@@ -29048,7 +29024,7 @@ const PIVOT = {
29048
29024
  break;
29049
29025
  case "HEADER":
29050
29026
  const valueAndFormat = pivot.getPivotHeaderValueAndFormat(pivotCell.domain);
29051
- result[col].push(addIndentAndAlignToPivotHeader(pivot, pivotCell.domain, valueAndFormat));
29027
+ result[col].push(addAlignFormatToPivotHeader(pivotCell.domain, valueAndFormat));
29052
29028
  break;
29053
29029
  case "MEASURE_HEADER":
29054
29030
  result[col].push(pivot.getPivotMeasureValue(pivotCell.measure, pivotCell.domain));
@@ -32975,12 +32951,13 @@ class StandaloneComposerStore extends AbstractComposerStore {
32975
32951
  return res;
32976
32952
  }
32977
32953
  getComposerContent() {
32954
+ let content = this._currentContent;
32978
32955
  if (this.editionMode === "inactive") {
32979
32956
  // References in the content might not be linked to the current active sheet
32980
32957
  // We here force the sheet name prefix for all references that are not in
32981
32958
  // the current active sheet
32982
32959
  const defaultRangeSheetId = this.args().defaultRangeSheetId;
32983
- return rangeTokenize(this.args().content)
32960
+ content = rangeTokenize(this.args().content)
32984
32961
  .map((token) => {
32985
32962
  if (token.type === "REFERENCE") {
32986
32963
  const range = this.getters.getRangeFromSheetXC(defaultRangeSheetId, token.value);
@@ -32990,7 +32967,7 @@ class StandaloneComposerStore extends AbstractComposerStore {
32990
32967
  })
32991
32968
  .join("");
32992
32969
  }
32993
- return this._currentContent;
32970
+ return localizeContent(content, this.getters.getLocale());
32994
32971
  }
32995
32972
  stopEdition() {
32996
32973
  this._stopEdition();
@@ -35669,11 +35646,6 @@ function buildTableStyle(name, templateName, primaryColor) {
35669
35646
  };
35670
35647
  }
35671
35648
 
35672
- /**
35673
- * Registry to draw icons on cells
35674
- */
35675
- const iconsOnCellRegistry = new Registry();
35676
-
35677
35649
  css /* scss */ `
35678
35650
  .o-spreadsheet {
35679
35651
  .o-icon {
@@ -35810,13 +35782,6 @@ const ICON_SETS = {
35810
35782
  bad: "dotBad",
35811
35783
  },
35812
35784
  };
35813
- iconsOnCellRegistry.add("conditional_formatting", (getters, position) => {
35814
- const icon = getters.getConditionalIcon(position);
35815
- if (icon) {
35816
- return ICONS[icon].svg;
35817
- }
35818
- return undefined;
35819
- });
35820
35785
 
35821
35786
  /**
35822
35787
  * Map of the different types of conversions warnings and their name in error messages
@@ -39924,6 +39889,22 @@ migrationStepRegistry
39924
39889
  }
39925
39890
  return data;
39926
39891
  },
39892
+ })
39893
+ .add("18.4.2", {
39894
+ migrate(data) {
39895
+ for (const sheet of data.sheets || []) {
39896
+ for (const figure of sheet.figures || []) {
39897
+ if (figure.tag !== "chart" || figure.data.type !== "scorecard") {
39898
+ continue;
39899
+ }
39900
+ const scData = figure.data;
39901
+ if (scData.baselineDescr) {
39902
+ scData.baselineDescr = { text: scData.baselineDescr };
39903
+ }
39904
+ }
39905
+ }
39906
+ return data;
39907
+ },
39927
39908
  });
39928
39909
  function fixOverlappingFilters(data) {
39929
39910
  for (const sheet of data.sheets || []) {
@@ -40798,7 +40779,7 @@ const REINSERT_DYNAMIC_PIVOT_CHILDREN = (env) => env.model.getters.getPivotIds()
40798
40779
  sequence: index,
40799
40780
  execute: (env) => {
40800
40781
  const zone = env.model.getters.getSelectedZone();
40801
- const table = env.model.getters.getPivot(pivotId).getTableStructure().export();
40782
+ const table = env.model.getters.getPivot(pivotId).getCollapsedTableStructure().export();
40802
40783
  env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
40803
40784
  pivotId,
40804
40785
  table,
@@ -40817,7 +40798,7 @@ const REINSERT_STATIC_PIVOT_CHILDREN = (env) => env.model.getters.getPivotIds().
40817
40798
  sequence: index,
40818
40799
  execute: (env) => {
40819
40800
  const zone = env.model.getters.getSelectedZone();
40820
- const table = env.model.getters.getPivot(pivotId).getTableStructure().export();
40801
+ const table = env.model.getters.getPivot(pivotId).getExpandedTableStructure().export();
40821
40802
  env.model.dispatch("INSERT_PIVOT_WITH_TABLE", {
40822
40803
  pivotId,
40823
40804
  table,
@@ -42198,6 +42179,218 @@ function getColumnsNumber(env) {
42198
42179
  }
42199
42180
  }
42200
42181
 
42182
+ const PREVIOUS_VALUE = "(previous)";
42183
+ const NEXT_VALUE = "(next)";
42184
+ function getDomainOfParentRow(pivot, domain) {
42185
+ const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
42186
+ return [...colDomain, ...rowDomain.slice(0, rowDomain.length - 1)];
42187
+ }
42188
+ function getDomainOfParentCol(pivot, domain) {
42189
+ const { colDomain, rowDomain } = domainToColRowDomain(pivot, domain);
42190
+ return [...colDomain.slice(0, colDomain.length - 1), ...rowDomain];
42191
+ }
42192
+ /**
42193
+ * Split a pivot domain into the part related to the rows of the pivot, and the part related to the columns.
42194
+ */
42195
+ function domainToColRowDomain(pivot, domain) {
42196
+ const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
42197
+ const rowDomain = domain.filter((node) => rowFields.includes(node.field));
42198
+ const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
42199
+ const colDomain = domain.filter((node) => columnFields.includes(node.field));
42200
+ return { colDomain, rowDomain };
42201
+ }
42202
+ function getDimensionDomain(pivot, dimension, domain) {
42203
+ return dimension === "column"
42204
+ ? domainToColRowDomain(pivot, domain).colDomain
42205
+ : domainToColRowDomain(pivot, domain).rowDomain;
42206
+ }
42207
+ function getFieldValueInDomain(fieldNameWithGranularity, domain) {
42208
+ const node = domain.find((n) => n.field === fieldNameWithGranularity);
42209
+ return node?.value;
42210
+ }
42211
+ function isDomainIsInPivot(pivot, domain) {
42212
+ for (const node of domain) {
42213
+ if (pivot.definition.rows.find((row) => row.nameWithGranularity === node.field) === undefined &&
42214
+ pivot.definition.columns.find((col) => col.nameWithGranularity === node.field) === undefined) {
42215
+ return false;
42216
+ }
42217
+ }
42218
+ const { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
42219
+ return (checkIfDomainInInTree(rowDomain, pivot.getExpandedTableStructure().getRowTree()) &&
42220
+ checkIfDomainInInTree(colDomain, pivot.getExpandedTableStructure().getColTree()));
42221
+ }
42222
+ function checkIfDomainInInTree(domain, tree) {
42223
+ return walkDomainTree(domain, tree) !== undefined;
42224
+ }
42225
+ /**
42226
+ * Given a tree of the col/rows of a pivot, and a domain related to those col/rows, return the node of the tree
42227
+ * corresponding to the domain.
42228
+ *
42229
+ * @param domain The domain to find in the tree
42230
+ * @param tree The tree to search in7
42231
+ * @param stopAtField If provided, the search will stop at the field with this name
42232
+ */
42233
+ function walkDomainTree(domain, tree, stopAtField) {
42234
+ let currentTreeNode = tree;
42235
+ for (const node of domain) {
42236
+ const child = currentTreeNode.find((n) => n.value === node.value);
42237
+ if (!child) {
42238
+ return undefined;
42239
+ }
42240
+ if (child.field === stopAtField) {
42241
+ return currentTreeNode;
42242
+ }
42243
+ currentTreeNode = child.children;
42244
+ }
42245
+ return currentTreeNode;
42246
+ }
42247
+ /**
42248
+ * Get the domain parent of the given domain with the field `parentFieldName` as leaf of the domain.
42249
+ *
42250
+ * In practice, if the `parentFieldName` is a row in the pivot, the helper will return a domain with the same column
42251
+ * domain, and with a row domain all groupBys children to `parentFieldName` removed.
42252
+ */
42253
+ function getFieldParentDomain(pivot, parentFieldName, domain) {
42254
+ let { rowDomain, colDomain } = domainToColRowDomain(pivot, domain);
42255
+ const dimension = getFieldDimensionType(pivot, parentFieldName);
42256
+ if (dimension === "row") {
42257
+ const index = rowDomain.findIndex((node) => node.field === parentFieldName);
42258
+ if (index === -1) {
42259
+ return domain;
42260
+ }
42261
+ rowDomain = rowDomain.slice(0, index + 1);
42262
+ }
42263
+ else {
42264
+ const index = colDomain.findIndex((node) => node.field === parentFieldName);
42265
+ if (index === -1) {
42266
+ return domain;
42267
+ }
42268
+ colDomain = colDomain.slice(0, index + 1);
42269
+ }
42270
+ return [...rowDomain, ...colDomain];
42271
+ }
42272
+ /**
42273
+ * Replace in the domain the value of the field `fieldNameWithGranularity` with the given `value`
42274
+ */
42275
+ function replaceFieldValueInDomain(domain, fieldNameWithGranularity, value) {
42276
+ domain = deepCopy(domain);
42277
+ const node = domain.find((n) => n.field === fieldNameWithGranularity);
42278
+ if (!node) {
42279
+ return domain;
42280
+ }
42281
+ node.value = value;
42282
+ return domain;
42283
+ }
42284
+ function isFieldInDomain(nameWithGranularity, domain) {
42285
+ return domain.some((node) => node.field === nameWithGranularity);
42286
+ }
42287
+ /**
42288
+ * Check if the field is in the rows or columns of the pivot
42289
+ */
42290
+ function getFieldDimensionType(pivot, nameWithGranularity) {
42291
+ const rowFields = pivot.definition.rows.map((c) => c.nameWithGranularity);
42292
+ if (rowFields.includes(nameWithGranularity)) {
42293
+ return "row";
42294
+ }
42295
+ const columnFields = pivot.definition.columns.map((c) => c.nameWithGranularity);
42296
+ if (columnFields.includes(nameWithGranularity)) {
42297
+ return "column";
42298
+ }
42299
+ throw new Error(`Field ${nameWithGranularity} not found in pivot`);
42300
+ }
42301
+ /**
42302
+ * Replace in the given domain the value of the field `fieldNameWithGranularity` with the previous or next value.
42303
+ */
42304
+ function getPreviousOrNextValueDomain(pivot, domain, fieldNameWithGranularity, direction) {
42305
+ const dimension = getFieldDimensionType(pivot, fieldNameWithGranularity);
42306
+ const tree = dimension === "row"
42307
+ ? pivot.getExpandedTableStructure().getRowTree()
42308
+ : pivot.getExpandedTableStructure().getColTree();
42309
+ const dimDomain = getDimensionDomain(pivot, dimension, domain);
42310
+ const currentTreeNode = walkDomainTree(dimDomain, tree, fieldNameWithGranularity);
42311
+ const values = currentTreeNode?.map((n) => n.value) ?? [];
42312
+ const value = getFieldValueInDomain(fieldNameWithGranularity, domain);
42313
+ if (value === undefined) {
42314
+ return undefined;
42315
+ }
42316
+ const valueIndex = values.indexOf(value);
42317
+ if (value === undefined || valueIndex === -1) {
42318
+ return undefined;
42319
+ }
42320
+ const offset = direction === PREVIOUS_VALUE ? -1 : 1;
42321
+ const newIndex = clip(valueIndex + offset, 0, values.length - 1);
42322
+ return replaceFieldValueInDomain(domain, fieldNameWithGranularity, values[newIndex]);
42323
+ }
42324
+ function domainToString(domain) {
42325
+ return domain ? domain.map(domainNodeToString).join(", ") : "";
42326
+ }
42327
+ function domainNodeToString(domainNode) {
42328
+ return domainNode ? `${domainNode.field}=${domainNode.value}` : "";
42329
+ }
42330
+ /**
42331
+ *
42332
+ * For the ranking, the pivot cell values of a column (or row) at the same depth are grouped together before being sorted
42333
+ * and ranked.
42334
+ *
42335
+ * The grouping of a pivot cell is done with both the value of the domain nodes that are parent of the field
42336
+ * `fieldNameWithGranularity` and the value of the last node of the domain of the pivot cell, if it's not the field
42337
+ * `fieldNameWithGranularity`.
42338
+ *
42339
+ * For example, let's take a pivot grouped by (Date:year, Stage, User, Product), where we want to rank by "Stage" field.
42340
+ * The domain nodes parents of the "Stage" are [Date:year]. The pivot cell with domain:
42341
+ * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
42342
+ * - [Date:year=2021, Stage=Lead] is grouped with the cells [Date:year=2021, Stage=*, User=None, Product=None],
42343
+ * and then ranked within the group
42344
+ * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=*, User=Bob, Product=None],
42345
+ * and then ranked within the group
42346
+ * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells [Date:year=2021, Stage=*, User=*, Product=Table],
42347
+ * and then ranked within the group
42348
+ *
42349
+ * If we rank the pivot on "User" instead, the parent domain becomes [Date:year, Sage] .The cell with domain:
42350
+ * - [Date:year=2021] is not ranked because it does not contain the "Stage" field
42351
+ * - [Date:year=2021, Stage=Lead] is not ranked because it does not contain the "User" field
42352
+ * - [Date:year=2021, Stage=Lead, User=Bob] is grouped with the cells [Date:year=2021, Stage=Lead, User=Bob, Product=None],
42353
+ * and then ranked within the group
42354
+ * - [Date:year=2021, Stage=Lead, User=Bob, Product=Table] is grouped with the cells with [Date:year=2021, Stage=Lead, User=*, Product=Table],
42355
+ * and then ranked within the group
42356
+ *
42357
+ */
42358
+ function getRankingDomainKey(domain, fieldNameWithGranularity) {
42359
+ const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
42360
+ if (index === -1) {
42361
+ return "";
42362
+ }
42363
+ const parent = domain.slice(0, index);
42364
+ const lastNode = domain.at(-1);
42365
+ return domainToString(lastNode.field === fieldNameWithGranularity ? parent : [...parent, lastNode]);
42366
+ }
42367
+ /**
42368
+ * The running total domain is the domain without the field `fieldNameWithGranularity`, ie. we do the running total of
42369
+ * all the pivot cells of the column that have any value for the field `fieldNameWithGranularity` and the same value for
42370
+ * the other fields.
42371
+ */
42372
+ function getRunningTotalDomainKey(domain, fieldNameWithGranularity) {
42373
+ const index = domain.findIndex((node) => node.field === fieldNameWithGranularity);
42374
+ if (index === -1) {
42375
+ return "";
42376
+ }
42377
+ return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
42378
+ }
42379
+ function sortPivotTree(tree, baseDomain, sortFn) {
42380
+ const sortedTree = [...tree];
42381
+ const domain = [...baseDomain];
42382
+ sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
42383
+ for (const node of tree) {
42384
+ const children = sortPivotTree(node.children, [...domain, node], sortFn);
42385
+ node.children = children;
42386
+ }
42387
+ return sortedTree;
42388
+ }
42389
+ function isParentDomain(domain, parentDomain) {
42390
+ return (domain.length > parentDomain.length &&
42391
+ parentDomain.every((node, i) => deepEquals(node, domain[i])));
42392
+ }
42393
+
42201
42394
  const pivotProperties = {
42202
42395
  name: _t("See pivot properties"),
42203
42396
  execute(env) {
@@ -43198,6 +43391,66 @@ class ArrayFormulaHighlight extends SpreadsheetStore {
43198
43391
  }
43199
43392
  }
43200
43393
 
43394
+ class ClientFocusStore extends SpreadsheetStore {
43395
+ mutators = [
43396
+ "focusClient",
43397
+ "unfocusClient",
43398
+ "showClientTag",
43399
+ "hideClientTag",
43400
+ "jumpToClient",
43401
+ ];
43402
+ _showClientTag = false;
43403
+ clientFocusTimeout = {};
43404
+ constructor(get) {
43405
+ super(get);
43406
+ this.onDispose(() => {
43407
+ for (const clientId in this.clientFocusTimeout) {
43408
+ this.unfocusClient(clientId);
43409
+ }
43410
+ });
43411
+ }
43412
+ get focusedClients() {
43413
+ const focused = new Set();
43414
+ this.model.getters.getConnectedClients().forEach((client) => {
43415
+ if (this._showClientTag || this.clientFocusTimeout[client.id] !== undefined) {
43416
+ focused.add(client.id);
43417
+ }
43418
+ });
43419
+ return focused;
43420
+ }
43421
+ jumpToClient(clientId) {
43422
+ const client = this.model.getters.getClient(clientId);
43423
+ this.focusClient(clientId);
43424
+ if (client.position) {
43425
+ this.model.dispatch("ACTIVATE_SHEET", {
43426
+ sheetIdTo: client.position.sheetId,
43427
+ sheetIdFrom: this.getters.getActiveSheetId(),
43428
+ });
43429
+ this.model.dispatch("SCROLL_TO_CELL", { col: client.position.col, row: client.position.row });
43430
+ }
43431
+ }
43432
+ showClientTag() {
43433
+ this._showClientTag = true;
43434
+ }
43435
+ hideClientTag() {
43436
+ this._showClientTag = false;
43437
+ }
43438
+ focusClient(clientId) {
43439
+ if (this.clientFocusTimeout[clientId]) {
43440
+ clearTimeout(this.clientFocusTimeout[clientId]);
43441
+ }
43442
+ // This call to unfocus client isn't proxyfied and doesn't trigger a render.
43443
+ // The focus will visually disappear when the next render is triggered
43444
+ this.clientFocusTimeout[clientId] = setTimeout(() => this.unfocusClient(clientId), 3000);
43445
+ }
43446
+ unfocusClient(clientId) {
43447
+ if (this.clientFocusTimeout[clientId]) {
43448
+ clearTimeout(this.clientFocusTimeout[clientId]);
43449
+ }
43450
+ this.clientFocusTimeout[clientId] = undefined;
43451
+ }
43452
+ }
43453
+
43201
43454
  /**
43202
43455
  * Function to be used during a pointerdown event, this function allows to
43203
43456
  * perform actions related to the pointermove and pointerup events and adjusts the viewport
@@ -43471,6 +43724,11 @@ class ClientTag extends Component {
43471
43724
  get tagStyle() {
43472
43725
  const { col, row, color } = this.props;
43473
43726
  const { height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
43727
+ const visible = this.env.model.getters.isVisibleInViewport({
43728
+ sheetId: this.env.model.getters.getActiveSheetId(),
43729
+ col,
43730
+ row,
43731
+ });
43474
43732
  const { x, y } = this.env.model.getters.getVisibleRect({
43475
43733
  left: col,
43476
43734
  top: row,
@@ -43482,6 +43740,7 @@ class ClientTag extends Component {
43482
43740
  left: `${x - 1}px`,
43483
43741
  border: `1px solid ${color}`,
43484
43742
  "background-color": color,
43743
+ visibility: visible ? "visible" : "hidden",
43485
43744
  });
43486
43745
  }
43487
43746
  }
@@ -43907,151 +44166,6 @@ class GridComposer extends Component {
43907
44166
  }
43908
44167
  }
43909
44168
 
43910
- css /* scss */ `
43911
- .o-grid-cell-icon {
43912
- width: ${GRID_ICON_EDGE_LENGTH}px;
43913
- height: ${GRID_ICON_EDGE_LENGTH}px;
43914
- }
43915
- `;
43916
- class GridCellIcon extends Component {
43917
- static template = "o-spreadsheet-GridCellIcon";
43918
- static props = {
43919
- cellPosition: Object,
43920
- horizontalAlign: { type: String, optional: true },
43921
- verticalAlign: { type: String, optional: true },
43922
- slots: Object,
43923
- };
43924
- get iconStyle() {
43925
- const cellPosition = this.props.cellPosition;
43926
- const merge = this.env.model.getters.getMerge(cellPosition);
43927
- const zone = merge || positionToZone(cellPosition);
43928
- const rect = this.env.model.getters.getVisibleRectWithoutHeaders(zone);
43929
- const x = this.getIconHorizontalPosition(rect, cellPosition);
43930
- const y = this.getIconVerticalPosition(rect, cellPosition);
43931
- return cssPropertiesToCss({
43932
- top: `${y}px`,
43933
- left: `${x}px`,
43934
- });
43935
- }
43936
- getIconVerticalPosition(rect, cellPosition) {
43937
- const start = rect.y;
43938
- const end = rect.y + rect.height;
43939
- const cell = this.env.model.getters.getCell(cellPosition);
43940
- const align = this.props.verticalAlign || cell?.style?.verticalAlign || DEFAULT_VERTICAL_ALIGN;
43941
- switch (align) {
43942
- case "bottom":
43943
- return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
43944
- case "top":
43945
- return start + GRID_ICON_MARGIN;
43946
- default:
43947
- const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
43948
- return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
43949
- }
43950
- }
43951
- getIconHorizontalPosition(rect, cellPosition) {
43952
- const start = rect.x;
43953
- const end = rect.x + rect.width;
43954
- const cell = this.env.model.getters.getCell(cellPosition);
43955
- const evaluatedCell = this.env.model.getters.getEvaluatedCell(cellPosition);
43956
- const align = this.props.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
43957
- switch (align) {
43958
- case "right":
43959
- return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
43960
- case "left":
43961
- return start + GRID_ICON_MARGIN;
43962
- default:
43963
- const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
43964
- return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
43965
- }
43966
- }
43967
- isPositionVisible(position) {
43968
- const rect = this.env.model.getters.getVisibleRect(positionToZone(position));
43969
- return !(rect.width === 0 || rect.height === 0);
43970
- }
43971
- }
43972
-
43973
- const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
43974
- css /* scss */ `
43975
- .o-dv-checkbox {
43976
- margin: ${MARGIN}px;
43977
- /* required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
43978
- position: absolute;
43979
- }
43980
- `;
43981
- class DataValidationCheckbox extends Component {
43982
- static template = "o-spreadsheet-DataValidationCheckbox";
43983
- static components = {
43984
- Checkbox,
43985
- };
43986
- static props = {
43987
- cellPosition: Object,
43988
- };
43989
- onCheckboxChange(value) {
43990
- const { sheetId, col, row } = this.props.cellPosition;
43991
- const cellContent = value ? "TRUE" : "FALSE";
43992
- this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
43993
- }
43994
- get checkBoxValue() {
43995
- return !!this.env.model.getters.getEvaluatedCell(this.props.cellPosition).value;
43996
- }
43997
- get isDisabled() {
43998
- const cell = this.env.model.getters.getCell(this.props.cellPosition);
43999
- return this.env.model.getters.isReadonly() || !!cell?.isFormula;
44000
- }
44001
- }
44002
-
44003
- const ICON_WIDTH = 13;
44004
- css /* scss */ `
44005
- .o-dv-list-icon {
44006
- color: ${TEXT_BODY_MUTED};
44007
- border-radius: 1px;
44008
- height: ${GRID_ICON_EDGE_LENGTH}px;
44009
- width: ${GRID_ICON_EDGE_LENGTH}px;
44010
-
44011
- &:hover {
44012
- color: #ffffff;
44013
- background-color: ${TEXT_BODY_MUTED};
44014
- }
44015
-
44016
- svg {
44017
- width: ${ICON_WIDTH}px;
44018
- height: ${ICON_WIDTH}px;
44019
- }
44020
- }
44021
- `;
44022
- class DataValidationListIcon extends Component {
44023
- static template = "o-spreadsheet-DataValidationListIcon";
44024
- static props = {
44025
- cellPosition: Object,
44026
- };
44027
- onClick() {
44028
- const { col, row } = this.props.cellPosition;
44029
- this.env.model.selection.selectCell(col, row);
44030
- this.env.startCellEdition();
44031
- }
44032
- }
44033
-
44034
- class DataValidationOverlay extends Component {
44035
- static template = "o-spreadsheet-DataValidationOverlay";
44036
- static props = {};
44037
- static components = { GridCellIcon, DataValidationCheckbox, DataValidationListIcon };
44038
- get checkBoxCellPositions() {
44039
- return this.env.model.getters
44040
- .getVisibleCellPositions()
44041
- .filter((position) => this.env.model.getters.isCellValidCheckbox(position) &&
44042
- !this.env.model.getters.isFilterHeader(position));
44043
- }
44044
- get listIconsCellPositions() {
44045
- if (this.env.model.getters.isReadonly()) {
44046
- return [];
44047
- }
44048
- return this.env.model.getters
44049
- .getVisibleCellPositions()
44050
- .filter((position) => this.env.model.getters.cellHasListDataValidationIcon(position) &&
44051
- !this.env.model.getters.isFilterHeader(position));
44052
- }
44053
- }
44054
-
44055
44169
  function dragFigureForMove({ x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, initialFigure, { maxX, maxY }, { scrollX: initialScrollX, scrollY: initialScrollY }, { scrollX, scrollY }) {
44056
44170
  const deltaX = mouseX - mouseInitialX + scrollX - initialScrollX;
44057
44171
  const newX = clip(initialFigure.x + deltaX, 0, maxX - initialFigure.width);
@@ -44065,14 +44179,14 @@ function dragFigureForResize(initialFigure, dirX, dirY, { x: mouseX, y: mouseY }
44065
44179
  const deltaX = Math.min(dirX * (mouseInitialX - mouseX + scrollX - initialScrollX), width - minFigSize);
44066
44180
  const deltaY = Math.min(dirY * (mouseInitialY - mouseY + scrollY - initialScrollY), height - minFigSize);
44067
44181
  const fraction = Math.min(deltaX / width, deltaY / height);
44068
- width = width * (1 - fraction);
44069
- height = height * (1 - fraction);
44070
44182
  if (dirX < 0) {
44071
44183
  x = x + width * fraction;
44072
44184
  }
44073
44185
  if (dirY < 0) {
44074
44186
  y = y + height * fraction;
44075
44187
  }
44188
+ width = width * (1 - fraction);
44189
+ height = height * (1 - fraction);
44076
44190
  }
44077
44191
  else {
44078
44192
  const deltaX = Math.max(dirX * (mouseX - mouseInitialX + scrollX - initialScrollX), minFigSize - width);
@@ -44653,78 +44767,6 @@ class FiguresContainer extends Component {
44653
44767
  }
44654
44768
  }
44655
44769
 
44656
- css /* scss */ `
44657
- .o-filter-icon {
44658
- color: ${FILTERS_COLOR};
44659
- display: flex;
44660
- align-items: center;
44661
- justify-content: center;
44662
- width: ${GRID_ICON_EDGE_LENGTH}px;
44663
- height: ${GRID_ICON_EDGE_LENGTH}px;
44664
-
44665
- &:hover {
44666
- background: ${FILTERS_COLOR};
44667
- color: #fff;
44668
- }
44669
-
44670
- &.o-high-contrast {
44671
- color: #defade;
44672
- }
44673
- &.o-high-contrast:hover {
44674
- color: ${FILTERS_COLOR};
44675
- background: #fff;
44676
- }
44677
- }
44678
- .o-filter-icon:hover {
44679
- background: ${FILTERS_COLOR};
44680
- color: #fff;
44681
- }
44682
- `;
44683
- class FilterIcon extends Component {
44684
- static template = "o-spreadsheet-FilterIcon";
44685
- static props = {
44686
- cellPosition: Object,
44687
- };
44688
- cellPopovers;
44689
- setup() {
44690
- this.cellPopovers = useStore(CellPopoverStore);
44691
- }
44692
- onClick() {
44693
- const position = this.props.cellPosition;
44694
- const activePopover = this.cellPopovers.persistentCellPopover;
44695
- const { col, row } = position;
44696
- if (activePopover.isOpen &&
44697
- activePopover.col === col &&
44698
- activePopover.row === row &&
44699
- activePopover.type === "FilterMenu") {
44700
- this.cellPopovers.close();
44701
- return;
44702
- }
44703
- this.cellPopovers.open({ col, row }, "FilterMenu");
44704
- }
44705
- get isFilterActive() {
44706
- return this.env.model.getters.isFilterActive(this.props.cellPosition);
44707
- }
44708
- get iconClass() {
44709
- const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
44710
- const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
44711
- return luminance < 0.45 ? "o-high-contrast" : "";
44712
- }
44713
- }
44714
-
44715
- class FilterIconsOverlay extends Component {
44716
- static template = "o-spreadsheet-FilterIconsOverlay";
44717
- static props = {};
44718
- static components = {
44719
- GridCellIcon,
44720
- FilterIcon,
44721
- };
44722
- getFilterHeadersPositions() {
44723
- const sheetId = this.env.model.getters.getActiveSheetId();
44724
- return this.env.model.getters.getFilterHeaders(sheetId);
44725
- }
44726
- }
44727
-
44728
44770
  css /* scss */ `
44729
44771
  .o-validation {
44730
44772
  border-radius: 4px;
@@ -44865,6 +44907,78 @@ class GridAddRowsFooter extends Component {
44865
44907
  }
44866
44908
  }
44867
44909
 
44910
+ class GridCellIcon extends Component {
44911
+ static template = "o-spreadsheet-GridCellIcon";
44912
+ static props = {
44913
+ icon: Object,
44914
+ verticalAlign: { type: String, optional: true },
44915
+ slots: Object,
44916
+ };
44917
+ get iconStyle() {
44918
+ const cellPosition = this.props.icon.position;
44919
+ const merge = this.env.model.getters.getMerge(cellPosition);
44920
+ const zone = merge || positionToZone(cellPosition);
44921
+ const rect = this.env.model.getters.getVisibleRectWithoutHeaders(zone);
44922
+ const x = this.getIconHorizontalPosition(rect, cellPosition);
44923
+ const y = this.getIconVerticalPosition(rect, cellPosition);
44924
+ return cssPropertiesToCss({
44925
+ top: `${y}px`,
44926
+ left: `${x}px`,
44927
+ width: `${this.props.icon.size}px`,
44928
+ height: `${this.props.icon.size}px`,
44929
+ });
44930
+ }
44931
+ getIconVerticalPosition(rect, cellPosition) {
44932
+ const start = rect.y;
44933
+ const end = rect.y + rect.height;
44934
+ const cell = this.env.model.getters.getCell(cellPosition);
44935
+ const align = this.props.verticalAlign || cell?.style?.verticalAlign || DEFAULT_VERTICAL_ALIGN;
44936
+ switch (align) {
44937
+ case "bottom":
44938
+ return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
44939
+ case "top":
44940
+ return start + GRID_ICON_MARGIN;
44941
+ default:
44942
+ const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
44943
+ return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
44944
+ }
44945
+ }
44946
+ getIconHorizontalPosition(rect, cellPosition) {
44947
+ const start = rect.x;
44948
+ const end = rect.x + rect.width;
44949
+ const cell = this.env.model.getters.getCell(cellPosition);
44950
+ const evaluatedCell = this.env.model.getters.getEvaluatedCell(cellPosition);
44951
+ const align = this.props.icon.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
44952
+ switch (align) {
44953
+ case "right":
44954
+ return end - this.props.icon.size - this.props.icon.margin;
44955
+ case "left":
44956
+ return start + this.props.icon.margin;
44957
+ default:
44958
+ const centeringOffset = Math.floor((end - start - this.props.icon.size) / 2);
44959
+ return end - this.props.icon.size - centeringOffset;
44960
+ }
44961
+ }
44962
+ isPositionVisible(position) {
44963
+ const rect = this.env.model.getters.getVisibleRect(positionToZone(position));
44964
+ return !(rect.width === 0 || rect.height === 0);
44965
+ }
44966
+ }
44967
+
44968
+ class GridCellIconOverlay extends Component {
44969
+ static template = "o-spreadsheet-GridCellIconOverlay";
44970
+ static props = {};
44971
+ static components = { GridCellIcon };
44972
+ get icons() {
44973
+ const icons = [];
44974
+ for (const position of this.env.model.getters.getVisibleCellPositions()) {
44975
+ const cellIcons = this.env.model.getters.getCellIcons(position);
44976
+ icons.push(...cellIcons.filter((icon) => icon.component));
44977
+ }
44978
+ return icons;
44979
+ }
44980
+ }
44981
+
44868
44982
  /**
44869
44983
  * Manages an event listener on a ref. Useful for hooks that want to manage
44870
44984
  * event listeners, especially more than one. Prefer using t-on directly in
@@ -45116,9 +45230,8 @@ class GridOverlay extends Component {
45116
45230
  };
45117
45231
  static components = {
45118
45232
  FiguresContainer,
45119
- DataValidationOverlay,
45120
45233
  GridAddRowsFooter,
45121
- FilterIconsOverlay,
45234
+ GridCellIconOverlay,
45122
45235
  };
45123
45236
  static defaultProps = {
45124
45237
  onCellDoubleClicked: () => { },
@@ -46128,13 +46241,12 @@ class GridRenderer {
46128
46241
  // compute horizontal align start point parameter
46129
46242
  let x = box.x;
46130
46243
  if (align === "left") {
46131
- x += MIN_CELL_TEXT_MARGIN + (box.image ? box.image.size + MIN_CF_ICON_MARGIN : 0);
46244
+ const leftIconSize = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46245
+ x += MIN_CELL_TEXT_MARGIN + leftIconSize;
46132
46246
  }
46133
46247
  else if (align === "right") {
46134
- x +=
46135
- box.width -
46136
- MIN_CELL_TEXT_MARGIN -
46137
- (box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0);
46248
+ const rightIconSize = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46249
+ x += box.width - MIN_CELL_TEXT_MARGIN - rightIconSize;
46138
46250
  }
46139
46251
  else {
46140
46252
  x += box.width / 2;
@@ -46168,18 +46280,28 @@ class GridRenderer {
46168
46280
  drawIcon(renderingContext, boxes) {
46169
46281
  const { ctx } = renderingContext;
46170
46282
  for (const box of boxes) {
46171
- if (box.image && box.image.svg) {
46283
+ for (const icon of Object.values(box.icons)) {
46284
+ if (!icon || !icon.svg) {
46285
+ continue;
46286
+ }
46172
46287
  ctx.save();
46173
- if (box.image.clipIcon) {
46174
- ctx.beginPath();
46175
- const { x, y, width, height } = box.image.clipIcon;
46176
- ctx.rect(x, y, width, height);
46177
- ctx.clip();
46288
+ ctx.beginPath();
46289
+ ctx.rect(box.x, box.y, box.width, box.height);
46290
+ ctx.clip();
46291
+ const iconSize = icon.size;
46292
+ const iconY = this.computeTextYCoordinate(box, iconSize);
46293
+ const svg = icon.svg;
46294
+ let x;
46295
+ if (icon.horizontalAlign === "left") {
46296
+ x = box.x + icon.margin;
46297
+ }
46298
+ else if (icon.horizontalAlign === "right") {
46299
+ x = box.x + box.width - iconSize - icon.margin;
46178
46300
  }
46179
- const iconSize = box.image.size;
46180
- const y = this.computeTextYCoordinate(box, iconSize);
46181
- const svg = box.image.svg;
46182
- ctx.translate(box.x + MIN_CF_ICON_MARGIN, y);
46301
+ else {
46302
+ x = box.x + (box.width - iconSize) / 2;
46303
+ }
46304
+ ctx.translate(x, iconY);
46183
46305
  ctx.scale(iconSize / svg.width, iconSize / svg.height);
46184
46306
  ctx.fillStyle = svg.fillColor;
46185
46307
  ctx.fill(new Path2D(svg.path));
@@ -46356,13 +46478,11 @@ class GridRenderer {
46356
46478
  const position = { sheetId, col: col + 1, row };
46357
46479
  const nextCell = this.getters.getEvaluatedCell(position);
46358
46480
  const nextCellBorder = this.getters.getCellComputedBorder(position);
46359
- const cellHasIcon = this.getters.doesCellHaveGridIcon(position);
46360
- const cellHasCheckbox = this.getters.isCellValidCheckbox(position);
46481
+ const doesCellHaveGridIcon = this.getters.doesCellHaveGridIcon(position);
46361
46482
  if (nextCell.type !== CellValueType.empty ||
46362
46483
  this.getters.isInMerge(position) ||
46363
46484
  nextCellBorder?.left ||
46364
- cellHasIcon ||
46365
- cellHasCheckbox) {
46485
+ doesCellHaveGridIcon) {
46366
46486
  return col;
46367
46487
  }
46368
46488
  col++;
@@ -46376,13 +46496,11 @@ class GridRenderer {
46376
46496
  const position = { sheetId, col: col - 1, row };
46377
46497
  const previousCell = this.getters.getEvaluatedCell(position);
46378
46498
  const previousCellBorder = this.getters.getCellComputedBorder(position);
46379
- const cellHasIcon = this.getters.doesCellHaveGridIcon(position);
46380
- const cellHasCheckbox = this.getters.isCellValidCheckbox(position);
46499
+ const doesCellHaveGridIcon = this.getters.doesCellHaveGridIcon(position);
46381
46500
  if (previousCell.type !== CellValueType.empty ||
46382
46501
  this.getters.isInMerge(position) ||
46383
46502
  previousCellBorder?.right ||
46384
- cellHasIcon ||
46385
- cellHasCheckbox) {
46503
+ doesCellHaveGridIcon) {
46386
46504
  return col;
46387
46505
  }
46388
46506
  col--;
@@ -46418,6 +46536,12 @@ class GridRenderer {
46418
46536
  const dataBarFill = this.fingerprints.isEnabled
46419
46537
  ? undefined
46420
46538
  : this.getters.getConditionalDataBar(position);
46539
+ const iconsList = this.getters.getCellIcons(position);
46540
+ const cellIcons = {
46541
+ left: iconsList.find((icon) => icon?.horizontalAlign === "left"),
46542
+ right: iconsList.find((icon) => icon?.horizontalAlign === "right"),
46543
+ center: iconsList.find((icon) => icon?.horizontalAlign === "center"),
46544
+ };
46421
46545
  const box = {
46422
46546
  x,
46423
46547
  y,
@@ -46430,32 +46554,21 @@ class GridRenderer {
46430
46554
  overlayColor: this.hoveredTables.overlayColors.get(position),
46431
46555
  isError: (cell.type === CellValueType.error && !!cell.message) ||
46432
46556
  this.getters.isDataValidationInvalid(position),
46557
+ icons: cellIcons,
46433
46558
  };
46434
- /** Icon */
46435
- const iconSvg = this.getters.getCellIconSvg(position);
46436
46559
  const fontSizePX = computeTextFontSizeInPixels(box.style);
46437
- const iconBoxWidth = iconSvg ? MIN_CF_ICON_MARGIN + fontSizePX : 0;
46438
- if (iconSvg) {
46439
- box.image = {
46440
- type: "icon",
46441
- size: fontSizePX,
46442
- clipIcon: { x: box.x, y: box.y, width: Math.min(iconBoxWidth, width), height },
46443
- svg: iconSvg,
46444
- };
46445
- }
46446
- if (cell.type === CellValueType.empty || this.getters.isCellValidCheckbox(position)) {
46560
+ if (cell.type === CellValueType.empty || box.icons.center) {
46447
46561
  return box;
46448
46562
  }
46449
- /** Filter Header or data validation icon */
46450
- box.hasIcon = this.getters.doesCellHaveGridIcon(position);
46451
- const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
46452
46563
  /** Content */
46453
46564
  const wrapping = style.wrapping || "overflow";
46454
46565
  const wrapText = wrapping === "wrap" && !showFormula;
46455
46566
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
46456
46567
  const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
46457
46568
  const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
46458
- const contentWidth = iconBoxWidth + textWidth + headerIconWidth;
46569
+ const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46570
+ const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46571
+ const contentWidth = leftIconWidth + textWidth + rightIconWidth;
46459
46572
  const align = this.computeCellAlignment(position, contentWidth > width);
46460
46573
  box.content = {
46461
46574
  textLines: multiLineText,
@@ -46464,11 +46577,11 @@ class GridRenderer {
46464
46577
  };
46465
46578
  /** ClipRect */
46466
46579
  const isOverflowing = contentWidth > width || fontSizePX > height;
46467
- if (iconSvg || box.hasIcon) {
46580
+ if (box.icons.left || box.icons.right) {
46468
46581
  box.clipRect = {
46469
- x: box.x + iconBoxWidth,
46582
+ x: box.x + leftIconWidth,
46470
46583
  y: box.y,
46471
- width: Math.max(0, width - iconBoxWidth - headerIconWidth),
46584
+ width: Math.max(0, width - leftIconWidth - rightIconWidth),
46472
46585
  height,
46473
46586
  };
46474
46587
  }
@@ -48330,14 +48443,16 @@ class ChartTitle extends Component {
48330
48443
  static components = { Section, TextStyler };
48331
48444
  static props = {
48332
48445
  title: { type: String, optional: true },
48446
+ placeholder: { type: String, optional: true },
48333
48447
  updateTitle: Function,
48334
- name: { type: String, optional: true },
48448
+ name: { type: String },
48335
48449
  style: Object,
48336
48450
  defaultStyle: { type: Object, optional: true },
48337
48451
  updateStyle: Function,
48338
48452
  };
48339
48453
  static defaultProps = {
48340
48454
  title: "",
48455
+ placeholder: "",
48341
48456
  };
48342
48457
  updateTitle(ev) {
48343
48458
  this.props.updateTitle(ev.target.value);
@@ -49367,6 +49482,7 @@ class ScorecardChartDesignPanel extends Component {
49367
49482
  SidePanelCollapsible,
49368
49483
  Section,
49369
49484
  Checkbox,
49485
+ ChartTitle,
49370
49486
  };
49371
49487
  static props = {
49372
49488
  figureId: String,
@@ -49391,9 +49507,6 @@ class ScorecardChartDesignPanel extends Component {
49391
49507
  translate(term) {
49392
49508
  return _t(term);
49393
49509
  }
49394
- updateBaselineDescr(ev) {
49395
- this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
49396
- }
49397
49510
  setColor(color, colorPickerId) {
49398
49511
  switch (colorPickerId) {
49399
49512
  case "backgroundColor":
@@ -49407,6 +49520,38 @@ class ScorecardChartDesignPanel extends Component {
49407
49520
  break;
49408
49521
  }
49409
49522
  }
49523
+ get keyStyle() {
49524
+ return {
49525
+ align: "center",
49526
+ fontSize: DEFAULT_SCORECARD_KEY_VALUE_FONT_SIZE,
49527
+ ...this.props.definition.keyDescr,
49528
+ };
49529
+ }
49530
+ get baselineStyle() {
49531
+ return {
49532
+ align: "center",
49533
+ fontSize: DEFAULT_SCORECARD_BASELINE_FONT_SIZE,
49534
+ ...this.props.definition.baselineDescr,
49535
+ };
49536
+ }
49537
+ setKeyText(text) {
49538
+ this.props.updateChart(this.props.figureId, {
49539
+ keyDescr: { ...this.props.definition.keyDescr, text },
49540
+ });
49541
+ }
49542
+ updateKeyStyle(style) {
49543
+ const keyDescr = { ...this.keyStyle, ...style };
49544
+ this.props.updateChart(this.props.figureId, { keyDescr });
49545
+ }
49546
+ setBaselineText(text) {
49547
+ this.props.updateChart(this.props.figureId, {
49548
+ baselineDescr: { ...this.props.definition.baselineDescr, text },
49549
+ });
49550
+ }
49551
+ updateBaselineStyle(style) {
49552
+ const baselineDescr = { ...this.baselineStyle, ...style };
49553
+ this.props.updateChart(this.props.figureId, { baselineDescr });
49554
+ }
49410
49555
  }
49411
49556
 
49412
49557
  class SunburstChartDesignPanel extends Component {
@@ -52317,6 +52462,9 @@ class PivotMeasureEditor extends Component {
52317
52462
  }
52318
52463
  return undefined;
52319
52464
  }
52465
+ get isCalculatedMeasureInvalid() {
52466
+ return this.env.model.getters.getMeasureCompiledFormula(this.props.measure).isBadExpression;
52467
+ }
52320
52468
  }
52321
52469
 
52322
52470
  css /* scss */ `
@@ -52807,11 +52955,13 @@ class PivotRuntimeDefinition {
52807
52955
  columns;
52808
52956
  rows;
52809
52957
  sortedColumn;
52958
+ collapsedDomains;
52810
52959
  constructor(definition, fields) {
52811
52960
  this.measures = definition.measures.map((measure) => createMeasure(fields, measure));
52812
52961
  this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));
52813
52962
  this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));
52814
52963
  this.sortedColumn = definition.sortedColumn;
52964
+ this.collapsedDomains = definition.collapsedDomains;
52815
52965
  }
52816
52966
  getDimension(nameWithGranularity) {
52817
52967
  const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
@@ -52966,24 +53116,62 @@ class SpreadsheetPivotTable {
52966
53116
  rowTree;
52967
53117
  colTree;
52968
53118
  isSorted = false;
52969
- constructor(columns, rows, measures, fieldsType) {
52970
- this.columns = columns.map((row) => {
53119
+ constructor(columns, rows, measures, fieldsType, collapsedDomains = { COL: [], ROW: [] }) {
53120
+ this.measures = measures;
53121
+ this.fieldsType = fieldsType;
53122
+ if (collapsedDomains.COL.length) {
53123
+ columns = this.removeCollapsedColumns(columns, measures, collapsedDomains.COL);
53124
+ }
53125
+ this.columns = columns.map((cols) => {
52971
53126
  // offset in the pivot table
52972
53127
  // starts at 1 because the first column is the row title
52973
53128
  let offset = 1;
52974
- return row.map((col) => {
53129
+ return cols.map((col) => {
52975
53130
  col = { ...col, offset };
52976
53131
  offset += col.width;
52977
53132
  return col;
52978
53133
  });
52979
53134
  });
52980
- this.rows = rows;
52981
- this.measures = measures;
52982
- this.fieldsType = fieldsType;
53135
+ this.rows = rows.filter((row) => !this.isParentCollapsed(collapsedDomains.ROW, row));
52983
53136
  this.maxIndent = Math.max(...this.rows.map((row) => row.indent));
52984
53137
  this.rowTree = lazy(() => this.buildRowsTree());
52985
53138
  this.colTree = lazy(() => this.buildColumnsTree());
52986
53139
  }
53140
+ removeCollapsedColumns(columns, measures, collapsedDomains) {
53141
+ const replaceCollapsedChildrenWithSubTotalColumns = (parentCol, depth) => {
53142
+ const parentDomain = this.getDomain(parentCol);
53143
+ const cols = columns[depth];
53144
+ const startIndex = cols.findIndex((col) => isParentDomain(this.getDomain(col), parentDomain));
53145
+ const endIndex = cols.findLastIndex((col) => isParentDomain(this.getDomain(col), parentDomain));
53146
+ const isLeaf = depth === columns.length - 1;
53147
+ const newColumns = measures.map((measure) => {
53148
+ const fields = isLeaf ? [...parentCol.fields, "measure"] : [];
53149
+ const values = isLeaf ? [...parentCol.values, measure] : [];
53150
+ return { fields, values, width: 1, offset: 0, collapsedHeader: !isLeaf };
53151
+ });
53152
+ cols.splice(startIndex, endIndex - startIndex + 1, ...newColumns);
53153
+ };
53154
+ return columns.map((cols, i) => {
53155
+ for (const col of cols) {
53156
+ if (i >= columns.length - 2) {
53157
+ return cols;
53158
+ }
53159
+ const domain = this.getDomain(col);
53160
+ if (!collapsedDomains.some((collapsedDomain) => deepEquals(domain, collapsedDomain))) {
53161
+ continue;
53162
+ }
53163
+ col.width = measures.length;
53164
+ for (let depth = i + 1; depth < columns.length; depth++) {
53165
+ replaceCollapsedChildrenWithSubTotalColumns(col, depth);
53166
+ }
53167
+ }
53168
+ return cols;
53169
+ });
53170
+ }
53171
+ isParentCollapsed(collapsedDomains, dim) {
53172
+ const domain = this.getDomain(dim);
53173
+ return collapsedDomains.some((collapsedDomain) => isParentDomain(domain, collapsedDomain));
53174
+ }
52987
53175
  /**
52988
53176
  * Get the number of columns leafs (i.e. the number of the last row of columns)
52989
53177
  */
@@ -53039,19 +53227,19 @@ class SpreadsheetPivotTable {
53039
53227
  }
53040
53228
  else if (row <= colHeadersHeight - 1) {
53041
53229
  const domain = this.getColHeaderDomain(col, row);
53042
- return domain ? { type: "HEADER", domain } : EMPTY_PIVOT_CELL;
53230
+ return domain ? { type: "HEADER", domain, dimension: "COL" } : EMPTY_PIVOT_CELL;
53043
53231
  }
53044
53232
  else if (col === 0) {
53045
53233
  const rowIndex = row - colHeadersHeight;
53046
- const domain = this.getRowDomain(rowIndex);
53047
- return { type: "HEADER", domain };
53234
+ const domain = this.getDomain(this.rows[rowIndex]);
53235
+ return { type: "HEADER", domain, dimension: "ROW" };
53048
53236
  }
53049
53237
  else {
53050
53238
  const rowIndex = row - colHeadersHeight;
53051
53239
  if (!includeTotal && this.isTotalRow(rowIndex)) {
53052
53240
  return EMPTY_PIVOT_CELL;
53053
53241
  }
53054
- const domain = [...this.getRowDomain(rowIndex), ...this.getColDomain(col)];
53242
+ const domain = [...this.getDomain(this.rows[rowIndex]), ...this.getColDomain(col)];
53055
53243
  const measure = this.getColMeasure(col);
53056
53244
  return { type: "VALUE", domain, measure };
53057
53245
  }
@@ -53060,31 +53248,31 @@ class SpreadsheetPivotTable {
53060
53248
  if (col === 0) {
53061
53249
  return undefined;
53062
53250
  }
53063
- const domain = [];
53064
53251
  const pivotCol = this.columns[row].find((pivotCol) => pivotCol.offset === col);
53065
- if (!pivotCol) {
53252
+ if (!pivotCol || pivotCol.collapsedHeader) {
53066
53253
  return undefined;
53067
53254
  }
53068
- for (let i = 0; i < pivotCol.fields.length; i++) {
53069
- const fieldWithGranularity = pivotCol.fields[i];
53255
+ return this.getDomain(pivotCol);
53256
+ }
53257
+ getDomain(dim) {
53258
+ return dim.fields.map((fieldWithGranularity, i) => {
53070
53259
  if (fieldWithGranularity === "measure") {
53071
- domain.push({
53260
+ return {
53072
53261
  type: "char",
53073
53262
  field: fieldWithGranularity,
53074
- value: toNormalizedPivotValue({ displayName: "measure", type: "char" }, pivotCol.values[i]),
53075
- });
53263
+ value: toNormalizedPivotValue({ displayName: "measure", type: "char" }, dim.values[i]),
53264
+ };
53076
53265
  }
53077
53266
  else {
53078
53267
  const { fieldName, granularity } = parseDimension(fieldWithGranularity);
53079
53268
  const type = this.fieldsType[fieldName] || "char";
53080
- domain.push({
53269
+ return {
53081
53270
  type,
53082
53271
  field: fieldWithGranularity,
53083
- value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, pivotCol.values[i]),
53084
- });
53272
+ value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, dim.values[i]),
53273
+ };
53085
53274
  }
53086
- }
53087
- return domain;
53275
+ });
53088
53276
  }
53089
53277
  getColDomain(col) {
53090
53278
  const domain = this.getColHeaderDomain(col, this.columns.length - 1);
@@ -53098,20 +53286,6 @@ class SpreadsheetPivotTable {
53098
53286
  }
53099
53287
  return measure.toString();
53100
53288
  }
53101
- getRowDomain(row) {
53102
- const domain = [];
53103
- for (let i = 0; i < this.rows[row].fields.length; i++) {
53104
- const fieldWithGranularity = this.rows[row].fields[i];
53105
- const { fieldName, granularity } = parseDimension(fieldWithGranularity);
53106
- const type = this.fieldsType[fieldName] || "char";
53107
- domain.push({
53108
- type,
53109
- field: fieldWithGranularity,
53110
- value: toNormalizedPivotValue({ displayName: fieldName, type, granularity }, this.rows[row].values[i]),
53111
- });
53112
- }
53113
- return domain;
53114
- }
53115
53289
  buildRowsTree() {
53116
53290
  const tree = [];
53117
53291
  let depth = 0;
@@ -53216,7 +53390,7 @@ const EMPTY_PIVOT_CELL = { type: "EMPTY" };
53216
53390
  /**
53217
53391
  * This function converts a list of data entry into a spreadsheet pivot table.
53218
53392
  */
53219
- function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
53393
+ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition, mode) {
53220
53394
  const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
53221
53395
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
53222
53396
  computeWidthOfColumnsNodes(columnsTree, measureIds.length);
@@ -53235,7 +53409,8 @@ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
53235
53409
  for (const row of definition.rows) {
53236
53410
  fieldsType[row.fieldName] = row.type;
53237
53411
  }
53238
- return new SpreadsheetPivotTable(cols, rows, measureIds, fieldsType);
53412
+ const collapsedDomains = mode === "collapsed" ? definition.collapsedDomains : undefined;
53413
+ return new SpreadsheetPivotTable(cols, rows, measureIds, fieldsType, collapsedDomains);
53239
53414
  }
53240
53415
  // -----------------------------------------------------------------------------
53241
53416
  // ROWS
@@ -53608,7 +53783,8 @@ class SpreadsheetPivot {
53608
53783
  * This object contains the pivot table structure. It is created from the
53609
53784
  * data entries and the pivot definition.
53610
53785
  */
53611
- table;
53786
+ collapsedTable;
53787
+ expandedTable;
53612
53788
  /**
53613
53789
  * This error is set when the range is invalid. It is used to show an error
53614
53790
  * message to the user.
@@ -53641,7 +53817,8 @@ class SpreadsheetPivot {
53641
53817
  this.dataEntries = this.loadData();
53642
53818
  }
53643
53819
  if (type >= ReloadType.TABLE) {
53644
- this.table = undefined;
53820
+ this.collapsedTable = undefined;
53821
+ this.expandedTable = undefined;
53645
53822
  }
53646
53823
  }
53647
53824
  onDefinitionChange(nextDefinition) {
@@ -53801,14 +53978,23 @@ class SpreadsheetPivot {
53801
53978
  }
53802
53979
  return values;
53803
53980
  }
53804
- getTableStructure() {
53981
+ getCollapsedTableStructure() {
53805
53982
  if (!this.isValid()) {
53806
53983
  throw new Error("Pivot is not valid !");
53807
53984
  }
53808
- if (!this.table) {
53809
- this.table = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition);
53985
+ if (!this.collapsedTable) {
53986
+ this.collapsedTable = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition, "collapsed");
53987
+ }
53988
+ return this.collapsedTable;
53989
+ }
53990
+ getExpandedTableStructure() {
53991
+ if (!this.isValid()) {
53992
+ throw new Error("Pivot is not valid !");
53993
+ }
53994
+ if (!this.expandedTable) {
53995
+ this.expandedTable = dataEntriesToSpreadsheetPivotTable(this.dataEntries, this.definition, "expanded");
53810
53996
  }
53811
- return this.table;
53997
+ return this.expandedTable;
53812
53998
  }
53813
53999
  getFields() {
53814
54000
  return this.metaData.fields;
@@ -54166,6 +54352,13 @@ class PivotSidePanelStore extends SpreadsheetStore {
54166
54352
  })),
54167
54353
  sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
54168
54354
  };
54355
+ if (cleanedDefinition.collapsedDomains) {
54356
+ const { COL, ROW } = cleanedDefinition.collapsedDomains;
54357
+ cleanedDefinition.collapsedDomains = {
54358
+ COL: COL.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.columns)),
54359
+ ROW: ROW.filter((domain) => this.areDomainFieldsValid(domain, cleanedDefinition.rows)),
54360
+ };
54361
+ }
54169
54362
  if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
54170
54363
  return;
54171
54364
  }
@@ -54255,6 +54448,15 @@ class PivotSidePanelStore extends SpreadsheetStore {
54255
54448
  return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
54256
54449
  deepEquals(oldDefinition.columns, newDefinition.columns));
54257
54450
  }
54451
+ areDomainFieldsValid(domain, dims) {
54452
+ const fieldsNameWithGranularity = dims.map(({ fieldName, granularity }) => fieldName + (granularity ? `:${granularity}` : ""));
54453
+ for (let i = 0; i < domain.length; i++) {
54454
+ if (domain[i].field !== fieldsNameWithGranularity[i]) {
54455
+ return false;
54456
+ }
54457
+ }
54458
+ return true;
54459
+ }
54258
54460
  }
54259
54461
 
54260
54462
  class PivotSpreadsheetSidePanel extends Component {
@@ -55522,6 +55724,7 @@ class Grid extends Component {
55522
55724
  composerFocusStore;
55523
55725
  DOMFocusableElementStore;
55524
55726
  paintFormatStore;
55727
+ clientFocusStore;
55525
55728
  dragNDropGrid = useDragAndDropBeyondTheViewport(this.env);
55526
55729
  onMouseWheel;
55527
55730
  hoveredCell;
@@ -55539,6 +55742,7 @@ class Grid extends Component {
55539
55742
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
55540
55743
  this.sidePanel = useStore(SidePanelStore);
55541
55744
  this.paintFormatStore = useStore(PaintFormatStore);
55745
+ this.clientFocusStore = useStore(ClientFocusStore);
55542
55746
  useStore(ArrayFormulaHighlight);
55543
55747
  useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
55544
55748
  useExternalListener(document.body, "cut", this.copy.bind(this, true));
@@ -55821,6 +56025,9 @@ class Grid extends Component {
55821
56025
  isCellHovered(col, row) {
55822
56026
  return this.hoveredCell.col === col && this.hoveredCell.row === row;
55823
56027
  }
56028
+ get focusedClients() {
56029
+ return this.clientFocusStore.focusedClients;
56030
+ }
55824
56031
  getGridRect() {
55825
56032
  return {
55826
56033
  ...getRefBoundingRect(this.gridRef),
@@ -56139,6 +56346,51 @@ class Grid extends Component {
56139
56346
  const supportedPivotPositionalFormulaRegistry = new Registry();
56140
56347
  supportedPivotPositionalFormulaRegistry.add("SPREADSHEET", false);
56141
56348
 
56349
+ class FullScreenChart extends Component {
56350
+ static template = "o-spreadsheet-FullScreenChart";
56351
+ static props = {};
56352
+ static components = { ChartDashboardMenu };
56353
+ fullScreenChartStore;
56354
+ ref = useRef("fullScreenChart");
56355
+ spreadsheetRect = useSpreadsheetRect();
56356
+ figureRegistry = figureRegistry;
56357
+ setup() {
56358
+ this.fullScreenChartStore = useStore(FullScreenChartStore);
56359
+ const animationStore = useStore(ChartAnimationStore);
56360
+ let lastFigureId = undefined;
56361
+ onWillUpdateProps(() => {
56362
+ if (lastFigureId !== this.figureUI?.id) {
56363
+ animationStore.enableAnimationForChart(this.figureUI?.id + "-fullscreen");
56364
+ }
56365
+ lastFigureId = this.figureUI?.id;
56366
+ });
56367
+ useEffect((el) => el?.focus(), () => [this.ref.el]);
56368
+ }
56369
+ get figureUI() {
56370
+ return this.fullScreenChartStore.fullScreenFigure;
56371
+ }
56372
+ exitFullScreen() {
56373
+ if (this.figureUI) {
56374
+ this.fullScreenChartStore.toggleFullScreenChart(this.figureUI.id);
56375
+ }
56376
+ }
56377
+ onKeyDown(ev) {
56378
+ if (ev.key === "Escape") {
56379
+ this.exitFullScreen();
56380
+ }
56381
+ }
56382
+ get chartComponent() {
56383
+ if (!this.figureUI)
56384
+ return undefined;
56385
+ const type = this.env.model.getters.getChartType(this.figureUI.id);
56386
+ const component = chartComponentRegistry.get(type);
56387
+ if (!component) {
56388
+ throw new Error(`Component is not defined for type ${type}`);
56389
+ }
56390
+ return component;
56391
+ }
56392
+ }
56393
+
56142
56394
  css /* scss */ `
56143
56395
  .o_pivot_html_renderer {
56144
56396
  width: 100%;
@@ -56193,7 +56445,7 @@ class PivotHTMLRenderer extends Component {
56193
56445
  showMissingValuesOnly: false,
56194
56446
  });
56195
56447
  setup() {
56196
- const table = this.pivot.getTableStructure();
56448
+ const table = this.pivot.getExpandedTableStructure();
56197
56449
  const formulaId = this.env.model.getters.getPivotFormulaId(this.props.pivotId);
56198
56450
  this.data = {
56199
56451
  columns: this._buildColHeaders(formulaId, table),
@@ -56237,7 +56489,7 @@ class PivotHTMLRenderer extends Component {
56237
56489
  * The parent of "January" is "Australia"
56238
56490
  */
56239
56491
  addRecursiveRow(index) {
56240
- const rows = this.pivot.getTableStructure().rows;
56492
+ const rows = this.pivot.getExpandedTableStructure().rows;
56241
56493
  const row = [...rows[index].values];
56242
56494
  if (row.length <= 1) {
56243
56495
  return [index];
@@ -63646,10 +63898,9 @@ class Evaluator {
63646
63898
  return this.evaluatedCells.keysForSheet(sheetId);
63647
63899
  }
63648
63900
  getArrayFormulaSpreadingOn(position) {
63649
- const hasArrayFormulaResult = this.getEvaluatedCell(position).type !== CellValueType.empty &&
63650
- !this.getters.getCell(position)?.isFormula;
63651
- if (!hasArrayFormulaResult) {
63652
- return this.spreadingRelations.isArrayFormula(position) ? position : undefined;
63901
+ const isEmpty = this.getEvaluatedCell(position).type === CellValueType.empty;
63902
+ if (isEmpty) {
63903
+ return undefined;
63653
63904
  }
63654
63905
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
63655
63906
  return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
@@ -65051,6 +65302,323 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65051
65302
  }
65052
65303
  }
65053
65304
 
65305
+ const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
65306
+ css /* scss */ `
65307
+ .o-dv-checkbox {
65308
+ margin: ${MARGIN}px;
65309
+ /* required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
65310
+ position: absolute;
65311
+ }
65312
+ `;
65313
+ class DataValidationCheckbox extends Component {
65314
+ static template = "o-spreadsheet-DataValidationCheckbox";
65315
+ static components = {
65316
+ Checkbox,
65317
+ };
65318
+ static props = {
65319
+ cellPosition: Object,
65320
+ };
65321
+ onCheckboxChange(value) {
65322
+ const { sheetId, col, row } = this.props.cellPosition;
65323
+ const cellContent = value ? "TRUE" : "FALSE";
65324
+ this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
65325
+ }
65326
+ get checkBoxValue() {
65327
+ return !!this.env.model.getters.getEvaluatedCell(this.props.cellPosition).value;
65328
+ }
65329
+ get isDisabled() {
65330
+ const cell = this.env.model.getters.getCell(this.props.cellPosition);
65331
+ return this.env.model.getters.isReadonly() || !!cell?.isFormula;
65332
+ }
65333
+ }
65334
+
65335
+ const ICON_WIDTH = 13;
65336
+ css /* scss */ `
65337
+ .o-dv-list-icon {
65338
+ color: ${TEXT_BODY_MUTED};
65339
+ border-radius: 1px;
65340
+ height: ${GRID_ICON_EDGE_LENGTH}px;
65341
+ width: ${GRID_ICON_EDGE_LENGTH}px;
65342
+
65343
+ &:hover {
65344
+ color: #ffffff;
65345
+ background-color: ${TEXT_BODY_MUTED};
65346
+ }
65347
+
65348
+ svg {
65349
+ width: ${ICON_WIDTH}px;
65350
+ height: ${ICON_WIDTH}px;
65351
+ }
65352
+ }
65353
+ `;
65354
+ class DataValidationListIcon extends Component {
65355
+ static template = "o-spreadsheet-DataValidationListIcon";
65356
+ static props = {
65357
+ cellPosition: Object,
65358
+ };
65359
+ onClick() {
65360
+ const { col, row } = this.props.cellPosition;
65361
+ this.env.model.selection.selectCell(col, row);
65362
+ this.env.startCellEdition();
65363
+ }
65364
+ }
65365
+
65366
+ css /* scss */ `
65367
+ .o-filter-icon {
65368
+ color: ${FILTERS_COLOR};
65369
+ display: flex;
65370
+ align-items: center;
65371
+ justify-content: center;
65372
+ width: ${GRID_ICON_EDGE_LENGTH}px;
65373
+ height: ${GRID_ICON_EDGE_LENGTH}px;
65374
+
65375
+ &:hover {
65376
+ background: ${FILTERS_COLOR};
65377
+ color: #fff;
65378
+ }
65379
+
65380
+ &.o-high-contrast {
65381
+ color: #defade;
65382
+ }
65383
+ &.o-high-contrast:hover {
65384
+ color: ${FILTERS_COLOR};
65385
+ background: #fff;
65386
+ }
65387
+ }
65388
+ .o-filter-icon:hover {
65389
+ background: ${FILTERS_COLOR};
65390
+ color: #fff;
65391
+ }
65392
+ `;
65393
+ class FilterIcon extends Component {
65394
+ static template = "o-spreadsheet-FilterIcon";
65395
+ static props = {
65396
+ cellPosition: Object,
65397
+ };
65398
+ cellPopovers;
65399
+ setup() {
65400
+ this.cellPopovers = useStore(CellPopoverStore);
65401
+ }
65402
+ onClick() {
65403
+ const position = this.props.cellPosition;
65404
+ const activePopover = this.cellPopovers.persistentCellPopover;
65405
+ const { col, row } = position;
65406
+ if (activePopover.isOpen &&
65407
+ activePopover.col === col &&
65408
+ activePopover.row === row &&
65409
+ activePopover.type === "FilterMenu") {
65410
+ this.cellPopovers.close();
65411
+ return;
65412
+ }
65413
+ this.cellPopovers.open({ col, row }, "FilterMenu");
65414
+ }
65415
+ get isFilterActive() {
65416
+ return this.env.model.getters.isFilterActive(this.props.cellPosition);
65417
+ }
65418
+ get iconClass() {
65419
+ const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
65420
+ const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
65421
+ return luminance < 0.45 ? "o-high-contrast" : "";
65422
+ }
65423
+ }
65424
+
65425
+ css /* scss */ `
65426
+ .o-spreadsheet {
65427
+ .o-pivot-collapse-icon {
65428
+ cursor: pointer;
65429
+ width: 11px;
65430
+ height: 11px;
65431
+ border: 1px solid #777;
65432
+ background-color: #eee;
65433
+ margin: 3px 0 3px 6px;
65434
+
65435
+ .o-icon {
65436
+ width: 5px;
65437
+ height: 5px;
65438
+ }
65439
+ }
65440
+ }
65441
+ `;
65442
+ class PivotCollapseIcon extends Component {
65443
+ static template = "o-spreadsheet-PivotCollapseIcon";
65444
+ static props = {
65445
+ cellPosition: Object,
65446
+ };
65447
+ onClick() {
65448
+ const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
65449
+ const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
65450
+ if (!pivotId || pivotCell.type !== "HEADER") {
65451
+ return;
65452
+ }
65453
+ const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
65454
+ const collapsedDomains = definition.collapsedDomains?.[pivotCell.dimension]
65455
+ ? [...definition.collapsedDomains[pivotCell.dimension]]
65456
+ : [];
65457
+ const index = collapsedDomains.findIndex((domain) => deepEquals(domain, pivotCell.domain));
65458
+ if (index !== -1) {
65459
+ collapsedDomains.splice(index, 1);
65460
+ }
65461
+ else {
65462
+ collapsedDomains.push(pivotCell.domain);
65463
+ }
65464
+ const newDomains = definition.collapsedDomains
65465
+ ? { ...definition.collapsedDomains }
65466
+ : { COL: [], ROW: [] };
65467
+ newDomains[pivotCell.dimension] = collapsedDomains;
65468
+ this.env.model.dispatch("UPDATE_PIVOT", {
65469
+ pivotId,
65470
+ pivot: { ...definition, collapsedDomains: newDomains },
65471
+ });
65472
+ }
65473
+ get isCollapsed() {
65474
+ const pivotCell = this.env.model.getters.getPivotCellFromPosition(this.props.cellPosition);
65475
+ const pivotId = this.env.model.getters.getPivotIdFromPosition(this.props.cellPosition);
65476
+ if (!pivotId || pivotCell.type !== "HEADER") {
65477
+ return false;
65478
+ }
65479
+ const definition = this.env.model.getters.getPivotCoreDefinition(pivotId);
65480
+ const domains = definition.collapsedDomains?.[pivotCell.dimension] ?? [];
65481
+ return domains?.some((domain) => deepEquals(domain, pivotCell.domain));
65482
+ }
65483
+ }
65484
+
65485
+ /**
65486
+ * Registry to draw icons on cells
65487
+ */
65488
+ const iconsOnCellRegistry = new Registry();
65489
+ iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
65490
+ const hasIcon = getters.isCellValidCheckbox(position);
65491
+ if (hasIcon) {
65492
+ return {
65493
+ svg: undefined,
65494
+ priority: 2,
65495
+ horizontalAlign: "center",
65496
+ size: GRID_ICON_EDGE_LENGTH,
65497
+ margin: GRID_ICON_MARGIN,
65498
+ component: DataValidationCheckbox,
65499
+ position,
65500
+ };
65501
+ }
65502
+ return undefined;
65503
+ });
65504
+ iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
65505
+ const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
65506
+ if (hasIcon) {
65507
+ return {
65508
+ svg: undefined,
65509
+ priority: 2,
65510
+ horizontalAlign: "right",
65511
+ size: GRID_ICON_EDGE_LENGTH,
65512
+ margin: GRID_ICON_MARGIN,
65513
+ component: DataValidationListIcon,
65514
+ position,
65515
+ };
65516
+ }
65517
+ return undefined;
65518
+ });
65519
+ iconsOnCellRegistry.add("filter_icon", (getters, position) => {
65520
+ const hasIcon = getters.isFilterHeader(position);
65521
+ if (hasIcon) {
65522
+ return {
65523
+ svg: undefined,
65524
+ priority: 3,
65525
+ horizontalAlign: "right",
65526
+ size: GRID_ICON_EDGE_LENGTH,
65527
+ margin: GRID_ICON_MARGIN,
65528
+ component: FilterIcon,
65529
+ position,
65530
+ };
65531
+ }
65532
+ return undefined;
65533
+ });
65534
+ iconsOnCellRegistry.add("conditional_formatting", (getters, position) => {
65535
+ const icon = getters.getConditionalIcon(position);
65536
+ if (icon) {
65537
+ const style = getters.getCellStyle(position);
65538
+ return {
65539
+ svg: ICONS[icon].svg,
65540
+ priority: 1,
65541
+ horizontalAlign: "left",
65542
+ size: computeTextFontSizeInPixels(style),
65543
+ margin: MIN_CF_ICON_MARGIN,
65544
+ position,
65545
+ };
65546
+ }
65547
+ return undefined;
65548
+ });
65549
+ iconsOnCellRegistry.add("pivot_collapse", (getters, position) => {
65550
+ if (!getters.isSpillPivotFormula(position)) {
65551
+ return undefined;
65552
+ }
65553
+ const pivotCell = getters.getPivotCellFromPosition(position);
65554
+ const pivotId = getters.getPivotIdFromPosition(position);
65555
+ if (pivotCell.type === "HEADER" && pivotId && pivotCell.domain.length) {
65556
+ const definition = getters.getPivotCoreDefinition(pivotId);
65557
+ const isDashboard = getters.isDashboard();
65558
+ const fields = pivotCell.dimension === "COL" ? definition.columns : definition.rows;
65559
+ const component = !isDashboard && pivotCell.domain.length !== fields.length ? PivotCollapseIcon : undefined;
65560
+ return {
65561
+ priority: 4,
65562
+ horizontalAlign: "left",
65563
+ size: !!component || (!isDashboard && pivotCell.dimension === "ROW" && definition.rows.length > 1)
65564
+ ? GRID_ICON_EDGE_LENGTH
65565
+ : 0,
65566
+ margin: pivotCell.dimension === "ROW" ? (pivotCell.domain.length - 1) * PIVOT_INDENT : 0,
65567
+ component,
65568
+ position,
65569
+ };
65570
+ }
65571
+ return undefined;
65572
+ });
65573
+
65574
+ class CellIconPlugin extends CoreViewPlugin {
65575
+ static getters = ["doesCellHaveGridIcon", "getCellIcons"];
65576
+ cellIconsCache = {};
65577
+ handle(cmd) {
65578
+ if (cmd.type !== "SET_VIEWPORT_OFFSET") {
65579
+ this.cellIconsCache = {};
65580
+ }
65581
+ }
65582
+ getCellIcons(position) {
65583
+ if (!this.cellIconsCache[position.sheetId]) {
65584
+ this.cellIconsCache[position.sheetId] = {};
65585
+ }
65586
+ if (!this.cellIconsCache[position.sheetId][position.col]) {
65587
+ this.cellIconsCache[position.sheetId][position.col] = {};
65588
+ }
65589
+ if (!this.cellIconsCache[position.sheetId][position.col][position.row]) {
65590
+ this.cellIconsCache[position.sheetId][position.col][position.row] =
65591
+ this.computeCellIcons(position);
65592
+ }
65593
+ return this.cellIconsCache[position.sheetId][position.col][position.row];
65594
+ }
65595
+ computeCellIcons(position) {
65596
+ const icons = { left: undefined, right: undefined, center: undefined };
65597
+ const callbacks = iconsOnCellRegistry.getAll();
65598
+ for (const callback of callbacks) {
65599
+ const icon = callback(this.getters, position);
65600
+ if (icon &&
65601
+ (!icons[icon.horizontalAlign] || icon.priority > icons[icon.horizontalAlign].priority)) {
65602
+ icons[icon.horizontalAlign] = icon;
65603
+ }
65604
+ }
65605
+ if (icons.center && (icons.left || icons.right)) {
65606
+ const sideIconsPriority = Math.max(icons.left?.priority || 0, icons.right?.priority || 0);
65607
+ if (icons.center.priority < sideIconsPriority) {
65608
+ icons.center = undefined;
65609
+ }
65610
+ else {
65611
+ icons.left = undefined;
65612
+ icons.right = undefined;
65613
+ }
65614
+ }
65615
+ return Object.values(icons).filter(isDefined);
65616
+ }
65617
+ doesCellHaveGridIcon(position) {
65618
+ return Boolean(this.getCellIcons(position).length);
65619
+ }
65620
+ }
65621
+
65054
65622
  class DynamicTablesPlugin extends CoreViewPlugin {
65055
65623
  static getters = [
65056
65624
  "canCreateDynamicTableOnZones",
@@ -65490,7 +66058,7 @@ function withPivotPresentationLayer (PivotClass) {
65490
66058
  }
65491
66059
  getValuesToAggregate(measure, domain) {
65492
66060
  const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
65493
- const table = super.getTableStructure();
66061
+ const table = super.getExpandedTableStructure();
65494
66062
  const values = [];
65495
66063
  if (colDomain.length === 0 &&
65496
66064
  rowDomain.length < this.definition.rows.length &&
@@ -65910,7 +66478,7 @@ function withPivotPresentationLayer (PivotClass) {
65910
66478
  return this.strictMeasureValueToNumber(comparedValue);
65911
66479
  }
65912
66480
  getPivotValueCells(measureId) {
65913
- return this.getTableStructure()
66481
+ return this.getCollapsedTableStructure()
65914
66482
  .getPivotCells()
65915
66483
  .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
65916
66484
  .filter((col) => col.length > 0);
@@ -65934,8 +66502,13 @@ function withPivotPresentationLayer (PivotClass) {
65934
66502
  }
65935
66503
  throw new Error(`Value ${result.value} is not a number`);
65936
66504
  }
65937
- getTableStructure() {
65938
- const table = super.getTableStructure();
66505
+ getCollapsedTableStructure() {
66506
+ const table = super.getCollapsedTableStructure();
66507
+ this.sortTableStructure(table);
66508
+ return table;
66509
+ }
66510
+ getExpandedTableStructure() {
66511
+ const table = super.getExpandedTableStructure();
65939
66512
  this.sortTableStructure(table);
65940
66513
  return table;
65941
66514
  }
@@ -66125,7 +66698,7 @@ class PivotUIPlugin extends CoreViewPlugin {
66125
66698
  const includeColumnHeaders = toScalar(args[3]);
66126
66699
  const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
66127
66700
  const pivotCells = pivot
66128
- .getTableStructure()
66701
+ .getCollapsedTableStructure()
66129
66702
  .getPivotCells(shouldIncludeTotal, shouldIncludeColumnHeaders);
66130
66703
  const pivotCol = position.col - mainPosition.col;
66131
66704
  const pivotRow = position.row - mainPosition.row;
@@ -66142,9 +66715,11 @@ class PivotUIPlugin extends CoreViewPlugin {
66142
66715
  }
66143
66716
  else if (functionName === "PIVOT.HEADER") {
66144
66717
  const domain = pivot.parseArgsToPivotDomain(args.slice(1).map((value) => ({ value })));
66718
+ const colRowDomain = domainToColRowDomain(pivot, domain);
66145
66719
  return {
66146
66720
  type: "HEADER",
66147
66721
  domain,
66722
+ dimension: colRowDomain.colDomain.length ? "COL" : "ROW",
66148
66723
  };
66149
66724
  }
66150
66725
  const [measure, ...domainArgs] = args.slice(1);
@@ -68144,8 +68719,11 @@ class Session extends EventBus {
68144
68719
  version: MESSAGE_VERSION,
68145
68720
  });
68146
68721
  }
68147
- getClient() {
68148
- const client = this.clients[this.clientId];
68722
+ getCurrentClient() {
68723
+ return this.getClient(this.clientId);
68724
+ }
68725
+ getClient(clientId) {
68726
+ const client = this.clients[clientId];
68149
68727
  if (!client) {
68150
68728
  throw new ClientDisconnectedError("The client left the session");
68151
68729
  }
@@ -68179,7 +68757,7 @@ class Session extends EventBus {
68179
68757
  return;
68180
68758
  }
68181
68759
  const type = currentPosition ? "CLIENT_MOVED" : "CLIENT_JOINED";
68182
- const client = this.getClient();
68760
+ const client = this.getCurrentClient();
68183
68761
  this.clients[this.clientId] = { ...client, position };
68184
68762
  this.transportService.sendMessage({
68185
68763
  type,
@@ -68390,6 +68968,7 @@ class CollaborativePlugin extends UIPlugin {
68390
68968
  static getters = [
68391
68969
  "getClientsToDisplay",
68392
68970
  "getClient",
68971
+ "getCurrentClient",
68393
68972
  "getConnectedClients",
68394
68973
  "isFullySynchronized",
68395
68974
  ];
@@ -68405,11 +68984,16 @@ class CollaborativePlugin extends UIPlugin {
68405
68984
  return (position.row < this.getters.getNumberRows(position.sheetId) &&
68406
68985
  position.col < this.getters.getNumberCols(position.sheetId));
68407
68986
  }
68408
- getClient() {
68409
- return this.session.getClient();
68987
+ getClient(clientId) {
68988
+ return this.session.getClient(clientId);
68989
+ }
68990
+ getCurrentClient() {
68991
+ return this.session.getCurrentClient();
68410
68992
  }
68411
68993
  getConnectedClients() {
68412
- return this.session.getConnectedClients();
68994
+ return [...this.session.getConnectedClients()].map((client) => {
68995
+ return { ...client, color: this.colors[client.id] };
68996
+ });
68413
68997
  }
68414
68998
  isFullySynchronized() {
68415
68999
  return this.session.isFullySynchronized();
@@ -68420,7 +69004,7 @@ class CollaborativePlugin extends UIPlugin {
68420
69004
  */
68421
69005
  getClientsToDisplay() {
68422
69006
  try {
68423
- this.getters.getClient();
69007
+ this.getters.getCurrentClient();
68424
69008
  }
68425
69009
  catch (e) {
68426
69010
  if (e instanceof ClientDisconnectedError) {
@@ -68433,16 +69017,14 @@ class CollaborativePlugin extends UIPlugin {
68433
69017
  const sheetId = this.getters.getActiveSheetId();
68434
69018
  const clients = [];
68435
69019
  for (const client of this.getters.getConnectedClients()) {
68436
- if (client.id !== this.getters.getClient().id &&
69020
+ if (client.id !== this.getters.getCurrentClient().id &&
68437
69021
  client.position &&
68438
69022
  client.position.sheetId === sheetId &&
68439
69023
  this.isPositionValid(client.position)) {
68440
- const position = client.position;
68441
69024
  if (!this.colors[client.id]) {
68442
69025
  this.colors[client.id] = this.availableColors.next();
68443
69026
  }
68444
- const color = this.colors[client.id];
68445
- clients.push({ ...client, position, color });
69027
+ clients.push({ ...client, color: this.colors[client.id], position: client.position });
68446
69028
  }
68447
69029
  }
68448
69030
  return clients;
@@ -68968,7 +69550,7 @@ class InsertPivotPlugin extends UIPlugin {
68968
69550
  sheetIdTo: sheetId,
68969
69551
  });
68970
69552
  const pivot = this.getters.getPivot(pivotId);
68971
- this.insertPivotWithTable(sheetId, 0, 0, pivotId, pivot.getTableStructure().export(), "dynamic");
69553
+ this.insertPivotWithTable(sheetId, 0, 0, pivotId, pivot.getCollapsedTableStructure().export(), "dynamic");
68972
69554
  }
68973
69555
  duplicatePivotInNewSheet(pivotId, newPivotId, newSheetId) {
68974
69556
  this.dispatch("DUPLICATE_PIVOT", {
@@ -68991,7 +69573,7 @@ class InsertPivotPlugin extends UIPlugin {
68991
69573
  if (result.isSuccessful) {
68992
69574
  this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
68993
69575
  const pivot = this.getters.getPivot(pivotId);
68994
- this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
69576
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getCollapsedTableStructure().export(), "dynamic");
68995
69577
  }
68996
69578
  }
68997
69579
  getPivotDuplicateSheetName(pivotName) {
@@ -69283,9 +69865,7 @@ class UIOptionsPlugin extends UIPlugin {
69283
69865
 
69284
69866
  class SheetUIPlugin extends UIPlugin {
69285
69867
  static getters = [
69286
- "doesCellHaveGridIcon",
69287
69868
  "getCellWidth",
69288
- "getCellIconSvg",
69289
69869
  "getTextWidth",
69290
69870
  "getCellText",
69291
69871
  "getCellMultiLineText",
@@ -69340,12 +69920,8 @@ class SheetUIPlugin extends UIPlugin {
69340
69920
  const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);
69341
69921
  contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));
69342
69922
  }
69343
- const icon = this.getters.getCellIconSvg(position);
69344
- if (icon) {
69345
- contentWidth += computeIconWidth(style);
69346
- }
69347
- if (this.getters.doesCellHaveGridIcon(position)) {
69348
- contentWidth += ICON_EDGE_LENGTH + GRID_ICON_MARGIN;
69923
+ for (const icon of this.getters.getCellIcons(position)) {
69924
+ contentWidth += icon.margin + icon.size;
69349
69925
  }
69350
69926
  if (contentWidth === 0) {
69351
69927
  return 0;
@@ -69357,16 +69933,6 @@ class SheetUIPlugin extends UIPlugin {
69357
69933
  }
69358
69934
  return contentWidth;
69359
69935
  }
69360
- getCellIconSvg(position) {
69361
- const callbacks = iconsOnCellRegistry.getAll();
69362
- for (const callback of callbacks) {
69363
- const imageSrc = callback(this.getters, position);
69364
- if (imageSrc) {
69365
- return imageSrc;
69366
- }
69367
- }
69368
- return undefined;
69369
- }
69370
69936
  getTextWidth(text, style) {
69371
69937
  return computeTextWidth(this.ctx, text, style);
69372
69938
  }
@@ -69406,11 +69972,6 @@ class SheetUIPlugin extends UIPlugin {
69406
69972
  });
69407
69973
  return splitTextToWidth(this.ctx, text, style, args.wrapText ? args.maxWidth : undefined);
69408
69974
  }
69409
- doesCellHaveGridIcon(position) {
69410
- const isFilterHeader = this.getters.isFilterHeader(position);
69411
- const hasListIcon = !this.getters.isReadonly() && this.getters.cellHasListDataValidationIcon(position);
69412
- return isFilterHeader || hasListIcon;
69413
- }
69414
69975
  /**
69415
69976
  * Expands the given zone until bordered by empty cells or reached the sheet boundaries.
69416
69977
  */
@@ -73302,7 +73863,8 @@ const coreViewsPluginRegistry = new Registry()
73302
73863
  .add("data_validation_ui", EvaluationDataValidationPlugin)
73303
73864
  .add("dynamic_tables", DynamicTablesPlugin)
73304
73865
  .add("custom_colors", CustomColorsPlugin)
73305
- .add("pivot_ui", PivotUIPlugin);
73866
+ .add("pivot_ui", PivotUIPlugin)
73867
+ .add("cell_icon", CellIconPlugin);
73306
73868
 
73307
73869
  autoCompleteProviders.add("dataValidation", {
73308
73870
  displayAllOnInitialContent: true,
@@ -74452,6 +75014,21 @@ class TopBarComponentRegistry extends Registry {
74452
75014
  }
74453
75015
  const topbarComponentRegistry = new TopBarComponentRegistry();
74454
75016
 
75017
+ class LocalTransportService {
75018
+ listeners = [];
75019
+ async sendMessage(message) {
75020
+ for (const { callback } of this.listeners) {
75021
+ callback(message);
75022
+ }
75023
+ }
75024
+ onNewMessage(id, callback) {
75025
+ this.listeners.push({ id, callback });
75026
+ }
75027
+ leave(id) {
75028
+ this.listeners = this.listeners.filter((listener) => listener.id !== id);
75029
+ }
75030
+ }
75031
+
74455
75032
  class ImageProvider {
74456
75033
  fileStore;
74457
75034
  constructor(fileStore) {
@@ -77289,6 +77866,7 @@ class Spreadsheet extends Component {
77289
77866
  SidePanel,
77290
77867
  SpreadsheetDashboard,
77291
77868
  HeaderGroupContainer,
77869
+ FullScreenChart,
77292
77870
  };
77293
77871
  sidePanel;
77294
77872
  spreadsheetRef = useRef("spreadsheet");
@@ -77447,21 +78025,6 @@ class Spreadsheet extends Component {
77447
78025
  }
77448
78026
  }
77449
78027
 
77450
- class LocalTransportService {
77451
- listeners = [];
77452
- async sendMessage(message) {
77453
- for (const { callback } of this.listeners) {
77454
- callback(message);
77455
- }
77456
- }
77457
- onNewMessage(id, callback) {
77458
- this.listeners.push({ id, callback });
77459
- }
77460
- leave(id) {
77461
- this.listeners = this.listeners.filter((listener) => listener.id !== id);
77462
- }
77463
- }
77464
-
77465
78028
  function inverseCommand(cmd) {
77466
78029
  return inverseCommandRegistry.get(cmd.type)(cmd);
77467
78030
  }
@@ -81885,6 +82448,7 @@ const components = {
81885
82448
  RadioSelection,
81886
82449
  GeoChartRegionSelectSection,
81887
82450
  ChartDashboardMenu,
82451
+ FullScreenChart,
81888
82452
  };
81889
82453
  const hooks = {
81890
82454
  useDragAndDropListItems,
@@ -81911,6 +82475,7 @@ const stores = {
81911
82475
  SidePanelStore,
81912
82476
  PivotSidePanelStore,
81913
82477
  PivotMeasureDisplayPanelStore,
82478
+ ClientFocusStore,
81914
82479
  };
81915
82480
  function addFunction(functionName, functionDescription) {
81916
82481
  functionRegistry.add(functionName, functionDescription);
@@ -81926,9 +82491,9 @@ const constants = {
81926
82491
  };
81927
82492
  const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
81928
82493
 
81929
- 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, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
82494
+ export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, CoreViewPlugin, DispatchResult, EvaluationError, LocalTransportService, 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, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
81930
82495
 
81931
82496
 
81932
- __info__.version = "18.4.0-alpha.3";
81933
- __info__.date = "2025-05-13T17:54:54.061Z";
81934
- __info__.hash = "70ad365";
82497
+ __info__.version = "18.4.0-alpha.4";
82498
+ __info__.date = "2025-05-20T05:57:45.452Z";
82499
+ __info__.hash = "5c28bca";