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