@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
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, useExternalListener, onWillUpdateProps, onWillStart, onWillPatch, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
@@ -20,13 +20,21 @@ function createAction(item) {
20
20
  const icon = item.icon;
21
21
  const secondaryIcon = item.secondaryIcon;
22
22
  const itemId = item.id || nextItemId++;
23
+ const isEnabled = item.isEnabled ? item.isEnabled : () => true;
23
24
  return {
24
25
  id: itemId.toString(),
25
26
  name: typeof name === "function" ? name : () => name,
26
27
  isVisible: item.isVisible ? item.isVisible : () => true,
27
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
28
+ isEnabled: isEnabled,
28
29
  isActive: item.isActive,
29
- execute: item.execute,
30
+ execute: item.execute
31
+ ? (env, isMiddleClick) => {
32
+ if (isEnabled(env)) {
33
+ return item.execute(env, isMiddleClick);
34
+ }
35
+ return undefined;
36
+ }
37
+ : undefined,
30
38
  children: children
31
39
  ? (env) => {
32
40
  return children
@@ -297,6 +305,7 @@ const GROUP_LAYER_WIDTH = 21;
297
305
  const GRID_ICON_MARGIN = 2;
298
306
  const GRID_ICON_EDGE_LENGTH = 17;
299
307
  const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT;
308
+ const DATA_VALIDATION_CHIP_MARGIN = 5;
300
309
  // 768px is a common breakpoint for small screens
301
310
  // Typically inside Odoo, it is the threshold for switching to mobile view
302
311
  const MOBILE_WIDTH_BREAKPOINT = 768;
@@ -642,9 +651,6 @@ function parseSheetUrl(sheetLink) {
642
651
  function isDefined(argument) {
643
652
  return argument !== undefined;
644
653
  }
645
- function isNotNull(argument) {
646
- return argument !== null;
647
- }
648
654
  /**
649
655
  * Check if all the values of an object, and all the values of the objects inside of it, are undefined.
650
656
  */
@@ -1355,9 +1361,16 @@ function darkenColor(color, percentage) {
1355
1361
  if (percentage === 1) {
1356
1362
  return "#000";
1357
1363
  }
1364
+ // increase saturation to compensate and make it more vivid
1365
+ hsla.s = Math.min(100, percentage * hsla.s + hsla.s);
1358
1366
  hsla.l = hsla.l - percentage * hsla.l;
1359
1367
  return hslaToHex(hsla);
1360
1368
  }
1369
+ function chipTextColor(chipBackgroundColor) {
1370
+ return relativeLuminance(chipBackgroundColor) < 0.6
1371
+ ? lightenColor(chipBackgroundColor, 0.9)
1372
+ : darkenColor(chipBackgroundColor, 0.75);
1373
+ }
1361
1374
  const COLORS_SM = [
1362
1375
  "#4EA7F2", // Blue
1363
1376
  "#EA6175", // Red
@@ -21686,6 +21699,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21686
21699
  if (isTrendLineAxis(dataset.xAxisID) || dataset.hidden) {
21687
21700
  continue;
21688
21701
  }
21702
+ const yAxisScale = chart.scales[dataset.yAxisID];
21689
21703
  for (let i = 0; i < dataset._parsed.length; i++) {
21690
21704
  const parsedValue = dataset._parsed[i];
21691
21705
  const value = Number(chart.config.type === "radar" ? parsedValue.r : parsedValue.y);
@@ -21696,10 +21710,18 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21696
21710
  const xPosition = point.x;
21697
21711
  let yPosition = 0;
21698
21712
  if (chart.config.type === "line" || chart.config.type === "radar") {
21699
- yPosition = point.y - 10;
21713
+ yPosition = value < 0 ? point.y + 10 : point.y - 10;
21700
21714
  }
21701
21715
  else {
21702
- yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21716
+ const yZeroLine = yAxisScale.getPixelForValue(0);
21717
+ const distanceFromAxisOrigin = Math.abs(yZeroLine - point.y);
21718
+ const textHeight = 12; // ChartJS default text height
21719
+ if (distanceFromAxisOrigin < textHeight) {
21720
+ yPosition = value < 0 ? yZeroLine + textHeight / 2 : yZeroLine - textHeight / 2;
21721
+ }
21722
+ else {
21723
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21724
+ }
21703
21725
  }
21704
21726
  yPosition = Math.min(yPosition, yMax);
21705
21727
  yPosition = Math.max(yPosition, yMin);
@@ -21709,7 +21731,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21709
21731
  }
21710
21732
  for (const otherPosition of textsPositions[xPosition] || []) {
21711
21733
  if (Math.abs(otherPosition - yPosition) < 13) {
21712
- yPosition = otherPosition - 13;
21734
+ yPosition = value < 0 ? otherPosition + 13 : otherPosition - 13;
21713
21735
  }
21714
21736
  }
21715
21737
  textsPositions[xPosition].push(yPosition);
@@ -21728,6 +21750,8 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21728
21750
  if (isTrendLineAxis(dataset.xAxisID)) {
21729
21751
  return; // ignore trend lines
21730
21752
  }
21753
+ const xAxisScale = chart.scales[dataset.xAxisID];
21754
+ const xZeroLine = xAxisScale.getPixelForValue(0);
21731
21755
  for (let i = 0; i < dataset._parsed.length; i++) {
21732
21756
  const value = Number(dataset._parsed[i].x);
21733
21757
  if (isNaN(value)) {
@@ -21736,17 +21760,27 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21736
21760
  const displayValue = options.callback(value, dataset, i);
21737
21761
  const point = dataset.data[i];
21738
21762
  const yPosition = point.y;
21739
- let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21740
- xPosition = Math.min(xPosition, xMax);
21741
- xPosition = Math.max(xPosition, xMin);
21763
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21764
+ const distanceFromAxisOrigin = Math.abs(point.x - xZeroLine);
21765
+ const PADDING = 3;
21766
+ let xPosition;
21767
+ if (distanceFromAxisOrigin < textWidth) {
21768
+ xPosition =
21769
+ value < 0 ? xZeroLine - textWidth / 2 - PADDING : xZeroLine + textWidth / 2 + PADDING;
21770
+ }
21771
+ else {
21772
+ xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21773
+ xPosition = Math.min(xPosition, xMax);
21774
+ xPosition = Math.max(xPosition, xMin);
21775
+ }
21742
21776
  // Avoid overlapping texts with same Y
21743
21777
  if (!textsPositions[yPosition]) {
21744
21778
  textsPositions[yPosition] = [];
21745
21779
  }
21746
- const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21747
21780
  for (const otherPosition of textsPositions[yPosition]) {
21748
21781
  if (Math.abs(otherPosition - xPosition) < textWidth) {
21749
- xPosition = otherPosition + textWidth + 3;
21782
+ xPosition =
21783
+ value < 0 ? otherPosition - textWidth - PADDING : otherPosition + textWidth + PADDING;
21750
21784
  }
21751
21785
  }
21752
21786
  textsPositions[yPosition].push(xPosition);
@@ -25680,7 +25714,9 @@ function getPyramidChartShowValues(definition, args) {
25680
25714
  background: definition.background,
25681
25715
  callback: (value, dataset) => {
25682
25716
  value = Math.abs(Number(value));
25683
- return formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25717
+ return value === 0
25718
+ ? ""
25719
+ : formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25684
25720
  },
25685
25721
  };
25686
25722
  }
@@ -29341,6 +29377,12 @@ class Menu extends Component {
29341
29377
  menu.onStopHover?.(this.env);
29342
29378
  this.props.onMouseLeave?.(menu, ev);
29343
29379
  }
29380
+ onClickMenu(menu, ev) {
29381
+ if (!this.isEnabled(menu)) {
29382
+ return;
29383
+ }
29384
+ this.props.onClickMenu?.(menu, ev);
29385
+ }
29344
29386
  }
29345
29387
 
29346
29388
  /**
@@ -29839,9 +29881,6 @@ class MenuPopover extends Component {
29839
29881
  this.subMenu.parentMenu = undefined;
29840
29882
  }
29841
29883
  onClickMenu(menu, ev) {
29842
- if (!this.isEnabled(menu)) {
29843
- return;
29844
- }
29845
29884
  if (this.isRoot(menu)) {
29846
29885
  this.openSubMenu(menu, ev.currentTarget);
29847
29886
  }
@@ -31092,11 +31131,9 @@ criterionEvaluatorRegistry.add("isValueInRange", {
31092
31131
  if (!value) {
31093
31132
  return false;
31094
31133
  }
31095
- const range = getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
31096
- const criterionValues = getters.getRangeValues(range);
31134
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
31097
31135
  return criterionValues
31098
- .filter(isNotNull)
31099
- .map((value) => value.toString().toLowerCase())
31136
+ .map((value) => value.toLowerCase())
31100
31137
  .includes(value.toString().toLowerCase());
31101
31138
  },
31102
31139
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
@@ -31268,6 +31305,12 @@ class TextValueProvider extends Component {
31268
31305
  selectedElement?.scrollIntoView?.({ block: "nearest" });
31269
31306
  }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
31270
31307
  }
31308
+ getCss(html) {
31309
+ return cssPropertiesToCss({
31310
+ color: html.color || "#000000",
31311
+ background: html.backgroundColor,
31312
+ });
31313
+ }
31271
31314
  }
31272
31315
 
31273
31316
  class ContentEditableHelper {
@@ -31406,7 +31449,6 @@ class ContentEditableHelper {
31406
31449
  // We can only modify a node in place if it has the same type as the content
31407
31450
  // that we would insert, which are spans.
31408
31451
  // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
31409
- // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)
31410
31452
  const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
31411
31453
  if (childIsSpan && compareContentToSpanElement(content, child)) {
31412
31454
  continue;
@@ -31441,9 +31483,7 @@ class ContentEditableHelper {
31441
31483
  }
31442
31484
  // Empty line
31443
31485
  if (!p.hasChildNodes()) {
31444
- const span = document.createElement("span");
31445
- span.appendChild(document.createElement("br"));
31446
- p.appendChild(span);
31486
+ p.appendChild(document.createElement("span"));
31447
31487
  }
31448
31488
  // replace p if necessary
31449
31489
  if (newChild) {
@@ -31490,13 +31530,10 @@ class ContentEditableHelper {
31490
31530
  }
31491
31531
  getText() {
31492
31532
  let text = "";
31493
- const it = iterateChildren(this.el);
31494
- let current = it.next();
31495
31533
  let isFirstParagraph = true;
31496
- while (!current.done) {
31497
- if (!current.value.hasChildNodes()) {
31498
- text += current.value.textContent;
31499
- }
31534
+ let emptyParagraph = false;
31535
+ const it = iterateChildren(this.el);
31536
+ for (let current = it.next(); !current.done; current = it.next()) {
31500
31537
  if (current.value.nodeName === "P" ||
31501
31538
  (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
31502
31539
  ) {
@@ -31506,8 +31543,15 @@ class ContentEditableHelper {
31506
31543
  else {
31507
31544
  text += NEWLINE;
31508
31545
  }
31546
+ emptyParagraph = ["<br>", "<span><br></span>"].includes(current.value.innerHTML);
31547
+ continue;
31548
+ }
31549
+ if (!current.value.hasChildNodes()) {
31550
+ if (current.value.nodeName === "BR" && !emptyParagraph) {
31551
+ text += NEWLINE;
31552
+ }
31553
+ text += current.value.textContent;
31509
31554
  }
31510
- current = it.next();
31511
31555
  }
31512
31556
  return text;
31513
31557
  }
@@ -32857,6 +32901,12 @@ class Composer extends Component {
32857
32901
  useEffect(() => {
32858
32902
  this.processTokenAtCursor();
32859
32903
  }, () => [this.props.composerStore.editionMode !== "inactive"]);
32904
+ useEffect(() => {
32905
+ this.contentHelper.scrollSelectionIntoView();
32906
+ }, () => [
32907
+ this.props.composerStore.composerSelection.start,
32908
+ this.props.composerStore.composerSelection.end,
32909
+ ]);
32860
32910
  }
32861
32911
  // ---------------------------------------------------------------------------
32862
32912
  // Handlers
@@ -33067,6 +33117,7 @@ class Composer extends Component {
33067
33117
  if (this.env.isMobile() && !isIOS()) {
33068
33118
  return;
33069
33119
  }
33120
+ this.debouncedHover.stopDebounce();
33070
33121
  this.contentHelper.removeSelection();
33071
33122
  }
33072
33123
  onMouseup() {
@@ -33145,7 +33196,6 @@ class Composer extends Component {
33145
33196
  const { start, end } = this.props.composerStore.composerSelection;
33146
33197
  this.contentHelper.selectRange(start, end);
33147
33198
  }
33148
- this.contentHelper.scrollSelectionIntoView();
33149
33199
  }
33150
33200
  this.shouldProcessInputEvents = true;
33151
33201
  }
@@ -33621,6 +33671,414 @@ class SingleInputCriterionForm extends CriterionForm {
33621
33671
  }
33622
33672
  }
33623
33673
 
33674
+ /**
33675
+ * Start listening to pointer events and apply the given callbacks.
33676
+ *
33677
+ * @returns A function to remove the listeners.
33678
+ */
33679
+ function startDnd(onPointerMove, onPointerUp) {
33680
+ const removeListeners = () => {
33681
+ window.removeEventListener("pointerup", _onPointerUp, { capture: true });
33682
+ window.removeEventListener("dragstart", _onDragStart);
33683
+ window.removeEventListener("pointermove", onPointerMove);
33684
+ window.removeEventListener("wheel", onPointerMove);
33685
+ };
33686
+ const _onPointerUp = (ev) => {
33687
+ onPointerUp(ev);
33688
+ removeListeners();
33689
+ };
33690
+ function _onDragStart(ev) {
33691
+ ev.preventDefault();
33692
+ }
33693
+ window.addEventListener("pointerup", _onPointerUp, { capture: true });
33694
+ window.addEventListener("dragstart", _onDragStart);
33695
+ window.addEventListener("pointermove", onPointerMove);
33696
+ // mouse wheel on window is by default a passive event.
33697
+ // preventDefault() is not allowed in passive event handler.
33698
+ // https://chromestatus.com/feature/6662647093133312
33699
+ window.addEventListener("wheel", onPointerMove, { passive: false });
33700
+ return removeListeners;
33701
+ }
33702
+
33703
+ const LINE_VERTICAL_PADDING = 1;
33704
+ const PICKER_PADDING = 8;
33705
+ const ITEM_BORDER_WIDTH = 1;
33706
+ const ITEM_EDGE_LENGTH = 18;
33707
+ const ITEMS_PER_LINE = 10;
33708
+ const MAGNIFIER_EDGE = 16;
33709
+ const ITEM_GAP = 2;
33710
+ const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
33711
+ const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
33712
+ const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
33713
+ const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
33714
+ css /* scss */ `
33715
+ .o-color-picker {
33716
+ padding: ${PICKER_PADDING}px 0;
33717
+ /* FIXME: this is useless, overiden by the popover container */
33718
+ box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
33719
+ background-color: white;
33720
+ line-height: 1.2;
33721
+ overflow-y: auto;
33722
+ overflow-x: hidden;
33723
+ width: ${CONTAINER_WIDTH}px;
33724
+
33725
+ .o-color-picker-section-name {
33726
+ margin: 0px ${ITEM_BORDER_WIDTH}px;
33727
+ padding: 4px ${PICKER_PADDING}px;
33728
+ }
33729
+ .colors-grid {
33730
+ display: grid;
33731
+ padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
33732
+ grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
33733
+ grid-gap: ${ITEM_GAP}px;
33734
+ }
33735
+ .o-color-picker-toggler-button {
33736
+ display: flex;
33737
+ .o-color-picker-toggler-sign {
33738
+ display: flex;
33739
+ margin: auto auto;
33740
+ width: 55%;
33741
+ height: 55%;
33742
+ .o-icon {
33743
+ width: 100%;
33744
+ height: 100%;
33745
+ }
33746
+ }
33747
+ }
33748
+ .o-color-picker-line-item {
33749
+ width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
33750
+ height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
33751
+ margin: 0px;
33752
+ border-radius: 50px;
33753
+ border: ${ITEM_BORDER_WIDTH}px solid #666666;
33754
+ padding: 0px;
33755
+ font-size: 16px;
33756
+ background: white;
33757
+ &:hover {
33758
+ background-color: rgba(0, 0, 0, 0.08);
33759
+ outline: 1px solid gray;
33760
+ cursor: pointer;
33761
+ }
33762
+ }
33763
+ .o-buttons {
33764
+ padding: ${PICKER_PADDING}px;
33765
+ display: flex;
33766
+ .o-cancel {
33767
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33768
+ width: 100%;
33769
+ padding: 5px;
33770
+ font-size: 14px;
33771
+ background: white;
33772
+ border-radius: 4px;
33773
+ &:hover:enabled {
33774
+ background-color: rgba(0, 0, 0, 0.08);
33775
+ }
33776
+ }
33777
+ }
33778
+ .o-add-button {
33779
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33780
+ padding: 4px;
33781
+ background: white;
33782
+ border-radius: 4px;
33783
+ &:hover:enabled {
33784
+ background-color: rgba(0, 0, 0, 0.08);
33785
+ }
33786
+ }
33787
+ .o-separator {
33788
+ border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
33789
+ margin-top: ${MENU_SEPARATOR_PADDING}px;
33790
+ margin-bottom: ${MENU_SEPARATOR_PADDING}px;
33791
+ }
33792
+
33793
+ .o-custom-selector {
33794
+ padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
33795
+ position: relative;
33796
+ .o-gradient {
33797
+ margin-bottom: ${MAGNIFIER_EDGE / 2}px;
33798
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33799
+ width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
33800
+ height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
33801
+ position: relative;
33802
+ }
33803
+
33804
+ .magnifier {
33805
+ height: ${MAGNIFIER_EDGE}px;
33806
+ width: ${MAGNIFIER_EDGE}px;
33807
+ border-radius: 50%;
33808
+ border: 2px solid #fff;
33809
+ box-shadow: 0px 0px 3px #c0c0c0;
33810
+ position: absolute;
33811
+ z-index: 2;
33812
+ }
33813
+ .saturation {
33814
+ background: linear-gradient(to right, #fff 0%, transparent 100%);
33815
+ }
33816
+ .lightness {
33817
+ background: linear-gradient(to top, #000 0%, transparent 100%);
33818
+ }
33819
+ .o-hue-picker {
33820
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
33821
+ width: 100%;
33822
+ height: 12px;
33823
+ border-radius: 4px;
33824
+ background: linear-gradient(
33825
+ to right,
33826
+ hsl(0 100% 50%) 0%,
33827
+ hsl(0.2turn 100% 50%) 20%,
33828
+ hsl(0.3turn 100% 50%) 30%,
33829
+ hsl(0.4turn 100% 50%) 40%,
33830
+ hsl(0.5turn 100% 50%) 50%,
33831
+ hsl(0.6turn 100% 50%) 60%,
33832
+ hsl(0.7turn 100% 50%) 70%,
33833
+ hsl(0.8turn 100% 50%) 80%,
33834
+ hsl(0.9turn 100% 50%) 90%,
33835
+ hsl(1turn 100% 50%) 100%
33836
+ );
33837
+ position: relative;
33838
+ cursor: crosshair;
33839
+ }
33840
+ .o-hue-slider {
33841
+ margin-top: -3px;
33842
+ }
33843
+ .o-custom-input-preview {
33844
+ padding: 2px 0px;
33845
+ display: flex;
33846
+ input {
33847
+ width: 50%;
33848
+ border-radius: 4px;
33849
+ padding: 4px 23px 4px 10px;
33850
+ height: 24px;
33851
+ border: 1px solid #c0c0c0;
33852
+ margin-right: 2px;
33853
+ }
33854
+ .o-wrong-color {
33855
+ /* FIXME bootstrap class instead? */
33856
+ outline-color: red;
33857
+ border-color: red;
33858
+ &:focus {
33859
+ outline-style: solid;
33860
+ outline-width: 1px;
33861
+ }
33862
+ }
33863
+ }
33864
+ .o-custom-input-buttons {
33865
+ padding: 2px 0px;
33866
+ display: flex;
33867
+ justify-content: end;
33868
+ }
33869
+ .o-color-preview {
33870
+ border: 1px solid #c0c0c0;
33871
+ border-radius: 4px;
33872
+ width: 50%;
33873
+ }
33874
+ }
33875
+ }
33876
+ `;
33877
+ class ColorPicker extends Component {
33878
+ static template = "o-spreadsheet-ColorPicker";
33879
+ static props = {
33880
+ onColorPicked: Function,
33881
+ currentColor: { type: String, optional: true },
33882
+ maxHeight: { type: Number, optional: true },
33883
+ anchorRect: Object,
33884
+ disableNoColor: { type: Boolean, optional: true },
33885
+ };
33886
+ static defaultProps = { currentColor: "" };
33887
+ static components = { Popover };
33888
+ COLORS = COLOR_PICKER_DEFAULTS;
33889
+ state = useState({
33890
+ showGradient: false,
33891
+ currentHslaColor: isColorValid(this.props.currentColor)
33892
+ ? { ...hexToHSLA(this.props.currentColor), a: 1 }
33893
+ : { h: 0, s: 100, l: 100, a: 1 },
33894
+ customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
33895
+ });
33896
+ get colorPickerStyle() {
33897
+ if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
33898
+ return cssPropertiesToCss({ display: "none" });
33899
+ }
33900
+ return "";
33901
+ }
33902
+ get popoverProps() {
33903
+ return {
33904
+ anchorRect: this.props.anchorRect,
33905
+ maxHeight: this.props.maxHeight,
33906
+ positioning: "bottom-left",
33907
+ verticalOffset: 0,
33908
+ };
33909
+ }
33910
+ get gradientHueStyle() {
33911
+ const hue = this.state.currentHslaColor?.h || 0;
33912
+ return cssPropertiesToCss({
33913
+ background: `hsl(${hue} 100% 50%)`,
33914
+ });
33915
+ }
33916
+ get sliderStyle() {
33917
+ const hue = this.state.currentHslaColor?.h || 0;
33918
+ const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
33919
+ const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
33920
+ return cssPropertiesToCss({
33921
+ "margin-left": `${left}px`,
33922
+ });
33923
+ }
33924
+ get pointerStyle() {
33925
+ const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
33926
+ const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
33927
+ const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
33928
+ return cssPropertiesToCss({
33929
+ left: `${-MAGNIFIER_EDGE / 2 + left}px`,
33930
+ top: `${-MAGNIFIER_EDGE / 2 + top}px`,
33931
+ background: hslaToHex(this.state.currentHslaColor),
33932
+ });
33933
+ }
33934
+ get colorPreviewStyle() {
33935
+ return cssPropertiesToCss({
33936
+ "background-color": hslaToHex(this.state.currentHslaColor),
33937
+ });
33938
+ }
33939
+ get checkmarkColor() {
33940
+ return chartFontColor(this.props.currentColor);
33941
+ }
33942
+ get isHexColorInputValid() {
33943
+ return !this.state.customHexColor || isColorValid(this.state.customHexColor);
33944
+ }
33945
+ setCustomGradient({ x, y }) {
33946
+ const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
33947
+ const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
33948
+ const deltaX = offsetX / INNER_GRADIENT_WIDTH;
33949
+ const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
33950
+ const s = 100 * deltaX;
33951
+ const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
33952
+ this.updateColor({ s, l });
33953
+ }
33954
+ setCustomHue(x) {
33955
+ // needs to be capped such that h is in [0°, 359°]
33956
+ const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
33957
+ this.updateColor({ h });
33958
+ }
33959
+ updateColor(newHsl) {
33960
+ this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
33961
+ this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
33962
+ }
33963
+ onColorClick(color) {
33964
+ if (color) {
33965
+ this.props.onColorPicked(toHex(color));
33966
+ }
33967
+ }
33968
+ resetColor() {
33969
+ this.props.onColorPicked("");
33970
+ }
33971
+ toggleColorPicker() {
33972
+ this.state.showGradient = !this.state.showGradient;
33973
+ }
33974
+ dragGradientPointer(ev) {
33975
+ const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
33976
+ this.setCustomGradient(initialGradientCoordinates);
33977
+ const initialMousePosition = { x: ev.clientX, y: ev.clientY };
33978
+ const onMouseMove = (ev) => {
33979
+ const currentMousePosition = { x: ev.clientX, y: ev.clientY };
33980
+ const deltaX = currentMousePosition.x - initialMousePosition.x;
33981
+ const deltaY = currentMousePosition.y - initialMousePosition.y;
33982
+ const currentGradientCoordinates = {
33983
+ x: initialGradientCoordinates.x + deltaX,
33984
+ y: initialGradientCoordinates.y + deltaY,
33985
+ };
33986
+ this.setCustomGradient(currentGradientCoordinates);
33987
+ };
33988
+ startDnd(onMouseMove, () => { });
33989
+ }
33990
+ dragHuePointer(ev) {
33991
+ const initialX = ev.offsetX;
33992
+ const initialMouseX = ev.clientX;
33993
+ this.setCustomHue(initialX);
33994
+ const onMouseMove = (ev) => {
33995
+ const currentMouseX = ev.clientX;
33996
+ const deltaX = currentMouseX - initialMouseX;
33997
+ const x = initialX + deltaX;
33998
+ this.setCustomHue(x);
33999
+ };
34000
+ startDnd(onMouseMove, () => { });
34001
+ }
34002
+ setHexColor(ev) {
34003
+ // only support HEX code input
34004
+ const val = ev.target.value.replace("##", "#").slice(0, 7);
34005
+ this.state.customHexColor = val;
34006
+ if (!isColorValid(val)) ;
34007
+ else {
34008
+ this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
34009
+ }
34010
+ }
34011
+ addCustomColor(ev) {
34012
+ if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
34013
+ return;
34014
+ }
34015
+ this.props.onColorPicked(toHex(this.state.customHexColor));
34016
+ }
34017
+ isSameColor(color1, color2) {
34018
+ return isSameColor(color1, color2);
34019
+ }
34020
+ }
34021
+
34022
+ class Section extends Component {
34023
+ static template = "o_spreadsheet.Section";
34024
+ static props = {
34025
+ class: { type: String, optional: true },
34026
+ title: { type: String, optional: true },
34027
+ slots: Object,
34028
+ };
34029
+ }
34030
+
34031
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
34032
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
34033
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
34034
+ </svg>
34035
+ `;
34036
+ css /* scss */ `
34037
+ .o-round-color-picker-button {
34038
+ width: 20px;
34039
+ height: 20px;
34040
+ cursor: pointer;
34041
+ border: 1px solid ${GRAY_300};
34042
+ background-position: 1px 1px;
34043
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
34044
+ }
34045
+ `;
34046
+ class RoundColorPicker extends Component {
34047
+ static template = "o-spreadsheet.RoundColorPicker";
34048
+ static components = { Section, ColorPicker };
34049
+ static props = {
34050
+ currentColor: { type: String, optional: true },
34051
+ title: { type: String, optional: true },
34052
+ onColorPicked: Function,
34053
+ disableNoColor: { type: Boolean, optional: true },
34054
+ };
34055
+ colorPickerButtonRef = useRef("colorPickerButton");
34056
+ state;
34057
+ setup() {
34058
+ this.state = useState({ pickerOpened: false });
34059
+ useExternalListener(window, "click", this.closePicker);
34060
+ }
34061
+ closePicker() {
34062
+ this.state.pickerOpened = false;
34063
+ }
34064
+ togglePicker() {
34065
+ this.state.pickerOpened = !this.state.pickerOpened;
34066
+ }
34067
+ onColorPicked(color) {
34068
+ this.props.onColorPicked(color);
34069
+ this.state.pickerOpened = false;
34070
+ }
34071
+ get colorPickerAnchorRect() {
34072
+ const button = this.colorPickerButtonRef.el;
34073
+ return getBoundingRectAsPOJO(button);
34074
+ }
34075
+ get buttonStyle() {
34076
+ return cssPropertiesToCss({
34077
+ background: this.props.currentColor,
34078
+ });
34079
+ }
34080
+ }
34081
+
33624
34082
  css /* scss */ `
33625
34083
  .o-dv-list-item-delete {
33626
34084
  color: #666666;
@@ -33629,7 +34087,7 @@ css /* scss */ `
33629
34087
  `;
33630
34088
  class ListCriterionForm extends CriterionForm {
33631
34089
  static template = "o-spreadsheet-ListCriterionForm";
33632
- static components = { CriterionInput };
34090
+ static components = { CriterionInput, RoundColorPicker };
33633
34091
  state = useState({
33634
34092
  numberOfValues: Math.max(this.props.criterion.values.length, 2),
33635
34093
  });
@@ -33637,7 +34095,7 @@ class ListCriterionForm extends CriterionForm {
33637
34095
  super.setup();
33638
34096
  const setupDefault = (props) => {
33639
34097
  if (props.criterion.displayStyle === undefined) {
33640
- this.updateCriterion({ displayStyle: "arrow" });
34098
+ this.updateCriterion({ displayStyle: "chip" });
33641
34099
  }
33642
34100
  };
33643
34101
  onWillUpdateProps(setupDefault);
@@ -33648,6 +34106,11 @@ class ListCriterionForm extends CriterionForm {
33648
34106
  values[index] = value;
33649
34107
  this.updateCriterion({ values });
33650
34108
  }
34109
+ onColorChanged(color, value) {
34110
+ const colors = { ...this.props.criterion.colors };
34111
+ colors[value] = color || undefined;
34112
+ this.updateCriterion({ colors });
34113
+ }
33651
34114
  onAddAnotherValue() {
33652
34115
  this.state.numberOfValues++;
33653
34116
  }
@@ -33683,35 +34146,6 @@ class ListCriterionForm extends CriterionForm {
33683
34146
  }
33684
34147
  }
33685
34148
 
33686
- /**
33687
- * Start listening to pointer events and apply the given callbacks.
33688
- *
33689
- * @returns A function to remove the listeners.
33690
- */
33691
- function startDnd(onPointerMove, onPointerUp) {
33692
- const removeListeners = () => {
33693
- window.removeEventListener("pointerup", _onPointerUp, { capture: true });
33694
- window.removeEventListener("dragstart", _onDragStart);
33695
- window.removeEventListener("pointermove", onPointerMove);
33696
- window.removeEventListener("wheel", onPointerMove);
33697
- };
33698
- const _onPointerUp = (ev) => {
33699
- onPointerUp(ev);
33700
- removeListeners();
33701
- };
33702
- function _onDragStart(ev) {
33703
- ev.preventDefault();
33704
- }
33705
- window.addEventListener("pointerup", _onPointerUp, { capture: true });
33706
- window.addEventListener("dragstart", _onDragStart);
33707
- window.addEventListener("pointermove", onPointerMove);
33708
- // mouse wheel on window is by default a passive event.
33709
- // preventDefault() is not allowed in passive event handler.
33710
- // https://chromestatus.com/feature/6662647093133312
33711
- window.addEventListener("wheel", onPointerMove, { passive: false });
33712
- return removeListeners;
33713
- }
33714
-
33715
34149
  function useDragAndDropListItems() {
33716
34150
  let dndHelper;
33717
34151
  const previousCursor = document.body.style.cursor;
@@ -34571,12 +35005,12 @@ class SelectionInput extends Component {
34571
35005
 
34572
35006
  class ValueInRangeCriterionForm extends CriterionForm {
34573
35007
  static template = "o-spreadsheet-ValueInRangeCriterionForm";
34574
- static components = { SelectionInput };
35008
+ static components = { RoundColorPicker, SelectionInput };
34575
35009
  setup() {
34576
35010
  super.setup();
34577
35011
  const setupDefault = (props) => {
34578
35012
  if (props.criterion.displayStyle === undefined) {
34579
- this.updateCriterion({ displayStyle: "arrow" });
35013
+ this.updateCriterion({ displayStyle: "chip" });
34580
35014
  }
34581
35015
  };
34582
35016
  onWillUpdateProps(setupDefault);
@@ -34589,6 +35023,16 @@ class ValueInRangeCriterionForm extends CriterionForm {
34589
35023
  const displayStyle = ev.target.value;
34590
35024
  this.updateCriterion({ displayStyle });
34591
35025
  }
35026
+ onColorChanged(color, value) {
35027
+ const colors = { ...this.props.criterion.colors };
35028
+ colors[value] = color || undefined;
35029
+ this.updateCriterion({ colors });
35030
+ }
35031
+ get values() {
35032
+ const sheetId = this.env.model.getters.getActiveSheetId();
35033
+ const values = this.env.model.getters.getDataValidationRangeValues(sheetId, this.props.criterion);
35034
+ return new Set(values);
35035
+ }
34592
35036
  }
34593
35037
 
34594
35038
  const criterionCategoriesSequences = {
@@ -36167,19 +36611,44 @@ const RED_DOT = {
36167
36611
  height: 512,
36168
36612
  paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36169
36613
  };
36170
- const CARET_DOWN = {
36171
- width: 512,
36172
- height: 512,
36173
- paths: [{ fillColor: TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
36174
- };
36175
- const HOVERED_CARET_DOWN = {
36176
- width: 512,
36177
- height: 512,
36178
- paths: [
36179
- { fillColor: TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
36180
- { fillColor: "#fff", path: "M120 195 h270 l-135 130" },
36181
- ],
36182
- };
36614
+ function getCaretDownSvg(color) {
36615
+ return {
36616
+ width: 512,
36617
+ height: 512,
36618
+ paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
36619
+ };
36620
+ }
36621
+ function getHoveredCaretDownSvg(color) {
36622
+ return {
36623
+ width: 512,
36624
+ height: 512,
36625
+ paths: [
36626
+ { fillColor: color.textColor || TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
36627
+ { fillColor: color.fillColor || "#fff", path: "M120 195 h270 l-135 130" },
36628
+ ],
36629
+ };
36630
+ }
36631
+ const CHIP_CARET_DOWN_PATH = "M40 185 h270 l-135 128";
36632
+ function getChipSvg(chipStyle) {
36633
+ return {
36634
+ width: 512,
36635
+ height: 512,
36636
+ paths: [{ fillColor: chipStyle.textColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH }],
36637
+ };
36638
+ }
36639
+ function getHoveredChipSvg(chipStyle) {
36640
+ return {
36641
+ width: 512,
36642
+ height: 512,
36643
+ paths: [
36644
+ {
36645
+ fillColor: chipStyle.textColor || TEXT_BODY_MUTED,
36646
+ path: "M0,225 A175,175 0 1,0 350,225 A175,175 0 1,0 0,225",
36647
+ },
36648
+ { fillColor: chipStyle.fillColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH },
36649
+ ],
36650
+ };
36651
+ }
36183
36652
  const CHECKBOX_UNCHECKED = {
36184
36653
  width: 512,
36185
36654
  height: 512,
@@ -41087,6 +41556,10 @@ const REMOVE_ROWS_ACTION = (env) => {
41087
41556
  });
41088
41557
  };
41089
41558
  const CAN_REMOVE_COLUMNS_ROWS = (dimension, env) => {
41559
+ if ((dimension === "COL" && env.model.getters.getActiveRows().size > 0) ||
41560
+ (dimension === "ROW" && env.model.getters.getActiveCols().size > 0)) {
41561
+ return false;
41562
+ }
41090
41563
  const sheetId = env.model.getters.getActiveSheetId();
41091
41564
  const selectedElements = env.model.getters.getElementsFromSelection(dimension);
41092
41565
  const includesAllVisibleHeaders = env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, dimension, selectedElements);
@@ -42641,7 +43114,7 @@ const insertDropdown = {
42641
43114
  criterion: {
42642
43115
  type: "isValueInList",
42643
43116
  values: [],
42644
- displayStyle: "arrow",
43117
+ displayStyle: "chip",
42645
43118
  },
42646
43119
  },
42647
43120
  });
@@ -44556,6 +45029,11 @@ class GridComposer extends Component {
44556
45029
  rect = this.defaultRect;
44557
45030
  isEditing = false;
44558
45031
  isCellReferenceVisible = false;
45032
+ currentEditedCell = {
45033
+ col: 0,
45034
+ row: 0,
45035
+ sheetId: this.env.model.getters.getActiveSheetId(),
45036
+ };
44559
45037
  composerStore;
44560
45038
  composerFocusStore;
44561
45039
  composerInterface;
@@ -44680,12 +45158,17 @@ class GridComposer extends Component {
44680
45158
  if (!isEditing && this.composerFocusStore.activeComposer !== this.composerInterface) {
44681
45159
  this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "inactive" });
44682
45160
  }
45161
+ let shouldRecomputeRect = !deepEquals(this.currentEditedCell, this.composerStore.currentEditedCell);
44683
45162
  if (this.isEditing !== isEditing) {
44684
45163
  this.isEditing = isEditing;
44685
45164
  if (!isEditing) {
44686
45165
  this.rect = this.defaultRect;
44687
45166
  return;
44688
45167
  }
45168
+ this.currentEditedCell = this.composerStore.currentEditedCell;
45169
+ shouldRecomputeRect = true;
45170
+ }
45171
+ if (shouldRecomputeRect) {
44689
45172
  const position = this.env.model.getters.getActivePosition();
44690
45173
  const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
44691
45174
  this.rect = this.env.model.getters.getVisibleRect(zone);
@@ -45791,11 +46274,14 @@ class GridOverlay extends Component {
45791
46274
  onCellClicked(ev) {
45792
46275
  const openedPopover = this.cellPopovers.persistentCellPopover;
45793
46276
  const [col, row] = this.getCartesianCoordinates(ev);
46277
+ const clickedIcon = this.getInteractiveIconAtEvent(ev);
46278
+ if (clickedIcon) {
46279
+ this.env.model.selection.getBackToDefault();
46280
+ }
45794
46281
  this.props.onCellClicked(col, row, {
45795
46282
  expandZone: ev.shiftKey,
45796
46283
  addZone: isCtrlKey(ev),
45797
46284
  }, ev);
45798
- const clickedIcon = this.getInteractiveIconAtEvent(ev);
45799
46285
  if (clickedIcon?.onClick) {
45800
46286
  clickedIcon.onClick(clickedIcon.position, this.env);
45801
46287
  }
@@ -46660,6 +47146,19 @@ class GridRenderer {
46660
47146
  const width = box.width * (percentage / 100);
46661
47147
  ctx.fillRect(box.x, box.y, width, box.height);
46662
47148
  }
47149
+ if (box?.chip) {
47150
+ ctx.save();
47151
+ ctx.beginPath();
47152
+ ctx.rect(box.x, box.y, box.width, box.height);
47153
+ ctx.clip();
47154
+ const chip = box.chip;
47155
+ ctx.fillStyle = chip.color;
47156
+ const radius = 10;
47157
+ ctx.beginPath();
47158
+ ctx.roundRect(chip.x, chip.y, chip.width, chip.height, radius);
47159
+ ctx.fill();
47160
+ ctx.restore();
47161
+ }
46663
47162
  if (box.overlayColor) {
46664
47163
  ctx.fillStyle = box.overlayColor;
46665
47164
  ctx.fillRect(box.x, box.y, box.width, box.height);
@@ -46799,19 +47298,6 @@ class GridRenderer {
46799
47298
  ctx.font = font;
46800
47299
  }
46801
47300
  ctx.fillStyle = style.textColor || "#000";
46802
- // compute horizontal align start point parameter
46803
- let x = box.x;
46804
- if (align === "left") {
46805
- const leftIconSize = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46806
- x += MIN_CELL_TEXT_MARGIN + leftIconSize;
46807
- }
46808
- else if (align === "right") {
46809
- const rightIconSize = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46810
- x += box.width - MIN_CELL_TEXT_MARGIN - rightIconSize;
46811
- }
46812
- else {
46813
- x += box.width / 2;
46814
- }
46815
47301
  // horizontal align text direction
46816
47302
  ctx.textAlign = align;
46817
47303
  // clip rect if needed
@@ -46822,15 +47308,13 @@ class GridRenderer {
46822
47308
  ctx.rect(x, y, width, height);
46823
47309
  ctx.clip();
46824
47310
  }
46825
- // compute vertical align start point parameter:
46826
- const textLineHeight = computeTextFontSizeInPixels(style);
46827
- const numberOfLines = box.content.textLines.length;
46828
- let y = this.getters.computeTextYCoordinate(box, textLineHeight, style.verticalAlign, numberOfLines);
47311
+ const x = box.content.x;
47312
+ let y = box.content.y;
46829
47313
  // use the horizontal and the vertical start points to:
46830
47314
  // fill text / fill strikethrough / fill underline
46831
47315
  for (const brokenLine of box.content.textLines) {
46832
- drawDecoratedText(ctx, brokenLine, { x: Math.round(x), y: Math.round(y) }, style.underline, style.strikethrough);
46833
- y += MIN_CELL_TEXT_MARGIN + textLineHeight;
47316
+ drawDecoratedText(ctx, brokenLine, { x, y }, style.underline, style.strikethrough);
47317
+ y += MIN_CELL_TEXT_MARGIN + box.content.fontSizePx;
46834
47318
  }
46835
47319
  if (box.clipRect) {
46836
47320
  ctx.restore();
@@ -47065,11 +47549,15 @@ class GridRenderer {
47065
47549
  const showFormula = this.getters.shouldShowFormulas();
47066
47550
  const { x, y, width, height } = this.getters.getRect(zone);
47067
47551
  const { verticalAlign } = this.getters.getCellStyle(position);
47552
+ const chipStyle = this.getters.getDataValidationChipStyle(position);
47068
47553
  let style = this.getters.getCellComputedStyle(position);
47069
47554
  if (this.fingerprints.isEnabled) {
47070
47555
  const fingerprintColor = this.fingerprints.colors.get(position);
47071
47556
  style = { ...style, fillColor: fingerprintColor };
47072
47557
  }
47558
+ if (chipStyle?.textColor) {
47559
+ style = { ...style, textColor: chipStyle.textColor };
47560
+ }
47073
47561
  const dataBarFill = this.fingerprints.isEnabled
47074
47562
  ? undefined
47075
47563
  : this.getters.getConditionalDataBar(position);
@@ -47103,22 +47591,55 @@ class GridRenderer {
47103
47591
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
47104
47592
  const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
47105
47593
  const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
47594
+ const chipMargin = chipStyle ? DATA_VALIDATION_CHIP_MARGIN : 0;
47106
47595
  const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
47596
+ const leftMargin = leftIconWidth + chipMargin;
47107
47597
  const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
47108
- const contentWidth = leftIconWidth + textWidth + rightIconWidth;
47598
+ const rightMargin = rightIconWidth + chipMargin;
47599
+ const contentWidth = leftMargin + textWidth + rightMargin;
47109
47600
  const align = this.computeCellAlignment(position, contentWidth > width);
47601
+ // compute vertical align start point parameter:
47602
+ const numberOfLines = multiLineText.length;
47603
+ const contentY = Math.round(this.getters.computeTextYCoordinate(box, fontSizePX, style.verticalAlign, numberOfLines));
47604
+ // compute horizontal align start point parameter
47605
+ let contentX = box.x;
47606
+ if (align === "left") {
47607
+ contentX += MIN_CELL_TEXT_MARGIN + leftMargin;
47608
+ }
47609
+ else if (align === "right") {
47610
+ contentX += box.width - MIN_CELL_TEXT_MARGIN - rightMargin;
47611
+ }
47612
+ else {
47613
+ contentX += box.width / 2;
47614
+ }
47615
+ contentX = Math.round(contentX);
47616
+ const textHeight = computeTextLinesHeight(fontSizePX, numberOfLines);
47110
47617
  box.content = {
47111
47618
  textLines: multiLineText,
47112
47619
  width: wrapping === "overflow" ? textWidth : width,
47113
47620
  align,
47621
+ x: contentX,
47622
+ y: contentY,
47623
+ fontSizePx: fontSizePX,
47114
47624
  };
47625
+ if (chipStyle?.fillColor) {
47626
+ const chipMarginLeft = leftMargin;
47627
+ const chipMarginRight = DATA_VALIDATION_CHIP_MARGIN;
47628
+ box.chip = {
47629
+ color: chipStyle.fillColor,
47630
+ width: box.width - chipMarginLeft - chipMarginRight,
47631
+ height: textHeight + 2,
47632
+ x: box.x + chipMarginLeft,
47633
+ y: contentY - 2,
47634
+ };
47635
+ }
47115
47636
  /** ClipRect */
47116
47637
  const isOverflowing = contentWidth > width || fontSizePX > height;
47117
- if (box.icons.left || box.icons.right) {
47638
+ if (box.icons.left || box.icons.right || box.chip) {
47118
47639
  box.clipRect = {
47119
- x: box.x + leftIconWidth,
47640
+ x: box.x + leftMargin,
47120
47641
  y: box.y,
47121
- width: Math.max(0, width - leftIconWidth - rightIconWidth),
47642
+ width: Math.max(0, width - leftMargin - rightMargin),
47122
47643
  height,
47123
47644
  };
47124
47645
  }
@@ -47818,15 +48339,6 @@ class Selection extends Component {
47818
48339
  }
47819
48340
  }
47820
48341
 
47821
- class Section extends Component {
47822
- static template = "o_spreadsheet.Section";
47823
- static props = {
47824
- class: { type: String, optional: true },
47825
- title: { type: String, optional: true },
47826
- slots: Object,
47827
- };
47828
- }
47829
-
47830
48342
  class ChartDataSeries extends Component {
47831
48343
  static template = "o-spreadsheet.ChartDataSeries";
47832
48344
  static components = { SelectionInput, Section };
@@ -48375,325 +48887,6 @@ class ActionButton extends Component {
48375
48887
  }
48376
48888
  }
48377
48889
 
48378
- const LINE_VERTICAL_PADDING = 1;
48379
- const PICKER_PADDING = 8;
48380
- const ITEM_BORDER_WIDTH = 1;
48381
- const ITEM_EDGE_LENGTH = 18;
48382
- const ITEMS_PER_LINE = 10;
48383
- const MAGNIFIER_EDGE = 16;
48384
- const ITEM_GAP = 2;
48385
- const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
48386
- const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
48387
- const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
48388
- const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
48389
- css /* scss */ `
48390
- .o-color-picker {
48391
- padding: ${PICKER_PADDING}px 0;
48392
- /* FIXME: this is useless, overiden by the popover container */
48393
- box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
48394
- background-color: white;
48395
- line-height: 1.2;
48396
- overflow-y: auto;
48397
- overflow-x: hidden;
48398
- width: ${CONTAINER_WIDTH}px;
48399
-
48400
- .o-color-picker-section-name {
48401
- margin: 0px ${ITEM_BORDER_WIDTH}px;
48402
- padding: 4px ${PICKER_PADDING}px;
48403
- }
48404
- .colors-grid {
48405
- display: grid;
48406
- padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
48407
- grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
48408
- grid-gap: ${ITEM_GAP}px;
48409
- }
48410
- .o-color-picker-toggler-button {
48411
- display: flex;
48412
- .o-color-picker-toggler-sign {
48413
- display: flex;
48414
- margin: auto auto;
48415
- width: 55%;
48416
- height: 55%;
48417
- .o-icon {
48418
- width: 100%;
48419
- height: 100%;
48420
- }
48421
- }
48422
- }
48423
- .o-color-picker-line-item {
48424
- width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48425
- height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48426
- margin: 0px;
48427
- border-radius: 50px;
48428
- border: ${ITEM_BORDER_WIDTH}px solid #666666;
48429
- padding: 0px;
48430
- font-size: 16px;
48431
- background: white;
48432
- &:hover {
48433
- background-color: rgba(0, 0, 0, 0.08);
48434
- outline: 1px solid gray;
48435
- cursor: pointer;
48436
- }
48437
- }
48438
- .o-buttons {
48439
- padding: ${PICKER_PADDING}px;
48440
- display: flex;
48441
- .o-cancel {
48442
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48443
- width: 100%;
48444
- padding: 5px;
48445
- font-size: 14px;
48446
- background: white;
48447
- border-radius: 4px;
48448
- &:hover:enabled {
48449
- background-color: rgba(0, 0, 0, 0.08);
48450
- }
48451
- }
48452
- }
48453
- .o-add-button {
48454
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48455
- padding: 4px;
48456
- background: white;
48457
- border-radius: 4px;
48458
- &:hover:enabled {
48459
- background-color: rgba(0, 0, 0, 0.08);
48460
- }
48461
- }
48462
- .o-separator {
48463
- border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
48464
- margin-top: ${MENU_SEPARATOR_PADDING}px;
48465
- margin-bottom: ${MENU_SEPARATOR_PADDING}px;
48466
- }
48467
-
48468
- .o-custom-selector {
48469
- padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
48470
- position: relative;
48471
- .o-gradient {
48472
- margin-bottom: ${MAGNIFIER_EDGE / 2}px;
48473
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48474
- width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
48475
- height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
48476
- position: relative;
48477
- }
48478
-
48479
- .magnifier {
48480
- height: ${MAGNIFIER_EDGE}px;
48481
- width: ${MAGNIFIER_EDGE}px;
48482
- border-radius: 50%;
48483
- border: 2px solid #fff;
48484
- box-shadow: 0px 0px 3px #c0c0c0;
48485
- position: absolute;
48486
- z-index: 2;
48487
- }
48488
- .saturation {
48489
- background: linear-gradient(to right, #fff 0%, transparent 100%);
48490
- }
48491
- .lightness {
48492
- background: linear-gradient(to top, #000 0%, transparent 100%);
48493
- }
48494
- .o-hue-picker {
48495
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48496
- width: 100%;
48497
- height: 12px;
48498
- border-radius: 4px;
48499
- background: linear-gradient(
48500
- to right,
48501
- hsl(0 100% 50%) 0%,
48502
- hsl(0.2turn 100% 50%) 20%,
48503
- hsl(0.3turn 100% 50%) 30%,
48504
- hsl(0.4turn 100% 50%) 40%,
48505
- hsl(0.5turn 100% 50%) 50%,
48506
- hsl(0.6turn 100% 50%) 60%,
48507
- hsl(0.7turn 100% 50%) 70%,
48508
- hsl(0.8turn 100% 50%) 80%,
48509
- hsl(0.9turn 100% 50%) 90%,
48510
- hsl(1turn 100% 50%) 100%
48511
- );
48512
- position: relative;
48513
- cursor: crosshair;
48514
- }
48515
- .o-hue-slider {
48516
- margin-top: -3px;
48517
- }
48518
- .o-custom-input-preview {
48519
- padding: 2px 0px;
48520
- display: flex;
48521
- input {
48522
- width: 50%;
48523
- border-radius: 4px;
48524
- padding: 4px 23px 4px 10px;
48525
- height: 24px;
48526
- border: 1px solid #c0c0c0;
48527
- margin-right: 2px;
48528
- }
48529
- .o-wrong-color {
48530
- /* FIXME bootstrap class instead? */
48531
- outline-color: red;
48532
- border-color: red;
48533
- &:focus {
48534
- outline-style: solid;
48535
- outline-width: 1px;
48536
- }
48537
- }
48538
- }
48539
- .o-custom-input-buttons {
48540
- padding: 2px 0px;
48541
- display: flex;
48542
- justify-content: end;
48543
- }
48544
- .o-color-preview {
48545
- border: 1px solid #c0c0c0;
48546
- border-radius: 4px;
48547
- width: 50%;
48548
- }
48549
- }
48550
- }
48551
- `;
48552
- class ColorPicker extends Component {
48553
- static template = "o-spreadsheet-ColorPicker";
48554
- static props = {
48555
- onColorPicked: Function,
48556
- currentColor: { type: String, optional: true },
48557
- maxHeight: { type: Number, optional: true },
48558
- anchorRect: Object,
48559
- disableNoColor: { type: Boolean, optional: true },
48560
- };
48561
- static defaultProps = { currentColor: "" };
48562
- static components = { Popover };
48563
- COLORS = COLOR_PICKER_DEFAULTS;
48564
- state = useState({
48565
- showGradient: false,
48566
- currentHslaColor: isColorValid(this.props.currentColor)
48567
- ? { ...hexToHSLA(this.props.currentColor), a: 1 }
48568
- : { h: 0, s: 100, l: 100, a: 1 },
48569
- customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
48570
- });
48571
- get colorPickerStyle() {
48572
- if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
48573
- return cssPropertiesToCss({ display: "none" });
48574
- }
48575
- return "";
48576
- }
48577
- get popoverProps() {
48578
- return {
48579
- anchorRect: this.props.anchorRect,
48580
- maxHeight: this.props.maxHeight,
48581
- positioning: "bottom-left",
48582
- verticalOffset: 0,
48583
- };
48584
- }
48585
- get gradientHueStyle() {
48586
- const hue = this.state.currentHslaColor?.h || 0;
48587
- return cssPropertiesToCss({
48588
- background: `hsl(${hue} 100% 50%)`,
48589
- });
48590
- }
48591
- get sliderStyle() {
48592
- const hue = this.state.currentHslaColor?.h || 0;
48593
- const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
48594
- const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
48595
- return cssPropertiesToCss({
48596
- "margin-left": `${left}px`,
48597
- });
48598
- }
48599
- get pointerStyle() {
48600
- const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
48601
- const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
48602
- const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
48603
- return cssPropertiesToCss({
48604
- left: `${-MAGNIFIER_EDGE / 2 + left}px`,
48605
- top: `${-MAGNIFIER_EDGE / 2 + top}px`,
48606
- background: hslaToHex(this.state.currentHslaColor),
48607
- });
48608
- }
48609
- get colorPreviewStyle() {
48610
- return cssPropertiesToCss({
48611
- "background-color": hslaToHex(this.state.currentHslaColor),
48612
- });
48613
- }
48614
- get checkmarkColor() {
48615
- return chartFontColor(this.props.currentColor);
48616
- }
48617
- get isHexColorInputValid() {
48618
- return !this.state.customHexColor || isColorValid(this.state.customHexColor);
48619
- }
48620
- setCustomGradient({ x, y }) {
48621
- const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
48622
- const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
48623
- const deltaX = offsetX / INNER_GRADIENT_WIDTH;
48624
- const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
48625
- const s = 100 * deltaX;
48626
- const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
48627
- this.updateColor({ s, l });
48628
- }
48629
- setCustomHue(x) {
48630
- // needs to be capped such that h is in [0°, 359°]
48631
- const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
48632
- this.updateColor({ h });
48633
- }
48634
- updateColor(newHsl) {
48635
- this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
48636
- this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
48637
- }
48638
- onColorClick(color) {
48639
- if (color) {
48640
- this.props.onColorPicked(toHex(color));
48641
- }
48642
- }
48643
- resetColor() {
48644
- this.props.onColorPicked("");
48645
- }
48646
- toggleColorPicker() {
48647
- this.state.showGradient = !this.state.showGradient;
48648
- }
48649
- dragGradientPointer(ev) {
48650
- const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
48651
- this.setCustomGradient(initialGradientCoordinates);
48652
- const initialMousePosition = { x: ev.clientX, y: ev.clientY };
48653
- const onMouseMove = (ev) => {
48654
- const currentMousePosition = { x: ev.clientX, y: ev.clientY };
48655
- const deltaX = currentMousePosition.x - initialMousePosition.x;
48656
- const deltaY = currentMousePosition.y - initialMousePosition.y;
48657
- const currentGradientCoordinates = {
48658
- x: initialGradientCoordinates.x + deltaX,
48659
- y: initialGradientCoordinates.y + deltaY,
48660
- };
48661
- this.setCustomGradient(currentGradientCoordinates);
48662
- };
48663
- startDnd(onMouseMove, () => { });
48664
- }
48665
- dragHuePointer(ev) {
48666
- const initialX = ev.offsetX;
48667
- const initialMouseX = ev.clientX;
48668
- this.setCustomHue(initialX);
48669
- const onMouseMove = (ev) => {
48670
- const currentMouseX = ev.clientX;
48671
- const deltaX = currentMouseX - initialMouseX;
48672
- const x = initialX + deltaX;
48673
- this.setCustomHue(x);
48674
- };
48675
- startDnd(onMouseMove, () => { });
48676
- }
48677
- setHexColor(ev) {
48678
- // only support HEX code input
48679
- const val = ev.target.value.replace("##", "#").slice(0, 7);
48680
- this.state.customHexColor = val;
48681
- if (!isColorValid(val)) ;
48682
- else {
48683
- this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
48684
- }
48685
- }
48686
- addCustomColor(ev) {
48687
- if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
48688
- return;
48689
- }
48690
- this.props.onColorPicked(toHex(this.state.customHexColor));
48691
- }
48692
- isSameColor(color1, color2) {
48693
- return isSameColor(color1, color2);
48694
- }
48695
- }
48696
-
48697
48890
  css /* scss */ `
48698
48891
  .o-color-picker-widget {
48699
48892
  display: flex;
@@ -49161,57 +49354,6 @@ class RadioSelection extends Component {
49161
49354
  };
49162
49355
  }
49163
49356
 
49164
- const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
49165
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
49166
- <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
49167
- </svg>
49168
- `;
49169
- css /* scss */ `
49170
- .o-round-color-picker-button {
49171
- width: 20px;
49172
- height: 20px;
49173
- cursor: pointer;
49174
- border: 1px solid ${GRAY_300};
49175
- background-position: 1px 1px;
49176
- background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
49177
- }
49178
- `;
49179
- class RoundColorPicker extends Component {
49180
- static template = "o-spreadsheet.RoundColorPicker";
49181
- static components = { Section, ColorPicker };
49182
- static props = {
49183
- currentColor: { type: String, optional: true },
49184
- title: { type: String, optional: true },
49185
- onColorPicked: Function,
49186
- disableNoColor: { type: Boolean, optional: true },
49187
- };
49188
- colorPickerButtonRef = useRef("colorPickerButton");
49189
- state;
49190
- setup() {
49191
- this.state = useState({ pickerOpened: false });
49192
- useExternalListener(window, "click", this.closePicker);
49193
- }
49194
- closePicker() {
49195
- this.state.pickerOpened = false;
49196
- }
49197
- togglePicker() {
49198
- this.state.pickerOpened = !this.state.pickerOpened;
49199
- }
49200
- onColorPicked(color) {
49201
- this.props.onColorPicked(color);
49202
- this.state.pickerOpened = false;
49203
- }
49204
- get colorPickerAnchorRect() {
49205
- const button = this.colorPickerButtonRef.el;
49206
- return getBoundingRectAsPOJO(button);
49207
- }
49208
- get buttonStyle() {
49209
- return cssPropertiesToCss({
49210
- background: this.props.currentColor,
49211
- });
49212
- }
49213
- }
49214
-
49215
49357
  class GeneralDesignEditor extends Component {
49216
49358
  static template = "o-spreadsheet-GeneralDesignEditor";
49217
49359
  static components = {
@@ -59542,7 +59684,7 @@ class DataValidationPlugin extends CorePlugin {
59542
59684
  if (!rule)
59543
59685
  return false;
59544
59686
  return ((rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
59545
- rule.criterion.displayStyle === "arrow");
59687
+ (rule.criterion.displayStyle === "arrow" || rule.criterion.displayStyle === "chip"));
59546
59688
  }
59547
59689
  addDataValidationRule(sheetId, newRule) {
59548
59690
  const rules = this.rules[sheetId];
@@ -65908,7 +66050,10 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65908
66050
  "getDataValidationInvalidCriterionValueMessage",
65909
66051
  "getInvalidDataValidationMessage",
65910
66052
  "getValidationResultForCellValue",
66053
+ "getDataValidationRangeValues",
65911
66054
  "isCellValidCheckbox",
66055
+ "getDataValidationCellStyle",
66056
+ "getDataValidationChipStyle",
65912
66057
  "isDataValidationInvalid",
65913
66058
  ];
65914
66059
  validationResults = {};
@@ -65929,6 +66074,18 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65929
66074
  isDataValidationInvalid(cellPosition) {
65930
66075
  return !this.getValidationResultForCell(cellPosition).isValid;
65931
66076
  }
66077
+ getDataValidationCellStyle(position) {
66078
+ if (this.hasChip(position)) {
66079
+ return undefined; // The style is not applied on the cell if it's a chip
66080
+ }
66081
+ return this.getDataValidationStyle(position);
66082
+ }
66083
+ getDataValidationChipStyle(position) {
66084
+ if (this.hasChip(position)) {
66085
+ return this.getDataValidationStyle(position) ?? { fillColor: GRAY_200 };
66086
+ }
66087
+ return undefined;
66088
+ }
65932
66089
  getInvalidDataValidationMessage(cellPosition) {
65933
66090
  const validationResult = this.getValidationResultForCell(cellPosition);
65934
66091
  return validationResult.isValid ? undefined : validationResult.error;
@@ -65951,6 +66108,11 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65951
66108
  }
65952
66109
  return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;
65953
66110
  }
66111
+ getDataValidationRangeValues(sheetId, criterion) {
66112
+ const range = this.getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
66113
+ const criterionValues = this.getters.getRangeValues(range);
66114
+ return criterionValues.map((value) => value?.toString()).filter(isDefined);
66115
+ }
65954
66116
  isCellValidCheckbox(cellPosition) {
65955
66117
  if (!this.getters.isMainCellPosition(cellPosition)) {
65956
66118
  return false;
@@ -65970,6 +66132,38 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65970
66132
  const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
65971
66133
  return error ? { error, rule, isValid: false } : VALID_RESULT;
65972
66134
  }
66135
+ hasChip(position) {
66136
+ const rule = this.getters.getValidationRuleForCell(position);
66137
+ return ((rule?.criterion.type === "isValueInList" || rule?.criterion.type === "isValueInRange") &&
66138
+ rule.criterion.displayStyle === "chip");
66139
+ }
66140
+ getDataValidationStyle(position) {
66141
+ const rule = this.getters.getValidationRuleForCell(position);
66142
+ if (!rule || this.isDataValidationInvalid(position)) {
66143
+ return undefined;
66144
+ }
66145
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
66146
+ const color = this.getValueColor(rule, evaluatedCell.value);
66147
+ if (!color) {
66148
+ return undefined;
66149
+ }
66150
+ const style = {
66151
+ fillColor: color,
66152
+ textColor: chipTextColor(color),
66153
+ };
66154
+ return style;
66155
+ }
66156
+ getValueColor(rule, value) {
66157
+ if (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange") {
66158
+ return undefined;
66159
+ }
66160
+ for (const criterionValue in rule.criterion.colors) {
66161
+ if (criterionValue.toLowerCase() === String(value).toLowerCase()) {
66162
+ return rule.criterion.colors[criterionValue];
66163
+ }
66164
+ }
66165
+ return undefined;
66166
+ }
65973
66167
  isValidFormula(value) {
65974
66168
  return !compile(value).isBadExpression;
65975
66169
  }
@@ -66066,12 +66260,35 @@ iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
66066
66260
  }
66067
66261
  return undefined;
66068
66262
  });
66263
+ iconsOnCellRegistry.add("data_validation_chip_icon", (getters, position) => {
66264
+ const chipStyle = getters.getDataValidationChipStyle(position);
66265
+ if (chipStyle) {
66266
+ const cellStyle = getters.getCellComputedStyle(position);
66267
+ return {
66268
+ svg: getChipSvg(chipStyle),
66269
+ hoverSvg: getHoveredChipSvg(chipStyle),
66270
+ priority: 10,
66271
+ horizontalAlign: "right",
66272
+ size: computeTextFontSizeInPixels(cellStyle),
66273
+ margin: 4,
66274
+ position,
66275
+ onClick: (position, env) => {
66276
+ const { col, row } = position;
66277
+ env.model.selection.selectCell(col, row);
66278
+ env.startCellEdition();
66279
+ },
66280
+ type: "data_validation_chip_icon",
66281
+ };
66282
+ }
66283
+ return undefined;
66284
+ });
66069
66285
  iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
66070
66286
  const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
66071
66287
  if (hasIcon) {
66288
+ const cellStyle = getters.getCellComputedStyle(position);
66072
66289
  return {
66073
- svg: CARET_DOWN,
66074
- hoverSvg: HOVERED_CARET_DOWN,
66290
+ svg: getCaretDownSvg(cellStyle),
66291
+ hoverSvg: getHoveredCaretDownSvg(cellStyle),
66075
66292
  priority: 2,
66076
66293
  horizontalAlign: "right",
66077
66294
  size: GRID_ICON_EDGE_LENGTH,
@@ -68613,11 +68830,11 @@ class OTRegistry extends Registry {
68613
68830
  * transformation function given
68614
68831
  */
68615
68832
  addTransformation(executed, toTransforms, fn) {
68833
+ if (!this.content[executed]) {
68834
+ this.content[executed] = new Map();
68835
+ }
68616
68836
  for (const toTransform of toTransforms) {
68617
- if (!this.content[toTransform]) {
68618
- this.content[toTransform] = new Map();
68619
- }
68620
- this.content[toTransform].set(executed, fn);
68837
+ this.content[executed].set(toTransform, fn);
68621
68838
  }
68622
68839
  return this;
68623
68840
  }
@@ -68626,7 +68843,7 @@ class OTRegistry extends Registry {
68626
68843
  * that the executed command happened.
68627
68844
  */
68628
68845
  getTransformation(toTransform, executed) {
68629
- return this.content[toTransform] && this.content[toTransform].get(executed);
68846
+ return this.content[executed] && this.content[executed].get(toTransform);
68630
68847
  }
68631
68848
  }
68632
68849
  const otRegistry = new OTRegistry();
@@ -68956,10 +69173,20 @@ function adaptTransform(toTransform, executed) {
68956
69173
  */
68957
69174
  function transformAll(toTransform, executed) {
68958
69175
  let transformedCommands = [...toTransform];
69176
+ const possibleTransformations = new Set(otRegistry.getKeys());
68959
69177
  for (const executedCommand of executed) {
68960
- transformedCommands = transformedCommands
68961
- .map((cmd) => transform(cmd, executedCommand))
68962
- .filter(isDefined);
69178
+ // If the executed command is not in the registry, we skip it
69179
+ // because we know there won't be any transformation impacting the
69180
+ // commands to transform.
69181
+ if (possibleTransformations.has(executedCommand.type)) {
69182
+ transformedCommands = transformedCommands.reduce((acc, cmd) => {
69183
+ const transformed = transform(cmd, executedCommand);
69184
+ if (transformed) {
69185
+ acc.push(transformed);
69186
+ }
69187
+ return acc;
69188
+ }, []);
69189
+ }
68963
69190
  }
68964
69191
  return transformedCommands;
68965
69192
  }
@@ -70516,6 +70743,9 @@ class SheetUIPlugin extends UIPlugin {
70516
70743
  for (const icon of this.getters.getCellIcons(position)) {
70517
70744
  contentWidth += icon.margin + icon.size;
70518
70745
  }
70746
+ if (this.getters.getDataValidationChipStyle(position)) {
70747
+ contentWidth += DATA_VALIDATION_CHIP_MARGIN * 2;
70748
+ }
70519
70749
  if (contentWidth === 0) {
70520
70750
  return 0;
70521
70751
  }
@@ -70673,7 +70903,7 @@ class SheetUIPlugin extends UIPlugin {
70673
70903
  }
70674
70904
  const position = this.getters.getCellPosition(cell.id);
70675
70905
  const colSize = this.getters.getColSize(sheetId, position.col);
70676
- if (cell.isFormula) {
70906
+ if (cell.isFormula || this.getters.getArrayFormulaSpreadingOn(position)) {
70677
70907
  const content = this.getters.getEvaluatedCell(position).formattedValue;
70678
70908
  const evaluatedSize = getCellContentHeight(this.ctx, content, cell?.style, colSize);
70679
70909
  if (evaluatedSize > evaluatedRowSize && evaluatedSize > DEFAULT_CELL_HEIGHT) {
@@ -70876,6 +71106,8 @@ class CellComputedStylePlugin extends UIPlugin {
70876
71106
  if (invalidateEvaluationCommands.has(cmd.type) ||
70877
71107
  cmd.type === "UPDATE_CELL" ||
70878
71108
  cmd.type === "SET_FORMATTING" ||
71109
+ cmd.type === "ADD_DATA_VALIDATION_RULE" ||
71110
+ cmd.type === "REMOVE_DATA_VALIDATION_RULE" ||
70879
71111
  cmd.type === "EVALUATE_CELLS") {
70880
71112
  this.styles = {};
70881
71113
  this.borders = {};
@@ -70947,8 +71179,10 @@ class CellComputedStylePlugin extends UIPlugin {
70947
71179
  const cell = this.getters.getCell(position);
70948
71180
  const cfStyle = this.getters.getCellConditionalFormatStyle(position);
70949
71181
  const tableStyle = this.getters.getCellTableStyle(position);
71182
+ const dataValidationStyle = this.getters.getDataValidationCellStyle(position);
70950
71183
  const computedStyle = {
70951
71184
  ...removeFalsyAttributes(tableStyle),
71185
+ ...removeFalsyAttributes(dataValidationStyle),
70952
71186
  ...removeFalsyAttributes(cell?.style),
70953
71187
  ...removeFalsyAttributes(cfStyle),
70954
71188
  };
@@ -74510,19 +74744,29 @@ autoCompleteProviders.add("dataValidation", {
74510
74744
  (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange")) {
74511
74745
  return [];
74512
74746
  }
74513
- let values;
74514
- if (rule.criterion.type === "isValueInList") {
74515
- values = rule.criterion.values;
74516
- }
74517
- else {
74518
- const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
74519
- values = Array.from(new Set(this.getters
74520
- .getRangeValues(range)
74521
- .filter(isNotNull)
74522
- .map((value) => value.toString())
74523
- .filter((val) => val !== "")));
74524
- }
74525
- return values.map((value) => ({ text: value }));
74747
+ const sheetId = this.composer.currentEditedCell.sheetId;
74748
+ const values = rule.criterion.type === "isValueInRange"
74749
+ ? Array.from(new Set(this.getters.getDataValidationRangeValues(sheetId, rule.criterion)))
74750
+ : rule.criterion.values;
74751
+ const isChip = rule.criterion.displayStyle === "chip";
74752
+ if (!isChip) {
74753
+ return values.map((value) => ({ text: value }));
74754
+ }
74755
+ const colors = rule.criterion.colors;
74756
+ return values.map((value) => {
74757
+ const color = colors?.[value];
74758
+ return {
74759
+ text: value,
74760
+ htmlContent: [
74761
+ {
74762
+ value,
74763
+ color: color ? chipTextColor(color) : undefined,
74764
+ backgroundColor: color || GRAY_200,
74765
+ classes: ["badge rounded-pill fs-6 fw-normal w-100 mt-1 text-start"],
74766
+ },
74767
+ ],
74768
+ };
74769
+ });
74526
74770
  },
74527
74771
  selectProposal(tokenAtCursor, value) {
74528
74772
  this.composer.setCurrentContent(value);
@@ -83342,6 +83586,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
83342
83586
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, ClientDisconnectedError, CommandResult, CorePlugin, CoreViewPlugin, DispatchResult, EvaluationError, LocalTransportService, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, chartHelpers, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
83343
83587
 
83344
83588
 
83345
- __info__.version = "18.4.0-alpha.8";
83346
- __info__.date = "2025-06-12T09:53:48.133Z";
83347
- __info__.hash = "9b7a8d0";
83589
+ __info__.version = "18.4.0-alpha.9";
83590
+ __info__.date = "2025-06-19T18:23:22.025Z";
83591
+ __info__.hash = "6d4d685";