@odoo/o-spreadsheet 18.4.0-alpha.8 → 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.8
6
- * @date 2025-06-12T09:53:48.133Z
7
- * @hash 9b7a8d0
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
@@ -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
@@ -21688,6 +21701,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21688
21701
  if (isTrendLineAxis(dataset.xAxisID) || dataset.hidden) {
21689
21702
  continue;
21690
21703
  }
21704
+ const yAxisScale = chart.scales[dataset.yAxisID];
21691
21705
  for (let i = 0; i < dataset._parsed.length; i++) {
21692
21706
  const parsedValue = dataset._parsed[i];
21693
21707
  const value = Number(chart.config.type === "radar" ? parsedValue.r : parsedValue.y);
@@ -21698,10 +21712,18 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21698
21712
  const xPosition = point.x;
21699
21713
  let yPosition = 0;
21700
21714
  if (chart.config.type === "line" || chart.config.type === "radar") {
21701
- yPosition = point.y - 10;
21715
+ yPosition = value < 0 ? point.y + 10 : point.y - 10;
21702
21716
  }
21703
21717
  else {
21704
- 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
+ }
21705
21727
  }
21706
21728
  yPosition = Math.min(yPosition, yMax);
21707
21729
  yPosition = Math.max(yPosition, yMin);
@@ -21711,7 +21733,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21711
21733
  }
21712
21734
  for (const otherPosition of textsPositions[xPosition] || []) {
21713
21735
  if (Math.abs(otherPosition - yPosition) < 13) {
21714
- yPosition = otherPosition - 13;
21736
+ yPosition = value < 0 ? otherPosition + 13 : otherPosition - 13;
21715
21737
  }
21716
21738
  }
21717
21739
  textsPositions[xPosition].push(yPosition);
@@ -21730,6 +21752,8 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21730
21752
  if (isTrendLineAxis(dataset.xAxisID)) {
21731
21753
  return; // ignore trend lines
21732
21754
  }
21755
+ const xAxisScale = chart.scales[dataset.xAxisID];
21756
+ const xZeroLine = xAxisScale.getPixelForValue(0);
21733
21757
  for (let i = 0; i < dataset._parsed.length; i++) {
21734
21758
  const value = Number(dataset._parsed[i].x);
21735
21759
  if (isNaN(value)) {
@@ -21738,17 +21762,27 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21738
21762
  const displayValue = options.callback(value, dataset, i);
21739
21763
  const point = dataset.data[i];
21740
21764
  const yPosition = point.y;
21741
- let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21742
- xPosition = Math.min(xPosition, xMax);
21743
- 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
+ }
21744
21778
  // Avoid overlapping texts with same Y
21745
21779
  if (!textsPositions[yPosition]) {
21746
21780
  textsPositions[yPosition] = [];
21747
21781
  }
21748
- const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21749
21782
  for (const otherPosition of textsPositions[yPosition]) {
21750
21783
  if (Math.abs(otherPosition - xPosition) < textWidth) {
21751
- xPosition = otherPosition + textWidth + 3;
21784
+ xPosition =
21785
+ value < 0 ? otherPosition - textWidth - PADDING : otherPosition + textWidth + PADDING;
21752
21786
  }
21753
21787
  }
21754
21788
  textsPositions[yPosition].push(xPosition);
@@ -25682,7 +25716,9 @@ function getPyramidChartShowValues(definition, args) {
25682
25716
  background: definition.background,
25683
25717
  callback: (value, dataset) => {
25684
25718
  value = Math.abs(Number(value));
25685
- return formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25719
+ return value === 0
25720
+ ? ""
25721
+ : formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25686
25722
  },
25687
25723
  };
25688
25724
  }
@@ -29343,6 +29379,12 @@ class Menu extends owl.Component {
29343
29379
  menu.onStopHover?.(this.env);
29344
29380
  this.props.onMouseLeave?.(menu, ev);
29345
29381
  }
29382
+ onClickMenu(menu, ev) {
29383
+ if (!this.isEnabled(menu)) {
29384
+ return;
29385
+ }
29386
+ this.props.onClickMenu?.(menu, ev);
29387
+ }
29346
29388
  }
29347
29389
 
