@odoo/o-spreadsheet 18.4.0-alpha.7 → 18.4.0-alpha.9

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.7
6
- * @date 2025-06-06T09:32:44.285Z
7
- * @hash 2bfbe64
5
+ * @version 18.4.0-alpha.9
6
+ * @date 2025-06-19T18:23:22.025Z
7
+ * @hash 6d4d685
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';
@@ -20,13 +20,21 @@ function createAction(item) {
20
20
  const icon = item.icon;
21
21
  const secondaryIcon = item.secondaryIcon;
22
22
  const itemId = item.id || nextItemId++;
23
+ const isEnabled = item.isEnabled ? item.isEnabled : () => true;
23
24
  return {
24
25
  id: itemId.toString(),
25
26
  name: typeof name === "function" ? name : () => name,
26
27
  isVisible: item.isVisible ? item.isVisible : () => true,
27
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
28
+ isEnabled: isEnabled,
28
29
  isActive: item.isActive,
29
- execute: item.execute,
30
+ execute: item.execute
31
+ ? (env, isMiddleClick) => {
32
+ if (isEnabled(env)) {
33
+ return item.execute(env, isMiddleClick);
34
+ }
35
+ return undefined;
36
+ }
37
+ : undefined,
30
38
  children: children
31
39
  ? (env) => {
32
40
  return children
@@ -130,7 +138,7 @@ class Registry {
130
138
 
131
139
  const CANVAS_SHIFT = 0.5;
132
140
  // Colors
133
- const HIGHLIGHT_COLOR = "#37A850";
141
+ const HIGHLIGHT_COLOR = "#017E84";
134
142
  const BACKGROUND_GRAY_COLOR = "#f5f5f5";
135
143
  const BACKGROUND_HEADER_COLOR = "#F8F9FA";
136
144
  const BACKGROUND_HEADER_SELECTED_COLOR = "#E8EAED";
@@ -143,7 +151,7 @@ const CELL_BORDER_COLOR = "#E2E3E3";
143
151
  const BACKGROUND_CHART_COLOR = "#FFFFFF";
144
152
  const DISABLED_TEXT_COLOR = "#CACACA";
145
153
  const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;
146
- const LINK_COLOR = "#017E84";
154
+ const LINK_COLOR = HIGHLIGHT_COLOR;
147
155
  const FILTERS_COLOR = "#188038";
148
156
  const SEPARATOR_COLOR = "#E0E2E4";
149
157
  const ICONS_COLOR = "#4A4F59";
@@ -172,7 +180,7 @@ const BUTTON_HOVER_BG = GRAY_300;
172
180
  const BUTTON_HOVER_TEXT_COLOR = "#111827";
173
181
  const BUTTON_ACTIVE_BG = "#e6f2f3";
174
182
  const BUTTON_ACTIVE_TEXT_COLOR = "#111827";
175
- const ACTION_COLOR = "#017E84";
183
+ const ACTION_COLOR = HIGHLIGHT_COLOR;
176
184
  const ACTION_COLOR_HOVER = "#01585c";
177
185
  const ALERT_WARNING_BG = "#FBEBCC";
178
186
  const ALERT_WARNING_BORDER = "#F8E2B3";
@@ -297,6 +305,7 @@ const GROUP_LAYER_WIDTH = 21;
297
305
  const GRID_ICON_MARGIN = 2;
298
306
  const GRID_ICON_EDGE_LENGTH = 17;
299
307
  const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT;
308
+ const DATA_VALIDATION_CHIP_MARGIN = 5;
300
309
  // 768px is a common breakpoint for small screens
301
310
  // Typically inside Odoo, it is the threshold for switching to mobile view
302
311
  const MOBILE_WIDTH_BREAKPOINT = 768;
@@ -642,9 +651,6 @@ function parseSheetUrl(sheetLink) {
642
651
  function isDefined(argument) {
643
652
  return argument !== undefined;
644
653
  }
645
- function isNotNull(argument) {
646
- return argument !== null;
647
- }
648
654
  /**
649
655
  * Check if all the values of an object, and all the values of the objects inside of it, are undefined.
650
656
  */
@@ -1355,9 +1361,16 @@ function darkenColor(color, percentage) {
1355
1361
  if (percentage === 1) {
1356
1362
  return "#000";
1357
1363
  }
1364
+ // increase saturation to compensate and make it more vivid
1365
+ hsla.s = Math.min(100, percentage * hsla.s + hsla.s);
1358
1366
  hsla.l = hsla.l - percentage * hsla.l;
1359
1367
  return hslaToHex(hsla);
1360
1368
  }
1369
+ function chipTextColor(chipBackgroundColor) {
1370
+ return relativeLuminance(chipBackgroundColor) < 0.6
1371
+ ? lightenColor(chipBackgroundColor, 0.9)
1372
+ : darkenColor(chipBackgroundColor, 0.75);
1373
+ }
1361
1374
  const COLORS_SM = [
1362
1375
  "#4EA7F2", // Blue
1363
1376
  "#EA6175", // Red
@@ -1566,6 +1579,19 @@ class AlternatingColorGenerator extends ColorGenerator {
1566
1579
  this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
1567
1580
  }
1568
1581
  }
1582
+ class AlternatingColorMap {
1583
+ availableColors;
1584
+ colors = {};
1585
+ constructor(paletteSize = 12) {
1586
+ this.availableColors = new AlternatingColorGenerator(paletteSize);
1587
+ }
1588
+ get(id) {
1589
+ if (!this.colors[id]) {
1590
+ this.colors[id] = this.availableColors.next();
1591
+ }
1592
+ return this.colors[id];
1593
+ }
1594
+ }
1569
1595
  /**
1570
1596
  * Returns a function that maps a value to a color using a color scale defined by the given
1571
1597
  * color/threshold values pairs.
@@ -5005,7 +5031,9 @@ function isTextFormat(format) {
5005
5031
  }
5006
5032
 
5007
5033
  function evaluateLiteral(literalCell, localeFormat) {
5008
- const value = isTextFormat(localeFormat.format) ? literalCell.content : literalCell.parsedValue;
5034
+ const value = isTextFormat(localeFormat.format) && literalCell.parsedValue !== null
5035
+ ? literalCell.content
5036
+ : literalCell.parsedValue;
5009
5037
  const functionResult = { value, format: localeFormat.format };
5010
5038
  return createEvaluatedCell(functionResult, localeFormat.locale);
5011
5039
  }
@@ -5054,6 +5082,9 @@ function _createEvaluatedCell(functionResult, locale, cell) {
5054
5082
  if (isEvaluationError(value)) {
5055
5083
  return errorCell(value, message);
5056
5084
  }
5085
+ if (value === null) {
5086
+ return emptyCell(format);
5087
+ }
5057
5088
  if (isTextFormat(format)) {
5058
5089
  // TO DO:
5059
5090
  // with the next line, the value of the cell is transformed depending on the format.
@@ -5061,9 +5092,6 @@ function _createEvaluatedCell(functionResult, locale, cell) {
5061
5092
  // to interpret the value as a number.
5062
5093
  return textCell(toString(value), format, formattedValue);
5063
5094
  }
5064
- if (value === null) {
5065
- return emptyCell(format);
5066
- }
5067
5095
  if (typeof value === "number") {
5068
5096
  if (isDateTimeFormat(format || "")) {
5069
5097
  return dateTimeCell(value, format, formattedValue);
@@ -19351,8 +19379,9 @@ const PIVOT = {
19351
19379
  arg("include_total (boolean, default=TRUE)", _t("Whether to include total/sub-totals or not.")),
19352
19380
  arg("include_column_titles (boolean, default=TRUE)", _t("Whether to include the column titles or not.")),
19353
19381
  arg("column_count (number, optional)", _t("number of columns")),
19382
+ arg("include_measure_titles (boolean, default=TRUE)", _t("Whether to include the measure titles row or not.")),
19354
19383
  ],
19355
- compute: function (pivotFormulaId, rowCount = { value: 10000 }, includeTotal = { value: true }, includeColumnHeaders = { value: true }, columnCount = { value: Number.MAX_VALUE }) {
19384
+ compute: function (pivotFormulaId, rowCount = { value: 10000 }, includeTotal = { value: true }, includeColumnHeaders = { value: true }, columnCount = { value: Number.MAX_VALUE }, includeMeasureTitles = { value: true }) {
19356
19385
  const _pivotFormulaId = toString(pivotFormulaId);
19357
19386
  const _rowCount = toNumber(rowCount, this.locale);
19358
19387
  if (_rowCount < 0) {
@@ -19362,8 +19391,11 @@ const PIVOT = {
19362
19391
  if (_columnCount < 0) {
19363
19392
  return new EvaluationError(_t("The number of columns must be positive."));
19364
19393
  }
19365
- const _includeColumnHeaders = toBoolean(includeColumnHeaders);
19366
- const _includedTotal = toBoolean(includeTotal);
19394
+ const visibilityOptions = {
19395
+ displayColumnHeaders: toBoolean(includeColumnHeaders),
19396
+ displayTotals: toBoolean(includeTotal),
19397
+ displayMeasuresRow: toBoolean(includeMeasureTitles),
19398
+ };
19367
19399
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
19368
19400
  const pivot = this.getters.getPivot(pivotId);
19369
19401
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
@@ -19374,9 +19406,15 @@ const PIVOT = {
19374
19406
  return error;
19375
19407
  }
19376
19408
  const table = pivot.getCollapsedTableStructure();
19377
- const cells = table.getPivotCells(_includedTotal, _includeColumnHeaders);
19378
- const headerRows = _includeColumnHeaders ? table.columns.length : 0;
19379
- const pivotTitle = this.getters.getPivotDisplayName(pivotId);
19409
+ const cells = table.getPivotCells(visibilityOptions);
19410
+ let headerRows = 0;
19411
+ if (visibilityOptions.displayColumnHeaders) {
19412
+ headerRows = table.columns.length - 1;
19413
+ }
19414
+ if (visibilityOptions.displayMeasuresRow) {
19415
+ headerRows++;
19416
+ }
19417
+ const pivotTitle = this.getters.getPivotName(pivotId);
19380
19418
  const tableHeight = Math.min(headerRows + _rowCount, cells[0].length);
19381
19419
  if (tableHeight === 0) {
19382
19420
  return [[{ value: pivotTitle }]];
@@ -19404,7 +19442,7 @@ const PIVOT = {
19404
19442
  }
19405
19443
  }
19406
19444
  }
19407
- if (_includeColumnHeaders) {
19445
+ if (visibilityOptions.displayColumnHeaders || visibilityOptions.displayMeasuresRow) {
19408
19446
  result[0][0] = { value: pivotTitle };
19409
19447
  }
19410
19448
  return result;
@@ -21661,6 +21699,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21661
21699
  if (isTrendLineAxis(dataset.xAxisID) || dataset.hidden) {
21662
21700
  continue;
21663
21701
  }
21702
+ const yAxisScale = chart.scales[dataset.yAxisID];
21664
21703
  for (let i = 0; i < dataset._parsed.length; i++) {
21665
21704
  const parsedValue = dataset._parsed[i];
21666
21705
  const value = Number(chart.config.type === "radar" ? parsedValue.r : parsedValue.y);
@@ -21671,10 +21710,18 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21671
21710
  const xPosition = point.x;
21672
21711
  let yPosition = 0;
21673
21712
  if (chart.config.type === "line" || chart.config.type === "radar") {
21674
- yPosition = point.y - 10;
21713
+ yPosition = value < 0 ? point.y + 10 : point.y - 10;
21675
21714
  }
21676
21715
  else {
21677
- yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21716
+ const yZeroLine = yAxisScale.getPixelForValue(0);
21717
+ const distanceFromAxisOrigin = Math.abs(yZeroLine - point.y);
21718
+ const textHeight = 12; // ChartJS default text height
21719
+ if (distanceFromAxisOrigin < textHeight) {
21720
+ yPosition = value < 0 ? yZeroLine + textHeight / 2 : yZeroLine - textHeight / 2;
21721
+ }
21722
+ else {
21723
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21724
+ }
21678
21725
  }
21679
21726
  yPosition = Math.min(yPosition, yMax);
21680
21727
  yPosition = Math.max(yPosition, yMin);
@@ -21684,7 +21731,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21684
21731
  }
21685
21732
  for (const otherPosition of textsPositions[xPosition] || []) {
21686
21733
  if (Math.abs(otherPosition - yPosition) < 13) {
21687
- yPosition = otherPosition - 13;
21734
+ yPosition = value < 0 ? otherPosition + 13 : otherPosition - 13;
21688
21735
  }
21689
21736
  }
21690
21737
  textsPositions[xPosition].push(yPosition);
@@ -21703,6 +21750,8 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21703
21750
  if (isTrendLineAxis(dataset.xAxisID)) {
21704
21751
  return; // ignore trend lines
21705
21752
  }
21753
+ const xAxisScale = chart.scales[dataset.xAxisID];
21754
+ const xZeroLine = xAxisScale.getPixelForValue(0);
21706
21755
  for (let i = 0; i < dataset._parsed.length; i++) {
21707
21756
  const value = Number(dataset._parsed[i].x);
21708
21757
  if (isNaN(value)) {
@@ -21711,17 +21760,27 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21711
21760
  const displayValue = options.callback(value, dataset, i);
21712
21761
  const point = dataset.data[i];
21713
21762
  const yPosition = point.y;
21714
- let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21715
- xPosition = Math.min(xPosition, xMax);
21716
- xPosition = Math.max(xPosition, xMin);
21763
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21764
+ const distanceFromAxisOrigin = Math.abs(point.x - xZeroLine);
21765
+ const PADDING = 3;
21766
+ let xPosition;
21767
+ if (distanceFromAxisOrigin < textWidth) {
21768
+ xPosition =
21769
+ value < 0 ? xZeroLine - textWidth / 2 - PADDING : xZeroLine + textWidth / 2 + PADDING;
21770
+ }
21771
+ else {
21772
+ xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21773
+ xPosition = Math.min(xPosition, xMax);
21774
+ xPosition = Math.max(xPosition, xMin);
21775
+ }
21717
21776
  // Avoid overlapping texts with same Y
21718
21777
  if (!textsPositions[yPosition]) {
21719
21778
  textsPositions[yPosition] = [];
21720
21779
  }
21721
- const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21722
21780
  for (const otherPosition of textsPositions[yPosition]) {
21723
21781
  if (Math.abs(otherPosition - xPosition) < textWidth) {
21724
- xPosition = otherPosition + textWidth + 3;
21782
+ xPosition =
21783
+ value < 0 ? otherPosition - textWidth - PADDING : otherPosition + textWidth + PADDING;
21725
21784
  }
21726
21785
  }
21727
21786
  textsPositions[yPosition].push(xPosition);
@@ -21743,10 +21802,22 @@ function drawPieChartValues(chart, options, ctx) {
21743
21802
  const midAngle = (startAngle + endAngle) / 2;
21744
21803
  const midRadius = (innerRadius + outerRadius) / 2;
21745
21804
  const x = bar.x + midRadius * Math.cos(midAngle);
21746
- const y = bar.y + midRadius * Math.sin(midAngle) + 7;
21805
+ const y = bar.y + midRadius * Math.sin(midAngle);
21806
+ const displayValue = options.callback(value, dataset, i);
21807
+ const textHeight = 12; // ChartJS default
21808
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: textHeight }, "px");
21809
+ const radius = outerRadius - innerRadius;
21810
+ // Check if the text fits in the slice. Not perfect, but good enough heuristic.
21811
+ if (textWidth >= radius || radius < textHeight) {
21812
+ continue;
21813
+ }
21814
+ const sliceAngle = endAngle - startAngle;
21815
+ const midWidth = 2 * midRadius * Math.tan(sliceAngle / 2);
21816
+ if (sliceAngle < Math.PI / 2 && (textWidth >= midWidth || midWidth < textHeight)) {
21817
+ continue;
21818
+ }
21747
21819
  ctx.fillStyle = chartFontColor(options.background);
21748
21820
  ctx.strokeStyle = options.background || "#ffffff";
21749
- const displayValue = options.callback(value, dataset, i);
21750
21821
  drawTextWithBackground(displayValue, x, y, ctx);
21751
21822
  }
21752
21823
  }
@@ -25643,7 +25714,9 @@ function getPyramidChartShowValues(definition, args) {
25643
25714
  background: definition.background,
25644
25715
  callback: (value, dataset) => {
25645
25716
  value = Math.abs(Number(value));
25646
- return formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25717
+ return value === 0
25718
+ ? ""
25719
+ : formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25647
25720
  },
25648
25721
  };
25649
25722
  }
@@ -29304,6 +29377,12 @@ class Menu extends Component {
29304
29377
  menu.onStopHover?.(this.env);
29305
29378
  this.props.onMouseLeave?.(menu, ev);
29306
29379
  }
29380
+ onClickMenu(menu, ev) {
29381
+ if (!this.isEnabled(menu)) {
29382
+ return;
29383
+ }
29384
+ this.props.onClickMenu?.(menu, ev);
29385
+ }
29307
29386
  }
29308
29387
 
29309
29388
  /**
@@ -29802,9 +29881,6 @@ class MenuPopover extends Component {
29802
29881
  this.subMenu.parentMenu = undefined;
29803
29882
  }
29804
29883
  onClickMenu(menu, ev) {
29805
- if (!this.isEnabled(menu)) {
29806
- return;
29807
- }
29808
29884
  if (this.isRoot(menu)) {
29809
29885
  this.openSubMenu(menu, ev.currentTarget);
29810
29886
  }
@@ -31055,11 +31131,9 @@ criterionEvaluatorRegistry.add("isValueInRange", {
31055
31131
  if (!value) {
31056
31132
  return false;
31057
31133
  }
31058
- const range = getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
31059
- const criterionValues = getters.getRangeValues(range);
31134
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
31060
31135
  return criterionValues
31061
- .filter(isNotNull)
31062
- .map((value) => value.toString().toLowerCase())
31136
+ .map((value) => value.toLowerCase())
31063
31137
  .includes(value.toString().toLowerCase());
31064
31138
  },
31065
31139
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
@@ -31231,6 +31305,12 @@ class TextValueProvider extends Component {
31231
31305
  selectedElement?.scrollIntoView?.({ block: "nearest" });
31232
31306
  }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
31233
31307
  }
31308
+ getCss(html) {
31309
+ return cssPropertiesToCss({
31310
+ color: html.color || "#000000",
31311
+ background: html.backgroundColor,
31312
+ });
31313
+ }
31234
31314
  }
31235
31315
 
31236
31316
  class ContentEditableHelper {
@@ -31369,7 +31449,6 @@ class ContentEditableHelper {
31369
31449
  // We can only modify a node in place if it has the same type as the content
31370
31450
  // that we would insert, which are spans.
31371
31451
  // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
31372
- // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)
31373
31452
  const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
31374
31453
  if (childIsSpan && compareContentToSpanElement(content, child)) {
31375
31454
  continue;
@@ -31404,9 +31483,7 @@ class ContentEditableHelper {
31404
31483
  }
31405
31484
  // Empty line
31406
31485
  if (!p.hasChildNodes()) {
31407
- const span = document.createElement("span");
31408
- span.appendChild(document.createElement("br"));
31409
- p.appendChild(span);
31486
+ p.appendChild(document.createElement("span"));
31410
31487
  }
31411
31488
  // replace p if necessary
31412
31489
  if (newChild) {
@@ -31453,13 +31530,10 @@ class ContentEditableHelper {
31453
31530
  }
31454
31531
  getText() {
31455
31532
  let text = "";
31456
- const it = iterateChildren(this.el);
31457
- let current = it.next();
31458
31533
  let isFirstParagraph = true;
31459
- while (!current.done) {
31460
- if (!current.value.hasChildNodes()) {
31461
- text += current.value.textContent;
31462
- }
31534
+ let emptyParagraph = false;
31535
+ const it = iterateChildren(this.el);
31536
+ for (let current = it.next(); !current.done; current = it.next()) {
31463
31537
  if (current.value.nodeName === "P" ||
31464
31538
  (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
31465
31539
  ) {
@@ -31469,8 +31543,15 @@ class ContentEditableHelper {
31469
31543
  else {
31470
31544
  text += NEWLINE;
31471
31545
  }
31546
+ emptyParagraph = ["<br>", "<span><br></span>"].includes(current.value.innerHTML);
31547
+ continue;
31548
+ }
31549
+ if (!current.value.hasChildNodes()) {
31550
+ if (current.value.nodeName === "BR" && !emptyParagraph) {
31551
+ text += NEWLINE;
31552
+ }
31553
+ text += current.value.textContent;
31472
31554
  }
31473
- current = it.next();
31474
31555
  }
31475
31556
  return text;
31476
31557
  }
@@ -32820,6 +32901,12 @@ class Composer extends Component {
32820
32901
  useEffect(() => {
32821
32902
  this.processTokenAtCursor();
32822
32903
  }, () => [this.props.composerStore.editionMode !== "inactive"]);
32904
+ useEffect(() => {
32905
+ this.contentHelper.scrollSelectionIntoView();
32906
+ }, () => [
32907
+ this.props.composerStore.composerSelection.start,
32908
+ this.props.composerStore.composerSelection.end,
32909
+ ]);
32823
32910
  }
32824
32911
  // ---------------------------------------------------------------------------
32825
32912
  // Handlers
@@ -33030,6 +33117,7 @@ class Composer extends Component {
33030
33117
  if (this.env.isMobile() && !isIOS()) {
33031
33118
  return;
33032
33119
  }
33120
+ this.debouncedHover.stopDebounce();
33033
33121
  this.contentHelper.removeSelection();
33034
33122
  }
33035
33123
  onMouseup() {
@@ -33108,7 +33196,6 @@ class Composer extends Component {
33108
33196
  const { start, end } = this.props.composerStore.composerSelection;
33109
33197
  this.contentHelper.selectRange(start, end);
33110
33198
  }
33111
- this.contentHelper.scrollSelectionIntoView();
33112
33199
  }
33113
33200
  this.shouldProcessInputEvents = true;
33114
33201
  }
@@ -33584,6 +33671,414 @@ class SingleInputCriterionForm extends CriterionForm {
33584
33671
  }
33585
33672
  }
33586
33673
 
33674
+ /**
33675
+ * Start listening to pointer events and apply the given callbacks.
33676
+ *
33677
+ * @returns A function to remove the listeners.
33678
+ */
33679
+ function startDnd(onPointerMove, onPointerUp) {
33680
+ const removeListeners = () => {
33681
+ window.removeEventListener("pointerup", _onPointerUp, { capture: true });
33682
+ window.removeEventListener("dragstart", _onDragStart);
33683
+ window.removeEventListener("pointermove", onPointerMove);
33684
+ window.removeEventListener("wheel", onPointerMove);
33685
+ };
33686
+ const _onPointerUp = (ev) => {
33687
+ onPointerUp(ev);
33688
+ removeListeners();
33689
+ };
33690
+ function _onDragStart(ev) {
33691
+ ev.preventDefault();
33692
+ }
33693
+ window.addEventListener("pointerup", _onPointerUp, { capture: true });
33694
+ window.addEventListener("dragstart", _onDragStart);
33695
+ window.addEventListener("pointermove", onPointerMove);
33696
+ // mouse wheel on window is by default a passive event.
33697
+ // preventDefault() is not allowed in passive event handler.
33698
+ // https://chromestatus.com/feature/6662647093133312
33699
+ window.addEventListener("wheel", onPointerMove, { passive: false });
33700
+ return removeListeners;
33701
+ }
33702
+
33703
+ const LINE_VERTICAL_PADDING = 1;
33704
+ const PICKER_PADDING = 8;
33705
+ const ITEM_BORDER_WIDTH = 1;
33706
+ const ITEM_EDGE_LENGTH = 18;
33707
+ const ITEMS_PER_LINE = 10;
33708
+ const MAGNIFIER_EDGE = 16;
33709
+ const ITEM_GAP = 2;
33710
+ const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
33711
+ const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
33712
+ const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
33713
+ const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
33714
+ css /* scss */ `
33715
+ .o-color-picker {
33716
+ padding: ${PICKER_PADDING}px 0;
33717
+ /* FIXME: this is useless, overiden by the popover container */
33718
+ box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
33719
+ background-color: white;
33720
+ line-height: 1.2;
33721
+ overflow-y: auto;
33722
+ overflow-x: hidden;
33723
+ width: ${CONTAINER_WIDTH}px;
33724
+
33725
+ .o-color-picker-section-name {
33726
+ margin: 0px ${ITEM_BORDER_WIDTH}px;
33727
+ padding: 4px ${PICKER_PADDING}px;
33728
+ }
33729
+ .colors-grid {
33730
+ display: grid;
33731
+ padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
33732
+ grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
33733
+ grid-gap: ${ITEM_GAP}px;
33734
+ }
33735
+ .o-color-picker-toggler-button {
33736
+ display: flex;
33737
+ .o-color-picker-toggler-sign {
33738
+ display: flex;
33739
+ margin: auto auto;
33740
+ width: 55%;
33741
+ height: 55%;
33742
+ .o-icon {
33743
+ width: 100%;
33744
+ height: 100%;
33745
+ }
33746
+ }
33747
+ }
33748
+ .o-color-picker-line-item {
33749
+ width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
33750
+ height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
33751
+ margin: 0px;
33752
+ border-radius: 50px;
33753
+ border: ${ITEM_BORDER_WIDTH}px solid #666666;
33754
+ padding: 0px;
33755
+ font-size: 16px;
33756
+ background: white;
33757
+ &:hover {
33758
+ background-color: rgba(0, 0, 0, 0.08);
33759
+ outline: 1px solid gray;
33760
+ cursor: pointer;
33761
+ }
33762
+ }
33763
+ .o-buttons {
33764
+ padding: ${PICKER_PADDING}px;
33765
+ display: flex;
33766
+ .o-cancel {
33767
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33768
+ width: 100%;
33769
+ padding: 5px;
33770
+ font-size: 14px;
33771
+ background: white;
33772
+ border-radius: 4px;
33773
+ &:hover:enabled {
33774
+ background-color: rgba(0, 0, 0, 0.08);
33775
+ }
33776
+ }
33777
+ }
33778
+ .o-add-button {
33779
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33780
+ padding: 4px;
33781
+ background: white;
33782
+ border-radius: 4px;
33783
+ &:hover:enabled {
33784
+ background-color: rgba(0, 0, 0, 0.08);
33785
+ }
33786
+ }
33787
+ .o-separator {
33788
+ border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
33789
+ margin-top: ${MENU_SEPARATOR_PADDING}px;
33790
+ margin-bottom: ${MENU_SEPARATOR_PADDING}px;
33791
+ }
33792
+
33793
+ .o-custom-selector {
33794
+ padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
33795
+ position: relative;
33796
+ .o-gradient {
33797
+ margin-bottom: ${MAGNIFIER_EDGE / 2}px;
33798
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33799
+ width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
33800
+ height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
33801
+ position: relative;
33802
+ }
33803
+
33804
+ .magnifier {
33805
+ height: ${MAGNIFIER_EDGE}px;
33806
+ width: ${MAGNIFIER_EDGE}px;
33807
+ border-radius: 50%;
33808
+ border: 2px solid #fff;
33809
+ box-shadow: 0px 0px 3px #c0c0c0;
33810
+ position: absolute;
33811
+ z-index: 2;
33812
+ }
33813
+ .saturation {
33814
+ background: linear-gradient(to right, #fff 0%, transparent 100%);
33815
+ }
33816
+ .lightness {
33817
+ background: linear-gradient(to top, #000 0%, transparent 100%);
33818
+ }
33819
+ .o-hue-picker {
33820
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33821
+ width: 100%;
33822
+ height: 12px;
33823
+ border-radius: 4px;
33824
+ background: linear-gradient(
33825
+ to right,
33826
+ hsl(0 100% 50%) 0%,
33827
+ hsl(0.2turn 100% 50%) 20%,
33828
+ hsl(0.3turn 100% 50%) 30%,
33829
+ hsl(0.4turn 100% 50%) 40%,
33830
+ hsl(0.5turn 100% 50%) 50%,
33831
+ hsl(0.6turn 100% 50%) 60%,
33832
+ hsl(0.7turn 100% 50%) 70%,
33833
+ hsl(0.8turn 100% 50%) 80%,
33834
+ hsl(0.9turn 100% 50%) 90%,
33835
+ hsl(1turn 100% 50%) 100%
33836
+ );
33837
+ position: relative;
33838
+ cursor: crosshair;
33839
+ }
33840
+ .o-hue-slider {
33841
+ margin-top: -3px;
33842
+ }
33843
+ .o-custom-input-preview {
33844
+ padding: 2px 0px;
33845
+ display: flex;
33846
+ input {
33847
+ width: 50%;
33848
+ border-radius: 4px;
33849
+ padding: 4px 23px 4px 10px;
33850
+ height: 24px;
33851
+ border: 1px solid #c0c0c0;
33852
+ margin-right: 2px;
33853
+ }
33854
+ .o-wrong-color {
33855
+ /* FIXME bootstrap class instead? */
33856
+ outline-color: red;
33857
+ border-color: red;
33858
+ &:focus {
33859
+ outline-style: solid;
33860
+ outline-width: 1px;
33861
+ }
33862
+ }
33863
+ }
33864
+ .o-custom-input-buttons {
33865
+ padding: 2px 0px;
33866
+ display: flex;
33867
+ justify-content: end;
33868
+ }
33869
+ .o-color-preview {
33870
+ border: 1px solid #c0c0c0;
33871
+ border-radius: 4px;
33872
+ width: 50%;
33873
+ }
33874
+ }
33875
+ }
33876
+ `;
33877
+ class ColorPicker extends Component {
33878
+ static template = "o-spreadsheet-ColorPicker";
33879
+ static props = {
33880
+ onColorPicked: Function,
33881
+ currentColor: { type: String, optional: true },
33882
+ maxHeight: { type: Number, optional: true },
33883
+ anchorRect: Object,
33884
+ disableNoColor: { type: Boolean, optional: true },
33885
+ };
33886
+ static defaultProps = { currentColor: "" };
33887
+ static components = { Popover };
33888
+ COLORS = COLOR_PICKER_DEFAULTS;
33889
+ state = useState({
33890
+ showGradient: false,
33891
+ currentHslaColor: isColorValid(this.props.currentColor)
33892
+ ? { ...hexToHSLA(this.props.currentColor), a: 1 }
33893
+ : { h: 0, s: 100, l: 100, a: 1 },
33894
+ customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
33895
+ });
33896
+ get colorPickerStyle() {
33897
+ if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
33898
+ return cssPropertiesToCss({ display: "none" });
33899
+ }
33900
+ return "";
33901
+ }
33902
+ get popoverProps() {
33903
+ return {
33904
+ anchorRect: this.props.anchorRect,
33905
+ maxHeight: this.props.maxHeight,
33906
+ positioning: "bottom-left",
33907
+ verticalOffset: 0,
33908
+ };
33909
+ }
33910
+ get gradientHueStyle() {
33911
+ const hue = this.state.currentHslaColor?.h || 0;
33912
+ return cssPropertiesToCss({
33913
+ background: `hsl(${hue} 100% 50%)`,
33914
+ });
33915
+ }
33916
+ get sliderStyle() {
33917
+ const hue = this.state.currentHslaColor?.h || 0;
33918
+ const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
33919
+ const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
33920
+ return cssPropertiesToCss({
33921
+ "margin-left": `${left}px`,
33922
+ });
33923
+ }
33924
+ get pointerStyle() {
33925
+ const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
33926
+ const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
33927
+ const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
33928
+ return cssPropertiesToCss({
33929
+ left: `${-MAGNIFIER_EDGE / 2 + left}px`,
33930
+ top: `${-MAGNIFIER_EDGE / 2 + top}px`,
33931
+ background: hslaToHex(this.state.currentHslaColor),
33932
+ });
33933
+ }
33934
+ get colorPreviewStyle() {
33935
+ return cssPropertiesToCss({
33936
+ "background-color": hslaToHex(this.state.currentHslaColor),
33937
+ });
33938
+ }
33939
+ get checkmarkColor() {
33940
+ return chartFontColor(this.props.currentColor);
33941
+ }
33942
+ get isHexColorInputValid() {
33943
+ return !this.state.customHexColor || isColorValid(this.state.customHexColor);
33944
+ }
33945
+ setCustomGradient({ x, y }) {
33946
+ const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
33947
+ const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
33948
+ const deltaX = offsetX / INNER_GRADIENT_WIDTH;
33949
+ const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
33950
+ const s = 100 * deltaX;
33951
+ const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
33952
+ this.updateColor({ s, l });
33953
+ }
33954
+ setCustomHue(x) {
33955
+ // needs to be capped such that h is in [0°, 359°]
33956
+ const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
33957
+ this.updateColor({ h });
33958
+ }
33959
+ updateColor(newHsl) {
33960
+ this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
33961
+ this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
33962
+ }
33963
+ onColorClick(color) {
33964
+ if (color) {
33965
+ this.props.onColorPicked(toHex(color));
33966
+ }
33967
+ }
33968
+ resetColor() {
33969
+ this.props.onColorPicked("");
33970
+ }
33971
+ toggleColorPicker() {
33972
+ this.state.showGradient = !this.state.showGradient;
33973
+ }
33974
+ dragGradientPointer(ev) {
33975
+ const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
33976
+ this.setCustomGradient(initialGradientCoordinates);
33977
+ const initialMousePosition = { x: ev.clientX, y: ev.clientY };
33978
+ const onMouseMove = (ev) => {
33979
+ const currentMousePosition = { x: ev.clientX, y: ev.clientY };
33980
+ const deltaX = currentMousePosition.x - initialMousePosition.x;
33981
+ const deltaY = currentMousePosition.y - initialMousePosition.y;
33982
+ const currentGradientCoordinates = {
33983
+ x: initialGradientCoordinates.x + deltaX,
33984
+ y: initialGradientCoordinates.y + deltaY,
33985
+ };
33986
+ this.setCustomGradient(currentGradientCoordinates);
33987
+ };
33988
+ startDnd(onMouseMove, () => { });
33989
+ }
33990
+ dragHuePointer(ev) {
33991
+ const initialX = ev.offsetX;
33992
+ const initialMouseX = ev.clientX;
33993
+ this.setCustomHue(initialX);
33994
+ const onMouseMove = (ev) => {
33995
+ const currentMouseX = ev.clientX;
33996
+ const deltaX = currentMouseX - initialMouseX;
33997
+ const x = initialX + deltaX;
33998
+ this.setCustomHue(x);
33999
+ };
34000
+ startDnd(onMouseMove, () => { });
34001
+ }
34002
+ setHexColor(ev) {
34003
+ // only support HEX code input
34004
+ const val = ev.target.value.replace("##", "#").slice(0, 7);
34005
+ this.state.customHexColor = val;
34006
+ if (!isColorValid(val)) ;
34007
+ else {
34008
+ this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
34009
+ }
34010
+ }
34011
+ addCustomColor(ev) {
34012
+ if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
34013
+ return;
34014
+ }
34015
+ this.props.onColorPicked(toHex(this.state.customHexColor));
34016
+ }
34017
+ isSameColor(color1, color2) {
34018
+ return isSameColor(color1, color2);
34019
+ }
34020
+ }
34021
+
34022
+ class Section extends Component {
34023
+ static template = "o_spreadsheet.Section";
34024
+ static props = {
34025
+ class: { type: String, optional: true },
34026
+ title: { type: String, optional: true },
34027
+ slots: Object,
34028
+ };
34029
+ }
34030
+
34031
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
34032
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
34033
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
34034
+ </svg>
34035
+ `;
34036
+ css /* scss */ `
34037
+ .o-round-color-picker-button {
34038
+ width: 20px;
34039
+ height: 20px;
34040
+ cursor: pointer;
34041
+ border: 1px solid ${GRAY_300};
34042
+ background-position: 1px 1px;
34043
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
34044
+ }
34045
+ `;
34046
+ class RoundColorPicker extends Component {
34047
+ static template = "o-spreadsheet.RoundColorPicker";
34048
+ static components = { Section, ColorPicker };
34049
+ static props = {
34050
+ currentColor: { type: String, optional: true },
34051
+ title: { type: String, optional: true },
34052
+ onColorPicked: Function,
34053
+ disableNoColor: { type: Boolean, optional: true },
34054
+ };
34055
+ colorPickerButtonRef = useRef("colorPickerButton");
34056
+ state;
34057
+ setup() {
34058
+ this.state = useState({ pickerOpened: false });
34059
+ useExternalListener(window, "click", this.closePicker);
34060
+ }
34061
+ closePicker() {
34062
+ this.state.pickerOpened = false;
34063
+ }
34064
+ togglePicker() {
34065
+ this.state.pickerOpened = !this.state.pickerOpened;
34066
+ }
34067
+ onColorPicked(color) {
34068
+ this.props.onColorPicked(color);
34069
+ this.state.pickerOpened = false;
34070
+ }
34071
+ get colorPickerAnchorRect() {
34072
+ const button = this.colorPickerButtonRef.el;
34073
+ return getBoundingRectAsPOJO(button);
34074
+ }
34075
+ get buttonStyle() {
34076
+ return cssPropertiesToCss({
34077
+ background: this.props.currentColor,
34078
+ });
34079
+ }
34080
+ }
34081
+
33587
34082
  css /* scss */ `
33588
34083
  .o-dv-list-item-delete {
33589
34084
  color: #666666;
@@ -33592,7 +34087,7 @@ css /* scss */ `
33592
34087
  `;
33593
34088
  class ListCriterionForm extends CriterionForm {
33594
34089
  static template = "o-spreadsheet-ListCriterionForm";
33595
- static components = { CriterionInput };
34090
+ static components = { CriterionInput, RoundColorPicker };
33596
34091
  state = useState({
33597
34092
  numberOfValues: Math.max(this.props.criterion.values.length, 2),
33598
34093
  });
@@ -33600,7 +34095,7 @@ class ListCriterionForm extends CriterionForm {
33600
34095
  super.setup();
33601
34096
  const setupDefault = (props) => {
33602
34097
  if (props.criterion.displayStyle === undefined) {
33603
- this.updateCriterion({ displayStyle: "arrow" });
34098
+ this.updateCriterion({ displayStyle: "chip" });
33604
34099
  }
33605
34100
  };
33606
34101
  onWillUpdateProps(setupDefault);
@@ -33611,6 +34106,11 @@ class ListCriterionForm extends CriterionForm {
33611
34106
  values[index] = value;
33612
34107
  this.updateCriterion({ values });
33613
34108
  }
34109
+ onColorChanged(color, value) {
34110
+ const colors = { ...this.props.criterion.colors };
34111
+ colors[value] = color || undefined;
34112
+ this.updateCriterion({ colors });
34113
+ }
33614
34114
  onAddAnotherValue() {
33615
34115
  this.state.numberOfValues++;
33616
34116
  }
@@ -33646,35 +34146,6 @@ class ListCriterionForm extends CriterionForm {
33646
34146
  }
33647
34147
  }
33648
34148
 
33649
- /**
33650
- * Start listening to pointer events and apply the given callbacks.
33651
- *
33652
- * @returns A function to remove the listeners.
33653
- */
33654
- function startDnd(onPointerMove, onPointerUp) {
33655
- const removeListeners = () => {
33656
- window.removeEventListener("pointerup", _onPointerUp, { capture: true });
33657
- window.removeEventListener("dragstart", _onDragStart);
33658
- window.removeEventListener("pointermove", onPointerMove);
33659
- window.removeEventListener("wheel", onPointerMove);
33660
- };
33661
- const _onPointerUp = (ev) => {
33662
- onPointerUp(ev);
33663
- removeListeners();
33664
- };
33665
- function _onDragStart(ev) {
33666
- ev.preventDefault();
33667
- }
33668
- window.addEventListener("pointerup", _onPointerUp, { capture: true });
33669
- window.addEventListener("dragstart", _onDragStart);
33670
- window.addEventListener("pointermove", onPointerMove);
33671
- // mouse wheel on window is by default a passive event.
33672
- // preventDefault() is not allowed in passive event handler.
33673
- // https://chromestatus.com/feature/6662647093133312
33674
- window.addEventListener("wheel", onPointerMove, { passive: false });
33675
- return removeListeners;
33676
- }
33677
-
33678
34149
  function useDragAndDropListItems() {
33679
34150
  let dndHelper;
33680
34151
  const previousCursor = document.body.style.cursor;
@@ -34534,12 +35005,12 @@ class SelectionInput extends Component {
34534
35005
 
34535
35006
  class ValueInRangeCriterionForm extends CriterionForm {
34536
35007
  static template = "o-spreadsheet-ValueInRangeCriterionForm";
34537
- static components = { SelectionInput };
35008
+ static components = { RoundColorPicker, SelectionInput };
34538
35009
  setup() {
34539
35010
  super.setup();
34540
35011
  const setupDefault = (props) => {
34541
35012
  if (props.criterion.displayStyle === undefined) {
34542
- this.updateCriterion({ displayStyle: "arrow" });
35013
+ this.updateCriterion({ displayStyle: "chip" });
34543
35014
  }
34544
35015
  };
34545
35016
  onWillUpdateProps(setupDefault);
@@ -34552,6 +35023,16 @@ class ValueInRangeCriterionForm extends CriterionForm {
34552
35023
  const displayStyle = ev.target.value;
34553
35024
  this.updateCriterion({ displayStyle });
34554
35025
  }
35026
+ onColorChanged(color, value) {
35027
+ const colors = { ...this.props.criterion.colors };
35028
+ colors[value] = color || undefined;
35029
+ this.updateCriterion({ colors });
35030
+ }
35031
+ get values() {
35032
+ const sheetId = this.env.model.getters.getActiveSheetId();
35033
+ const values = this.env.model.getters.getDataValidationRangeValues(sheetId, this.props.criterion);
35034
+ return new Set(values);
35035
+ }
34555
35036
  }
34556
35037
 
34557
35038
  const criterionCategoriesSequences = {
@@ -34939,17 +35420,22 @@ class FilterMenuValueList extends Component {
34939
35420
  static components = { FilterMenuValueItem };
34940
35421
  state = useState({
34941
35422
  values: [],
35423
+ displayedValues: [],
34942
35424
  textFilter: "",
34943
35425
  selectedValue: undefined,
35426
+ numberOfDisplayedValues: 50,
35427
+ hasMoreValues: false,
34944
35428
  });
34945
35429
  searchBar = useRef("filterMenuSearchBar");
34946
35430
  setup() {
34947
35431
  onWillUpdateProps((nextProps) => {
34948
35432
  if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
34949
35433
  this.state.values = this.getFilterHiddenValues(nextProps.filterPosition);
35434
+ this.computeDisplayedValues();
34950
35435
  }
34951
35436
  });
34952
35437
  this.state.values = this.getFilterHiddenValues(this.props.filterPosition);
35438
+ this.computeDisplayedValues();
34953
35439
  }
34954
35440
  getFilterHiddenValues(position) {
34955
35441
  const sheetId = this.env.model.getters.getActiveSheetId();
@@ -34967,21 +35453,28 @@ class FilterMenuValueList extends Component {
34967
35453
  }
34968
35454
  const cellValues = cells.map((val) => val.cellValue);
34969
35455
  const filterValues = filterValue?.filterType === "values" ? filterValue.hiddenValues : [];
34970
- const strValues = [...cellValues, ...filterValues];
34971
- const normalizedFilteredValues = filterValues.map(toLowerCase);
34972
- // Set with lowercase values to avoid duplicates
34973
- const normalizedValues = [...new Set(strValues.map(toLowerCase))];
34974
- const sortedValues = normalizedValues.sort((val1, val2) => val1.localeCompare(val2, undefined, { numeric: true, sensitivity: "base" }));
34975
- return sortedValues.map((normalizedValue) => {
34976
- let checked = false;
34977
- if (filterValue?.filterType !== "criterion") {
34978
- checked = normalizedFilteredValues.findIndex((val) => val === normalizedValue) === -1;
35456
+ const normalizedFilteredValues = new Set(filterValues.map(toLowerCase));
35457
+ const set = new Set();
35458
+ const values = [];
35459
+ const addValue = (value) => {
35460
+ const normalizedValue = toLowerCase(value);
35461
+ if (!set.has(normalizedValue)) {
35462
+ values.push({
35463
+ string: value || "",
35464
+ checked: filterValue?.filterType !== "criterion"
35465
+ ? !normalizedFilteredValues.has(normalizedValue)
35466
+ : false,
35467
+ normalizedValue,
35468
+ });
35469
+ set.add(normalizedValue);
34979
35470
  }
34980
- return {
34981
- checked,
34982
- string: strValues.find((val) => toLowerCase(val) === normalizedValue) || "",
34983
- };
34984
- });
35471
+ };
35472
+ cellValues.forEach(addValue);
35473
+ filterValues.forEach(addValue);
35474
+ return values.sort((val1, val2) => val1.normalizedValue.localeCompare(val2.normalizedValue, undefined, {
35475
+ numeric: true,
35476
+ sensitivity: "base",
35477
+ }));
34985
35478
  }
34986
35479
  checkValue(value) {
34987
35480
  this.state.selectedValue = value.string;
@@ -34993,25 +35486,37 @@ class FilterMenuValueList extends Component {
34993
35486
  this.state.selectedValue = value.string;
34994
35487
  }
34995
35488
  selectAll() {
34996
- this.displayedValues.forEach((value) => (value.checked = true));
34997
- this.updateHiddenValues();
35489
+ this.state.displayedValues.forEach((value) => (value.checked = true));
35490
+ this.props.onUpdateHiddenValues([]);
34998
35491
  }
34999
35492
  clearAll() {
35000
- this.displayedValues.forEach((value) => (value.checked = false));
35001
- this.updateHiddenValues();
35493
+ this.state.displayedValues.forEach((value) => (value.checked = false));
35494
+ const hiddenValues = this.state.values.map((val) => val.string);
35495
+ this.props.onUpdateHiddenValues(hiddenValues);
35002
35496
  }
35003
35497
  updateHiddenValues() {
35004
35498
  const hiddenValues = this.state.values.filter((val) => !val.checked).map((val) => val.string);
35005
35499
  this.props.onUpdateHiddenValues(hiddenValues);
35006
35500
  }
35007
- get displayedValues() {
35008
- if (!this.state.textFilter) {
35009
- return this.state.values;
35010
- }
35011
- return fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
35501
+ updateSearch(ev) {
35502
+ const target = ev.target;
35503
+ this.state.textFilter = target.value;
35504
+ this.state.selectedValue = undefined;
35505
+ this.computeDisplayedValues();
35506
+ }
35507
+ computeDisplayedValues() {
35508
+ const values = !this.state.textFilter
35509
+ ? this.state.values
35510
+ : fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
35511
+ this.state.displayedValues = values.slice(0, this.state.numberOfDisplayedValues);
35512
+ this.state.hasMoreValues = values.length > this.state.numberOfDisplayedValues;
35513
+ }
35514
+ loadMoreValues() {
35515
+ this.state.numberOfDisplayedValues += 100;
35516
+ this.computeDisplayedValues();
35012
35517
  }
35013
35518
  onKeyDown(ev) {
35014
- const displayedValues = this.displayedValues;
35519
+ const displayedValues = this.state.displayedValues;
35015
35520
  if (displayedValues.length === 0)
35016
35521
  return;
35017
35522
  let selectedIndex = undefined;
@@ -36106,19 +36611,44 @@ const RED_DOT = {
36106
36611
  height: 512,
36107
36612
  paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36108
36613
  };
36109
- const CARET_DOWN = {
36110
- width: 512,
36111
- height: 512,
36112
- paths: [{ fillColor: TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
36113
- };
36114
- const HOVERED_CARET_DOWN = {
36115
- width: 512,
36116
- height: 512,
36117
- paths: [
36118
- { fillColor: TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
36119
- { fillColor: "#fff", path: "M120 195 h270 l-135 130" },
36120
- ],
36121
- };
36614
+ function getCaretDownSvg(color) {
36615
+ return {
36616
+ width: 512,
36617
+ height: 512,
36618
+ paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
36619
+ };
36620
+ }
36621
+ function getHoveredCaretDownSvg(color) {
36622
+ return {
36623
+ width: 512,
36624
+ height: 512,
36625
+ paths: [
36626
+ { fillColor: color.textColor || TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
36627
+ { fillColor: color.fillColor || "#fff", path: "M120 195 h270 l-135 130" },
36628
+ ],
36629
+ };
36630
+ }
36631
+ const CHIP_CARET_DOWN_PATH = "M40 185 h270 l-135 128";
36632
+ function getChipSvg(chipStyle) {
36633
+ return {
36634
+ width: 512,
36635
+ height: 512,
36636
+ paths: [{ fillColor: chipStyle.textColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH }],
36637
+ };
36638
+ }
36639
+ function getHoveredChipSvg(chipStyle) {
36640
+ return {
36641
+ width: 512,
36642
+ height: 512,
36643
+ paths: [
36644
+ {
36645
+ fillColor: chipStyle.textColor || TEXT_BODY_MUTED,
36646
+ path: "M0,225 A175,175 0 1,0 350,225 A175,175 0 1,0 0,225",
36647
+ },
36648
+ { fillColor: chipStyle.fillColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH },
36649
+ ],
36650
+ };
36651
+ }
36122
36652
  const CHECKBOX_UNCHECKED = {
36123
36653
  width: 512,
36124
36654
  height: 512,
@@ -38484,7 +39014,7 @@ class XlsxBaseExtractor {
38484
39014
  */
38485
39015
  handleMissingValue(parentElement, missingElementName, optionalArgs) {
38486
39016
  if (optionalArgs?.required) {
38487
- if (optionalArgs?.default) {
39017
+ if (optionalArgs?.default !== undefined) {
38488
39018
  this.warningManager.addParsingWarning(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, replacing it by the default value ${optionalArgs.default}`);
38489
39019
  }
38490
39020
  else {
@@ -41026,6 +41556,10 @@ const REMOVE_ROWS_ACTION = (env) => {
41026
41556
  });
41027
41557
  };
41028
41558
  const CAN_REMOVE_COLUMNS_ROWS = (dimension, env) => {
41559
+ if ((dimension === "COL" && env.model.getters.getActiveRows().size > 0) ||
41560
+ (dimension === "ROW" && env.model.getters.getActiveCols().size > 0)) {
41561
+ return false;
41562
+ }
41029
41563
  const sheetId = env.model.getters.getActiveSheetId();
41030
41564
  const selectedElements = env.model.getters.getElementsFromSelection(dimension);
41031
41565
  const includesAllVisibleHeaders = env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, dimension, selectedElements);
@@ -42580,7 +43114,7 @@ const insertDropdown = {
42580
43114
  criterion: {
42581
43115
  type: "isValueInList",
42582
43116
  values: [],
42583
- displayStyle: "arrow",
43117
+ displayStyle: "chip",
42584
43118
  },
42585
43119
  },
42586
43120
  });
@@ -44495,6 +45029,11 @@ class GridComposer extends Component {
44495
45029
  rect = this.defaultRect;
44496
45030
  isEditing = false;
44497
45031
  isCellReferenceVisible = false;
45032
+ currentEditedCell = {
45033
+ col: 0,
45034
+ row: 0,
45035
+ sheetId: this.env.model.getters.getActiveSheetId(),
45036
+ };
44498
45037
  composerStore;
44499
45038
  composerFocusStore;
44500
45039
  composerInterface;
@@ -44619,12 +45158,17 @@ class GridComposer extends Component {
44619
45158
  if (!isEditing && this.composerFocusStore.activeComposer !== this.composerInterface) {
44620
45159
  this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "inactive" });
44621
45160
  }
45161
+ let shouldRecomputeRect = !deepEquals(this.currentEditedCell, this.composerStore.currentEditedCell);
44622
45162
  if (this.isEditing !== isEditing) {
44623
45163
  this.isEditing = isEditing;
44624
45164
  if (!isEditing) {
44625
45165
  this.rect = this.defaultRect;
44626
45166
  return;
44627
45167
  }
45168
+ this.currentEditedCell = this.composerStore.currentEditedCell;
45169
+ shouldRecomputeRect = true;
45170
+ }
45171
+ if (shouldRecomputeRect) {
44628
45172
  const position = this.env.model.getters.getActivePosition();
44629
45173
  const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
44630
45174
  this.rect = this.env.model.getters.getVisibleRect(zone);
@@ -45730,11 +46274,14 @@ class GridOverlay extends Component {
45730
46274
  onCellClicked(ev) {
45731
46275
  const openedPopover = this.cellPopovers.persistentCellPopover;
45732
46276
  const [col, row] = this.getCartesianCoordinates(ev);
46277
+ const clickedIcon = this.getInteractiveIconAtEvent(ev);
46278
+ if (clickedIcon) {
46279
+ this.env.model.selection.getBackToDefault();
46280
+ }
45733
46281
  this.props.onCellClicked(col, row, {
45734
46282
  expandZone: ev.shiftKey,
45735
46283
  addZone: isCtrlKey(ev),
45736
46284
  }, ev);
45737
- const clickedIcon = this.getInteractiveIconAtEvent(ev);
45738
46285
  if (clickedIcon?.onClick) {
45739
46286
  clickedIcon.onClick(clickedIcon.position, this.env);
45740
46287
  }
@@ -46599,6 +47146,19 @@ class GridRenderer {
46599
47146
  const width = box.width * (percentage / 100);
46600
47147
  ctx.fillRect(box.x, box.y, width, box.height);
46601
47148
  }
47149
+ if (box?.chip) {
47150
+ ctx.save();
47151
+ ctx.beginPath();
47152
+ ctx.rect(box.x, box.y, box.width, box.height);
47153
+ ctx.clip();
47154
+ const chip = box.chip;
47155
+ ctx.fillStyle = chip.color;
47156
+ const radius = 10;
47157
+ ctx.beginPath();
47158
+ ctx.roundRect(chip.x, chip.y, chip.width, chip.height, radius);
47159
+ ctx.fill();
47160
+ ctx.restore();
47161
+ }
46602
47162
  if (box.overlayColor) {
46603
47163
  ctx.fillStyle = box.overlayColor;
46604
47164
  ctx.fillRect(box.x, box.y, box.width, box.height);
@@ -46738,19 +47298,6 @@ class GridRenderer {
46738
47298
  ctx.font = font;
46739
47299
  }
46740
47300
  ctx.fillStyle = style.textColor || "#000";
46741
- // compute horizontal align start point parameter
46742
- let x = box.x;
46743
- if (align === "left") {
46744
- const leftIconSize = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46745
- x += MIN_CELL_TEXT_MARGIN + leftIconSize;
46746
- }
46747
- else if (align === "right") {
46748
- const rightIconSize = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46749
- x += box.width - MIN_CELL_TEXT_MARGIN - rightIconSize;
46750
- }
46751
- else {
46752
- x += box.width / 2;
46753
- }
46754
47301
  // horizontal align text direction
46755
47302
  ctx.textAlign = align;
46756
47303
  // clip rect if needed
@@ -46761,15 +47308,13 @@ class GridRenderer {
46761
47308
  ctx.rect(x, y, width, height);
46762
47309
  ctx.clip();
46763
47310
  }
46764
- // compute vertical align start point parameter:
46765
- const textLineHeight = computeTextFontSizeInPixels(style);
46766
- const numberOfLines = box.content.textLines.length;
46767
- let y = this.getters.computeTextYCoordinate(box, textLineHeight, style.verticalAlign, numberOfLines);
47311
+ const x = box.content.x;
47312
+ let y = box.content.y;
46768
47313
  // use the horizontal and the vertical start points to:
46769
47314
  // fill text / fill strikethrough / fill underline
46770
47315
  for (const brokenLine of box.content.textLines) {
46771
- drawDecoratedText(ctx, brokenLine, { x: Math.round(x), y: Math.round(y) }, style.underline, style.strikethrough);
46772
- y += MIN_CELL_TEXT_MARGIN + textLineHeight;
47316
+ drawDecoratedText(ctx, brokenLine, { x, y }, style.underline, style.strikethrough);
47317
+ y += MIN_CELL_TEXT_MARGIN + box.content.fontSizePx;
46773
47318
  }
46774
47319
  if (box.clipRect) {
46775
47320
  ctx.restore();
@@ -47004,11 +47549,15 @@ class GridRenderer {
47004
47549
  const showFormula = this.getters.shouldShowFormulas();
47005
47550
  const { x, y, width, height } = this.getters.getRect(zone);
47006
47551
  const { verticalAlign } = this.getters.getCellStyle(position);
47552
+ const chipStyle = this.getters.getDataValidationChipStyle(position);
47007
47553
  let style = this.getters.getCellComputedStyle(position);
47008
47554
  if (this.fingerprints.isEnabled) {
47009
47555
  const fingerprintColor = this.fingerprints.colors.get(position);
47010
47556
  style = { ...style, fillColor: fingerprintColor };
47011
47557
  }
47558
+ if (chipStyle?.textColor) {
47559
+ style = { ...style, textColor: chipStyle.textColor };
47560
+ }
47012
47561
  const dataBarFill = this.fingerprints.isEnabled
47013
47562
  ? undefined
47014
47563
  : this.getters.getConditionalDataBar(position);
@@ -47042,22 +47591,55 @@ class GridRenderer {
47042
47591
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
47043
47592
  const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
47044
47593
  const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
47594
+ const chipMargin = chipStyle ? DATA_VALIDATION_CHIP_MARGIN : 0;
47045
47595
  const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
47596
+ const leftMargin = leftIconWidth + chipMargin;
47046
47597
  const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
47047
- const contentWidth = leftIconWidth + textWidth + rightIconWidth;
47598
+ const rightMargin = rightIconWidth + chipMargin;
47599
+ const contentWidth = leftMargin + textWidth + rightMargin;
47048
47600
  const align = this.computeCellAlignment(position, contentWidth > width);
47601
+ // compute vertical align start point parameter:
47602
+ const numberOfLines = multiLineText.length;
47603
+ const contentY = Math.round(this.getters.computeTextYCoordinate(box, fontSizePX, style.verticalAlign, numberOfLines));
47604
+ // compute horizontal align start point parameter
47605
+ let contentX = box.x;
47606
+ if (align === "left") {
47607
+ contentX += MIN_CELL_TEXT_MARGIN + leftMargin;
47608
+ }
47609
+ else if (align === "right") {
47610
+ contentX += box.width - MIN_CELL_TEXT_MARGIN - rightMargin;
47611
+ }
47612
+ else {
47613
+ contentX += box.width / 2;
47614
+ }
47615
+ contentX = Math.round(contentX);
47616
+ const textHeight = computeTextLinesHeight(fontSizePX, numberOfLines);
47049
47617
  box.content = {
47050
47618
  textLines: multiLineText,
47051
47619
  width: wrapping === "overflow" ? textWidth : width,
47052
47620
  align,
47621
+ x: contentX,
47622
+ y: contentY,
47623
+ fontSizePx: fontSizePX,
47053
47624
  };
47625
+ if (chipStyle?.fillColor) {
47626
+ const chipMarginLeft = leftMargin;
47627
+ const chipMarginRight = DATA_VALIDATION_CHIP_MARGIN;
47628
+ box.chip = {
47629
+ color: chipStyle.fillColor,
47630
+ width: box.width - chipMarginLeft - chipMarginRight,
47631
+ height: textHeight + 2,
47632
+ x: box.x + chipMarginLeft,
47633
+ y: contentY - 2,
47634
+ };
47635
+ }
47054
47636
  /** ClipRect */
47055
47637
  const isOverflowing = contentWidth > width || fontSizePX > height;
47056
- if (box.icons.left || box.icons.right) {
47638
+ if (box.icons.left || box.icons.right || box.chip) {
47057
47639
  box.clipRect = {
47058
- x: box.x + leftIconWidth,
47640
+ x: box.x + leftMargin,
47059
47641
  y: box.y,
47060
- width: Math.max(0, width - leftIconWidth - rightIconWidth),
47642
+ width: Math.max(0, width - leftMargin - rightMargin),
47061
47643
  height,
47062
47644
  };
47063
47645
  }
@@ -47757,15 +48339,6 @@ class Selection extends Component {
47757
48339
  }
47758
48340
  }
47759
48341
 
47760
- class Section extends Component {
47761
- static template = "o_spreadsheet.Section";
47762
- static props = {
47763
- class: { type: String, optional: true },
47764
- title: { type: String, optional: true },
47765
- slots: Object,
47766
- };
47767
- }
47768
-
47769
48342
  class ChartDataSeries extends Component {
47770
48343
  static template = "o-spreadsheet.ChartDataSeries";
47771
48344
  static components = { SelectionInput, Section };
@@ -48314,325 +48887,6 @@ class ActionButton extends Component {
48314
48887
  }
48315
48888
  }
48316
48889
 
48317
- const LINE_VERTICAL_PADDING = 1;
48318
- const PICKER_PADDING = 8;
48319
- const ITEM_BORDER_WIDTH = 1;
48320
- const ITEM_EDGE_LENGTH = 18;
48321
- const ITEMS_PER_LINE = 10;
48322
- const MAGNIFIER_EDGE = 16;
48323
- const ITEM_GAP = 2;
48324
- const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
48325
- const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
48326
- const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
48327
- const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
48328
- css /* scss */ `
48329
- .o-color-picker {
48330
- padding: ${PICKER_PADDING}px 0;
48331
- /* FIXME: this is useless, overiden by the popover container */
48332
- box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
48333
- background-color: white;
48334
- line-height: 1.2;
48335
- overflow-y: auto;
48336
- overflow-x: hidden;
48337
- width: ${CONTAINER_WIDTH}px;
48338
-
48339
- .o-color-picker-section-name {
48340
- margin: 0px ${ITEM_BORDER_WIDTH}px;
48341
- padding: 4px ${PICKER_PADDING}px;
48342
- }
48343
- .colors-grid {
48344
- display: grid;
48345
- padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
48346
- grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
48347
- grid-gap: ${ITEM_GAP}px;
48348
- }
48349
- .o-color-picker-toggler-button {
48350
- display: flex;
48351
- .o-color-picker-toggler-sign {
48352
- display: flex;
48353
- margin: auto auto;
48354
- width: 55%;
48355
- height: 55%;
48356
- .o-icon {
48357
- width: 100%;
48358
- height: 100%;
48359
- }
48360
- }
48361
- }
48362
- .o-color-picker-line-item {
48363
- width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48364
- height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48365
- margin: 0px;
48366
- border-radius: 50px;
48367
- border: ${ITEM_BORDER_WIDTH}px solid #666666;
48368
- padding: 0px;
48369
- font-size: 16px;
48370
- background: white;
48371
- &:hover {
48372
- background-color: rgba(0, 0, 0, 0.08);
48373
- outline: 1px solid gray;
48374
- cursor: pointer;
48375
- }
48376
- }
48377
- .o-buttons {
48378
- padding: ${PICKER_PADDING}px;
48379
- display: flex;
48380
- .o-cancel {
48381
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48382
- width: 100%;
48383
- padding: 5px;
48384
- font-size: 14px;
48385
- background: white;
48386
- border-radius: 4px;
48387
- &:hover:enabled {
48388
- background-color: rgba(0, 0, 0, 0.08);
48389
- }
48390
- }
48391
- }
48392
- .o-add-button {
48393
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48394
- padding: 4px;
48395
- background: white;
48396
- border-radius: 4px;
48397
- &:hover:enabled {
48398
- background-color: rgba(0, 0, 0, 0.08);
48399
- }
48400
- }
48401
- .o-separator {
48402
- border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
48403
- margin-top: ${MENU_SEPARATOR_PADDING}px;
48404
- margin-bottom: ${MENU_SEPARATOR_PADDING}px;
48405
- }
48406
-
48407
- .o-custom-selector {
48408
- padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
48409
- position: relative;
48410
- .o-gradient {
48411
- margin-bottom: ${MAGNIFIER_EDGE / 2}px;
48412
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48413
- width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
48414
- height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
48415
- position: relative;
48416
- }
48417
-
48418
- .magnifier {
48419
- height: ${MAGNIFIER_EDGE}px;
48420
- width: ${MAGNIFIER_EDGE}px;
48421
- border-radius: 50%;
48422
- border: 2px solid #fff;
48423
- box-shadow: 0px 0px 3px #c0c0c0;
48424
- position: absolute;
48425
- z-index: 2;
48426
- }
48427
- .saturation {
48428
- background: linear-gradient(to right, #fff 0%, transparent 100%);
48429
- }
48430
- .lightness {
48431
- background: linear-gradient(to top, #000 0%, transparent 100%);
48432
- }
48433
- .o-hue-picker {
48434
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48435
- width: 100%;
48436
- height: 12px;
48437
- border-radius: 4px;
48438
- background: linear-gradient(
48439
- to right,
48440
- hsl(0 100% 50%) 0%,
48441
- hsl(0.2turn 100% 50%) 20%,
48442
- hsl(0.3turn 100% 50%) 30%,
48443
- hsl(0.4turn 100% 50%) 40%,
48444
- hsl(0.5turn 100% 50%) 50%,
48445
- hsl(0.6turn 100% 50%) 60%,
48446
- hsl(0.7turn 100% 50%) 70%,
48447
- hsl(0.8turn 100% 50%) 80%,
48448
- hsl(0.9turn 100% 50%) 90%,
48449
- hsl(1turn 100% 50%) 100%
48450
- );
48451
- position: relative;
48452
- cursor: crosshair;
48453
- }
48454
- .o-hue-slider {
48455
- margin-top: -3px;
48456
- }
48457
- .o-custom-input-preview {
48458
- padding: 2px 0px;
48459
- display: flex;
48460
- input {
48461
- width: 50%;
48462
- border-radius: 4px;
48463
- padding: 4px 23px 4px 10px;
48464
- height: 24px;
48465
- border: 1px solid #c0c0c0;
48466
- margin-right: 2px;
48467
- }
48468
- .o-wrong-color {
48469
- /* FIXME bootstrap class instead? */
48470
- outline-color: red;
48471
- border-color: red;
48472
- &:focus {
48473
- outline-style: solid;
48474
- outline-width: 1px;
48475
- }
48476
- }
48477
- }
48478
- .o-custom-input-buttons {
48479
- padding: 2px 0px;
48480
- display: flex;
48481
- justify-content: end;
48482
- }
48483
- .o-color-preview {
48484
- border: 1px solid #c0c0c0;
48485
- border-radius: 4px;
48486
- width: 50%;
48487
- }
48488
- }
48489
- }
48490
- `;
48491
- class ColorPicker extends Component {
48492
- static template = "o-spreadsheet-ColorPicker";
48493
- static props = {
48494
- onColorPicked: Function,
48495
- currentColor: { type: String, optional: true },
48496
- maxHeight: { type: Number, optional: true },
48497
- anchorRect: Object,
48498
- disableNoColor: { type: Boolean, optional: true },
48499
- };
48500
- static defaultProps = { currentColor: "" };
48501
- static components = { Popover };
48502
- COLORS = COLOR_PICKER_DEFAULTS;
48503
- state = useState({
48504
- showGradient: false,
48505
- currentHslaColor: isColorValid(this.props.currentColor)
48506
- ? { ...hexToHSLA(this.props.currentColor), a: 1 }
48507
- : { h: 0, s: 100, l: 100, a: 1 },
48508
- customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
48509
- });
48510
- get colorPickerStyle() {
48511
- if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
48512
- return cssPropertiesToCss({ display: "none" });
48513
- }
48514
- return "";
48515
- }
48516
- get popoverProps() {
48517
- return {
48518
- anchorRect: this.props.anchorRect,
48519
- maxHeight: this.props.maxHeight,
48520
- positioning: "bottom-left",
48521
- verticalOffset: 0,
48522
- };
48523
- }
48524
- get gradientHueStyle() {
48525
- const hue = this.state.currentHslaColor?.h || 0;
48526
- return cssPropertiesToCss({
48527
- background: `hsl(${hue} 100% 50%)`,
48528
- });
48529
- }
48530
- get sliderStyle() {
48531
- const hue = this.state.currentHslaColor?.h || 0;
48532
- const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
48533
- const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
48534
- return cssPropertiesToCss({
48535
- "margin-left": `${left}px`,
48536
- });
48537
- }
48538
- get pointerStyle() {
48539
- const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
48540
- const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
48541
- const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
48542
- return cssPropertiesToCss({
48543
- left: `${-MAGNIFIER_EDGE / 2 + left}px`,
48544
- top: `${-MAGNIFIER_EDGE / 2 + top}px`,
48545
- background: hslaToHex(this.state.currentHslaColor),
48546
- });
48547
- }
48548
- get colorPreviewStyle() {
48549
- return cssPropertiesToCss({
48550
- "background-color": hslaToHex(this.state.currentHslaColor),
48551
- });
48552
- }
48553
- get checkmarkColor() {
48554
- return chartFontColor(this.props.currentColor);
48555
- }
48556
- get isHexColorInputValid() {
48557
- return !this.state.customHexColor || isColorValid(this.state.customHexColor);
48558
- }
48559
- setCustomGradient({ x, y }) {
48560
- const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
48561
- const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
48562
- const deltaX = offsetX / INNER_GRADIENT_WIDTH;
48563
- const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
48564
- const s = 100 * deltaX;
48565
- const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
48566
- this.updateColor({ s, l });
48567
- }
48568
- setCustomHue(x) {
48569
- // needs to be capped such that h is in [0°, 359°]
48570
- const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
48571
- this.updateColor({ h });
48572
- }
48573
- updateColor(newHsl) {
48574
- this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
48575
- this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
48576
- }
48577
- onColorClick(color) {
48578
- if (color) {
48579
- this.props.onColorPicked(toHex(color));
48580
- }
48581
- }
48582
- resetColor() {
48583
- this.props.onColorPicked("");
48584
- }
48585
- toggleColorPicker() {
48586
- this.state.showGradient = !this.state.showGradient;
48587
- }
48588
- dragGradientPointer(ev) {
48589
- const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
48590
- this.setCustomGradient(initialGradientCoordinates);
48591
- const initialMousePosition = { x: ev.clientX, y: ev.clientY };
48592
- const onMouseMove = (ev) => {
48593
- const currentMousePosition = { x: ev.clientX, y: ev.clientY };
48594
- const deltaX = currentMousePosition.x - initialMousePosition.x;
48595
- const deltaY = currentMousePosition.y - initialMousePosition.y;
48596
- const currentGradientCoordinates = {
48597
- x: initialGradientCoordinates.x + deltaX,
48598
- y: initialGradientCoordinates.y + deltaY,
48599
- };
48600
- this.setCustomGradient(currentGradientCoordinates);
48601
- };
48602
- startDnd(onMouseMove, () => { });
48603
- }
48604
- dragHuePointer(ev) {
48605
- const initialX = ev.offsetX;
48606
- const initialMouseX = ev.clientX;
48607
- this.setCustomHue(initialX);
48608
- const onMouseMove = (ev) => {
48609
- const currentMouseX = ev.clientX;
48610
- const deltaX = currentMouseX - initialMouseX;
48611
- const x = initialX + deltaX;
48612
- this.setCustomHue(x);
48613
- };
48614
- startDnd(onMouseMove, () => { });
48615
- }
48616
- setHexColor(ev) {
48617
- // only support HEX code input
48618
- const val = ev.target.value.replace("##", "#").slice(0, 7);
48619
- this.state.customHexColor = val;
48620
- if (!isColorValid(val)) ;
48621
- else {
48622
- this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
48623
- }
48624
- }
48625
- addCustomColor(ev) {
48626
- if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
48627
- return;
48628
- }
48629
- this.props.onColorPicked(toHex(this.state.customHexColor));
48630
- }
48631
- isSameColor(color1, color2) {
48632
- return isSameColor(color1, color2);
48633
- }
48634
- }
48635
-
48636
48890
  css /* scss */ `
48637
48891
  .o-color-picker-widget {
48638
48892
  display: flex;
@@ -48707,7 +48961,8 @@ css /* scss */ `
48707
48961
  input.o-font-size {
48708
48962
  outline: none;
48709
48963
  height: 20px;
48710
- width: 23px;
48964
+ width: 31px;
48965
+ text-align: center;
48711
48966
  }
48712
48967
  }
48713
48968
  .o-text-options > div {
@@ -49099,57 +49354,6 @@ class RadioSelection extends Component {
49099
49354
  };
49100
49355
  }
49101
49356
 
49102
- const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
49103
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
49104
- <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
49105
- </svg>
49106
- `;
49107
- css /* scss */ `
49108
- .o-round-color-picker-button {
49109
- width: 20px;
49110
- height: 20px;
49111
- cursor: pointer;
49112
- border: 1px solid ${GRAY_300};
49113
- background-position: 1px 1px;
49114
- background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
49115
- }
49116
- `;
49117
- class RoundColorPicker extends Component {
49118
- static template = "o-spreadsheet.RoundColorPicker";
49119
- static components = { Section, ColorPicker };
49120
- static props = {
49121
- currentColor: { type: String, optional: true },
49122
- title: { type: String, optional: true },
49123
- onColorPicked: Function,
49124
- disableNoColor: { type: Boolean, optional: true },
49125
- };
49126
- colorPickerButtonRef = useRef("colorPickerButton");
49127
- state;
49128
- setup() {
49129
- this.state = useState({ pickerOpened: false });
49130
- useExternalListener(window, "click", this.closePicker);
49131
- }
49132
- closePicker() {
49133
- this.state.pickerOpened = false;
49134
- }
49135
- togglePicker() {
49136
- this.state.pickerOpened = !this.state.pickerOpened;
49137
- }
49138
- onColorPicked(color) {
49139
- this.props.onColorPicked(color);
49140
- this.state.pickerOpened = false;
49141
- }
49142
- get colorPickerAnchorRect() {
49143
- const button = this.colorPickerButtonRef.el;
49144
- return getBoundingRectAsPOJO(button);
49145
- }
49146
- get buttonStyle() {
49147
- return cssPropertiesToCss({
49148
- background: this.props.currentColor,
49149
- });
49150
- }
49151
- }
49152
-
49153
49357
  class GeneralDesignEditor extends Component {
49154
49358
  static template = "o-spreadsheet-GeneralDesignEditor";
49155
49359
  static components = {
@@ -53785,28 +53989,45 @@ class SpreadsheetPivotTable {
53785
53989
  getNumberOfDataColumns() {
53786
53990
  return this.columns.at(-1)?.length || 0;
53787
53991
  }
53788
- getPivotCells(includeTotal = true, includeColumnHeaders = true) {
53789
- const key = JSON.stringify({ includeTotal, includeColumnHeaders });
53992
+ getSkippedRows(visibilityOptions) {
53993
+ const skippedRows = new Set();
53994
+ if (!visibilityOptions.displayColumnHeaders) {
53995
+ for (let i = 0; i < this.columns.length - 1; i++) {
53996
+ skippedRows.add(i);
53997
+ }
53998
+ }
53999
+ if (!visibilityOptions.displayMeasuresRow) {
54000
+ skippedRows.add(this.columns.length - 1);
54001
+ }
54002
+ return skippedRows;
54003
+ }
54004
+ getPivotCells(visibilityOptions = {
54005
+ displayColumnHeaders: true,
54006
+ displayTotals: true,
54007
+ displayMeasuresRow: true,
54008
+ }) {
54009
+ const key = JSON.stringify(visibilityOptions);
53790
54010
  if (!this.pivotCells[key]) {
54011
+ const { displayTotals } = visibilityOptions;
53791
54012
  const numberOfDataRows = this.rows.length;
53792
54013
  const numberOfDataColumns = this.getNumberOfDataColumns();
53793
54014
  let pivotHeight = this.columns.length + numberOfDataRows;
53794
54015
  let pivotWidth = 1 /*(row headers)*/ + numberOfDataColumns;
53795
- if (!includeTotal && numberOfDataRows !== 1) {
54016
+ if (!displayTotals && numberOfDataRows !== 1) {
53796
54017
  pivotHeight -= 1;
53797
54018
  }
53798
- if (!includeTotal && numberOfDataColumns !== this.measures.length) {
54019
+ if (!displayTotals && numberOfDataColumns !== this.measures.length) {
53799
54020
  pivotWidth -= this.measures.length;
53800
54021
  }
53801
54022
  const domainArray = [];
53802
- const startRow = includeColumnHeaders ? 0 : this.columns.length;
54023
+ const skippedRows = this.getSkippedRows(visibilityOptions);
53803
54024
  for (let col = 0; col < pivotWidth; col++) {
53804
54025
  domainArray.push([]);
53805
- for (let row = startRow; row < pivotHeight; row++) {
53806
- if (!includeTotal && row === pivotHeight) {
54026
+ for (let row = 0; row < pivotHeight; row++) {
54027
+ if (skippedRows.has(row)) {
53807
54028
  continue;
53808
54029
  }
53809
- domainArray[col].push(this.getPivotCell(col, row, includeTotal));
54030
+ domainArray[col].push(this.getPivotCell(col, row, displayTotals));
53810
54031
  }
53811
54032
  }
53812
54033
  this.pivotCells[key] = domainArray;
@@ -59463,7 +59684,7 @@ class DataValidationPlugin extends CorePlugin {
59463
59684
  if (!rule)
59464
59685
  return false;
59465
59686
  return ((rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
59466
- rule.criterion.displayStyle === "arrow");
59687
+ (rule.criterion.displayStyle === "arrow" || rule.criterion.displayStyle === "chip"));
59467
59688
  }
59468
59689
  addDataValidationRule(sheetId, newRule) {
59469
59690
  const rules = this.rules[sheetId];
@@ -61997,7 +62218,9 @@ class TablePlugin extends CorePlugin {
61997
62218
  const ranges = cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData));
61998
62219
  const union = this.getters.getRangesUnion(ranges);
61999
62220
  const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
62000
- this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
62221
+ if (mergesInTarget.length) {
62222
+ this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
62223
+ }
62001
62224
  const id = this.consumeNextId();
62002
62225
  const config = cmd.config || DEFAULT_TABLE_CONFIG;
62003
62226
  const newTable = cmd.tableType === "dynamic"
@@ -62096,14 +62319,16 @@ class TablePlugin extends CorePlugin {
62096
62319
  const zoneToCheckIfEmpty = direction === "down"
62097
62320
  ? { ...zone, bottom: zone.bottom + 1, top: zone.bottom + 1 }
62098
62321
  : { ...zone, right: zone.right + 1, left: zone.right + 1 };
62099
- for (const position of positions(zoneToCheckIfEmpty)) {
62100
- const cellPosition = { sheetId, ...position };
62101
- // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content
62102
- const cellContent = this.getters.getCell(cellPosition)?.content;
62103
- if (cellContent ||
62104
- this.getters.isInMerge(cellPosition) ||
62105
- this.getTablesOverlappingZones(sheetId, [positionToZone(position)]).length) {
62106
- return "none";
62322
+ for (let row = zoneToCheckIfEmpty.top; row <= zoneToCheckIfEmpty.bottom; row++) {
62323
+ for (let col = zoneToCheckIfEmpty.left; col <= zoneToCheckIfEmpty.right; col++) {
62324
+ const cellPosition = { sheetId, col, row };
62325
+ // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content
62326
+ const cellContent = this.getters.getCell(cellPosition)?.content;
62327
+ if (cellContent ||
62328
+ this.getters.isInMerge(cellPosition) ||
62329
+ this.getTablesOverlappingZones(sheetId, [positionToZone(cellPosition)]).length) {
62330
+ return "none";
62331
+ }
62107
62332
  }
62108
62333
  }
62109
62334
  return direction;
@@ -65825,7 +66050,10 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65825
66050
  "getDataValidationInvalidCriterionValueMessage",
65826
66051
  "getInvalidDataValidationMessage",
65827
66052
  "getValidationResultForCellValue",
66053
+ "getDataValidationRangeValues",
65828
66054
  "isCellValidCheckbox",
66055
+ "getDataValidationCellStyle",
66056
+ "getDataValidationChipStyle",
65829
66057
  "isDataValidationInvalid",
65830
66058
  ];
65831
66059
  validationResults = {};
@@ -65846,6 +66074,18 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65846
66074
  isDataValidationInvalid(cellPosition) {
65847
66075
  return !this.getValidationResultForCell(cellPosition).isValid;
65848
66076
  }
66077
+ getDataValidationCellStyle(position) {
66078
+ if (this.hasChip(position)) {
66079
+ return undefined; // The style is not applied on the cell if it's a chip
66080
+ }
66081
+ return this.getDataValidationStyle(position);
66082
+ }
66083
+ getDataValidationChipStyle(position) {
66084
+ if (this.hasChip(position)) {
66085
+ return this.getDataValidationStyle(position) ?? { fillColor: GRAY_200 };
66086
+ }
66087
+ return undefined;
66088
+ }
65849
66089
  getInvalidDataValidationMessage(cellPosition) {
65850
66090
  const validationResult = this.getValidationResultForCell(cellPosition);
65851
66091
  return validationResult.isValid ? undefined : validationResult.error;
@@ -65868,6 +66108,11 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65868
66108
  }
65869
66109
  return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;
65870
66110
  }
66111
+ getDataValidationRangeValues(sheetId, criterion) {
66112
+ const range = this.getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
66113
+ const criterionValues = this.getters.getRangeValues(range);
66114
+ return criterionValues.map((value) => value?.toString()).filter(isDefined);
66115
+ }
65871
66116
  isCellValidCheckbox(cellPosition) {
65872
66117
  if (!this.getters.isMainCellPosition(cellPosition)) {
65873
66118
  return false;
@@ -65887,6 +66132,38 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65887
66132
  const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
65888
66133
  return error ? { error, rule, isValid: false } : VALID_RESULT;
65889
66134
  }
66135
+ hasChip(position) {
66136
+ const rule = this.getters.getValidationRuleForCell(position);
66137
+ return ((rule?.criterion.type === "isValueInList" || rule?.criterion.type === "isValueInRange") &&
66138
+ rule.criterion.displayStyle === "chip");
66139
+ }
66140
+ getDataValidationStyle(position) {
66141
+ const rule = this.getters.getValidationRuleForCell(position);
66142
+ if (!rule || this.isDataValidationInvalid(position)) {
66143
+ return undefined;
66144
+ }
66145
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
66146
+ const color = this.getValueColor(rule, evaluatedCell.value);
66147
+ if (!color) {
66148
+ return undefined;
66149
+ }
66150
+ const style = {
66151
+ fillColor: color,
66152
+ textColor: chipTextColor(color),
66153
+ };
66154
+ return style;
66155
+ }
66156
+ getValueColor(rule, value) {
66157
+ if (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange") {
66158
+ return undefined;
66159
+ }
66160
+ for (const criterionValue in rule.criterion.colors) {
66161
+ if (criterionValue.toLowerCase() === String(value).toLowerCase()) {
66162
+ return rule.criterion.colors[criterionValue];
66163
+ }
66164
+ }
66165
+ return undefined;
66166
+ }
65890
66167
  isValidFormula(value) {
65891
66168
  return !compile(value).isBadExpression;
65892
66169
  }
@@ -65983,12 +66260,35 @@ iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
65983
66260
  }
65984
66261
  return undefined;
65985
66262
  });
66263
+ iconsOnCellRegistry.add("data_validation_chip_icon", (getters, position) => {
66264
+ const chipStyle = getters.getDataValidationChipStyle(position);
66265
+ if (chipStyle) {
66266
+ const cellStyle = getters.getCellComputedStyle(position);
66267
+ return {
66268
+ svg: getChipSvg(chipStyle),
66269
+ hoverSvg: getHoveredChipSvg(chipStyle),
66270
+ priority: 10,
66271
+ horizontalAlign: "right",
66272
+ size: computeTextFontSizeInPixels(cellStyle),
66273
+ margin: 4,
66274
+ position,
66275
+ onClick: (position, env) => {
66276
+ const { col, row } = position;
66277
+ env.model.selection.selectCell(col, row);
66278
+ env.startCellEdition();
66279
+ },
66280
+ type: "data_validation_chip_icon",
66281
+ };
66282
+ }
66283
+ return undefined;
66284
+ });
65986
66285
  iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
65987
66286
  const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
65988
66287
  if (hasIcon) {
66288
+ const cellStyle = getters.getCellComputedStyle(position);
65989
66289
  return {
65990
- svg: CARET_DOWN,
65991
- hoverSvg: HOVERED_CARET_DOWN,
66290
+ svg: getCaretDownSvg(cellStyle),
66291
+ hoverSvg: getHoveredCaretDownSvg(cellStyle),
65992
66292
  priority: 2,
65993
66293
  horizontalAlign: "right",
65994
66294
  size: GRID_ICON_EDGE_LENGTH,
@@ -67256,10 +67556,15 @@ class PivotUIPlugin extends CoreViewPlugin {
67256
67556
  const includeTotal = toScalar(args[2]);
67257
67557
  const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal);
67258
67558
  const includeColumnHeaders = toScalar(args[3]);
67559
+ const includeMeasures = toScalar(args[5]);
67560
+ const shouldIncludeMeasures = includeMeasures === undefined ? true : toBoolean(includeMeasures);
67259
67561
  const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
67260
- const pivotCells = pivot
67261
- .getCollapsedTableStructure()
67262
- .getPivotCells(shouldIncludeTotal, shouldIncludeColumnHeaders);
67562
+ const visibilityOptions = {
67563
+ displayColumnHeaders: shouldIncludeColumnHeaders,
67564
+ displayTotals: shouldIncludeTotal,
67565
+ displayMeasuresRow: shouldIncludeMeasures,
67566
+ };
67567
+ const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(visibilityOptions);
67263
67568
  const pivotCol = position.col - mainPosition.col;
67264
67569
  const pivotRow = position.row - mainPosition.row;
67265
67570
  return pivotCells[pivotCol][pivotRow];
@@ -68525,11 +68830,11 @@ class OTRegistry extends Registry {
68525
68830
  * transformation function given
68526
68831
  */
68527
68832
  addTransformation(executed, toTransforms, fn) {
68833
+ if (!this.content[executed]) {
68834
+ this.content[executed] = new Map();
68835
+ }
68528
68836
  for (const toTransform of toTransforms) {
68529
- if (!this.content[toTransform]) {
68530
- this.content[toTransform] = new Map();
68531
- }
68532
- this.content[toTransform].set(executed, fn);
68837
+ this.content[executed].set(toTransform, fn);
68533
68838
  }
68534
68839
  return this;
68535
68840
  }
@@ -68538,7 +68843,7 @@ class OTRegistry extends Registry {
68538
68843
  * that the executed command happened.
68539
68844
  */
68540
68845
  getTransformation(toTransform, executed) {
68541
- return this.content[toTransform] && this.content[toTransform].get(executed);
68846
+ return this.content[executed] && this.content[executed].get(toTransform);
68542
68847
  }
68543
68848
  }
68544
68849
  const otRegistry = new OTRegistry();
@@ -68868,10 +69173,20 @@ function adaptTransform(toTransform, executed) {
68868
69173
  */
68869
69174
  function transformAll(toTransform, executed) {
68870
69175
  let transformedCommands = [...toTransform];
69176
+ const possibleTransformations = new Set(otRegistry.getKeys());
68871
69177
  for (const executedCommand of executed) {
68872
- transformedCommands = transformedCommands
68873
- .map((cmd) => transform(cmd, executedCommand))
68874
- .filter(isDefined);
69178
+ // If the executed command is not in the registry, we skip it
69179
+ // because we know there won't be any transformation impacting the
69180
+ // commands to transform.
69181
+ if (possibleTransformations.has(executedCommand.type)) {
69182
+ transformedCommands = transformedCommands.reduce((acc, cmd) => {
69183
+ const transformed = transform(cmd, executedCommand);
69184
+ if (transformed) {
69185
+ acc.push(transformed);
69186
+ }
69187
+ return acc;
69188
+ }, []);
69189
+ }
68875
69190
  }
68876
69191
  return transformedCommands;
68877
69192
  }
@@ -69481,8 +69796,7 @@ class CollaborativePlugin extends UIPlugin {
69481
69796
  "isFullySynchronized",
69482
69797
  ];
69483
69798
  static layers = ["Selection"];
69484
- availableColors = new AlternatingColorGenerator(12);
69485
- colors = {};
69799
+ colors = new AlternatingColorMap(12);
69486
69800
  session;
69487
69801
  constructor(config) {
69488
69802
  super(config);
@@ -69500,7 +69814,7 @@ class CollaborativePlugin extends UIPlugin {
69500
69814
  }
69501
69815
  getConnectedClients() {
69502
69816
  return [...this.session.getConnectedClients()].map((client) => {
69503
- return { ...client, color: this.colors[client.id] };
69817
+ return { ...client, color: this.colors.get(client.id) };
69504
69818
  });
69505
69819
  }
69506
69820
  isFullySynchronized() {
@@ -69529,10 +69843,7 @@ class CollaborativePlugin extends UIPlugin {
69529
69843
  client.position &&
69530
69844
  client.position.sheetId === sheetId &&
69531
69845
  this.isPositionValid(client.position)) {
69532
- if (!this.colors[client.id]) {
69533
- this.colors[client.id] = this.availableColors.next();
69534
- }
69535
- clients.push({ ...client, color: this.colors[client.id], position: client.position });
69846
+ clients.push({ ...client, position: client.position });
69536
69847
  }
69537
69848
  }
69538
69849
  return clients;
@@ -70432,6 +70743,9 @@ class SheetUIPlugin extends UIPlugin {
70432
70743
  for (const icon of this.getters.getCellIcons(position)) {
70433
70744
  contentWidth += icon.margin + icon.size;
70434
70745
  }
70746
+ if (this.getters.getDataValidationChipStyle(position)) {
70747
+ contentWidth += DATA_VALIDATION_CHIP_MARGIN * 2;
70748
+ }
70435
70749
  if (contentWidth === 0) {
70436
70750
  return 0;
70437
70751
  }
@@ -70589,7 +70903,7 @@ class SheetUIPlugin extends UIPlugin {
70589
70903
  }
70590
70904
  const position = this.getters.getCellPosition(cell.id);
70591
70905
  const colSize = this.getters.getColSize(sheetId, position.col);
70592
- if (cell.isFormula) {
70906
+ if (cell.isFormula || this.getters.getArrayFormulaSpreadingOn(position)) {
70593
70907
  const content = this.getters.getEvaluatedCell(position).formattedValue;
70594
70908
  const evaluatedSize = getCellContentHeight(this.ctx, content, cell?.style, colSize);
70595
70909
  if (evaluatedSize > evaluatedRowSize && evaluatedSize > DEFAULT_CELL_HEIGHT) {
@@ -70792,6 +71106,8 @@ class CellComputedStylePlugin extends UIPlugin {
70792
71106
  if (invalidateEvaluationCommands.has(cmd.type) ||
70793
71107
  cmd.type === "UPDATE_CELL" ||
70794
71108
  cmd.type === "SET_FORMATTING" ||
71109
+ cmd.type === "ADD_DATA_VALIDATION_RULE" ||
71110
+ cmd.type === "REMOVE_DATA_VALIDATION_RULE" ||
70795
71111
  cmd.type === "EVALUATE_CELLS") {
70796
71112
  this.styles = {};
70797
71113
  this.borders = {};
@@ -70863,8 +71179,10 @@ class CellComputedStylePlugin extends UIPlugin {
70863
71179
  const cell = this.getters.getCell(position);
70864
71180
  const cfStyle = this.getters.getCellConditionalFormatStyle(position);
70865
71181
  const tableStyle = this.getters.getCellTableStyle(position);
71182
+ const dataValidationStyle = this.getters.getDataValidationCellStyle(position);
70866
71183
  const computedStyle = {
70867
71184
  ...removeFalsyAttributes(tableStyle),
71185
+ ...removeFalsyAttributes(dataValidationStyle),
70868
71186
  ...removeFalsyAttributes(cell?.style),
70869
71187
  ...removeFalsyAttributes(cfStyle),
70870
71188
  };
@@ -72406,9 +72724,10 @@ class FilterEvaluationPlugin extends UIPlugin {
72406
72724
  const filteredValues = filterValue.hiddenValues?.map(toLowerCase);
72407
72725
  if (!filteredValues)
72408
72726
  continue;
72727
+ const filteredValuesSet = new Set(filteredValues);
72409
72728
  for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
72410
72729
  const value = this.getCellValueAsString(sheetId, filter.col, row);
72411
- if (filteredValues.includes(value)) {
72730
+ if (filteredValuesSet.has(value)) {
72412
72731
  hiddenRows.add(row);
72413
72732
  }
72414
72733
  }
@@ -74425,19 +74744,29 @@ autoCompleteProviders.add("dataValidation", {
74425
74744
  (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange")) {
74426
74745
  return [];
74427
74746
  }
74428
- let values;
74429
- if (rule.criterion.type === "isValueInList") {
74430
- values = rule.criterion.values;
74431
- }
74432
- else {
74433
- const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
74434
- values = Array.from(new Set(this.getters
74435
- .getRangeValues(range)
74436
- .filter(isNotNull)
74437
- .map((value) => value.toString())
74438
- .filter((val) => val !== "")));
74439
- }
74440
- return values.map((value) => ({ text: value }));
74747
+ const sheetId = this.composer.currentEditedCell.sheetId;
74748
+ const values = rule.criterion.type === "isValueInRange"
74749
+ ? Array.from(new Set(this.getters.getDataValidationRangeValues(sheetId, rule.criterion)))
74750
+ : rule.criterion.values;
74751
+ const isChip = rule.criterion.displayStyle === "chip";
74752
+ if (!isChip) {
74753
+ return values.map((value) => ({ text: value }));
74754
+ }
74755
+ const colors = rule.criterion.colors;
74756
+ return values.map((value) => {
74757
+ const color = colors?.[value];
74758
+ return {
74759
+ text: value,
74760
+ htmlContent: [
74761
+ {
74762
+ value,
74763
+ color: color ? chipTextColor(color) : undefined,
74764
+ backgroundColor: color || GRAY_200,
74765
+ classes: ["badge rounded-pill fs-6 fw-normal w-100 mt-1 text-start"],
74766
+ },
74767
+ ],
74768
+ };
74769
+ });
74441
74770
  },
74442
74771
  selectProposal(tokenAtCursor, value) {
74443
74772
  this.composer.setCurrentContent(value);
@@ -75090,7 +75419,7 @@ topbarMenuRegistry
75090
75419
  })
75091
75420
  .addChild("settings", ["file"], {
75092
75421
  name: _t("Settings"),
75093
- sequence: 100,
75422
+ sequence: 200,
75094
75423
  execute: (env) => env.openSidePanel("Settings"),
75095
75424
  isEnabled: (env) => !env.isSmall,
75096
75425
  icon: "o-spreadsheet-Icon.COG",
@@ -77999,7 +78328,7 @@ css /* scss */ `
77999
78328
 
78000
78329
  .o-spreadsheet-topbar {
78001
78330
  line-height: 1.2;
78002
- font-size: 13px;
78331
+ font-size: 14px;
78003
78332
  font-weight: 500;
78004
78333
  background-color: #fff;
78005
78334
 
@@ -83254,9 +83583,9 @@ const constants = {
83254
83583
  };
83255
83584
  const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
83256
83585
 
83257
- 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 };
83586
+ export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, ClientDisconnectedError, 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 };
83258
83587
 
83259
83588
 
83260
- __info__.version = "18.4.0-alpha.7";
83261
- __info__.date = "2025-06-06T09:32:44.285Z";
83262
- __info__.hash = "2bfbe64";
83589
+ __info__.version = "18.4.0-alpha.9";
83590
+ __info__.date = "2025-06-19T18:23:22.025Z";
83591
+ __info__.hash = "6d4d685";