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