29348
29390
  /**
@@ -29841,9 +29883,6 @@ class MenuPopover extends owl.Component {
29841
29883
  this.subMenu.parentMenu = undefined;
29842
29884
  }
29843
29885
  onClickMenu(menu, ev) {
29844
- if (!this.isEnabled(menu)) {
29845
- return;
29846
- }
29847
29886
  if (this.isRoot(menu)) {
29848
29887
  this.openSubMenu(menu, ev.currentTarget);
29849
29888
  }
@@ -31094,11 +31133,9 @@ criterionEvaluatorRegistry.add("isValueInRange", {
31094
31133
  if (!value) {
31095
31134
  return false;
31096
31135
  }
31097
- const range = getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
31098
- const criterionValues = getters.getRangeValues(range);
31136
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
31099
31137
  return criterionValues
31100
- .filter(isNotNull)
31101
- .map((value) => value.toString().toLowerCase())
31138
+ .map((value) => value.toLowerCase())
31102
31139
  .includes(value.toString().toLowerCase());
31103
31140
  },
31104
31141
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
@@ -31270,6 +31307,12 @@ class TextValueProvider extends owl.Component {
31270
31307
  selectedElement?.scrollIntoView?.({ block: "nearest" });
31271
31308
  }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
31272
31309
  }
31310
+ getCss(html) {
31311
+ return cssPropertiesToCss({
31312
+ color: html.color || "#000000",
31313
+ background: html.backgroundColor,
31314
+ });
31315
+ }
31273
31316
  }
31274
31317
 
31275
31318
  class ContentEditableHelper {
@@ -31408,7 +31451,6 @@ class ContentEditableHelper {
31408
31451
  // We can only modify a node in place if it has the same type as the content
31409
31452
  // that we would insert, which are spans.
31410
31453
  // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
31411
- // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)
31412
31454
  const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
31413
31455
  if (childIsSpan && compareContentToSpanElement(content, child)) {
31414
31456
  continue;
@@ -31443,9 +31485,7 @@ class ContentEditableHelper {
31443
31485
  }
31444
31486
  // Empty line
31445
31487
  if (!p.hasChildNodes()) {
31446
- const span = document.createElement("span");
31447
- span.appendChild(document.createElement("br"));
31448
- p.appendChild(span);
31488
+ p.appendChild(document.createElement("span"));
31449
31489
  }
31450
31490
  // replace p if necessary
31451
31491
  if (newChild) {
@@ -31492,13 +31532,10 @@ class ContentEditableHelper {
31492
31532
  }
31493
31533
  getText() {
31494
31534
  let text = "";
31495
- const it = iterateChildren(this.el);
31496
- let current = it.next();
31497
31535
  let isFirstParagraph = true;
31498
- while (!current.done) {
31499
- if (!current.value.hasChildNodes()) {
31500
- text += current.value.textContent;
31501
- }
31536
+ let emptyParagraph = false;
31537
+ const it = iterateChildren(this.el);
31538
+ for (let current = it.next(); !current.done; current = it.next()) {
31502
31539
  if (current.value.nodeName === "P" ||
31503
31540
  (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
31504
31541
  ) {
@@ -31508,8 +31545,15 @@ class ContentEditableHelper {
31508
31545
  else {
31509
31546
  text += NEWLINE;
31510
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;
31511
31556
  }
31512
- current = it.next();
31513
31557
  }
31514
31558
  return text;
31515
31559
  }
@@ -32859,6 +32903,12 @@ class Composer extends owl.Component {
32859
32903
  owl.useEffect(() => {
32860
32904
  this.processTokenAtCursor();
32861
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
+ ]);
32862
32912
  }
32863
32913
  // ---------------------------------------------------------------------------
32864
32914
  // Handlers
@@ -33069,6 +33119,7 @@ class Composer extends owl.Component {
33069
33119
  if (this.env.isMobile() && !isIOS()) {
33070
33120
  return;
33071
33121
  }
33122
+ this.debouncedHover.stopDebounce();
33072
33123
  this.contentHelper.removeSelection();
33073
33124
  }
33074
33125
  onMouseup() {
@@ -33147,7 +33198,6 @@ class Composer extends owl.Component {
33147
33198
  const { start, end } = this.props.composerStore.composerSelection;
33148
33199
  this.contentHelper.selectRange(start, end);
33149
33200
  }
33150
- this.contentHelper.scrollSelectionIntoView();
33151
33201
  }
33152
33202
  this.shouldProcessInputEvents = true;
33153
33203
  }
@@ -33623,6 +33673,414 @@ class SingleInputCriterionForm extends CriterionForm {
33623
33673
  }
33624
33674
  }
33625
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
+
33626
34084
  css /* scss */ `
33627
34085
  .o-dv-list-item-delete {
33628
34086
  color: #666666;
@@ -33631,7 +34089,7 @@ css /* scss */ `
33631
34089
  `;
33632
34090
  class ListCriterionForm extends CriterionForm {
33633
34091
  static template = "o-spreadsheet-ListCriterionForm";
33634
- static components = { CriterionInput };
34092
+ static components = { CriterionInput, RoundColorPicker };
33635
34093
  state = owl.useState({
33636
34094
  numberOfValues: Math.max(this.props.criterion.values.length, 2),
33637
34095
  });
@@ -33639,7 +34097,7 @@ class ListCriterionForm extends CriterionForm {
33639
34097
  super.setup();
33640
34098
  const setupDefault = (props) => {
33641
34099
  if (props.criterion.displayStyle === undefined) {
33642
- this.updateCriterion({ displayStyle: "arrow" });
34100
+ this.updateCriterion({ displayStyle: "chip" });
33643
34101
  }
33644
34102
  };
33645
34103
  owl.onWillUpdateProps(setupDefault);
@@ -33650,6 +34108,11 @@ class ListCriterionForm extends CriterionForm {
33650
34108
  values[index] = value;
33651
34109
  this.updateCriterion({ values });
33652
34110
  }
34111
+ onColorChanged(color, value) {
34112
+ const colors = { ...this.props.criterion.colors };
34113
+ colors[value] = color || undefined;
34114
+ this.updateCriterion({ colors });
34115
+ }
33653
34116
  onAddAnotherValue() {
33654
34117
  this.state.numberOfValues++;
33655
34118
  }
@@ -33685,35 +34148,6 @@ class ListCriterionForm extends CriterionForm {
33685
34148
  }
33686
34149
  }
33687
34150
 
33688
- /**
33689
- * Start listening to pointer events and apply the given callbacks.
33690
- *
33691
- * @returns A function to remove the listeners.
33692
- */
33693
- function startDnd(onPointerMove, onPointerUp) {
33694
- const removeListeners = () => {
33695
- window.removeEventListener("pointerup", _onPointerUp, { capture: true });
33696
- window.removeEventListener("dragstart", _onDragStart);
33697
- window.removeEventListener("pointermove", onPointerMove);
33698
- window.removeEventListener("wheel", onPointerMove);
33699
- };
33700
- const _onPointerUp = (ev) => {
33701
- onPointerUp(ev);
33702
- removeListeners();
33703
- };
33704
- function _onDragStart(ev) {
33705
- ev.preventDefault();
33706
- }
33707
- window.addEventListener("pointerup", _onPointerUp, { capture: true });
33708
- window.addEventListener("dragstart", _onDragStart);
33709
- window.addEventListener("pointermove", onPointerMove);
33710
- // mouse wheel on window is by default a passive event.
33711
- // preventDefault() is not allowed in passive event handler.
33712
- // https://chromestatus.com/feature/6662647093133312
33713
- window.addEventListener("wheel", onPointerMove, { passive: false });
33714
- return removeListeners;
33715
- }
33716
-
33717
34151
  function useDragAndDropListItems() {
33718
34152
  let dndHelper;
33719
34153
  const previousCursor = document.body.style.cursor;
@@ -34573,12 +35007,12 @@ class SelectionInput extends owl.Component {
34573
35007
 
34574
35008
  class ValueInRangeCriterionForm extends CriterionForm {
34575
35009
  static template = "o-spreadsheet-ValueInRangeCriterionForm";
34576
- static components = { SelectionInput };
35010
+ static components = { RoundColorPicker, SelectionInput };
34577
35011
  setup() {
34578
35012
  super.setup();
34579
35013
  const setupDefault = (props) => {
34580
35014
  if (props.criterion.displayStyle === undefined) {
34581
- this.updateCriterion({ displayStyle: "arrow" });
35015
+ this.updateCriterion({ displayStyle: "chip" });
34582
35016
  }
34583
35017
  };
34584
35018
  owl.onWillUpdateProps(setupDefault);
@@ -34591,6 +35025,16 @@ class ValueInRangeCriterionForm extends CriterionForm {
34591
35025
  const displayStyle = ev.target.value;
34592
35026
  this.updateCriterion({ displayStyle });
34593
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
+ }
34594
35038
  }
34595
35039
 
34596
35040
  const criterionCategoriesSequences = {
@@ -36169,19 +36613,44 @@ const RED_DOT = {
36169
36613
  height: 512,
36170
36614
  paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36171
36615
  };
36172
- const CARET_DOWN = {
36173
- width: 512,
36174
- height: 512,
36175
- paths: [{ fillColor: TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
36176
- };
36177
- const HOVERED_CARET_DOWN = {
36178
- width: 512,
36179
- height: 512,
36180
- paths: [
36181
- { fillColor: TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
36182
- { fillColor: "#fff", path: "M120 195 h270 l-135 130" },
36183
- ],
36184
- };
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
+ }
36185
36654
  const CHECKBOX_UNCHECKED = {
36186
36655
  width: 512,
36187
36656
  height: 512,
@@ -41089,6 +41558,10 @@ const REMOVE_ROWS_ACTION = (env) => {
41089
41558
  });
41090
41559
  };
41091
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
+ }
41092
41565
  const sheetId = env.model.getters.getActiveSheetId();
41093
41566
  const selectedElements = env.model.getters.getElementsFromSelection(dimension);
41094
41567
  const includesAllVisibleHeaders = env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, dimension, selectedElements);
@@ -42643,7 +43116,7 @@ const insertDropdown = {
42643
43116
  criterion: {
42644
43117
  type: "isValueInList",
42645
43118
  values: [],
42646
- displayStyle: "arrow",
43119
+ displayStyle: "chip",
42647
43120
  },
42648
43121
  },
42649
43122
  });
@@ -44558,6 +45031,11 @@ class GridComposer extends owl.Component {
44558
45031
  rect = this.defaultRect;
44559
45032
  isEditing = false;
44560
45033
  isCellReferenceVisible = false;
45034
+ currentEditedCell = {
45035
+ col: 0,
45036
+ row: 0,
45037
+ sheetId: this.env.model.getters.getActiveSheetId(),
45038
+ };
44561
45039
  composerStore;
44562
45040
  composerFocusStore;
44563
45041
  composerInterface;
@@ -44682,12 +45160,17 @@ class GridComposer extends owl.Component {
44682
45160
  if (!isEditing && this.composerFocusStore.activeComposer !== this.composerInterface) {
44683
45161
  this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "inactive" });
44684
45162
  }
45163
+ let shouldRecomputeRect = !deepEquals(this.currentEditedCell, this.composerStore.currentEditedCell);
44685
45164
  if (this.isEditing !== isEditing) {
44686
45165
  this.isEditing = isEditing;
44687
45166
  if (!isEditing) {
44688
45167
  this.rect = this.defaultRect;
44689
45168
  return;
44690
45169
  }
45170
+ this.currentEditedCell = this.composerStore.currentEditedCell;
45171
+ shouldRecomputeRect = true;
45172
+ }
45173
+ if (shouldRecomputeRect) {
44691
45174
  const position = this.env.model.getters.getActivePosition();
44692
45175
  const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
44693
45176
  this.rect = this.env.model.getters.getVisibleRect(zone);
@@ -45793,11 +46276,14 @@ class GridOverlay extends owl.Component {
45793
46276
  onCellClicked(ev) {
45794
46277
  const openedPopover = this.cellPopovers.persistentCellPopover;
45795
46278
  const [col, row] = this.getCartesianCoordinates(ev);
46279
+ const clickedIcon = this.getInteractiveIconAtEvent(ev);
46280
+ if (clickedIcon) {
46281
+ this.env.model.selection.getBackToDefault();
46282
+ }
45796
46283
  this.props.onCellClicked(col, row, {
45797
46284
  expandZone: ev.shiftKey,
45798
46285
  addZone: isCtrlKey(ev),
45799
46286
  }, ev);
45800
- const clickedIcon = this.getInteractiveIconAtEvent(ev);
45801
46287
  if (clickedIcon?.onClick) {
45802
46288
  clickedIcon.onClick(clickedIcon.position, this.env);
45803
46289
  }
@@ -46662,6 +47148,19 @@ class GridRenderer {
46662
47148
  const width = box.width * (percentage / 100);
46663
47149
  ctx.fillRect(box.x, box.y, width, box.height);
46664
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
+ }
46665
47164
  if (box.overlayColor) {
46666
47165
  ctx.fillStyle = box.overlayColor;
46667
47166
  ctx.fillRect(box.x, box.y, box.width, box.height);
@@ -46801,19 +47300,6 @@ class GridRenderer {
46801
47300
  ctx.font = font;
46802
47301
  }
46803
47302
  ctx.fillStyle = style.textColor || "#000";
46804
- // compute horizontal align start point parameter
46805
- let x = box.x;
46806
- if (align === "left") {
46807
- const leftIconSize = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46808
- x += MIN_CELL_TEXT_MARGIN + leftIconSize;
46809
- }
46810
- else if (align === "right") {
46811
- const rightIconSize = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46812
- x += box.width - MIN_CELL_TEXT_MARGIN - rightIconSize;
46813
- }
46814
- else {
46815
- x += box.width / 2;
46816
- }
46817
47303
  // horizontal align text direction
46818
47304
  ctx.textAlign = align;
46819
47305
  // clip rect if needed
@@ -46824,15 +47310,13 @@ class GridRenderer {
46824
47310
  ctx.rect(x, y, width, height);
46825
47311
  ctx.clip();
46826
47312
  }
46827
- // compute vertical align start point parameter:
46828
- const textLineHeight = computeTextFontSizeInPixels(style);
46829
- const numberOfLines = box.content.textLines.length;
46830
- let y = this.getters.computeTextYCoordinate(box, textLineHeight, style.verticalAlign, numberOfLines);
47313
+ const x = box.content.x;
47314
+ let y = box.content.y;
46831
47315
  // use the horizontal and the vertical start points to:
46832
47316
  // fill text / fill strikethrough / fill underline
46833
47317
  for (const brokenLine of box.content.textLines) {
46834
- drawDecoratedText(ctx, brokenLine, { x: Math.round(x), y: Math.round(y) }, style.underline, style.strikethrough);
46835
- 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;
46836
47320
  }
46837
47321
  if (box.clipRect) {
46838
47322
  ctx.restore();
@@ -47067,11 +47551,15 @@ class GridRenderer {
47067
47551
  const showFormula = this.getters.shouldShowFormulas();
47068
47552
  const { x, y, width, height } = this.getters.getRect(zone);
47069
47553
  const { verticalAlign } = this.getters.getCellStyle(position);
47554
+ const chipStyle = this.getters.getDataValidationChipStyle(position);
47070
47555
  let style = this.getters.getCellComputedStyle(position);
47071
47556
  if (this.fingerprints.isEnabled) {
47072
47557
  const fingerprintColor = this.fingerprints.colors.get(position);
47073
47558
  style = { ...style, fillColor: fingerprintColor };
47074
47559
  }
47560
+ if (chipStyle?.textColor) {
47561
+ style = { ...style, textColor: chipStyle.textColor };
47562
+ }
47075
47563
  const dataBarFill = this.fingerprints.isEnabled
47076
47564
  ? undefined
47077
47565
  : this.getters.getConditionalDataBar(position);
@@ -47105,22 +47593,55 @@ class GridRenderer {
47105
47593
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
47106
47594
  const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
47107
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;
47108
47597
  const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
47598
+ const leftMargin = leftIconWidth + chipMargin;
47109
47599
  const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
47110
- const contentWidth = leftIconWidth + textWidth + rightIconWidth;
47600
+ const rightMargin = rightIconWidth + chipMargin;
47601
+ const contentWidth = leftMargin + textWidth + rightMargin;
47111
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);
47112
47619
  box.content = {
47113
47620
  textLines: multiLineText,
47114
47621
  width: wrapping === "overflow" ? textWidth : width,
47115
47622
  align,
47623
+ x: contentX,
47624
+ y: contentY,
47625
+ fontSizePx: fontSizePX,
47116
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
+ }
47117
47638
  /** ClipRect */
47118
47639
  const isOverflowing = contentWidth > width || fontSizePX > height;
47119
- if (box.icons.left || box.icons.right) {
47640
+ if (box.icons.left || box.icons.right || box.chip) {
47120
47641
  box.clipRect = {
47121
- x: box.x + leftIconWidth,
47642
+ x: box.x + leftMargin,
47122
47643
  y: box.y,
47123
- width: Math.max(0, width - leftIconWidth - rightIconWidth),
47644
+ width: Math.max(0, width - leftMargin - rightMargin),
47124
47645
  height,
47125
47646
  };
47126
47647
  }
@@ -47820,15 +48341,6 @@ class Selection extends owl.Component {
47820
48341
  }
47821
48342
  }
47822
48343
 
47823
- class Section extends owl.Component {
47824
- static template = "o_spreadsheet.Section";
47825
- static props = {
47826
- class: { type: String, optional: true },
47827
- title: { type: String, optional: true },
47828
- slots: Object,
47829
- };
47830
- }
47831
-
47832
48344
  class ChartDataSeries extends owl.Component {
47833
48345
  static template = "o-spreadsheet.ChartDataSeries";
47834
48346
  static components = { SelectionInput, Section };
@@ -48377,325 +48889,6 @@ class ActionButton extends owl.Component {
48377
48889
  }
48378
48890
  }
48379
48891
 
48380
- const LINE_VERTICAL_PADDING = 1;
48381
- const PICKER_PADDING = 8;
48382
- const ITEM_BORDER_WIDTH = 1;
48383
- const ITEM_EDGE_LENGTH = 18;
48384
- const ITEMS_PER_LINE = 10;
48385
- const MAGNIFIER_EDGE = 16;
48386
- const ITEM_GAP = 2;
48387
- const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
48388
- const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
48389
- const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
48390
- const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
48391
- css /* scss */ `
48392
- .o-color-picker {
48393
- padding: ${PICKER_PADDING}px 0;
48394
- /* FIXME: this is useless, overiden by the popover container */
48395
- box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
48396
- background-color: white;
48397
- line-height: 1.2;
48398
- overflow-y: auto;
48399
- overflow-x: hidden;
48400
- width: ${CONTAINER_WIDTH}px;
48401
-
48402
- .o-color-picker-section-name {
48403
- margin: 0px ${ITEM_BORDER_WIDTH}px;
48404
- padding: 4px ${PICKER_PADDING}px;
48405
- }
48406
- .colors-grid {
48407
- display: grid;
48408
- padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
48409
- grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
48410
- grid-gap: ${ITEM_GAP}px;
48411
- }
48412
- .o-color-picker-toggler-button {
48413
- display: flex;
48414
- .o-color-picker-toggler-sign {
48415
- display: flex;
48416
- margin: auto auto;
48417
- width: 55%;
48418
- height: 55%;
48419
- .o-icon {
48420
- width: 100%;
48421
- height: 100%;
48422
- }
48423
- }
48424
- }
48425
- .o-color-picker-line-item {
48426
- width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48427
- height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48428
- margin: 0px;
48429
- border-radius: 50px;
48430
- border: ${ITEM_BORDER_WIDTH}px solid #666666;
48431
- padding: 0px;
48432
- font-size: 16px;
48433
- background: white;
48434
- &:hover {
48435
- background-color: rgba(0, 0, 0, 0.08);
48436
- outline: 1px solid gray;
48437
- cursor: pointer;
48438
- }
48439
- }
48440
- .o-buttons {
48441
- padding: ${PICKER_PADDING}px;
48442
- display: flex;
48443
- .o-cancel {
48444
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48445
- width: 100%;
48446
- padding: 5px;
48447
- font-size: 14px;
48448
- background: white;
48449
- border-radius: 4px;
48450
- &:hover:enabled {
48451
- background-color: rgba(0, 0, 0, 0.08);
48452
- }
48453
- }
48454
- }
48455
- .o-add-button {
48456
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48457
- padding: 4px;
48458
- background: white;
48459
- border-radius: 4px;
48460
- &:hover:enabled {
48461
- background-color: rgba(0, 0, 0, 0.08);
48462
- }
48463
- }
48464
- .o-separator {
48465
- border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
48466
- margin-top: ${MENU_SEPARATOR_PADDING}px;
48467
- margin-bottom: ${MENU_SEPARATOR_PADDING}px;
48468
- }
48469
-
48470
- .o-custom-selector {
48471
- padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
48472
- position: relative;
48473
- .o-gradient {
48474
- margin-bottom: ${MAGNIFIER_EDGE / 2}px;
48475
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48476
- width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
48477
- height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
48478
- position: relative;
48479
- }
48480
-
48481
- .magnifier {
48482
- height: ${MAGNIFIER_EDGE}px;
48483
- width: ${MAGNIFIER_EDGE}px;
48484
- border-radius: 50%;
48485
- border: 2px solid #fff;
48486
- box-shadow: 0px 0px 3px #c0c0c0;
48487
- position: absolute;
48488
- z-index: 2;
48489
- }
48490
- .saturation {
48491
- background: linear-gradient(to right, #fff 0%, transparent 100%);
48492
- }
48493
- .lightness {
48494
- background: linear-gradient(to top, #000 0%, transparent 100%);
48495
- }
48496
- .o-hue-picker {
48497
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48498
- width: 100%;
48499
- height: 12px;
48500
- border-radius: 4px;
48501
- background: linear-gradient(
48502
- to right,
48503
- hsl(0 100% 50%) 0%,
48504
- hsl(0.2turn 100% 50%) 20%,
48505
- hsl(0.3turn 100% 50%) 30%,
48506
- hsl(0.4turn 100% 50%) 40%,
48507
- hsl(0.5turn 100% 50%) 50%,
48508
- hsl(0.6turn 100% 50%) 60%,
48509
- hsl(0.7turn 100% 50%) 70%,
48510
- hsl(0.8turn 100% 50%) 80%,
48511
- hsl(0.9turn 100% 50%) 90%,
48512
- hsl(1turn 100% 50%) 100%
48513
- );
48514
- position: relative;
48515
- cursor: crosshair;
48516
- }
48517
- .o-hue-slider {
48518
- margin-top: -3px;
48519
- }
48520
- .o-custom-input-preview {
48521
- padding: 2px 0px;
48522
- display: flex;
48523
- input {
48524
- width: 50%;
48525
- border-radius: 4px;
48526
- padding: 4px 23px 4px 10px;
48527
- height: 24px;
48528
- border: 1px solid #c0c0c0;
48529
- margin-right: 2px;
48530
- }
48531
- .o-wrong-color {
48532
- /* FIXME bootstrap class instead? */
48533
- outline-color: red;
48534
- border-color: red;
48535
- &:focus {
48536
- outline-style: solid;
48537
- outline-width: 1px;
48538
- }
48539
- }
48540
- }
48541
- .o-custom-input-buttons {
48542
- padding: 2px 0px;
48543
- display: flex;
48544
- justify-content: end;
48545
- }
48546
- .o-color-preview {
48547
- border: 1px solid #c0c0c0;
48548
- border-radius: 4px;
48549
- width: 50%;
48550
- }
48551
- }
48552
- }
48553
- `;
48554
- class ColorPicker extends owl.Component {
48555
- static template = "o-spreadsheet-ColorPicker";
48556
- static props = {
48557
- onColorPicked: Function,
48558
- currentColor: { type: String, optional: true },
48559
- maxHeight: { type: Number, optional: true },
48560
- anchorRect: Object,
48561
- disableNoColor: { type: Boolean, optional: true },
48562
- };
48563
- static defaultProps = { currentColor: "" };
48564
- static components = { Popover };
48565
- COLORS = COLOR_PICKER_DEFAULTS;
48566
- state = owl.useState({
48567
- showGradient: false,
48568
- currentHslaColor: isColorValid(this.props.currentColor)
48569
- ? { ...hexToHSLA(this.props.currentColor), a: 1 }
48570
- : { h: 0, s: 100, l: 100, a: 1 },
48571
- customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
48572
- });
48573
- get colorPickerStyle() {
48574
- if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
48575
- return cssPropertiesToCss({ display: "none" });
48576
- }
48577
- return "";
48578
- }
48579
- get popoverProps() {
48580
- return {
48581
- anchorRect: this.props.anchorRect,
48582
- maxHeight: this.props.maxHeight,
48583
- positioning: "bottom-left",
48584
- verticalOffset: 0,
48585
- };
48586
- }
48587
- get gradientHueStyle() {
48588
- const hue = this.state.currentHslaColor?.h || 0;
48589
- return cssPropertiesToCss({
48590
- background: `hsl(${hue} 100% 50%)`,
48591
- });
48592
- }
48593
- get sliderStyle() {
48594
- const hue = this.state.currentHslaColor?.h || 0;
48595
- const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
48596
- const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
48597
- return cssPropertiesToCss({
48598
- "margin-left": `${left}px`,
48599
- });
48600
- }
48601
- get pointerStyle() {
48602
- const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
48603
- const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
48604
- const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
48605
- return cssPropertiesToCss({
48606
- left: `${-MAGNIFIER_EDGE / 2 + left}px`,
48607
- top: `${-MAGNIFIER_EDGE / 2 + top}px`,
48608
- background: hslaToHex(this.state.currentHslaColor),
48609
- });
48610
- }
48611
- get colorPreviewStyle() {
48612
- return cssPropertiesToCss({
48613
- "background-color": hslaToHex(this.state.currentHslaColor),
48614
- });
48615
- }
48616
- get checkmarkColor() {
48617
- return chartFontColor(this.props.currentColor);
48618
- }
48619
- get isHexColorInputValid() {
48620
- return !this.state.customHexColor || isColorValid(this.state.customHexColor);
48621
- }
48622
- setCustomGradient({ x, y }) {
48623
- const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
48624
- const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
48625
- const deltaX = offsetX / INNER_GRADIENT_WIDTH;
48626
- const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
48627
- const s = 100 * deltaX;
48628
- const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
48629
- this.updateColor({ s, l });
48630
- }
48631
- setCustomHue(x) {
48632
- // needs to be capped such that h is in [0°, 359°]
48633
- const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
48634
- this.updateColor({ h });
48635
- }
48636
- updateColor(newHsl) {
48637
- this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
48638
- this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
48639
- }
48640
- onColorClick(color) {
48641
- if (color) {
48642
- this.props.onColorPicked(toHex(color));
48643
- }
48644
- }
48645
- resetColor() {
48646
- this.props.onColorPicked("");
48647
- }
48648
- toggleColorPicker() {
48649
- this.state.showGradient = !this.state.showGradient;
48650
- }
48651
- dragGradientPointer(ev) {
48652
- const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
48653
- this.setCustomGradient(initialGradientCoordinates);
48654
- const initialMousePosition = { x: ev.clientX, y: ev.clientY };
48655
- const onMouseMove = (ev) => {
48656
- const currentMousePosition = { x: ev.clientX, y: ev.clientY };
48657
- const deltaX = currentMousePosition.x - initialMousePosition.x;
48658
- const deltaY = currentMousePosition.y - initialMousePosition.y;
48659
- const currentGradientCoordinates = {
48660
- x: initialGradientCoordinates.x + deltaX,
48661
- y: initialGradientCoordinates.y + deltaY,
48662
- };
48663
- this.setCustomGradient(currentGradientCoordinates);
48664
- };
48665
- startDnd(onMouseMove, () => { });
48666
- }
48667
- dragHuePointer(ev) {
48668
- const initialX = ev.offsetX;
48669
- const initialMouseX = ev.clientX;
48670
- this.setCustomHue(initialX);
48671
- const onMouseMove = (ev) => {
48672
- const currentMouseX = ev.clientX;
48673
- const deltaX = currentMouseX - initialMouseX;
48674
- const x = initialX + deltaX;
48675
- this.setCustomHue(x);
48676
- };
48677
- startDnd(onMouseMove, () => { });
48678
- }
48679
- setHexColor(ev) {
48680
- // only support HEX code input
48681
- const val = ev.target.value.replace("##", "#").slice(0, 7);
48682
- this.state.customHexColor = val;
48683
- if (!isColorValid(val)) ;
48684
- else {
48685
- this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
48686
- }
48687
- }
48688
- addCustomColor(ev) {
48689
- if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
48690
- return;
48691
- }
48692
- this.props.onColorPicked(toHex(this.state.customHexColor));
48693
- }
48694
- isSameColor(color1, color2) {
48695
- return isSameColor(color1, color2);
48696
- }
48697
- }
48698
-
48699
48892
  css /* scss */ `
48700
48893
  .o-color-picker-widget {
48701
48894
  display: flex;
@@ -49163,57 +49356,6 @@ class RadioSelection extends owl.Component {
49163
49356
  };
49164
49357
  }
49165
49358
 
49166
- const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
49167
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
49168
- <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
49169
- </svg>
49170
- `;
49171
- css /* scss */ `
49172
- .o-round-color-picker-button {
49173
- width: 20px;
49174
- height: 20px;
49175
- cursor: pointer;
49176
- border: 1px solid ${GRAY_300};
49177
- background-position: 1px 1px;
49178
- background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
49179
- }
49180
- `;
49181
- class RoundColorPicker extends owl.Component {
49182
- static template = "o-spreadsheet.RoundColorPicker";
49183
- static components = { Section, ColorPicker };
49184
- static props = {
49185
- currentColor: { type: String, optional: true },
49186
- title: { type: String, optional: true },
49187
- onColorPicked: Function,
49188
- disableNoColor: { type: Boolean, optional: true },
49189
- };
49190
- colorPickerButtonRef = owl.useRef("colorPickerButton");
49191
- state;
49192
- setup() {
49193
- this.state = owl.useState({ pickerOpened: false });
49194
- owl.useExternalListener(window, "click", this.closePicker);
49195
- }
49196
- closePicker() {
49197
- this.state.pickerOpened = false;
49198
- }
49199
- togglePicker() {
49200
- this.state.pickerOpened = !this.state.pickerOpened;
49201
- }
49202
- onColorPicked(color) {
49203
- this.props.onColorPicked(color);
49204
- this.state.pickerOpened = false;
49205
- }
49206
- get colorPickerAnchorRect() {
49207
- const button = this.colorPickerButtonRef.el;
49208
- return getBoundingRectAsPOJO(button);
49209
- }
49210
- get buttonStyle() {
49211
- return cssPropertiesToCss({
49212
- background: this.props.currentColor,
49213
- });
49214
- }
49215
- }
49216
-
49217
49359
  class GeneralDesignEditor extends owl.Component {
49218
49360
  static template = "o-spreadsheet-GeneralDesignEditor";
49219
49361
  static components = {
@@ -59544,7 +59686,7 @@ class DataValidationPlugin extends CorePlugin {
59544
59686
  if (!rule)
59545
59687
  return false;
59546
59688
  return ((rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
59547
- rule.criterion.displayStyle === "arrow");
59689
+ (rule.criterion.displayStyle === "arrow" || rule.criterion.displayStyle === "chip"));
59548
59690
  }
59549
59691
  addDataValidationRule(sheetId, newRule) {
59550
59692
  const rules = this.rules[sheetId];
@@ -65910,7 +66052,10 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65910
66052
  "getDataValidationInvalidCriterionValueMessage",
65911
66053
  "getInvalidDataValidationMessage",
65912
66054
  "getValidationResultForCellValue",
66055
+ "getDataValidationRangeValues",
65913
66056
  "isCellValidCheckbox",
66057
+ "getDataValidationCellStyle",
66058
+ "getDataValidationChipStyle",
65914
66059
  "isDataValidationInvalid",
65915
66060
  ];
65916
66061
  validationResults = {};
@@ -65931,6 +66076,18 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65931
66076
  isDataValidationInvalid(cellPosition) {
65932
66077
  return !this.getValidationResultForCell(cellPosition).isValid;
65933
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
+ }
65934
66091
  getInvalidDataValidationMessage(cellPosition) {
65935
66092
  const validationResult = this.getValidationResultForCell(cellPosition);
65936
66093
  return validationResult.isValid ? undefined : validationResult.error;
@@ -65953,6 +66110,11 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65953
66110
  }
65954
66111
  return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;
65955
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
+ }
65956
66118
  isCellValidCheckbox(cellPosition) {
65957
66119
  if (!this.getters.isMainCellPosition(cellPosition)) {
65958
66120
  return false;
@@ -65972,6 +66134,38 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65972
66134
  const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
65973
66135
  return error ? { error, rule, isValid: false } : VALID_RESULT;
65974
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
+ }
65975
66169
  isValidFormula(value) {
65976
66170
  return !compile(value).isBadExpression;
65977
66171
  }
@@ -66068,12 +66262,35 @@ iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
66068
66262
  }
66069
66263
  return undefined;
66070
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
+ });
66071
66287
  iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
66072
66288
  const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
66073
66289
  if (hasIcon) {
66290
+ const cellStyle = getters.getCellComputedStyle(position);
66074
66291
  return {
66075
- svg: CARET_DOWN,
66076
- hoverSvg: HOVERED_CARET_DOWN,
66292
+ svg: getCaretDownSvg(cellStyle),
66293
+ hoverSvg: getHoveredCaretDownSvg(cellStyle),
66077
66294
  priority: 2,
66078
66295
  horizontalAlign: "right",
66079
66296
  size: GRID_ICON_EDGE_LENGTH,
@@ -68615,11 +68832,11 @@ class OTRegistry extends Registry {
68615
68832
  * transformation function given
68616
68833
  */
68617
68834
  addTransformation(executed, toTransforms, fn) {
68835
+ if (!this.content[executed]) {
68836
+ this.content[executed] = new Map();
68837
+ }
68618
68838
  for (const toTransform of toTransforms) {
68619
- if (!this.content[toTransform]) {
68620
- this.content[toTransform] = new Map();
68621
- }
68622
- this.content[toTransform].set(executed, fn);
68839
+ this.content[executed].set(toTransform, fn);
68623
68840
  }
68624
68841
  return this;
68625
68842
  }
@@ -68628,7 +68845,7 @@ class OTRegistry extends Registry {
68628
68845
  * that the executed command happened.
68629
68846
  */
68630
68847
  getTransformation(toTransform, executed) {
68631
- return this.content[toTransform] && this.content[toTransform].get(executed);
68848
+ return this.content[executed] && this.content[executed].get(toTransform);
68632
68849
  }
68633
68850
  }
68634
68851
  const otRegistry = new OTRegistry();
@@ -68958,10 +69175,20 @@ function adaptTransform(toTransform, executed) {
68958
69175
  */
68959
69176
  function transformAll(toTransform, executed) {
68960
69177
  let transformedCommands = [...toTransform];
69178
+ const possibleTransformations = new Set(otRegistry.getKeys());
68961
69179
  for (const executedCommand of executed) {
68962
- transformedCommands = transformedCommands
68963
- .map((cmd) => transform(cmd, executedCommand))
68964
- .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
+ }
68965
69192
  }
68966
69193
  return transformedCommands;
68967
69194
  }
@@ -70518,6 +70745,9 @@ class SheetUIPlugin extends UIPlugin {
70518
70745
  for (const icon of this.getters.getCellIcons(position)) {
70519
70746
  contentWidth += icon.margin + icon.size;
70520
70747
  }
70748
+ if (this.getters.getDataValidationChipStyle(position)) {
70749
+ contentWidth += DATA_VALIDATION_CHIP_MARGIN * 2;
70750
+ }
70521
70751
  if (contentWidth === 0) {
70522
70752
  return 0;
70523
70753
  }
@@ -70675,7 +70905,7 @@ class SheetUIPlugin extends UIPlugin {
70675
70905
  }
70676
70906
  const position = this.getters.getCellPosition(cell.id);
70677
70907
  const colSize = this.getters.getColSize(sheetId, position.col);
70678
- if (cell.isFormula) {
70908
+ if (cell.isFormula || this.getters.getArrayFormulaSpreadingOn(position)) {
70679
70909
  const content = this.getters.getEvaluatedCell(position).formattedValue;
70680
70910
  const evaluatedSize = getCellContentHeight(this.ctx, content, cell?.style, colSize);
70681
70911
  if (evaluatedSize > evaluatedRowSize && evaluatedSize > DEFAULT_CELL_HEIGHT) {
@@ -70878,6 +71108,8 @@ class CellComputedStylePlugin extends UIPlugin {
70878
71108
  if (invalidateEvaluationCommands.has(cmd.type) ||
70879
71109
  cmd.type === "UPDATE_CELL" ||
70880
71110
  cmd.type === "SET_FORMATTING" ||
71111
+ cmd.type === "ADD_DATA_VALIDATION_RULE" ||
71112
+ cmd.type === "REMOVE_DATA_VALIDATION_RULE" ||
70881
71113
  cmd.type === "EVALUATE_CELLS") {
70882
71114
  this.styles = {};
70883
71115
  this.borders = {};
@@ -70949,8 +71181,10 @@ class CellComputedStylePlugin extends UIPlugin {
70949
71181
  const cell = this.getters.getCell(position);
70950
71182
  const cfStyle = this.getters.getCellConditionalFormatStyle(position);
70951
71183
  const tableStyle = this.getters.getCellTableStyle(position);
71184
+ const dataValidationStyle = this.getters.getDataValidationCellStyle(position);
70952
71185
  const computedStyle = {
70953
71186
  ...removeFalsyAttributes(tableStyle),
71187
+ ...removeFalsyAttributes(dataValidationStyle),
70954
71188
  ...removeFalsyAttributes(cell?.style),
70955
71189
  ...removeFalsyAttributes(cfStyle),
70956
71190
  };
@@ -74512,19 +74746,29 @@ autoCompleteProviders.add("dataValidation", {
74512
74746
  (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange")) {
74513
74747
  return [];
74514
74748
  }
74515
- let values;
74516
- if (rule.criterion.type === "isValueInList") {
74517
- values = rule.criterion.values;
74518
- }
74519
- else {
74520
- const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
74521
- values = Array.from(new Set(this.getters
74522
- .getRangeValues(range)
74523
- .filter(isNotNull)
74524
- .map((value) => value.toString())
74525
- .filter((val) => val !== "")));
74526
- }
74527
- 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
+ });
74528
74772
  },
74529
74773
  selectProposal(tokenAtCursor, value) {
74530
74774
  this.composer.setCurrentContent(value);
@@ -83392,6 +83636,6 @@ exports.tokenColors = tokenColors;
83392
83636
  exports.tokenize = tokenize;
83393
83637
 
83394
83638
 
83395
- __info__.version = "18.4.0-alpha.8";
83396
- __info__.date = "2025-06-12T09:53:48.133Z";
83397
- __info__.hash = "9b7a8d0";
83639
+ __info__.version = "18.4.0-alpha.9";
83640
+ __info__.date = "2025-06-19T18:23:22.025Z";
83641
+ __info__.hash = "6d4d685";