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