@odoo/o-spreadsheet 18.4.0-alpha.8 → 18.4.0

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
6
+ * @date 2025-06-24T11:19:24.606Z
7
+ * @hash a5b7cad
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
@@ -155,6 +163,7 @@
155
163
  const FROZEN_PANE_BORDER_COLOR = "#DADFE8";
156
164
  const COMPOSER_ASSISTANT_COLOR = "#9B359B";
157
165
  const COLOR_TRANSPARENT = "#00000000";
166
+ const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414";
158
167
  const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2";
159
168
  const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175";
160
169
  const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA";
@@ -298,6 +307,7 @@
298
307
  const GRID_ICON_MARGIN = 2;
299
308
  const GRID_ICON_EDGE_LENGTH = 17;
300
309
  const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT;
310
+ const DATA_VALIDATION_CHIP_MARGIN = 5;
301
311
  // 768px is a common breakpoint for small screens
302
312
  // Typically inside Odoo, it is the threshold for switching to mobile view
303
313
  const MOBILE_WIDTH_BREAKPOINT = 768;
@@ -643,9 +653,6 @@
643
653
  function isDefined(argument) {
644
654
  return argument !== undefined;
645
655
  }
646
- function isNotNull(argument) {
647
- return argument !== null;
648
- }
649
656
  /**
650
657
  * Check if all the values of an object, and all the values of the objects inside of it, are undefined.
651
658
  */
@@ -1356,9 +1363,16 @@
1356
1363
  if (percentage === 1) {
1357
1364
  return "#000";
1358
1365
  }
1366
+ // increase saturation to compensate and make it more vivid
1367
+ hsla.s = Math.min(100, percentage * hsla.s + hsla.s);
1359
1368
  hsla.l = hsla.l - percentage * hsla.l;
1360
1369
  return hslaToHex(hsla);
1361
1370
  }
1371
+ function chipTextColor(chipBackgroundColor) {
1372
+ return relativeLuminance(chipBackgroundColor) < 0.6
1373
+ ? lightenColor(chipBackgroundColor, 0.9)
1374
+ : darkenColor(chipBackgroundColor, 0.75);
1375
+ }
1362
1376
  const COLORS_SM = [
1363
1377
  "#4EA7F2", // Blue
1364
1378
  "#EA6175", // Red
@@ -6835,19 +6849,17 @@
6835
6849
  const textWidthCache = {};
6836
6850
  function computeTextWidth(context, text, style, fontUnit = "pt") {
6837
6851
  const font = computeTextFont(style, fontUnit);
6838
- context.save();
6839
- context.font = font;
6840
- const width = computeCachedTextWidth(context, text);
6841
- context.restore();
6842
- return width;
6852
+ return computeCachedTextWidth(context, text, font);
6843
6853
  }
6844
- function computeCachedTextWidth(context, text) {
6845
- const font = context.font;
6854
+ function computeCachedTextWidth(context, text, font) {
6846
6855
  if (!textWidthCache[font]) {
6847
6856
  textWidthCache[font] = {};
6848
6857
  }
6849
6858
  if (textWidthCache[font][text] === undefined) {
6859
+ const oldFont = context.font;
6860
+ context.font = font;
6850
6861
  textWidthCache[font][text] = context.measureText(text).width;
6862
+ context.font = oldFont;
6851
6863
  }
6852
6864
  return textWidthCache[font][text];
6853
6865
  }
@@ -7012,19 +7024,19 @@
7012
7024
  }
7013
7025
  // Inspired from https://stackoverflow.com/a/10511598
7014
7026
  function clipTextWithEllipsis(ctx, text, maxWidth) {
7015
- let width = computeCachedTextWidth(ctx, text);
7027
+ let width = computeCachedTextWidth(ctx, text, ctx.font);
7016
7028
  if (width <= maxWidth) {
7017
7029
  return text;
7018
7030
  }
7019
7031
  const ellipsis = "…";
7020
- const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis);
7032
+ const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis, ctx.font);
7021
7033
  if (width <= ellipsisWidth) {
7022
7034
  return text;
7023
7035
  }
7024
7036
  let len = text.length;
7025
7037
  while (width >= maxWidth - ellipsisWidth && len-- > 0) {
7026
7038
  text = text.substring(0, len);
7027
- width = computeCachedTextWidth(ctx, text);
7039
+ width = computeCachedTextWidth(ctx, text, ctx.font);
7028
7040
  }
7029
7041
  return text + ellipsis;
7030
7042
  }
@@ -7238,6 +7250,63 @@
7238
7250
  };
7239
7251
  return osClipboardContent;
7240
7252
  }
7253
+ /**
7254
+ * Applies each clipboard handler to paste its corresponding data into the target.
7255
+ */
7256
+ const applyClipboardHandlersPaste = (handlers, copiedData, target, options) => {
7257
+ handlers.forEach(({ handlerName, handler }) => {
7258
+ const data = copiedData[handlerName];
7259
+ if (data) {
7260
+ handler.paste(target, data, options);
7261
+ }
7262
+ });
7263
+ };
7264
+ /**
7265
+ * Returns the paste target based on clipboard handlers.
7266
+ * Also includes the full affected zone and the list of pasted zones for selection.
7267
+ */
7268
+ function getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options) {
7269
+ let zone = undefined;
7270
+ const selectedZones = [];
7271
+ const target = {
7272
+ sheetId,
7273
+ zones,
7274
+ };
7275
+ for (const { handlerName, handler } of handlers) {
7276
+ const handlerData = copiedData[handlerName];
7277
+ if (!handlerData) {
7278
+ continue;
7279
+ }
7280
+ const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);
7281
+ if (currentTarget.figureId) {
7282
+ target.figureId = currentTarget.figureId;
7283
+ }
7284
+ for (const targetZone of currentTarget.zones) {
7285
+ selectedZones.push(targetZone);
7286
+ if (zone === undefined) {
7287
+ zone = targetZone;
7288
+ continue;
7289
+ }
7290
+ zone = union(zone, targetZone);
7291
+ }
7292
+ }
7293
+ return {
7294
+ target,
7295
+ zone,
7296
+ selectedZones,
7297
+ };
7298
+ }
7299
+ /**
7300
+ * Updates the selection after a paste operation.
7301
+ */
7302
+ const selectPastedZone = (selection, sourceZones, pastedZones) => {
7303
+ const anchorCell = {
7304
+ col: sourceZones[0].left,
7305
+ row: sourceZones[0].top,
7306
+ };
7307
+ selection.getBackToDefault();
7308
+ selection.selectZone({ cell: anchorCell, zone: union(...pastedZones) }, { scrollIntoView: false });
7309
+ };
7241
7310
 
7242
7311
  class ClipboardHandler {
7243
7312
  getters;
@@ -9942,8 +10011,15 @@ stores.inject(MyMetaStore, storeInstance);
9942
10011
  const ModelStore = createAbstractStore("Model");
9943
10012
 
9944
10013
  class RendererStore {
9945
- mutators = ["register", "unRegister", "drawLayer"];
10014
+ mutators = ["register", "unRegister", "draw", "startAnimation", "stopAnimation"];
9946
10015
  renderers = {};
10016
+ model;
10017
+ context = undefined;
10018
+ animationFrameId = null;
10019
+ registeredAnimations = new Set();
10020
+ constructor(get) {
10021
+ this.model = get(ModelStore);
10022
+ }
9947
10023
  register(renderer) {
9948
10024
  if (!renderer.renderingLayers.length) {
9949
10025
  return;
@@ -9960,17 +10036,54 @@ stores.inject(MyMetaStore, storeInstance);
9960
10036
  this.renderers[layer] = this.renderers[layer].filter((r) => r !== renderer);
9961
10037
  }
9962
10038
  }
9963
- drawLayer(context, layer) {
10039
+ drawLayer(context, layer, timeStamp) {
9964
10040
  const renderers = this.renderers[layer];
9965
10041
  if (renderers) {
9966
10042
  for (const renderer of renderers) {
9967
10043
  context.ctx.save();
9968
- renderer.drawLayer(context, layer);
10044
+ renderer.drawLayer(context, layer, timeStamp);
9969
10045
  context.ctx.restore();
9970
10046
  }
9971
10047
  }
9972
10048
  return "noStateChange";
9973
10049
  }
10050
+ draw(context, timestamp) {
10051
+ context = context || this.context;
10052
+ if (!context) {
10053
+ throw new Error("Rendering context is not defined");
10054
+ }
10055
+ this.context = context;
10056
+ for (const layer of OrderedLayers()) {
10057
+ this.model.drawLayer(context, layer);
10058
+ this.drawLayer(context, layer, timestamp);
10059
+ }
10060
+ return "noStateChange";
10061
+ }
10062
+ startAnimation(animationId) {
10063
+ this.registeredAnimations.add(animationId);
10064
+ if (!this.animationFrameId) {
10065
+ const animationCallback = (timestamp) => {
10066
+ this.animationFrameId = requestAnimationFrame(animationCallback);
10067
+ this.draw(undefined, timestamp);
10068
+ };
10069
+ this.animationFrameId = requestAnimationFrame(animationCallback);
10070
+ }
10071
+ return "noStateChange";
10072
+ }
10073
+ stopAnimation(animationId) {
10074
+ this.registeredAnimations.delete(animationId);
10075
+ if (this.registeredAnimations.size === 0 && this.animationFrameId !== null) {
10076
+ cancelAnimationFrame(this.animationFrameId);
10077
+ this.animationFrameId = null;
10078
+ }
10079
+ return "noStateChange";
10080
+ }
10081
+ dispose() {
10082
+ if (this.animationFrameId) {
10083
+ cancelAnimationFrame(this.animationFrameId);
10084
+ this.animationFrameId = null;
10085
+ }
10086
+ }
9974
10087
  }
9975
10088
 
9976
10089
  class SpreadsheetStore extends DisposableStore {
@@ -9994,7 +10107,7 @@ stores.inject(MyMetaStore, storeInstance);
9994
10107
  }
9995
10108
  handle(cmd) { }
9996
10109
  finalize() { }
9997
- drawLayer(ctx, layer) { }
10110
+ drawLayer(ctx, layer, timestamp) { }
9998
10111
  }
9999
10112
 
10000
10113
  const VOID_COMPOSER = {
@@ -21687,6 +21800,7 @@ stores.inject(MyMetaStore, storeInstance);
21687
21800
  if (isTrendLineAxis(dataset.xAxisID) || dataset.hidden) {
21688
21801
  continue;
21689
21802
  }
21803
+ const yAxisScale = chart.scales[dataset.yAxisID];
21690
21804
  for (let i = 0; i < dataset._parsed.length; i++) {
21691
21805
  const parsedValue = dataset._parsed[i];
21692
21806
  const value = Number(chart.config.type === "radar" ? parsedValue.r : parsedValue.y);
@@ -21697,10 +21811,18 @@ stores.inject(MyMetaStore, storeInstance);
21697
21811
  const xPosition = point.x;
21698
21812
  let yPosition = 0;
21699
21813
  if (chart.config.type === "line" || chart.config.type === "radar") {
21700
- yPosition = point.y - 10;
21814
+ yPosition = value < 0 ? point.y + 10 : point.y - 10;
21701
21815
  }
21702
21816
  else {
21703
- yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21817
+ const yZeroLine = yAxisScale.getPixelForValue(0);
21818
+ const distanceFromAxisOrigin = Math.abs(yZeroLine - point.y);
21819
+ const textHeight = 12; // ChartJS default text height
21820
+ if (distanceFromAxisOrigin < textHeight) {
21821
+ yPosition = value < 0 ? yZeroLine + textHeight / 2 : yZeroLine - textHeight / 2;
21822
+ }
21823
+ else {
21824
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21825
+ }
21704
21826
  }
21705
21827
  yPosition = Math.min(yPosition, yMax);
21706
21828
  yPosition = Math.max(yPosition, yMin);
@@ -21710,7 +21832,7 @@ stores.inject(MyMetaStore, storeInstance);
21710
21832
  }
21711
21833
  for (const otherPosition of textsPositions[xPosition] || []) {
21712
21834
  if (Math.abs(otherPosition - yPosition) < 13) {
21713
- yPosition = otherPosition - 13;
21835
+ yPosition = value < 0 ? otherPosition + 13 : otherPosition - 13;
21714
21836
  }
21715
21837
  }
21716
21838
  textsPositions[xPosition].push(yPosition);
@@ -21729,6 +21851,8 @@ stores.inject(MyMetaStore, storeInstance);
21729
21851
  if (isTrendLineAxis(dataset.xAxisID)) {
21730
21852
  return; // ignore trend lines
21731
21853
  }
21854
+ const xAxisScale = chart.scales[dataset.xAxisID];
21855
+ const xZeroLine = xAxisScale.getPixelForValue(0);
21732
21856
  for (let i = 0; i < dataset._parsed.length; i++) {
21733
21857
  const value = Number(dataset._parsed[i].x);
21734
21858
  if (isNaN(value)) {
@@ -21737,17 +21861,27 @@ stores.inject(MyMetaStore, storeInstance);
21737
21861
  const displayValue = options.callback(value, dataset, i);
21738
21862
  const point = dataset.data[i];
21739
21863
  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);
21864
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21865
+ const distanceFromAxisOrigin = Math.abs(point.x - xZeroLine);
21866
+ const PADDING = 3;
21867
+ let xPosition;
21868
+ if (distanceFromAxisOrigin < textWidth) {
21869
+ xPosition =
21870
+ value < 0 ? xZeroLine - textWidth / 2 - PADDING : xZeroLine + textWidth / 2 + PADDING;
21871
+ }
21872
+ else {
21873
+ xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21874
+ xPosition = Math.min(xPosition, xMax);
21875
+ xPosition = Math.max(xPosition, xMin);
21876
+ }
21743
21877
  // Avoid overlapping texts with same Y
21744
21878
  if (!textsPositions[yPosition]) {
21745
21879
  textsPositions[yPosition] = [];
21746
21880
  }
21747
- const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21748
21881
  for (const otherPosition of textsPositions[yPosition]) {
21749
21882
  if (Math.abs(otherPosition - xPosition) < textWidth) {
21750
- xPosition = otherPosition + textWidth + 3;
21883
+ xPosition =
21884
+ value < 0 ? otherPosition - textWidth - PADDING : otherPosition + textWidth + PADDING;
21751
21885
  }
21752
21886
  }
21753
21887
  textsPositions[yPosition].push(xPosition);
@@ -23695,11 +23829,11 @@ stores.inject(MyMetaStore, storeInstance);
23695
23829
  function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
23696
23830
  const maxValue = runtime.maxValue;
23697
23831
  const minValue = runtime.minValue;
23698
- const gaugeValue = runtime.gaugeValue;
23832
+ const gaugeValue = getGaugeValue(runtime, "animated");
23699
23833
  const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
23700
23834
  const gaugeArcWidth = gaugeRect.width / 6;
23701
23835
  const gaugePercentage = gaugeValue
23702
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
23836
+ ? (gaugeValue - minValue.value) / (maxValue.value - minValue.value)
23703
23837
  : 0;
23704
23838
  const gaugeValuePosition = {
23705
23839
  x: boundingRect.width / 2,
@@ -23712,7 +23846,7 @@ stores.inject(MyMetaStore, storeInstance);
23712
23846
  }
23713
23847
  // Scale down the font size if the text is too long
23714
23848
  const maxTextWidth = gaugeRect.width / 2;
23715
- const gaugeLabel = gaugeValue?.label || "-";
23849
+ const gaugeLabel = runtime.gaugeValue?.label || "-";
23716
23850
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
23717
23851
  gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
23718
23852
  }
@@ -23852,7 +23986,7 @@ stores.inject(MyMetaStore, storeInstance);
23852
23986
  return inflectionValues;
23853
23987
  }
23854
23988
  function getGaugeColor(runtime) {
23855
- const gaugeValue = runtime.gaugeValue?.value;
23989
+ const gaugeValue = getGaugeValue(runtime, "final");
23856
23990
  if (gaugeValue === undefined) {
23857
23991
  return GAUGE_BACKGROUND_COLOR;
23858
23992
  }
@@ -23940,6 +24074,11 @@ stores.inject(MyMetaStore, storeInstance);
23940
24074
  };
23941
24075
  return { bottomLeft, bottomRight, topRight, topLeft };
23942
24076
  }
24077
+ function getGaugeValue(runtime, mode) {
24078
+ return mode === "animated" && runtime.animationValue !== undefined
24079
+ ? runtime.animationValue
24080
+ : runtime.gaugeValue?.value;
24081
+ }
23943
24082
 
23944
24083
  const CHART_COMMON_OPTIONS = {
23945
24084
  // https://www.chartjs.org/docs/latest/general/responsive.html
@@ -25681,7 +25820,9 @@ stores.inject(MyMetaStore, storeInstance);
25681
25820
  background: definition.background,
25682
25821
  callback: (value, dataset) => {
25683
25822
  value = Math.abs(Number(value));
25684
- return formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25823
+ return value === 0
25824
+ ? ""
25825
+ : formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25685
25826
  },
25686
25827
  };
25687
25828
  }
@@ -26292,6 +26433,320 @@ stores.inject(MyMetaStore, storeInstance);
26292
26433
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
26293
26434
  }
26294
26435
 
26436
+ const cellAnimationRegistry = new Registry();
26437
+ cellAnimationRegistry.add("animatedBackgroundColorChange", {
26438
+ id: "animatedBackgroundColorChange",
26439
+ easingFn: "easeOutCubic",
26440
+ hasAnimation: (oldBox, newBox) => {
26441
+ return oldBox?.style?.fillColor !== newBox?.style?.fillColor;
26442
+ },
26443
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26444
+ const colorScale = getColorScale([
26445
+ { value: 0, color: oldBox.style.fillColor || "#ffffff" },
26446
+ { value: 1, color: newBox.style.fillColor || "#ffffff" },
26447
+ ]);
26448
+ animatedBox.style.fillColor = colorScale(EASING_FN[this.easingFn](progress));
26449
+ },
26450
+ });
26451
+ cellAnimationRegistry.add("animatedTextColorChange", {
26452
+ id: "animatedTextColorChange",
26453
+ easingFn: "easeOutCubic",
26454
+ hasAnimation: (oldBox, newBox) => {
26455
+ return oldBox?.style?.textColor !== newBox?.style?.textColor;
26456
+ },
26457
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26458
+ const colorScale = getColorScale([
26459
+ { value: 0, color: oldBox.style.textColor || "#000000" },
26460
+ { value: 1, color: newBox.style.textColor || "#000000" },
26461
+ ]);
26462
+ animatedBox.style.textColor = colorScale(EASING_FN[this.easingFn](progress));
26463
+ },
26464
+ });
26465
+ cellAnimationRegistry.add("animatedDataBar", {
26466
+ id: "animatedDataBar",
26467
+ easingFn: "easeOutCubic",
26468
+ hasAnimation: (oldBox, newBox) => {
26469
+ return oldBox?.dataBarFill?.percentage !== newBox?.dataBarFill?.percentage;
26470
+ },
26471
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26472
+ const startingPercentage = oldBox?.dataBarFill?.percentage || 0;
26473
+ const endingPercentage = newBox?.dataBarFill?.percentage || 0;
26474
+ const value = EASING_FN[this.easingFn](progress);
26475
+ const percentage = startingPercentage + (endingPercentage - startingPercentage) * value;
26476
+ animatedBox.dataBarFill = {
26477
+ color: newBox.dataBarFill?.color || oldBox.dataBarFill?.color || "#ffffff",
26478
+ percentage: percentage,
26479
+ };
26480
+ },
26481
+ });
26482
+ cellAnimationRegistry.add("textFadeIn", {
26483
+ id: "textFadeIn",
26484
+ easingFn: "easeInCubic",
26485
+ hasAnimation: (oldBox, newBox) => {
26486
+ const oldText = oldBox?.content?.textLines?.join("\n");
26487
+ const newText = newBox?.content?.textLines?.join("\n");
26488
+ return Boolean(!oldText && newText);
26489
+ },
26490
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26491
+ animatedBox.textOpacity = EASING_FN[this.easingFn](progress);
26492
+ },
26493
+ });
26494
+ cellAnimationRegistry.add("textFadeOut", {
26495
+ id: "textFadeOut",
26496
+ easingFn: "easeOutCubic",
26497
+ hasAnimation: (oldBox, newBox) => {
26498
+ const oldText = oldBox?.content?.textLines?.join("\n");
26499
+ const newText = newBox?.content?.textLines?.join("\n");
26500
+ return Boolean(oldText && !newText);
26501
+ },
26502
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26503
+ const textOpacity = 1 - EASING_FN[this.easingFn](progress);
26504
+ const style = { ...oldBox.style };
26505
+ delete style.fillColor;
26506
+ animatedBox.textOpacity = textOpacity;
26507
+ animatedBox.content = oldBox.content;
26508
+ animatedBox.clipRect = oldBox.clipRect;
26509
+ Object.assign(animatedBox.style, style);
26510
+ },
26511
+ });
26512
+ cellAnimationRegistry.add("textChange", {
26513
+ id: "textChange",
26514
+ easingFn: "easeOutCubic",
26515
+ hasAnimation: (oldBox, newBox) => {
26516
+ const oldText = oldBox?.content?.textLines?.join("\n");
26517
+ const newText = newBox?.content?.textLines?.join("\n");
26518
+ // Note: here, we also animate changes to icons layout (margins/size change, or icon appearing/disappearing)
26519
+ // because a change to the icon layout will impact where the text is positioned.
26520
+ return (Boolean(oldText && newText && oldText !== newText) || hasIconLayoutChange(newBox, oldBox));
26521
+ },
26522
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26523
+ const value = EASING_FN[this.easingFn](progress);
26524
+ const slideInY = newBox.y + (value - 1) * newBox.height;
26525
+ const slideOutY = newBox.y + value * newBox.height;
26526
+ const iconLayoutChange = hasIconLayoutChange(newBox, oldBox);
26527
+ const slideInBox = {
26528
+ id: newBox.id + "-text-slide-in",
26529
+ x: newBox.x,
26530
+ y: slideInY,
26531
+ width: newBox.width,
26532
+ height: newBox.height,
26533
+ style: { ...newBox.style },
26534
+ skipCellGridLines: true,
26535
+ content: newBox.content,
26536
+ clipRect: newBox.clipRect || {
26537
+ ...newBox,
26538
+ // large width to avoid clipping the text it it didn't have a clipRect before,
26539
+ // we mainly want to clip the Y for the animation
26540
+ x: Math.max(0, newBox.x - (newBox.content?.width || 0)),
26541
+ width: newBox.width + (newBox.content?.width || 0) * 2,
26542
+ },
26543
+ icons: iconLayoutChange
26544
+ ? addClipRectToIcons(newBox.icons, newBox)
26545
+ : makeIconsEmpty(newBox.icons),
26546
+ };
26547
+ const slideOutBox = {
26548
+ id: oldBox.id + "-text-slide-out",
26549
+ x: newBox.x,
26550
+ y: slideOutY,
26551
+ width: newBox.width,
26552
+ height: newBox.height,
26553
+ style: { ...oldBox.style },
26554
+ skipCellGridLines: true,
26555
+ content: oldBox.content,
26556
+ clipRect: oldBox.clipRect || {
26557
+ ...newBox,
26558
+ x: Math.max(0, newBox.x - (oldBox.content?.width || 0)),
26559
+ width: newBox.width + (oldBox.content?.width || 0) * 2,
26560
+ },
26561
+ icons: iconLayoutChange
26562
+ ? addClipRectToIcons(oldBox.icons, newBox)
26563
+ : makeIconsEmpty(oldBox.icons),
26564
+ };
26565
+ if (newBox.content && oldBox.content && slideInBox.content && slideOutBox.content) {
26566
+ const slideInContentY = newBox.content.y + (value - 1) * newBox.height;
26567
+ const slideOutContentY = newBox.content.y + value * newBox.height;
26568
+ slideInBox.content.y = slideInContentY;
26569
+ slideOutBox.content.y = slideOutContentY;
26570
+ }
26571
+ slideOutBox.style.fillColor = slideInBox.style.fillColor = undefined;
26572
+ animatedBox.content = undefined;
26573
+ animatedBox.icons = iconLayoutChange ? {} : animatedBox.icons;
26574
+ return { newBoxes: [slideInBox, slideOutBox] };
26575
+ },
26576
+ });
26577
+ cellAnimationRegistry.add("borderFadeIn", {
26578
+ id: "borderFadeIn",
26579
+ easingFn: "easeInCubic",
26580
+ hasAnimation: (oldBox, newBox) => {
26581
+ return Boolean((!oldBox?.border?.bottom && newBox?.border?.bottom) ||
26582
+ (!oldBox?.border?.top && newBox?.border?.top) ||
26583
+ (!oldBox?.border?.left && newBox?.border?.left) ||
26584
+ (!oldBox?.border?.right && newBox?.border?.right));
26585
+ },
26586
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26587
+ const borderOpacity = EASING_FN[this.easingFn](progress);
26588
+ if (animatedBox.border?.top && newBox.border?.top && !oldBox.border?.top) {
26589
+ animatedBox.border.top.opacity = borderOpacity;
26590
+ }
26591
+ if (animatedBox.border?.bottom && newBox.border?.bottom && !oldBox.border?.bottom) {
26592
+ animatedBox.border.bottom.opacity = borderOpacity;
26593
+ }
26594
+ if (animatedBox.border?.left && newBox.border?.left && !oldBox.border?.left) {
26595
+ animatedBox.border.left.opacity = borderOpacity;
26596
+ }
26597
+ if (animatedBox.border?.right && newBox.border?.right && !oldBox.border?.right) {
26598
+ animatedBox.border.right.opacity = borderOpacity;
26599
+ }
26600
+ },
26601
+ });
26602
+ cellAnimationRegistry.add("borderFadeOut", {
26603
+ id: "borderFadeOut",
26604
+ easingFn: "easeOutCubic",
26605
+ hasAnimation: (oldBox, newBox) => {
26606
+ return Boolean((oldBox?.border?.bottom && !newBox?.border?.bottom) ||
26607
+ (oldBox?.border?.top && !newBox?.border?.top) ||
26608
+ (oldBox?.border?.left && !newBox?.border?.left) ||
26609
+ (oldBox?.border?.right && !newBox?.border?.right));
26610
+ },
26611
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26612
+ const borderOpacity = 1 - EASING_FN[this.easingFn](progress);
26613
+ if (!animatedBox.border) {
26614
+ animatedBox.border = {};
26615
+ }
26616
+ if (oldBox.border?.top && !newBox.border?.top) {
26617
+ animatedBox.border.top = { ...oldBox.border.top, opacity: borderOpacity };
26618
+ }
26619
+ if (oldBox.border?.bottom && !newBox.border?.bottom) {
26620
+ animatedBox.border.bottom = { ...oldBox.border.bottom, opacity: borderOpacity };
26621
+ }
26622
+ if (oldBox.border?.left && !newBox.border?.left) {
26623
+ animatedBox.border.left = { ...oldBox.border.left, opacity: borderOpacity };
26624
+ }
26625
+ if (oldBox.border?.right && !newBox.border?.right) {
26626
+ animatedBox.border.right = { ...oldBox.border.right, opacity: borderOpacity };
26627
+ }
26628
+ },
26629
+ });
26630
+ cellAnimationRegistry.add("borderColorChange", {
26631
+ id: "borderColorChange",
26632
+ easingFn: "easeOutCubic",
26633
+ hasAnimation: (oldBox, newBox) => {
26634
+ const oldBorder = oldBox?.border;
26635
+ const newBorder = newBox?.border;
26636
+ if (!oldBorder || !newBorder) {
26637
+ return false;
26638
+ }
26639
+ return Boolean(oldBorder.bottom?.color !== newBorder.bottom?.color ||
26640
+ oldBorder.top?.color !== newBorder.top?.color ||
26641
+ oldBorder.left?.color !== newBorder.left?.color ||
26642
+ oldBorder.right?.color !== newBorder.right?.color);
26643
+ },
26644
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26645
+ const animateBorderColor = (side) => {
26646
+ const oldBorder = oldBox?.border?.[side];
26647
+ const newBorder = newBox?.border?.[side];
26648
+ const animatedBorder = animatedBox.border?.[side];
26649
+ if (oldBorder && newBorder && animatedBorder) {
26650
+ const colorScale = getColorScale([
26651
+ { value: 0, color: oldBorder.color || "#000000" },
26652
+ { value: 1, color: newBorder.color || "#000000" },
26653
+ ]);
26654
+ animatedBorder.color = colorScale(EASING_FN[this.easingFn](progress));
26655
+ }
26656
+ };
26657
+ animateBorderColor("top");
26658
+ animateBorderColor("bottom");
26659
+ animateBorderColor("left");
26660
+ animateBorderColor("right");
26661
+ },
26662
+ });
26663
+ cellAnimationRegistry.add("iconChange", {
26664
+ id: "iconChange",
26665
+ easingFn: "easeOutCubic",
26666
+ hasAnimation: (oldBox, newBox) => {
26667
+ return (!hasIconLayoutChange(newBox, oldBox) &&
26668
+ Boolean(oldBox?.icons?.center?.svg?.name !== newBox?.icons?.center?.svg?.name ||
26669
+ oldBox?.icons?.left?.svg?.name !== newBox?.icons?.left?.svg?.name ||
26670
+ oldBox?.icons?.right?.svg?.name !== newBox?.icons?.right?.svg?.name));
26671
+ },
26672
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26673
+ const value = EASING_FN[this.easingFn](progress);
26674
+ const slideInY = newBox.y + (value - 1) * newBox.height;
26675
+ const slideOutY = newBox.y + value * newBox.height;
26676
+ const newBoxes = [];
26677
+ const animateIconChange = (side) => {
26678
+ const oldIcon = oldBox.icons?.[side];
26679
+ const newIcon = newBox.icons?.[side];
26680
+ const slideInBox = {
26681
+ id: `${newBox.id}-icon-${side}-slide-in`,
26682
+ style: { verticalAlign: newBox.style.verticalAlign },
26683
+ x: newBox.x,
26684
+ y: slideInY,
26685
+ width: newBox.width,
26686
+ height: newBox.height,
26687
+ skipCellGridLines: true,
26688
+ icons: { [side]: { ...newIcon, clipRect: newBox } },
26689
+ };
26690
+ const slideOutBox = {
26691
+ id: `${newBox.id}-icon-${side}-slide-out`,
26692
+ style: { verticalAlign: oldBox.style.verticalAlign },
26693
+ x: newBox.x,
26694
+ y: slideOutY,
26695
+ width: newBox.width,
26696
+ height: newBox.height,
26697
+ skipCellGridLines: true,
26698
+ icons: { [side]: { ...oldIcon, clipRect: newBox } },
26699
+ };
26700
+ animatedBox.icons[side] = makeIconsEmpty(newBox.icons)[side];
26701
+ newBoxes.push(slideInBox, slideOutBox);
26702
+ };
26703
+ animateIconChange("left");
26704
+ animateIconChange("right");
26705
+ animateIconChange("center");
26706
+ return { newBoxes };
26707
+ },
26708
+ });
26709
+ const EASING_FN = {
26710
+ linear: (t) => t,
26711
+ easeInCubic: (t) => t * t * t,
26712
+ easeOutCubic: (t) => (t -= 1) * t * t + 1,
26713
+ easeInOutCubic: (t) => ((t /= 0.5) < 1 ? 0.5 * t * t * t : 0.5 * ((t -= 2) * t * t + 2)),
26714
+ easeOutQuart: (t) => -((t -= 1) * t * t * t - 1),
26715
+ };
26716
+ function makeIconsEmpty(icons) {
26717
+ return {
26718
+ left: icons.left ? { ...icons.left, svg: undefined } : undefined,
26719
+ right: icons.right ? { ...icons.right, svg: undefined } : undefined,
26720
+ center: icons.center ? { ...icons.center, svg: undefined } : undefined,
26721
+ };
26722
+ }
26723
+ function addClipRectToIcons(icons, clipRect) {
26724
+ return {
26725
+ left: icons.left ? { ...icons.left, clipRect } : undefined,
26726
+ right: icons.right ? { ...icons.right, clipRect } : undefined,
26727
+ center: icons.center ? { ...icons.center, clipRect } : undefined,
26728
+ };
26729
+ }
26730
+ /**
26731
+ * Check if the icons have appeared, disappeared or changed margin/size/align. Those changes affect where the text is positioned.
26732
+ */
26733
+ function hasIconLayoutChange(newBox, oldBox) {
26734
+ const hasLayoutChange = (newIcon, oldIcon) => {
26735
+ if (oldIcon && newIcon) {
26736
+ return !!(newIcon.horizontalAlign !== oldIcon.horizontalAlign ||
26737
+ newIcon.size !== oldIcon.size ||
26738
+ newIcon.margin !== oldIcon.margin ||
26739
+ (newIcon.svg && !oldIcon.svg) ||
26740
+ (!newIcon.svg && oldIcon.svg));
26741
+ }
26742
+ return !!((newIcon && !oldIcon) || (!newIcon && oldIcon));
26743
+ };
26744
+ return (hasLayoutChange(newBox?.icons.left, oldBox?.icons.left) ||
26745
+ hasLayoutChange(newBox?.icons.right, oldBox?.icons.right) ||
26746
+ hasLayoutChange(newBox?.icons.center, oldBox?.icons.center));
26747
+ }
26748
+
26749
+ const ANIMATION_DURATION = 1000;
26295
26750
  class GaugeChartComponent extends owl.Component {
26296
26751
  static template = "o-spreadsheet-GaugeChartComponent";
26297
26752
  static props = {
@@ -26299,16 +26754,101 @@ stores.inject(MyMetaStore, storeInstance);
26299
26754
  isFullScreen: { type: Boolean, optional: true },
26300
26755
  };
26301
26756
  canvas = owl.useRef("chartContainer");
26757
+ animationStore;
26302
26758
  get runtime() {
26303
26759
  return this.env.model.getters.getChartRuntime(this.props.figureUI.id);
26304
26760
  }
26305
26761
  setup() {
26306
- owl.useEffect(() => drawGaugeChart(this.canvas.el, this.runtime), () => {
26307
- const canvas = this.canvas.el;
26308
- const rect = canvas.getBoundingClientRect();
26762
+ if (this.env.model.getters.isDashboard()) {
26763
+ this.animationStore = useStore(ChartAnimationStore);
26764
+ }
26765
+ let animation = null;
26766
+ let lastRuntime = undefined;
26767
+ owl.useEffect(() => {
26768
+ if (this.env.isDashboard() &&
26769
+ lastRuntime === undefined && // first render
26770
+ this.animationStore?.animationPlayed[this.animationFigureId] !== "gauge") {
26771
+ animation = this.drawGaugeWithAnimation();
26772
+ this.animationStore?.disableAnimationForChart(this.animationFigureId, "gauge");
26773
+ }
26774
+ else if (this.env.isDashboard() &&
26775
+ lastRuntime !== undefined && // not first render
26776
+ !deepEquals(this.runtime, lastRuntime)) {
26777
+ animation = this.drawGaugeWithAnimation();
26778
+ this.animationStore?.disableAnimationForChart(this.animationFigureId, "gauge");
26779
+ }
26780
+ else {
26781
+ drawGaugeChart(this.canvasEl, this.runtime);
26782
+ }
26783
+ lastRuntime = this.runtime;
26784
+ return () => animation?.stop();
26785
+ }, () => {
26786
+ const rect = this.canvasEl.getBoundingClientRect();
26309
26787
  return [rect.width, rect.height, this.runtime, this.canvas.el, window.devicePixelRatio];
26310
26788
  });
26311
26789
  }
26790
+ drawGaugeWithAnimation() {
26791
+ drawGaugeChart(this.canvasEl, { ...this.runtime, animationValue: 0 });
26792
+ const gaugeValue = this.runtime.gaugeValue?.value || 0;
26793
+ const upperBound = this.runtime.maxValue.value;
26794
+ const finalValue = Math.sign(gaugeValue) * Math.min(Math.abs(gaugeValue), Math.abs(upperBound));
26795
+ if (finalValue === 0) {
26796
+ return null;
26797
+ }
26798
+ const lowerBound = this.runtime.minValue.value;
26799
+ const animation = new Animation(lowerBound, finalValue, ANIMATION_DURATION, (animationValue) => drawGaugeChart(this.canvasEl, { ...this.runtime, animationValue }));
26800
+ animation.start();
26801
+ return animation;
26802
+ }
26803
+ get canvasEl() {
26804
+ return this.canvas.el;
26805
+ }
26806
+ get animationFigureId() {
26807
+ return this.props.isFullScreen
26808
+ ? this.props.figureUI.id + "-fullscreen"
26809
+ : this.props.figureUI.id;
26810
+ }
26811
+ }
26812
+ /**
26813
+ * Animation interpolating values using the ease-out quartic curve function (chartJS default easing)
26814
+ */
26815
+ class Animation {
26816
+ startValue;
26817
+ endValue;
26818
+ duration;
26819
+ callback;
26820
+ startTime = undefined;
26821
+ animationFrameId = null;
26822
+ constructor(startValue, endValue, duration, callback) {
26823
+ this.startValue = startValue;
26824
+ this.endValue = endValue;
26825
+ this.duration = duration;
26826
+ this.callback = callback;
26827
+ }
26828
+ start() {
26829
+ this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
26830
+ }
26831
+ stop() {
26832
+ if (this.animationFrameId) {
26833
+ cancelAnimationFrame(this.animationFrameId);
26834
+ this.animationFrameId = null;
26835
+ }
26836
+ }
26837
+ animate(timestamp) {
26838
+ if (!this.startTime) {
26839
+ this.startTime = timestamp;
26840
+ }
26841
+ const elapsed = timestamp - this.startTime;
26842
+ const progress = Math.min(elapsed / this.duration, 1);
26843
+ const currentValue = this.startValue + (this.endValue - this.startValue) * EASING_FN.easeOutQuart(progress);
26844
+ this.callback(currentValue);
26845
+ if (progress < 1) {
26846
+ this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
26847
+ }
26848
+ else {
26849
+ this.stop();
26850
+ }
26851
+ }
26312
26852
  }
26313
26853
 
26314
26854
  class ComboChart extends AbstractChart {
@@ -29342,6 +29882,12 @@ stores.inject(MyMetaStore, storeInstance);
29342
29882
  menu.onStopHover?.(this.env);
29343
29883
  this.props.onMouseLeave?.(menu, ev);
29344
29884
  }
29885
+ onClickMenu(menu, ev) {
29886
+ if (!this.isEnabled(menu)) {
29887
+ return;
29888
+ }
29889
+ this.props.onClickMenu?.(menu, ev);
29890
+ }
29345
29891
  }
29346
29892
 
29347
29893
  /**
@@ -29840,9 +30386,6 @@ stores.inject(MyMetaStore, storeInstance);
29840
30386
  this.subMenu.parentMenu = undefined;
29841
30387
  }
29842
30388
  onClickMenu(menu, ev) {
29843
- if (!this.isEnabled(menu)) {
29844
- return;
29845
- }
29846
30389
  if (this.isRoot(menu)) {
29847
30390
  this.openSubMenu(menu, ev.currentTarget);
29848
30391
  }
@@ -31093,11 +31636,9 @@ stores.inject(MyMetaStore, storeInstance);
31093
31636
  if (!value) {
31094
31637
  return false;
31095
31638
  }
31096
- const range = getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
31097
- const criterionValues = getters.getRangeValues(range);
31639
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
31098
31640
  return criterionValues
31099
- .filter(isNotNull)
31100
- .map((value) => value.toString().toLowerCase())
31641
+ .map((value) => value.toLowerCase())
31101
31642
  .includes(value.toString().toLowerCase());
31102
31643
  },
31103
31644
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
@@ -31269,6 +31810,12 @@ stores.inject(MyMetaStore, storeInstance);
31269
31810
  selectedElement?.scrollIntoView?.({ block: "nearest" });
31270
31811
  }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
31271
31812
  }
31813
+ getCss(html) {
31814
+ return cssPropertiesToCss({
31815
+ color: html.color || "#000000",
31816
+ background: html.backgroundColor,
31817
+ });
31818
+ }
31272
31819
  }
31273
31820
 
31274
31821
  class ContentEditableHelper {
@@ -31407,7 +31954,6 @@ stores.inject(MyMetaStore, storeInstance);
31407
31954
  // We can only modify a node in place if it has the same type as the content
31408
31955
  // that we would insert, which are spans.
31409
31956
  // 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
31957
  const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
31412
31958
  if (childIsSpan && compareContentToSpanElement(content, child)) {
31413
31959
  continue;
@@ -31442,9 +31988,7 @@ stores.inject(MyMetaStore, storeInstance);
31442
31988
  }
31443
31989
  // Empty line
31444
31990
  if (!p.hasChildNodes()) {
31445
- const span = document.createElement("span");
31446
- span.appendChild(document.createElement("br"));
31447
- p.appendChild(span);
31991
+ p.appendChild(document.createElement("span"));
31448
31992
  }
31449
31993
  // replace p if necessary
31450
31994
  if (newChild) {
@@ -31491,13 +32035,10 @@ stores.inject(MyMetaStore, storeInstance);
31491
32035
  }
31492
32036
  getText() {
31493
32037
  let text = "";
31494
- const it = iterateChildren(this.el);
31495
- let current = it.next();
31496
32038
  let isFirstParagraph = true;
31497
- while (!current.done) {
31498
- if (!current.value.hasChildNodes()) {
31499
- text += current.value.textContent;
31500
- }
32039
+ let emptyParagraph = false;
32040
+ const it = iterateChildren(this.el);
32041
+ for (let current = it.next(); !current.done; current = it.next()) {
31501
32042
  if (current.value.nodeName === "P" ||
31502
32043
  (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
31503
32044
  ) {
@@ -31507,8 +32048,15 @@ stores.inject(MyMetaStore, storeInstance);
31507
32048
  else {
31508
32049
  text += NEWLINE;
31509
32050
  }
32051
+ emptyParagraph = ["<br>", "<span><br></span>"].includes(current.value.innerHTML);
32052
+ continue;
32053
+ }
32054
+ if (!current.value.hasChildNodes()) {
32055
+ if (current.value.nodeName === "BR" && !emptyParagraph) {
32056
+ text += NEWLINE;
32057
+ }
32058
+ text += current.value.textContent;
31510
32059
  }
31511
- current = it.next();
31512
32060
  }
31513
32061
  return text;
31514
32062
  }
@@ -32858,6 +33406,12 @@ stores.inject(MyMetaStore, storeInstance);
32858
33406
  owl.useEffect(() => {
32859
33407
  this.processTokenAtCursor();
32860
33408
  }, () => [this.props.composerStore.editionMode !== "inactive"]);
33409
+ owl.useEffect(() => {
33410
+ this.contentHelper.scrollSelectionIntoView();
33411
+ }, () => [
33412
+ this.props.composerStore.composerSelection.start,
33413
+ this.props.composerStore.composerSelection.end,
33414
+ ]);
32861
33415
  }
32862
33416
  // ---------------------------------------------------------------------------
32863
33417
  // Handlers
@@ -33068,6 +33622,7 @@ stores.inject(MyMetaStore, storeInstance);
33068
33622
  if (this.env.isMobile() && !isIOS()) {
33069
33623
  return;
33070
33624
  }
33625
+ this.debouncedHover.stopDebounce();
33071
33626
  this.contentHelper.removeSelection();
33072
33627
  }
33073
33628
  onMouseup() {
@@ -33146,7 +33701,6 @@ stores.inject(MyMetaStore, storeInstance);
33146
33701
  const { start, end } = this.props.composerStore.composerSelection;
33147
33702
  this.contentHelper.selectRange(start, end);
33148
33703
  }
33149
- this.contentHelper.scrollSelectionIntoView();
33150
33704
  }
33151
33705
  this.shouldProcessInputEvents = true;
33152
33706
  }
@@ -33622,68 +34176,6 @@ stores.inject(MyMetaStore, storeInstance);
33622
34176
  }
33623
34177
  }
33624
34178
 
33625
- css /* scss */ `
33626
- .o-dv-list-item-delete {
33627
- color: #666666;
33628
- cursor: pointer;
33629
- }
33630
- `;
33631
- class ListCriterionForm extends CriterionForm {
33632
- static template = "o-spreadsheet-ListCriterionForm";
33633
- static components = { CriterionInput };
33634
- state = owl.useState({
33635
- numberOfValues: Math.max(this.props.criterion.values.length, 2),
33636
- });
33637
- setup() {
33638
- super.setup();
33639
- const setupDefault = (props) => {
33640
- if (props.criterion.displayStyle === undefined) {
33641
- this.updateCriterion({ displayStyle: "arrow" });
33642
- }
33643
- };
33644
- owl.onWillUpdateProps(setupDefault);
33645
- owl.onWillStart(() => setupDefault(this.props));
33646
- }
33647
- onValueChanged(value, index) {
33648
- const values = [...this.displayedValues];
33649
- values[index] = value;
33650
- this.updateCriterion({ values });
33651
- }
33652
- onAddAnotherValue() {
33653
- this.state.numberOfValues++;
33654
- }
33655
- removeItem(index) {
33656
- const values = [...this.displayedValues];
33657
- values.splice(index, 1);
33658
- this.state.numberOfValues--;
33659
- this.updateCriterion({ values });
33660
- }
33661
- onChangedDisplayStyle(ev) {
33662
- const displayStyle = ev.target.value;
33663
- this.updateCriterion({ displayStyle });
33664
- }
33665
- onKeyDown(ev, index) {
33666
- if ((ev.key === "Enter" || ev.key === "Tab") && index === this.state.numberOfValues - 1) {
33667
- this.onAddAnotherValue();
33668
- this.state.focusedValueIndex = index + 1;
33669
- ev.preventDefault();
33670
- }
33671
- else if (ev.key === "Enter") {
33672
- this.state.focusedValueIndex = index + 1;
33673
- }
33674
- }
33675
- onBlurInput() {
33676
- this.state.focusedValueIndex = undefined;
33677
- }
33678
- get displayedValues() {
33679
- const values = [];
33680
- for (let i = 0; i < this.state.numberOfValues; i++) {
33681
- values.push(this.props.criterion.values[i] || "");
33682
- }
33683
- return values;
33684
- }
33685
- }
33686
-
33687
34179
  /**
33688
34180
  * Start listening to pointer events and apply the given callbacks.
33689
34181
  *
@@ -33713,179 +34205,625 @@ stores.inject(MyMetaStore, storeInstance);
33713
34205
  return removeListeners;
33714
34206
  }
33715
34207
 
33716
- function useDragAndDropListItems() {
33717
- let dndHelper;
33718
- const previousCursor = document.body.style.cursor;
33719
- let cleanupFns = [];
33720
- const cleanUp = () => {
33721
- dndHelper = undefined;
33722
- document.body.style.cursor = previousCursor;
33723
- cleanupFns.forEach((fn) => fn());
33724
- cleanupFns = [];
33725
- };
33726
- const start = (direction, args) => {
33727
- const onChange = () => {
33728
- document.body.style.cursor = "move";
33729
- if (!dndHelper)
33730
- return;
33731
- Object.assign(state.itemsStyle, dndHelper.getItemStyles());
33732
- args.onChange?.();
33733
- };
33734
- state.cancel = () => {
33735
- state.draggedItemId = undefined;
33736
- state.itemsStyle = {};
33737
- document.body.style.cursor = previousCursor;
33738
- args.onCancel?.();
33739
- cleanUp();
33740
- };
33741
- const onDragEnd = (itemId, indexAtEnd) => {
33742
- state.draggedItemId = undefined;
33743
- state.itemsStyle = {};
33744
- document.body.style.cursor = previousCursor;
33745
- args.onDragEnd?.(itemId, indexAtEnd);
33746
- cleanUp();
33747
- };
33748
- document.body.style.cursor = "move";
33749
- state.draggedItemId = args.draggedItemId;
33750
- const container = direction === "horizontal"
33751
- ? new HorizontalContainer(args.scrollableContainerEl)
33752
- : new VerticalContainer(args.scrollableContainerEl);
33753
- dndHelper = new DOMDndHelper({
33754
- ...args,
33755
- container,
33756
- onChange,
33757
- onDragEnd,
33758
- onCancel: state.cancel,
33759
- });
33760
- const stopListening = startDnd(dndHelper.onMouseMove.bind(dndHelper), dndHelper.onMouseUp.bind(dndHelper));
33761
- cleanupFns.push(stopListening);
33762
- const onScroll = dndHelper.onScroll.bind(dndHelper);
33763
- args.scrollableContainerEl.addEventListener("scroll", onScroll);
33764
- cleanupFns.push(() => args.scrollableContainerEl.removeEventListener("scroll", onScroll));
33765
- cleanupFns.push(dndHelper.destroy.bind(dndHelper));
33766
- };
33767
- owl.onWillUnmount(() => {
33768
- cleanUp();
33769
- });
33770
- const state = owl.useState({
33771
- itemsStyle: {},
33772
- draggedItemId: undefined,
33773
- start,
33774
- cancel: () => { },
33775
- });
33776
- return state;
33777
- }
33778
- class DOMDndHelper {
33779
- draggedItemId;
33780
- items;
33781
- container;
33782
- initialMousePosition;
33783
- currentMousePosition;
33784
- initialScroll;
33785
- minPosition;
33786
- maxPosition;
33787
- edgeScrollIntervalId;
33788
- onChange;
33789
- onCancel;
33790
- onDragEnd;
33791
- /**
33792
- * The dead zone is an area in which the pointermove events are ignored.
33793
- *
33794
- * This is useful when swapping the dragged item with a larger item. After the swap,
33795
- * the mouse is still hovering on the item we just swapped with. In this case, we don't want
33796
- * a mouse move to trigger another swap the other way around, so we create a dead zone. We will clear
33797
- * the dead zone when the mouse leaves the swapped item.
33798
- */
33799
- deadZone;
33800
- constructor(args) {
33801
- this.items = args.items.map((item) => ({ ...item, positionAtStart: item.position }));
33802
- this.draggedItemId = args.draggedItemId;
33803
- this.container = args.container;
33804
- this.onChange = args.onChange;
33805
- this.onCancel = args.onCancel;
33806
- this.onDragEnd = args.onDragEnd;
33807
- this.initialMousePosition = args.initialMousePosition;
33808
- this.currentMousePosition = args.initialMousePosition;
33809
- this.initialScroll = this.container.scroll;
33810
- this.minPosition = this.items[0].position;
33811
- this.maxPosition =
33812
- this.items[this.items.length - 1].position + this.items[this.items.length - 1].size;
34208
+ const LINE_VERTICAL_PADDING = 1;
34209
+ const PICKER_PADDING = 8;
34210
+ const ITEM_BORDER_WIDTH = 1;
34211
+ const ITEM_EDGE_LENGTH = 18;
34212
+ const ITEMS_PER_LINE = 10;
34213
+ const MAGNIFIER_EDGE = 16;
34214
+ const ITEM_GAP = 2;
34215
+ const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
34216
+ const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
34217
+ const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
34218
+ const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
34219
+ css /* scss */ `
34220
+ .o-color-picker {
34221
+ padding: ${PICKER_PADDING}px 0;
34222
+ /* FIXME: this is useless, overiden by the popover container */
34223
+ box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
34224
+ background-color: white;
34225
+ line-height: 1.2;
34226
+ overflow-y: auto;
34227
+ overflow-x: hidden;
34228
+ width: ${CONTAINER_WIDTH}px;
34229
+
34230
+ .o-color-picker-section-name {
34231
+ margin: 0px ${ITEM_BORDER_WIDTH}px;
34232
+ padding: 4px ${PICKER_PADDING}px;
34233
+ }
34234
+ .colors-grid {
34235
+ display: grid;
34236
+ padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
34237
+ grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
34238
+ grid-gap: ${ITEM_GAP}px;
34239
+ }
34240
+ .o-color-picker-toggler-button {
34241
+ display: flex;
34242
+ .o-color-picker-toggler-sign {
34243
+ display: flex;
34244
+ margin: auto auto;
34245
+ width: 55%;
34246
+ height: 55%;
34247
+ .o-icon {
34248
+ width: 100%;
34249
+ height: 100%;
34250
+ }
33813
34251
  }
33814
- getItemStyles() {
33815
- const styles = {};
33816
- for (const item of this.items) {
33817
- styles[item.id] = this.getItemStyle(item.id);
33818
- }
33819
- return styles;
34252
+ }
34253
+ .o-color-picker-line-item {
34254
+ width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
34255
+ height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
34256
+ margin: 0px;
34257
+ border-radius: 50px;
34258
+ border: ${ITEM_BORDER_WIDTH}px solid #666666;
34259
+ padding: 0px;
34260
+ font-size: 16px;
34261
+ background: white;
34262
+ &:hover {
34263
+ background-color: rgba(0, 0, 0, 0.08);
34264
+ outline: 1px solid gray;
34265
+ cursor: pointer;
33820
34266
  }
33821
- getItemStyle(itemId) {
33822
- const position = this.container.cssPositionProperty;
33823
- const style = {};
33824
- style.position = "relative";
33825
- style[position] = (this.getItemsPositions()[itemId] || 0) + "px";
33826
- style.transition = `${position} 0.5s`;
33827
- style["pointer-events"] = "none";
33828
- if (this.draggedItemId === itemId) {
33829
- style.transition = `${position} 0s`;
33830
- style["z-index"] = "1000";
33831
- }
33832
- return cssPropertiesToCss(style);
34267
+ }
34268
+ .o-buttons {
34269
+ padding: ${PICKER_PADDING}px;
34270
+ display: flex;
34271
+ .o-cancel {
34272
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34273
+ width: 100%;
34274
+ padding: 5px;
34275
+ font-size: 14px;
34276
+ background: white;
34277
+ border-radius: 4px;
34278
+ &:hover:enabled {
34279
+ background-color: rgba(0, 0, 0, 0.08);
34280
+ }
33833
34281
  }
33834
- onScroll() {
33835
- this.moveDraggedItemToPosition(this.currentMousePosition + this.scrollOffset);
34282
+ }
34283
+ .o-add-button {
34284
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34285
+ padding: 4px;
34286
+ background: white;
34287
+ border-radius: 4px;
34288
+ &:hover:enabled {
34289
+ background-color: rgba(0, 0, 0, 0.08);
33836
34290
  }
33837
- onMouseMove(ev) {
33838
- if (ev.button > 1) {
33839
- this.onCancel();
33840
- return;
33841
- }
33842
- const mousePosition = this.container.getMousePosition(ev);
33843
- this.currentMousePosition = mousePosition;
33844
- if (mousePosition < this.container.start || mousePosition > this.container.end) {
33845
- this.startEdgeScroll(mousePosition < this.container.start ? -1 : 1);
33846
- return;
33847
- }
33848
- else {
33849
- this.stopEdgeScroll();
33850
- }
33851
- this.moveDraggedItemToPosition(mousePosition + this.scrollOffset);
34291
+ }
34292
+ .o-separator {
34293
+ border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
34294
+ margin-top: ${MENU_SEPARATOR_PADDING}px;
34295
+ margin-bottom: ${MENU_SEPARATOR_PADDING}px;
34296
+ }
34297
+
34298
+ .o-custom-selector {
34299
+ padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
34300
+ position: relative;
34301
+ .o-gradient {
34302
+ margin-bottom: ${MAGNIFIER_EDGE / 2}px;
34303
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34304
+ width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
34305
+ height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
34306
+ position: relative;
33852
34307
  }
33853
- moveDraggedItemToPosition(position) {
33854
- const hoveredItemIndex = this.getHoveredItemIndex(position, this.items);
33855
- const draggedItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
33856
- const draggedItem = this.items[draggedItemIndex];
33857
- if (this.deadZone && this.isInZone(position, this.deadZone)) {
33858
- this.onChange(this.getItemsPositions());
33859
- return;
33860
- }
33861
- else if (this.isInZone(position, {
33862
- start: draggedItem.position,
33863
- end: draggedItem.position + draggedItem.size,
33864
- })) {
33865
- this.deadZone = undefined;
33866
- }
33867
- if (draggedItemIndex === hoveredItemIndex) {
33868
- this.onChange(this.getItemsPositions());
33869
- return;
33870
- }
33871
- const startIndex = Math.min(draggedItemIndex, hoveredItemIndex);
33872
- const endIndex = Math.max(draggedItemIndex, hoveredItemIndex);
33873
- const direction = Math.sign(hoveredItemIndex - draggedItemIndex);
33874
- let draggedItemMoveSize = 0;
33875
- for (let i = startIndex; i <= endIndex; i++) {
33876
- if (i === draggedItemIndex) {
33877
- continue;
33878
- }
33879
- this.items[i].position -= direction * draggedItem.size;
33880
- draggedItemMoveSize += this.items[i].size;
33881
- }
33882
- draggedItem.position += direction * draggedItemMoveSize;
33883
- this.items.sort((item1, item2) => item1.position - item2.position);
33884
- this.deadZone =
33885
- direction > 0
33886
- ? { start: position, end: draggedItem.position }
33887
- : { start: draggedItem.position + draggedItem.size, end: position };
33888
- this.onChange(this.getItemsPositions());
34308
+
34309
+ .magnifier {
34310
+ height: ${MAGNIFIER_EDGE}px;
34311
+ width: ${MAGNIFIER_EDGE}px;
34312
+ border-radius: 50%;
34313
+ border: 2px solid #fff;
34314
+ box-shadow: 0px 0px 3px #c0c0c0;
34315
+ position: absolute;
34316
+ z-index: 2;
34317
+ }
34318
+ .saturation {
34319
+ background: linear-gradient(to right, #fff 0%, transparent 100%);
34320
+ }
34321
+ .lightness {
34322
+ background: linear-gradient(to top, #000 0%, transparent 100%);
34323
+ }
34324
+ .o-hue-picker {
34325
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34326
+ width: 100%;
34327
+ height: 12px;
34328
+ border-radius: 4px;
34329
+ background: linear-gradient(
34330
+ to right,
34331
+ hsl(0 100% 50%) 0%,
34332
+ hsl(0.2turn 100% 50%) 20%,
34333
+ hsl(0.3turn 100% 50%) 30%,
34334
+ hsl(0.4turn 100% 50%) 40%,
34335
+ hsl(0.5turn 100% 50%) 50%,
34336
+ hsl(0.6turn 100% 50%) 60%,
34337
+ hsl(0.7turn 100% 50%) 70%,
34338
+ hsl(0.8turn 100% 50%) 80%,
34339
+ hsl(0.9turn 100% 50%) 90%,
34340
+ hsl(1turn 100% 50%) 100%
34341
+ );
34342
+ position: relative;
34343
+ cursor: crosshair;
34344
+ }
34345
+ .o-hue-slider {
34346
+ margin-top: -3px;
34347
+ }
34348
+ .o-custom-input-preview {
34349
+ padding: 2px 0px;
34350
+ display: flex;
34351
+ input {
34352
+ width: 50%;
34353
+ border-radius: 4px;
34354
+ padding: 4px 23px 4px 10px;
34355
+ height: 24px;
34356
+ border: 1px solid #c0c0c0;
34357
+ margin-right: 2px;
34358
+ }
34359
+ .o-wrong-color {
34360
+ /* FIXME bootstrap class instead? */
34361
+ outline-color: red;
34362
+ border-color: red;
34363
+ &:focus {
34364
+ outline-style: solid;
34365
+ outline-width: 1px;
34366
+ }
34367
+ }
34368
+ }
34369
+ .o-custom-input-buttons {
34370
+ padding: 2px 0px;
34371
+ display: flex;
34372
+ justify-content: end;
34373
+ }
34374
+ .o-color-preview {
34375
+ border: 1px solid #c0c0c0;
34376
+ border-radius: 4px;
34377
+ width: 50%;
34378
+ }
34379
+ }
34380
+ }
34381
+ `;
34382
+ class ColorPicker extends owl.Component {
34383
+ static template = "o-spreadsheet-ColorPicker";
34384
+ static props = {
34385
+ onColorPicked: Function,
34386
+ currentColor: { type: String, optional: true },
34387
+ maxHeight: { type: Number, optional: true },
34388
+ anchorRect: Object,
34389
+ disableNoColor: { type: Boolean, optional: true },
34390
+ };
34391
+ static defaultProps = { currentColor: "" };
34392
+ static components = { Popover };
34393
+ COLORS = COLOR_PICKER_DEFAULTS;
34394
+ state = owl.useState({
34395
+ showGradient: false,
34396
+ currentHslaColor: isColorValid(this.props.currentColor)
34397
+ ? { ...hexToHSLA(this.props.currentColor), a: 1 }
34398
+ : { h: 0, s: 100, l: 100, a: 1 },
34399
+ customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
34400
+ });
34401
+ get colorPickerStyle() {
34402
+ if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
34403
+ return cssPropertiesToCss({ display: "none" });
34404
+ }
34405
+ return "";
34406
+ }
34407
+ get popoverProps() {
34408
+ return {
34409
+ anchorRect: this.props.anchorRect,
34410
+ maxHeight: this.props.maxHeight,
34411
+ positioning: "bottom-left",
34412
+ verticalOffset: 0,
34413
+ };
34414
+ }
34415
+ get gradientHueStyle() {
34416
+ const hue = this.state.currentHslaColor?.h || 0;
34417
+ return cssPropertiesToCss({
34418
+ background: `hsl(${hue} 100% 50%)`,
34419
+ });
34420
+ }
34421
+ get sliderStyle() {
34422
+ const hue = this.state.currentHslaColor?.h || 0;
34423
+ const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
34424
+ const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
34425
+ return cssPropertiesToCss({
34426
+ "margin-left": `${left}px`,
34427
+ });
34428
+ }
34429
+ get pointerStyle() {
34430
+ const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
34431
+ const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
34432
+ const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
34433
+ return cssPropertiesToCss({
34434
+ left: `${-MAGNIFIER_EDGE / 2 + left}px`,
34435
+ top: `${-MAGNIFIER_EDGE / 2 + top}px`,
34436
+ background: hslaToHex(this.state.currentHslaColor),
34437
+ });
34438
+ }
34439
+ get colorPreviewStyle() {
34440
+ return cssPropertiesToCss({
34441
+ "background-color": hslaToHex(this.state.currentHslaColor),
34442
+ });
34443
+ }
34444
+ get checkmarkColor() {
34445
+ return chartFontColor(this.props.currentColor);
34446
+ }
34447
+ get isHexColorInputValid() {
34448
+ return !this.state.customHexColor || isColorValid(this.state.customHexColor);
34449
+ }
34450
+ setCustomGradient({ x, y }) {
34451
+ const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
34452
+ const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
34453
+ const deltaX = offsetX / INNER_GRADIENT_WIDTH;
34454
+ const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
34455
+ const s = 100 * deltaX;
34456
+ const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
34457
+ this.updateColor({ s, l });
34458
+ }
34459
+ setCustomHue(x) {
34460
+ // needs to be capped such that h is in [0°, 359°]
34461
+ const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
34462
+ this.updateColor({ h });
34463
+ }
34464
+ updateColor(newHsl) {
34465
+ this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
34466
+ this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
34467
+ }
34468
+ onColorClick(color) {
34469
+ if (color) {
34470
+ this.props.onColorPicked(toHex(color));
34471
+ }
34472
+ }
34473
+ resetColor() {
34474
+ this.props.onColorPicked("");
34475
+ }
34476
+ toggleColorPicker() {
34477
+ this.state.showGradient = !this.state.showGradient;
34478
+ }
34479
+ dragGradientPointer(ev) {
34480
+ const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
34481
+ this.setCustomGradient(initialGradientCoordinates);
34482
+ const initialMousePosition = { x: ev.clientX, y: ev.clientY };
34483
+ const onMouseMove = (ev) => {
34484
+ const currentMousePosition = { x: ev.clientX, y: ev.clientY };
34485
+ const deltaX = currentMousePosition.x - initialMousePosition.x;
34486
+ const deltaY = currentMousePosition.y - initialMousePosition.y;
34487
+ const currentGradientCoordinates = {
34488
+ x: initialGradientCoordinates.x + deltaX,
34489
+ y: initialGradientCoordinates.y + deltaY,
34490
+ };
34491
+ this.setCustomGradient(currentGradientCoordinates);
34492
+ };
34493
+ startDnd(onMouseMove, () => { });
34494
+ }
34495
+ dragHuePointer(ev) {
34496
+ const initialX = ev.offsetX;
34497
+ const initialMouseX = ev.clientX;
34498
+ this.setCustomHue(initialX);
34499
+ const onMouseMove = (ev) => {
34500
+ const currentMouseX = ev.clientX;
34501
+ const deltaX = currentMouseX - initialMouseX;
34502
+ const x = initialX + deltaX;
34503
+ this.setCustomHue(x);
34504
+ };
34505
+ startDnd(onMouseMove, () => { });
34506
+ }
34507
+ setHexColor(ev) {
34508
+ // only support HEX code input
34509
+ const val = ev.target.value.replace("##", "#").slice(0, 7);
34510
+ this.state.customHexColor = val;
34511
+ if (!isColorValid(val)) ;
34512
+ else {
34513
+ this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
34514
+ }
34515
+ }
34516
+ addCustomColor(ev) {
34517
+ if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
34518
+ return;
34519
+ }
34520
+ this.props.onColorPicked(toHex(this.state.customHexColor));
34521
+ }
34522
+ isSameColor(color1, color2) {
34523
+ return isSameColor(color1, color2);
34524
+ }
34525
+ }
34526
+
34527
+ class Section extends owl.Component {
34528
+ static template = "o_spreadsheet.Section";
34529
+ static props = {
34530
+ class: { type: String, optional: true },
34531
+ title: { type: String, optional: true },
34532
+ slots: Object,
34533
+ };
34534
+ }
34535
+
34536
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
34537
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
34538
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
34539
+ </svg>
34540
+ `;
34541
+ css /* scss */ `
34542
+ .o-round-color-picker-button {
34543
+ width: 20px;
34544
+ height: 20px;
34545
+ cursor: pointer;
34546
+ border: 1px solid ${GRAY_300};
34547
+ background-position: 1px 1px;
34548
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
34549
+ }
34550
+ `;
34551
+ class RoundColorPicker extends owl.Component {
34552
+ static template = "o-spreadsheet.RoundColorPicker";
34553
+ static components = { Section, ColorPicker };
34554
+ static props = {
34555
+ currentColor: { type: String, optional: true },
34556
+ title: { type: String, optional: true },
34557
+ onColorPicked: Function,
34558
+ disableNoColor: { type: Boolean, optional: true },
34559
+ };
34560
+ colorPickerButtonRef = owl.useRef("colorPickerButton");
34561
+ state;
34562
+ setup() {
34563
+ this.state = owl.useState({ pickerOpened: false });
34564
+ owl.useExternalListener(window, "click", this.closePicker);
34565
+ }
34566
+ closePicker() {
34567
+ this.state.pickerOpened = false;
34568
+ }
34569
+ togglePicker() {
34570
+ this.state.pickerOpened = !this.state.pickerOpened;
34571
+ }
34572
+ onColorPicked(color) {
34573
+ this.props.onColorPicked(color);
34574
+ this.state.pickerOpened = false;
34575
+ }
34576
+ get colorPickerAnchorRect() {
34577
+ const button = this.colorPickerButtonRef.el;
34578
+ return getBoundingRectAsPOJO(button);
34579
+ }
34580
+ get buttonStyle() {
34581
+ return cssPropertiesToCss({
34582
+ background: this.props.currentColor,
34583
+ });
34584
+ }
34585
+ }
34586
+
34587
+ css /* scss */ `
34588
+ .o-dv-list-item-delete {
34589
+ color: #666666;
34590
+ cursor: pointer;
34591
+ }
34592
+ `;
34593
+ class ListCriterionForm extends CriterionForm {
34594
+ static template = "o-spreadsheet-ListCriterionForm";
34595
+ static components = { CriterionInput, RoundColorPicker };
34596
+ state = owl.useState({
34597
+ numberOfValues: Math.max(this.props.criterion.values.length, 2),
34598
+ });
34599
+ setup() {
34600
+ super.setup();
34601
+ const setupDefault = (props) => {
34602
+ if (props.criterion.displayStyle === undefined) {
34603
+ this.updateCriterion({ displayStyle: "chip" });
34604
+ }
34605
+ };
34606
+ owl.onWillUpdateProps(setupDefault);
34607
+ owl.onWillStart(() => setupDefault(this.props));
34608
+ }
34609
+ onValueChanged(value, index) {
34610
+ const values = [...this.displayedValues];
34611
+ values[index] = value;
34612
+ this.updateCriterion({ values });
34613
+ }
34614
+ onColorChanged(color, value) {
34615
+ const colors = { ...this.props.criterion.colors };
34616
+ colors[value] = color || undefined;
34617
+ this.updateCriterion({ colors });
34618
+ }
34619
+ onAddAnotherValue() {
34620
+ this.state.numberOfValues++;
34621
+ }
34622
+ removeItem(index) {
34623
+ const values = [...this.displayedValues];
34624
+ values.splice(index, 1);
34625
+ this.state.numberOfValues--;
34626
+ this.updateCriterion({ values });
34627
+ }
34628
+ onChangedDisplayStyle(ev) {
34629
+ const displayStyle = ev.target.value;
34630
+ this.updateCriterion({ displayStyle });
34631
+ }
34632
+ onKeyDown(ev, index) {
34633
+ if ((ev.key === "Enter" || ev.key === "Tab") && index === this.state.numberOfValues - 1) {
34634
+ this.onAddAnotherValue();
34635
+ this.state.focusedValueIndex = index + 1;
34636
+ ev.preventDefault();
34637
+ }
34638
+ else if (ev.key === "Enter") {
34639
+ this.state.focusedValueIndex = index + 1;
34640
+ }
34641
+ }
34642
+ onBlurInput() {
34643
+ this.state.focusedValueIndex = undefined;
34644
+ }
34645
+ get displayedValues() {
34646
+ const values = [];
34647
+ for (let i = 0; i < this.state.numberOfValues; i++) {
34648
+ values.push(this.props.criterion.values[i] || "");
34649
+ }
34650
+ return values;
34651
+ }
34652
+ }
34653
+
34654
+ function useDragAndDropListItems() {
34655
+ let dndHelper;
34656
+ const previousCursor = document.body.style.cursor;
34657
+ let cleanupFns = [];
34658
+ const cleanUp = () => {
34659
+ dndHelper = undefined;
34660
+ document.body.style.cursor = previousCursor;
34661
+ cleanupFns.forEach((fn) => fn());
34662
+ cleanupFns = [];
34663
+ };
34664
+ const start = (direction, args) => {
34665
+ const onChange = () => {
34666
+ document.body.style.cursor = "move";
34667
+ if (!dndHelper)
34668
+ return;
34669
+ Object.assign(state.itemsStyle, dndHelper.getItemStyles());
34670
+ args.onChange?.();
34671
+ };
34672
+ state.cancel = () => {
34673
+ state.draggedItemId = undefined;
34674
+ state.itemsStyle = {};
34675
+ document.body.style.cursor = previousCursor;
34676
+ args.onCancel?.();
34677
+ cleanUp();
34678
+ };
34679
+ const onDragEnd = (itemId, indexAtEnd) => {
34680
+ state.draggedItemId = undefined;
34681
+ state.itemsStyle = {};
34682
+ document.body.style.cursor = previousCursor;
34683
+ args.onDragEnd?.(itemId, indexAtEnd);
34684
+ cleanUp();
34685
+ };
34686
+ document.body.style.cursor = "move";
34687
+ state.draggedItemId = args.draggedItemId;
34688
+ const container = direction === "horizontal"
34689
+ ? new HorizontalContainer(args.scrollableContainerEl)
34690
+ : new VerticalContainer(args.scrollableContainerEl);
34691
+ dndHelper = new DOMDndHelper({
34692
+ ...args,
34693
+ container,
34694
+ onChange,
34695
+ onDragEnd,
34696
+ onCancel: state.cancel,
34697
+ });
34698
+ const stopListening = startDnd(dndHelper.onMouseMove.bind(dndHelper), dndHelper.onMouseUp.bind(dndHelper));
34699
+ cleanupFns.push(stopListening);
34700
+ const onScroll = dndHelper.onScroll.bind(dndHelper);
34701
+ args.scrollableContainerEl.addEventListener("scroll", onScroll);
34702
+ cleanupFns.push(() => args.scrollableContainerEl.removeEventListener("scroll", onScroll));
34703
+ cleanupFns.push(dndHelper.destroy.bind(dndHelper));
34704
+ };
34705
+ owl.onWillUnmount(() => {
34706
+ cleanUp();
34707
+ });
34708
+ const state = owl.useState({
34709
+ itemsStyle: {},
34710
+ draggedItemId: undefined,
34711
+ start,
34712
+ cancel: () => { },
34713
+ });
34714
+ return state;
34715
+ }
34716
+ class DOMDndHelper {
34717
+ draggedItemId;
34718
+ items;
34719
+ container;
34720
+ initialMousePosition;
34721
+ currentMousePosition;
34722
+ initialScroll;
34723
+ minPosition;
34724
+ maxPosition;
34725
+ edgeScrollIntervalId;
34726
+ onChange;
34727
+ onCancel;
34728
+ onDragEnd;
34729
+ /**
34730
+ * The dead zone is an area in which the pointermove events are ignored.
34731
+ *
34732
+ * This is useful when swapping the dragged item with a larger item. After the swap,
34733
+ * the mouse is still hovering on the item we just swapped with. In this case, we don't want
34734
+ * a mouse move to trigger another swap the other way around, so we create a dead zone. We will clear
34735
+ * the dead zone when the mouse leaves the swapped item.
34736
+ */
34737
+ deadZone;
34738
+ constructor(args) {
34739
+ this.items = args.items.map((item) => ({ ...item, positionAtStart: item.position }));
34740
+ this.draggedItemId = args.draggedItemId;
34741
+ this.container = args.container;
34742
+ this.onChange = args.onChange;
34743
+ this.onCancel = args.onCancel;
34744
+ this.onDragEnd = args.onDragEnd;
34745
+ this.initialMousePosition = args.initialMousePosition;
34746
+ this.currentMousePosition = args.initialMousePosition;
34747
+ this.initialScroll = this.container.scroll;
34748
+ this.minPosition = this.items[0].position;
34749
+ this.maxPosition =
34750
+ this.items[this.items.length - 1].position + this.items[this.items.length - 1].size;
34751
+ }
34752
+ getItemStyles() {
34753
+ const styles = {};
34754
+ for (const item of this.items) {
34755
+ styles[item.id] = this.getItemStyle(item.id);
34756
+ }
34757
+ return styles;
34758
+ }
34759
+ getItemStyle(itemId) {
34760
+ const position = this.container.cssPositionProperty;
34761
+ const style = {};
34762
+ style.position = "relative";
34763
+ style[position] = (this.getItemsPositions()[itemId] || 0) + "px";
34764
+ style.transition = `${position} 0.5s`;
34765
+ style["pointer-events"] = "none";
34766
+ if (this.draggedItemId === itemId) {
34767
+ style.transition = `${position} 0s`;
34768
+ style["z-index"] = "1000";
34769
+ }
34770
+ return cssPropertiesToCss(style);
34771
+ }
34772
+ onScroll() {
34773
+ this.moveDraggedItemToPosition(this.currentMousePosition + this.scrollOffset);
34774
+ }
34775
+ onMouseMove(ev) {
34776
+ if (ev.button > 1) {
34777
+ this.onCancel();
34778
+ return;
34779
+ }
34780
+ const mousePosition = this.container.getMousePosition(ev);
34781
+ this.currentMousePosition = mousePosition;
34782
+ if (mousePosition < this.container.start || mousePosition > this.container.end) {
34783
+ this.startEdgeScroll(mousePosition < this.container.start ? -1 : 1);
34784
+ return;
34785
+ }
34786
+ else {
34787
+ this.stopEdgeScroll();
34788
+ }
34789
+ this.moveDraggedItemToPosition(mousePosition + this.scrollOffset);
34790
+ }
34791
+ moveDraggedItemToPosition(position) {
34792
+ const hoveredItemIndex = this.getHoveredItemIndex(position, this.items);
34793
+ const draggedItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
34794
+ const draggedItem = this.items[draggedItemIndex];
34795
+ if (this.deadZone && this.isInZone(position, this.deadZone)) {
34796
+ this.onChange(this.getItemsPositions());
34797
+ return;
34798
+ }
34799
+ else if (this.isInZone(position, {
34800
+ start: draggedItem.position,
34801
+ end: draggedItem.position + draggedItem.size,
34802
+ })) {
34803
+ this.deadZone = undefined;
34804
+ }
34805
+ if (draggedItemIndex === hoveredItemIndex) {
34806
+ this.onChange(this.getItemsPositions());
34807
+ return;
34808
+ }
34809
+ const startIndex = Math.min(draggedItemIndex, hoveredItemIndex);
34810
+ const endIndex = Math.max(draggedItemIndex, hoveredItemIndex);
34811
+ const direction = Math.sign(hoveredItemIndex - draggedItemIndex);
34812
+ let draggedItemMoveSize = 0;
34813
+ for (let i = startIndex; i <= endIndex; i++) {
34814
+ if (i === draggedItemIndex) {
34815
+ continue;
34816
+ }
34817
+ this.items[i].position -= direction * draggedItem.size;
34818
+ draggedItemMoveSize += this.items[i].size;
34819
+ }
34820
+ draggedItem.position += direction * draggedItemMoveSize;
34821
+ this.items.sort((item1, item2) => item1.position - item2.position);
34822
+ this.deadZone =
34823
+ direction > 0
34824
+ ? { start: position, end: draggedItem.position }
34825
+ : { start: draggedItem.position + draggedItem.size, end: position };
34826
+ this.onChange(this.getItemsPositions());
33889
34827
  }
33890
34828
  onMouseUp(ev) {
33891
34829
  if (ev.button !== 0) {
@@ -34572,12 +35510,12 @@ stores.inject(MyMetaStore, storeInstance);
34572
35510
 
34573
35511
  class ValueInRangeCriterionForm extends CriterionForm {
34574
35512
  static template = "o-spreadsheet-ValueInRangeCriterionForm";
34575
- static components = { SelectionInput };
35513
+ static components = { RoundColorPicker, SelectionInput };
34576
35514
  setup() {
34577
35515
  super.setup();
34578
35516
  const setupDefault = (props) => {
34579
35517
  if (props.criterion.displayStyle === undefined) {
34580
- this.updateCriterion({ displayStyle: "arrow" });
35518
+ this.updateCriterion({ displayStyle: "chip" });
34581
35519
  }
34582
35520
  };
34583
35521
  owl.onWillUpdateProps(setupDefault);
@@ -34590,6 +35528,16 @@ stores.inject(MyMetaStore, storeInstance);
34590
35528
  const displayStyle = ev.target.value;
34591
35529
  this.updateCriterion({ displayStyle });
34592
35530
  }
35531
+ onColorChanged(color, value) {
35532
+ const colors = { ...this.props.criterion.colors };
35533
+ colors[value] = color || undefined;
35534
+ this.updateCriterion({ colors });
35535
+ }
35536
+ get values() {
35537
+ const sheetId = this.env.model.getters.getActiveSheetId();
35538
+ const values = this.env.model.getters.getDataValidationRangeValues(sheetId, this.props.criterion);
35539
+ return new Set(values);
35540
+ }
34593
35541
  }
34594
35542
 
34595
35543
  const criterionCategoriesSequences = {
@@ -36093,6 +37041,7 @@ stores.inject(MyMetaStore, storeInstance);
36093
37041
  // We need here the svg of the icons that we need to convert to images for the renderer
36094
37042
  // -----------------------------------------------------------------------------
36095
37043
  const ARROW_DOWN = {
37044
+ name: "ARROW_DOWN",
36096
37045
  width: 448,
36097
37046
  height: 512,
36098
37047
  paths: [
@@ -36103,6 +37052,7 @@ stores.inject(MyMetaStore, storeInstance);
36103
37052
  ],
36104
37053
  };
36105
37054
  const ARROW_UP = {
37055
+ name: "ARROW_UP",
36106
37056
  width: 448,
36107
37057
  height: 512,
36108
37058
  paths: [
@@ -36113,6 +37063,7 @@ stores.inject(MyMetaStore, storeInstance);
36113
37063
  ],
36114
37064
  };
36115
37065
  const ARROW_RIGHT = {
37066
+ name: "ARROW_RIGHT",
36116
37067
  width: 448,
36117
37068
  height: 512,
36118
37069
  paths: [
@@ -36123,6 +37074,7 @@ stores.inject(MyMetaStore, storeInstance);
36123
37074
  ],
36124
37075
  };
36125
37076
  const SMILE = {
37077
+ name: "SMILE",
36126
37078
  width: 496,
36127
37079
  height: 512,
36128
37080
  paths: [
@@ -36133,6 +37085,7 @@ stores.inject(MyMetaStore, storeInstance);
36133
37085
  ],
36134
37086
  };
36135
37087
  const MEH = {
37088
+ name: "MEH",
36136
37089
  width: 496,
36137
37090
  height: 512,
36138
37091
  paths: [
@@ -36143,6 +37096,7 @@ stores.inject(MyMetaStore, storeInstance);
36143
37096
  ],
36144
37097
  };
36145
37098
  const FROWN = {
37099
+ name: "FROWN",
36146
37100
  width: 496,
36147
37101
  height: 512,
36148
37102
  paths: [
@@ -36154,44 +37108,79 @@ stores.inject(MyMetaStore, storeInstance);
36154
37108
  };
36155
37109
  const DOT_PATH = "M256 9 a247 247 0 1 0.1 0 0";
36156
37110
  const GREEN_DOT = {
37111
+ name: "GREEN_DOT",
36157
37112
  width: 512,
36158
37113
  height: 512,
36159
37114
  paths: [{ fillColor: "#6AA84F", path: DOT_PATH }],
36160
37115
  };
36161
37116
  const YELLOW_DOT = {
37117
+ name: "YELLOW_DOT",
36162
37118
  width: 512,
36163
37119
  height: 512,
36164
37120
  paths: [{ fillColor: "#F0AD4E", path: DOT_PATH }],
36165
37121
  };
36166
37122
  const RED_DOT = {
37123
+ name: "RED_DOT",
36167
37124
  width: 512,
36168
37125
  height: 512,
36169
37126
  paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36170
37127
  };
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
- };
37128
+ function getCaretDownSvg(color) {
37129
+ return {
37130
+ name: "CARET_DOWN",
37131
+ width: 512,
37132
+ height: 512,
37133
+ paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
37134
+ };
37135
+ }
37136
+ function getHoveredCaretDownSvg(color) {
37137
+ return {
37138
+ name: "CARET_DOWN",
37139
+ width: 512,
37140
+ height: 512,
37141
+ paths: [
37142
+ { fillColor: color.textColor || TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
37143
+ { fillColor: color.fillColor || "#fff", path: "M120 195 h270 l-135 130" },
37144
+ ],
37145
+ };
37146
+ }
37147
+ const CHIP_CARET_DOWN_PATH = "M40 185 h270 l-135 128";
37148
+ function getChipSvg(chipStyle) {
37149
+ return {
37150
+ name: "CHIP",
37151
+ width: 512,
37152
+ height: 512,
37153
+ paths: [{ fillColor: chipStyle.textColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH }],
37154
+ };
37155
+ }
37156
+ function getHoveredChipSvg(chipStyle) {
37157
+ return {
37158
+ name: "CHIP",
37159
+ width: 512,
37160
+ height: 512,
37161
+ paths: [
37162
+ {
37163
+ fillColor: chipStyle.textColor || TEXT_BODY_MUTED,
37164
+ path: "M0,225 A175,175 0 1,0 350,225 A175,175 0 1,0 0,225",
37165
+ },
37166
+ { fillColor: chipStyle.fillColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH },
37167
+ ],
37168
+ };
37169
+ }
36184
37170
  const CHECKBOX_UNCHECKED = {
37171
+ name: "CHECKBOX_UNCHECKED",
36185
37172
  width: 512,
36186
37173
  height: 512,
36187
37174
  paths: [{ fillColor: GRAY_300, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36188
37175
  };
36189
37176
  const CHECKBOX_UNCHECKED_HOVERED = {
37177
+ name: "CHECKBOX_UNCHECKED",
36190
37178
  width: 512,
36191
37179
  height: 512,
36192
37180
  paths: [{ fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36193
37181
  };
36194
37182
  const CHECKBOX_CHECKED = {
37183
+ name: "CHECKBOX_CHECKED",
36195
37184
  width: 512,
36196
37185
  height: 512,
36197
37186
  paths: [
@@ -36204,6 +37193,7 @@ stores.inject(MyMetaStore, storeInstance);
36204
37193
  ? "M149,235 h213 v43 h-213 M235,149 h43 v213 h-43" // +
36205
37194
  : "M149,235 h213 v43 h-213"; // -
36206
37195
  return {
37196
+ name: "PIVOT_ICON",
36207
37197
  width: 512,
36208
37198
  height: 512,
36209
37199
  paths: [
@@ -36230,6 +37220,7 @@ stores.inject(MyMetaStore, storeInstance);
36230
37220
  colors.hoverBackgroundColor = "#fff";
36231
37221
  }
36232
37222
  return {
37223
+ name: "DATA_FILTER_ICON",
36233
37224
  width: isActive ? 24 : 850,
36234
37225
  height: isActive ? 24 : 850,
36235
37226
  paths: [
@@ -40425,6 +41416,23 @@ stores.inject(MyMetaStore, storeInstance);
40425
41416
  }
40426
41417
  return data;
40427
41418
  },
41419
+ })
41420
+ .add("18.4.3", {
41421
+ migrate(data) {
41422
+ if (!data.pivots) {
41423
+ return data;
41424
+ }
41425
+ for (const pivotId in data.pivots) {
41426
+ const pivot = data.pivots[pivotId];
41427
+ if (pivot.sortedColumn) {
41428
+ const measure = pivot.measures.find((measure) => measure.fieldName === pivot.sortedColumn?.measure);
41429
+ if (measure) {
41430
+ pivot.sortedColumn.measure = measure.id;
41431
+ }
41432
+ }
41433
+ }
41434
+ return data;
41435
+ },
40428
41436
  });
40429
41437
  function fixOverlappingFilters(data) {
40430
41438
  for (const sheet of data.sheets || []) {
@@ -41088,6 +42096,10 @@ stores.inject(MyMetaStore, storeInstance);
41088
42096
  });
41089
42097
  };
41090
42098
  const CAN_REMOVE_COLUMNS_ROWS = (dimension, env) => {
42099
+ if ((dimension === "COL" && env.model.getters.getActiveRows().size > 0) ||
42100
+ (dimension === "ROW" && env.model.getters.getActiveCols().size > 0)) {
42101
+ return false;
42102
+ }
41091
42103
  const sheetId = env.model.getters.getActiveSheetId();
41092
42104
  const selectedElements = env.model.getters.getElementsFromSelection(dimension);
41093
42105
  const includesAllVisibleHeaders = env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, dimension, selectedElements);
@@ -42642,7 +43654,7 @@ stores.inject(MyMetaStore, storeInstance);
42642
43654
  criterion: {
42643
43655
  type: "isValueInList",
42644
43656
  values: [],
42645
- displayStyle: "arrow",
43657
+ displayStyle: "chip",
42646
43658
  },
42647
43659
  },
42648
43660
  });
@@ -44557,6 +45569,11 @@ stores.inject(MyMetaStore, storeInstance);
44557
45569
  rect = this.defaultRect;
44558
45570
  isEditing = false;
44559
45571
  isCellReferenceVisible = false;
45572
+ currentEditedCell = {
45573
+ col: 0,
45574
+ row: 0,
45575
+ sheetId: this.env.model.getters.getActiveSheetId(),
45576
+ };
44560
45577
  composerStore;
44561
45578
  composerFocusStore;
44562
45579
  composerInterface;
@@ -44681,12 +45698,17 @@ stores.inject(MyMetaStore, storeInstance);
44681
45698
  if (!isEditing && this.composerFocusStore.activeComposer !== this.composerInterface) {
44682
45699
  this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "inactive" });
44683
45700
  }
45701
+ let shouldRecomputeRect = !deepEquals(this.currentEditedCell, this.composerStore.currentEditedCell);
44684
45702
  if (this.isEditing !== isEditing) {
44685
45703
  this.isEditing = isEditing;
44686
45704
  if (!isEditing) {
44687
45705
  this.rect = this.defaultRect;
44688
45706
  return;
44689
45707
  }
45708
+ this.currentEditedCell = this.composerStore.currentEditedCell;
45709
+ shouldRecomputeRect = true;
45710
+ }
45711
+ if (shouldRecomputeRect) {
44690
45712
  const position = this.env.model.getters.getActivePosition();
44691
45713
  const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
44692
45714
  this.rect = this.env.model.getters.getVisibleRect(zone);
@@ -45477,15 +46499,16 @@ stores.inject(MyMetaStore, storeInstance);
45477
46499
  return state;
45478
46500
  }
45479
46501
 
46502
+ const PAINT_FORMAT_HANDLER_KEYS = [
46503
+ "cell",
46504
+ "border",
46505
+ "table",
46506
+ "conditionalFormat",
46507
+ "merge",
46508
+ ];
45480
46509
  class PaintFormatStore extends SpreadsheetStore {
45481
46510
  mutators = ["activate", "cancel", "pasteFormat"];
45482
46511
  highlightStore = this.get(HighlightStore);
45483
- clipboardHandlers = [
45484
- new CellClipboardHandler(this.getters, this.model.dispatch),
45485
- new BorderClipboardHandler(this.getters, this.model.dispatch),
45486
- new TableClipboardHandler(this.getters, this.model.dispatch),
45487
- new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
45488
- ];
45489
46512
  status = "inactive";
45490
46513
  copiedData;
45491
46514
  constructor(get) {
@@ -45516,24 +46539,38 @@ stores.inject(MyMetaStore, storeInstance);
45516
46539
  get isActive() {
45517
46540
  return this.status !== "inactive";
45518
46541
  }
46542
+ get clipboardHandlers() {
46543
+ return PAINT_FORMAT_HANDLER_KEYS.map((handlerName) => {
46544
+ const HandlerClass = clipboardHandlersRegistries.cellHandlers.get(handlerName);
46545
+ return {
46546
+ handlerName,
46547
+ handler: new HandlerClass(this.getters, this.model.dispatch),
46548
+ };
46549
+ });
46550
+ }
45519
46551
  copyFormats() {
45520
46552
  const sheetId = this.getters.getActiveSheetId();
45521
46553
  const zones = this.getters.getSelectedZones();
45522
- const copiedData = {};
45523
- for (const handler of this.clipboardHandlers) {
45524
- Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones), false));
46554
+ const copiedData = { zones, sheetId };
46555
+ for (const { handlerName, handler } of this.clipboardHandlers) {
46556
+ const handlerResult = handler.copy(getClipboardDataPositions(sheetId, zones), false);
46557
+ if (handlerResult !== undefined) {
46558
+ copiedData[handlerName] = handlerResult;
46559
+ }
45525
46560
  }
45526
46561
  return copiedData;
45527
46562
  }
45528
46563
  paintFormat(sheetId, target) {
45529
- if (this.copiedData) {
45530
- for (const handler of this.clipboardHandlers) {
45531
- handler.paste({ zones: target, sheetId }, this.copiedData, {
45532
- isCutOperation: false,
45533
- pasteOption: "onlyFormat",
45534
- });
45535
- }
46564
+ if (!this.copiedData) {
46565
+ return;
45536
46566
  }
46567
+ const options = {
46568
+ isCutOperation: false,
46569
+ pasteOption: "onlyFormat",
46570
+ };
46571
+ const { target: pasteTarget, selectedZones } = getPasteTargetFromHandlers(sheetId, target, this.copiedData, this.clipboardHandlers, options);
46572
+ applyClipboardHandlersPaste(this.clipboardHandlers, this.copiedData, pasteTarget, options);
46573
+ selectPastedZone(this.model.selection, target, selectedZones);
45537
46574
  if (this.status === "oneOff") {
45538
46575
  this.cancel();
45539
46576
  }
@@ -45580,12 +46617,8 @@ stores.inject(MyMetaStore, storeInstance);
45580
46617
  this.row = undefined;
45581
46618
  }
45582
46619
  computeOverlay() {
45583
- if (!this.getters.isDashboard()) {
45584
- return;
45585
- }
45586
46620
  this.overlayColors = new PositionMap();
45587
- const col = this.col;
45588
- const row = this.row;
46621
+ const { col, row } = this;
45589
46622
  if (col === undefined || row === undefined) {
45590
46623
  return;
45591
46624
  }
@@ -45594,9 +46627,16 @@ stores.inject(MyMetaStore, storeInstance);
45594
46627
  if (!table) {
45595
46628
  return;
45596
46629
  }
45597
- const { left, right } = table.range.zone;
45598
- for (let c = left; c <= right; c++) {
45599
- this.overlayColors.set({ sheetId, col: c, row }, setColorAlpha("#017E84", 0.08));
46630
+ const { left, right, top } = table.range.zone;
46631
+ const isTableHeader = row < top + table.config.numberOfHeaders;
46632
+ const doesTableRowHaveContent = range(left, right + 1).some((col) => {
46633
+ return (!this.getters.isColHidden(sheetId, col) &&
46634
+ this.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue);
46635
+ });
46636
+ if (!isTableHeader && doesTableRowHaveContent) {
46637
+ for (let col = left; col <= right; col++) {
46638
+ this.overlayColors.set({ sheetId, col, row }, TABLE_HOVER_BACKGROUND_COLOR);
46639
+ }
45600
46640
  }
45601
46641
  }
45602
46642
  }
@@ -45792,11 +46832,14 @@ stores.inject(MyMetaStore, storeInstance);
45792
46832
  onCellClicked(ev) {
45793
46833
  const openedPopover = this.cellPopovers.persistentCellPopover;
45794
46834
  const [col, row] = this.getCartesianCoordinates(ev);
46835
+ const clickedIcon = this.getInteractiveIconAtEvent(ev);
46836
+ if (clickedIcon) {
46837
+ this.env.model.selection.getBackToDefault();
46838
+ }
45795
46839
  this.props.onCellClicked(col, row, {
45796
46840
  expandZone: ev.shiftKey,
45797
46841
  addZone: isCtrlKey(ev),
45798
46842
  }, ev);
45799
- const clickedIcon = this.getInteractiveIconAtEvent(ev);
45800
46843
  if (clickedIcon?.onClick) {
45801
46844
  clickedIcon.onClick(clickedIcon.position, this.env);
45802
46845
  }
@@ -45841,7 +46884,10 @@ stores.inject(MyMetaStore, storeInstance);
45841
46884
  }
45842
46885
  const icons = this.env.model.getters.getCellIcons(position);
45843
46886
  const icon = icons.find((icon) => {
45844
- return isPointInsideRect(x, y, this.env.model.getters.getCellIconRect(icon));
46887
+ const merge = this.env.model.getters.getMerge(position);
46888
+ const zone = merge || positionToZone(position);
46889
+ const cellRect = this.env.model.getters.getRect(zone);
46890
+ return isPointInsideRect(x, y, this.env.model.getters.getCellIconRect(icon, cellRect));
45845
46891
  });
45846
46892
  return icon?.onClick ? icon : undefined;
45847
46893
  }
@@ -46578,19 +47624,56 @@ stores.inject(MyMetaStore, storeInstance);
46578
47624
  }
46579
47625
  }
46580
47626
 
46581
- class GridRenderer {
46582
- getters;
46583
- renderer;
47627
+ const CELL_ANIMATION_DURATION = 200;
47628
+ class GridRenderer extends SpreadsheetStore {
46584
47629
  fingerprints;
46585
47630
  hoveredTables;
46586
47631
  hoveredIcon;
47632
+ lastRenderBoxes = new Map();
47633
+ preventNewAnimationsInNextFrame = false;
47634
+ zonesWithPreventedAnimationsInNextFrame = [];
47635
+ animations = new Map();
46587
47636
  constructor(get) {
47637
+ super(get);
46588
47638
  this.getters = get(ModelStore).getters;
46589
- this.renderer = get(RendererStore);
46590
47639
  this.fingerprints = get(FormulaFingerprintStore);
46591
47640
  this.hoveredTables = get(HoveredTableStore);
46592
47641
  this.hoveredIcon = get(HoveredIconStore);
46593
- this.renderer.register(this);
47642
+ }
47643
+ handle(cmd) {
47644
+ switch (cmd.type) {
47645
+ case "START":
47646
+ case "ACTIVATE_SHEET":
47647
+ case "ADD_COLUMNS_ROWS":
47648
+ case "REMOVE_COLUMNS_ROWS":
47649
+ this.animations.clear();
47650
+ this.preventNewAnimationsInNextFrame = true;
47651
+ break;
47652
+ case "RESIZE_COLUMNS_ROWS":
47653
+ this.preventNewAnimationsInNextFrame = true;
47654
+ break;
47655
+ case "REDO":
47656
+ this.zonesWithPreventedAnimationsInNextFrame = [];
47657
+ break;
47658
+ case "UNDO":
47659
+ for (const command of cmd.commands) {
47660
+ if (command.type === "ADD_COLUMNS_ROWS" ||
47661
+ command.type === "REMOVE_COLUMNS_ROWS" ||
47662
+ command.type === "RESIZE_COLUMNS_ROWS") {
47663
+ this.animations.clear();
47664
+ this.preventNewAnimationsInNextFrame = true;
47665
+ break;
47666
+ }
47667
+ }
47668
+ break;
47669
+ case "PASTE":
47670
+ this.zonesWithPreventedAnimationsInNextFrame.push(...this.getters.getSelectedZones());
47671
+ break;
47672
+ case "UPDATE_CELL":
47673
+ const zones = this.getters.getCommandZones(cmd);
47674
+ this.zonesWithPreventedAnimationsInNextFrame.push(...zones);
47675
+ break;
47676
+ }
46594
47677
  }
46595
47678
  get renderingLayers() {
46596
47679
  return ["Background", "Headers"];
@@ -46598,17 +47681,20 @@ stores.inject(MyMetaStore, storeInstance);
46598
47681
  // ---------------------------------------------------------------------------
46599
47682
  // Grid rendering
46600
47683
  // ---------------------------------------------------------------------------
46601
- drawLayer(renderingContext, layer) {
47684
+ drawLayer(renderingContext, layer, timeStamp) {
46602
47685
  switch (layer) {
46603
47686
  case "Background":
46604
47687
  this.drawGlobalBackground(renderingContext);
47688
+ const oldBoxes = this.lastRenderBoxes;
47689
+ this.lastRenderBoxes = new Map();
46605
47690
  for (const { zone, rect } of this.getters.getAllActiveViewportsZonesAndRect()) {
46606
47691
  const { ctx } = renderingContext;
46607
47692
  ctx.save();
46608
47693
  ctx.beginPath();
46609
47694
  ctx.rect(rect.x, rect.y, rect.width, rect.height);
46610
47695
  ctx.clip();
46611
- const boxes = this.getGridBoxes(zone);
47696
+ const boxesWithoutAnimations = this.getGridBoxes(zone);
47697
+ const boxes = this.getBoxesWithAnimations(boxesWithoutAnimations, oldBoxes, timeStamp);
46612
47698
  this.drawBackground(renderingContext, boxes);
46613
47699
  this.drawOverflowingCellBackground(renderingContext, boxes);
46614
47700
  this.drawCellBackground(renderingContext, boxes);
@@ -46618,6 +47704,8 @@ stores.inject(MyMetaStore, storeInstance);
46618
47704
  ctx.restore();
46619
47705
  }
46620
47706
  this.drawFrozenPanes(renderingContext);
47707
+ this.preventNewAnimationsInNextFrame = false;
47708
+ this.zonesWithPreventedAnimationsInNextFrame = [];
46621
47709
  break;
46622
47710
  case "Headers":
46623
47711
  if (!this.getters.isDashboard()) {
@@ -46641,6 +47729,8 @@ stores.inject(MyMetaStore, storeInstance);
46641
47729
  const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;
46642
47730
  if (areGridLinesVisible) {
46643
47731
  for (const box of boxes) {
47732
+ if (box.skipCellGridLines)
47733
+ continue;
46644
47734
  ctx.strokeStyle = CELL_BORDER_COLOR;
46645
47735
  ctx.lineWidth = thinLineWidth;
46646
47736
  ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);
@@ -46661,6 +47751,19 @@ stores.inject(MyMetaStore, storeInstance);
46661
47751
  const width = box.width * (percentage / 100);
46662
47752
  ctx.fillRect(box.x, box.y, width, box.height);
46663
47753
  }
47754
+ if (box?.chip) {
47755
+ ctx.save();
47756
+ ctx.beginPath();
47757
+ ctx.rect(box.x, box.y, box.width, box.height);
47758
+ ctx.clip();
47759
+ const chip = box.chip;
47760
+ ctx.fillStyle = chip.color;
47761
+ const radius = 10;
47762
+ ctx.beginPath();
47763
+ ctx.roundRect(chip.x, chip.y, chip.width, chip.height, radius);
47764
+ ctx.fill();
47765
+ ctx.restore();
47766
+ }
46664
47767
  if (box.overlayColor) {
46665
47768
  ctx.fillStyle = box.overlayColor;
46666
47769
  ctx.fillRect(box.x, box.y, box.width, box.height);
@@ -46735,7 +47838,8 @@ stores.inject(MyMetaStore, storeInstance);
46735
47838
  * each line and adding 1 pixel to the end of each line (depending on the direction of the
46736
47839
  * line).
46737
47840
  */
46738
- function drawBorder({ style, color }, x1, y1, x2, y2) {
47841
+ function drawBorder({ color, style, opacity }, x1, y1, x2, y2) {
47842
+ ctx.globalAlpha = opacity ?? 1;
46739
47843
  ctx.strokeStyle = color;
46740
47844
  switch (style) {
46741
47845
  case "medium":
@@ -46783,6 +47887,7 @@ stores.inject(MyMetaStore, storeInstance);
46783
47887
  ctx.stroke();
46784
47888
  ctx.lineWidth = 1;
46785
47889
  ctx.setLineDash([]);
47890
+ ctx.globalAlpha = 1;
46786
47891
  }
46787
47892
  }
46788
47893
  drawTexts(renderingContext, boxes) {
@@ -46791,6 +47896,7 @@ stores.inject(MyMetaStore, storeInstance);
46791
47896
  let currentFont;
46792
47897
  for (const box of boxes) {
46793
47898
  if (box.content) {
47899
+ ctx.globalAlpha = box.textOpacity ?? 1;
46794
47900
  const style = box.style || {};
46795
47901
  const align = box.content.align || "left";
46796
47902
  // compute font and textColor
@@ -46800,19 +47906,6 @@ stores.inject(MyMetaStore, storeInstance);
46800
47906
  ctx.font = font;
46801
47907
  }
46802
47908
  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
47909
  // horizontal align text direction
46817
47910
  ctx.textAlign = align;
46818
47911
  // clip rect if needed
@@ -46823,19 +47916,18 @@ stores.inject(MyMetaStore, storeInstance);
46823
47916
  ctx.rect(x, y, width, height);
46824
47917
  ctx.clip();
46825
47918
  }
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);
47919
+ const x = box.content.x;
47920
+ let y = box.content.y;
46830
47921
  // use the horizontal and the vertical start points to:
46831
47922
  // fill text / fill strikethrough / fill underline
46832
47923
  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;
47924
+ drawDecoratedText(ctx, brokenLine, { x, y }, style.underline, style.strikethrough);
47925
+ y += MIN_CELL_TEXT_MARGIN + box.content.fontSizePx;
46835
47926
  }
46836
47927
  if (box.clipRect) {
46837
47928
  ctx.restore();
46838
47929
  }
47930
+ ctx.globalAlpha = 1;
46839
47931
  }
46840
47932
  }
46841
47933
  }
@@ -46853,10 +47945,11 @@ stores.inject(MyMetaStore, storeInstance);
46853
47945
  }
46854
47946
  ctx.save();
46855
47947
  ctx.beginPath();
46856
- ctx.rect(box.x, box.y, box.width, box.height);
47948
+ const clipRect = icon.clipRect || box;
47949
+ ctx.rect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);
46857
47950
  ctx.clip();
46858
47951
  const iconSize = icon.size;
46859
- const { x, y } = this.getters.getCellIconRect(icon);
47952
+ const { x, y } = this.getters.getCellIconRect(icon, box);
46860
47953
  ctx.translate(x, y);
46861
47954
  ctx.scale(iconSize / svg.width, iconSize / svg.height);
46862
47955
  for (const path of svg.paths) {
@@ -47065,12 +48158,15 @@ stores.inject(MyMetaStore, storeInstance);
47065
48158
  const cell = this.getters.getEvaluatedCell(position);
47066
48159
  const showFormula = this.getters.shouldShowFormulas();
47067
48160
  const { x, y, width, height } = this.getters.getRect(zone);
47068
- const { verticalAlign } = this.getters.getCellStyle(position);
48161
+ const chipStyle = this.getters.getDataValidationChipStyle(position);
47069
48162
  let style = this.getters.getCellComputedStyle(position);
47070
48163
  if (this.fingerprints.isEnabled) {
47071
48164
  const fingerprintColor = this.fingerprints.colors.get(position);
47072
48165
  style = { ...style, fillColor: fingerprintColor };
47073
48166
  }
48167
+ if (chipStyle?.textColor) {
48168
+ style = { ...style, textColor: chipStyle.textColor };
48169
+ }
47074
48170
  const dataBarFill = this.fingerprints.isEnabled
47075
48171
  ? undefined
47076
48172
  : this.getters.getConditionalDataBar(position);
@@ -47081,6 +48177,7 @@ stores.inject(MyMetaStore, storeInstance);
47081
48177
  center: iconsList.find((icon) => icon?.horizontalAlign === "center"),
47082
48178
  };
47083
48179
  const box = {
48180
+ id: zoneToXc(zone),
47084
48181
  x,
47085
48182
  y,
47086
48183
  width,
@@ -47088,11 +48185,11 @@ stores.inject(MyMetaStore, storeInstance);
47088
48185
  border: this.getters.getCellComputedBorder(position) || undefined,
47089
48186
  style,
47090
48187
  dataBarFill,
47091
- verticalAlign,
47092
48188
  overlayColor: this.hoveredTables.overlayColors.get(position),
47093
48189
  isError: (cell.type === CellValueType.error && !!cell.message) ||
47094
48190
  this.getters.isDataValidationInvalid(position),
47095
48191
  icons: cellIcons,
48192
+ disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame.some((z) => isZoneInside(zone, z) || overlap(zone, z)),
47096
48193
  };
47097
48194
  const fontSizePX = computeTextFontSizeInPixels(box.style);
47098
48195
  if (cell.type === CellValueType.empty || box.icons.center) {
@@ -47104,22 +48201,55 @@ stores.inject(MyMetaStore, storeInstance);
47104
48201
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
47105
48202
  const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
47106
48203
  const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
48204
+ const chipMargin = chipStyle ? DATA_VALIDATION_CHIP_MARGIN : 0;
47107
48205
  const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
48206
+ const leftMargin = leftIconWidth + chipMargin;
47108
48207
  const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
47109
- const contentWidth = leftIconWidth + textWidth + rightIconWidth;
48208
+ const rightMargin = rightIconWidth + chipMargin;
48209
+ const contentWidth = leftMargin + textWidth + rightMargin;
47110
48210
  const align = this.computeCellAlignment(position, contentWidth > width);
48211
+ // compute vertical align start point parameter:
48212
+ const numberOfLines = multiLineText.length;
48213
+ const contentY = Math.round(this.getters.computeTextYCoordinate(box, fontSizePX, style.verticalAlign, numberOfLines));
48214
+ // compute horizontal align start point parameter
48215
+ let contentX = box.x;
48216
+ if (align === "left") {
48217
+ contentX += MIN_CELL_TEXT_MARGIN + leftMargin;
48218
+ }
48219
+ else if (align === "right") {
48220
+ contentX += box.width - MIN_CELL_TEXT_MARGIN - rightMargin;
48221
+ }
48222
+ else {
48223
+ contentX += box.width / 2;
48224
+ }
48225
+ contentX = Math.round(contentX);
48226
+ const textHeight = computeTextLinesHeight(fontSizePX, numberOfLines);
47111
48227
  box.content = {
47112
48228
  textLines: multiLineText,
47113
48229
  width: wrapping === "overflow" ? textWidth : width,
47114
48230
  align,
48231
+ x: contentX,
48232
+ y: contentY,
48233
+ fontSizePx: fontSizePX,
47115
48234
  };
48235
+ if (chipStyle?.fillColor) {
48236
+ const chipMarginLeft = leftMargin;
48237
+ const chipMarginRight = DATA_VALIDATION_CHIP_MARGIN;
48238
+ box.chip = {
48239
+ color: chipStyle.fillColor,
48240
+ width: box.width - chipMarginLeft - chipMarginRight,
48241
+ height: textHeight + 2,
48242
+ x: box.x + chipMarginLeft,
48243
+ y: contentY - 2,
48244
+ };
48245
+ }
47116
48246
  /** ClipRect */
47117
48247
  const isOverflowing = contentWidth > width || fontSizePX > height;
47118
- if (box.icons.left || box.icons.right) {
48248
+ if (box.icons.left || box.icons.right || box.chip) {
47119
48249
  box.clipRect = {
47120
- x: box.x + leftIconWidth,
48250
+ x: box.x + leftMargin,
47121
48251
  y: box.y,
47122
- width: Math.max(0, width - leftIconWidth - rightIconWidth),
48252
+ width: Math.max(0, width - leftMargin - rightMargin),
47123
48253
  height,
47124
48254
  };
47125
48255
  }
@@ -47229,6 +48359,77 @@ stores.inject(MyMetaStore, storeInstance);
47229
48359
  }
47230
48360
  return boxes;
47231
48361
  }
48362
+ getBoxesWithAnimations(boxes, oldBoxes, timeStamp) {
48363
+ this.updateAnimationsProgress(timeStamp);
48364
+ this.addNewAnimations(boxes, oldBoxes, timeStamp);
48365
+ if (this.animations.size > 0) {
48366
+ this.renderer.startAnimation("grid_renderer_animation");
48367
+ return this.updateBoxesWithAnimations(boxes);
48368
+ }
48369
+ else {
48370
+ this.renderer.stopAnimation("grid_renderer_animation");
48371
+ return boxes;
48372
+ }
48373
+ }
48374
+ updateBoxesWithAnimations(boxes) {
48375
+ const boxesWithAnimations = [];
48376
+ for (const box of boxes) {
48377
+ const animation = this.animations.get(box.id);
48378
+ if (!animation) {
48379
+ boxesWithAnimations.push(box);
48380
+ continue;
48381
+ }
48382
+ const animatedBox = deepCopy(box);
48383
+ boxesWithAnimations.push(animatedBox);
48384
+ for (const animationId of animation.animationTypes) {
48385
+ const animationItem = cellAnimationRegistry.get(animationId);
48386
+ const newBoxes = animationItem.updateAnimation(animation.progress, animatedBox, animation.oldBox, box);
48387
+ if (newBoxes) {
48388
+ boxesWithAnimations.push(...newBoxes.newBoxes);
48389
+ }
48390
+ }
48391
+ }
48392
+ return boxesWithAnimations;
48393
+ }
48394
+ updateAnimationsProgress(timeStamp) {
48395
+ if (timeStamp === undefined) {
48396
+ return;
48397
+ }
48398
+ for (const boxId of this.animations.keys()) {
48399
+ const animation = this.animations.get(boxId);
48400
+ if (animation.startTime === undefined) {
48401
+ animation.startTime = timeStamp;
48402
+ continue;
48403
+ }
48404
+ const elapsedTime = timeStamp - animation.startTime;
48405
+ const progress = Math.min(1, elapsedTime / CELL_ANIMATION_DURATION);
48406
+ if (progress >= 1) {
48407
+ this.animations.delete(boxId);
48408
+ }
48409
+ animation.progress = progress;
48410
+ }
48411
+ }
48412
+ addNewAnimations(boxes, oldBoxes, timeStamp) {
48413
+ for (const box of boxes) {
48414
+ this.lastRenderBoxes.set(box.id, box);
48415
+ const oldBox = oldBoxes.get(box.id);
48416
+ if (this.preventNewAnimationsInNextFrame || !oldBox || box.disabledAnimation) {
48417
+ continue;
48418
+ }
48419
+ const animationTypes = [];
48420
+ for (const animationItem of cellAnimationRegistry.getAll()) {
48421
+ if (animationItem.hasAnimation(oldBox, box)) {
48422
+ animationTypes.push(animationItem.id);
48423
+ }
48424
+ }
48425
+ const animation = animationTypes.length > 0
48426
+ ? { animationTypes, oldBox, progress: 0, startTime: timeStamp }
48427
+ : undefined;
48428
+ if (animation) {
48429
+ this.animations.set(box.id, animation);
48430
+ }
48431
+ }
48432
+ }
47232
48433
  }
47233
48434
 
47234
48435
  function useGridDrawing(refName, model, canvasSize) {
@@ -47263,10 +48464,7 @@ stores.inject(MyMetaStore, storeInstance);
47263
48464
  // http://diveintohtml5.info/canvas.html#pixel-madness
47264
48465
  ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT);
47265
48466
  ctx.scale(dpr, dpr);
47266
- for (const layer of OrderedLayers()) {
47267
- model.drawLayer(renderingContext, layer);
47268
- rendererStore.drawLayer(renderingContext, layer);
47269
- }
48467
+ rendererStore.draw(renderingContext);
47270
48468
  }
47271
48469
  }
47272
48470
 
@@ -47819,15 +49017,6 @@ stores.inject(MyMetaStore, storeInstance);
47819
49017
  }
47820
49018
  }
47821
49019
 
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
49020
  class ChartDataSeries extends owl.Component {
47832
49021
  static template = "o-spreadsheet.ChartDataSeries";
47833
49022
  static components = { SelectionInput, Section };
@@ -48376,325 +49565,6 @@ stores.inject(MyMetaStore, storeInstance);
48376
49565
  }
48377
49566
  }
48378
49567
 
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
49568
  css /* scss */ `
48699
49569
  .o-color-picker-widget {
48700
49570
  display: flex;
@@ -49162,57 +50032,6 @@ stores.inject(MyMetaStore, storeInstance);
49162
50032
  };
49163
50033
  }
49164
50034
 
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
50035
  class GeneralDesignEditor extends owl.Component {
49217
50036
  static template = "o-spreadsheet-GeneralDesignEditor";
49218
50037
  static components = {
@@ -54740,7 +55559,7 @@ stores.inject(MyMetaStore, storeInstance);
54740
55559
  }
54741
55560
  getTypeFromZone(sheetId, zone) {
54742
55561
  const cells = this.getters.getEvaluatedCellsInZone(sheetId, zone);
54743
- const nonEmptyCells = cells.filter((cell) => cell.type !== CellValueType.empty);
55562
+ const nonEmptyCells = cells.filter((cell) => !(cell.type === CellValueType.empty || cell.value === ""));
54744
55563
  if (nonEmptyCells.length === 0) {
54745
55564
  return "integer";
54746
55565
  }
@@ -55348,7 +56167,7 @@ stores.inject(MyMetaStore, storeInstance);
55348
56167
  `;
55349
56168
  class SettingsPanel extends owl.Component {
55350
56169
  static template = "o-spreadsheet-SettingsPanel";
55351
- static components = { Section, ValidationMessages };
56170
+ static components = { Section, ValidationMessages, BadgeSelection };
55352
56171
  static props = { onCloseSidePanel: Function };
55353
56172
  loadedLocales = [];
55354
56173
  setup() {
@@ -56232,49 +57051,111 @@ stores.inject(MyMetaStore, storeInstance);
56232
57051
  }
56233
57052
 
56234
57053
  const DEFAULT_SIDE_PANEL_SIZE = 350;
57054
+ const COLLAPSED_SIDE_PANEL_SIZE = 45;
56235
57055
  const MIN_SHEET_VIEW_WIDTH = 150;
56236
57056
  class SidePanelStore extends SpreadsheetStore {
56237
- mutators = ["open", "toggle", "close", "changePanelSize", "resetPanelSize"];
56238
- initialPanelProps = {};
56239
- componentTag = "";
56240
- panelSize = DEFAULT_SIDE_PANEL_SIZE;
57057
+ mutators = [
57058
+ "open",
57059
+ "toggle",
57060
+ "close",
57061
+ "changePanelSize",
57062
+ "resetPanelSize",
57063
+ "togglePinPanel",
57064
+ "closeMainPanel",
57065
+ "changeSpreadsheetWidth",
57066
+ "toggleCollapsePanel",
57067
+ ];
57068
+ mainPanel = undefined;
57069
+ secondaryPanel;
57070
+ availableWidth = 0;
56241
57071
  screenWidthStore = this.get(ScreenWidthStore);
56242
- get isOpen() {
56243
- if (!this.componentTag) {
56244
- return false;
56245
- }
56246
- return this.computeState(this.componentTag, this.initialPanelProps).isOpen;
57072
+ get isMainPanelOpen() {
57073
+ return this.mainPanel && this.mainPanel.componentTag
57074
+ ? this.computeState(this.mainPanel).isOpen
57075
+ : false;
57076
+ }
57077
+ get isSecondaryPanelOpen() {
57078
+ return this.secondaryPanel && this.secondaryPanel.componentTag
57079
+ ? this.computeState(this.secondaryPanel).isOpen
57080
+ : false;
57081
+ }
57082
+ get mainPanelProps() {
57083
+ return this.mainPanel ? this.getPanelProps(this.mainPanel) : undefined;
57084
+ }
57085
+ get mainPanelKey() {
57086
+ return this.mainPanel ? this.getPanelKey(this.mainPanel) : undefined;
57087
+ }
57088
+ get secondaryPanelProps() {
57089
+ return this.secondaryPanel ? this.getPanelProps(this.secondaryPanel) : undefined;
57090
+ }
57091
+ get secondaryPanelKey() {
57092
+ return this.secondaryPanel ? this.getPanelKey(this.secondaryPanel) : undefined;
56247
57093
  }
56248
- get panelProps() {
56249
- const state = this.computeState(this.componentTag, this.initialPanelProps);
57094
+ get totalPanelSize() {
57095
+ return (this.mainPanel?.size || 0) + (this.secondaryPanel?.size ?? 0);
57096
+ }
57097
+ getPanelProps(panelInfo) {
57098
+ const state = this.computeState(panelInfo);
56250
57099
  if (state.isOpen) {
56251
57100
  return state.props ?? {};
56252
57101
  }
56253
57102
  return {};
56254
57103
  }
56255
- get panelKey() {
56256
- const state = this.computeState(this.componentTag, this.initialPanelProps);
57104
+ getPanelKey(panelInfo) {
57105
+ const state = this.computeState(panelInfo);
56257
57106
  if (state.isOpen) {
56258
57107
  return state.key;
56259
57108
  }
56260
57109
  return undefined;
56261
57110
  }
56262
- open(componentTag, panelProps = {}) {
57111
+ open(componentTag, initialPanelProps = {}) {
56263
57112
  if (this.screenWidthStore.isSmall) {
56264
57113
  return;
56265
57114
  }
56266
- const state = this.computeState(componentTag, panelProps);
57115
+ const newPanelInfo = { initialPanelProps, componentTag, size: DEFAULT_SIDE_PANEL_SIZE };
57116
+ const state = this.computeState(newPanelInfo);
56267
57117
  if (!state.isOpen) {
56268
57118
  return;
56269
57119
  }
56270
- if (this.isOpen && componentTag !== this.componentTag) {
56271
- this.initialPanelProps?.onCloseSidePanel?.();
57120
+ const mainPanelKey = this.mainPanel ? this.getPanelKey(this.mainPanel) : undefined;
57121
+ if (!this.mainPanel || !this.mainPanel.isPinned || mainPanelKey === state.key) {
57122
+ this._openPanel("mainPanel", newPanelInfo, state);
57123
+ return;
57124
+ }
57125
+ // Try to open secondary panel if main panel is pinned
57126
+ const nonCollapsedPanelSize = this.mainPanel.isCollapsed
57127
+ ? DEFAULT_SIDE_PANEL_SIZE
57128
+ : this.mainPanel.size;
57129
+ if (!this.secondaryPanel &&
57130
+ nonCollapsedPanelSize + DEFAULT_SIDE_PANEL_SIZE > this.availableWidth) {
57131
+ this.get(NotificationStore).notifyUser({
57132
+ sticky: false,
57133
+ type: "warning",
57134
+ text: _t("The window is too small to display multiple side panels."),
57135
+ });
57136
+ return;
57137
+ }
57138
+ this._openPanel("secondaryPanel", newPanelInfo, state);
57139
+ }
57140
+ _openPanel(panel, newPanel, state) {
57141
+ const currentPanel = this[panel];
57142
+ if (currentPanel && newPanel.componentTag !== currentPanel.componentTag) {
57143
+ currentPanel.initialPanelProps?.onCloseSidePanel?.();
57144
+ }
57145
+ this[panel] = {
57146
+ initialPanelProps: state.props ?? {},
57147
+ componentTag: newPanel.componentTag,
57148
+ size: currentPanel?.size || DEFAULT_SIDE_PANEL_SIZE,
57149
+ isCollapsed: currentPanel?.isCollapsed || false,
57150
+ isPinned: currentPanel && "isPinned" in currentPanel ? currentPanel.isPinned : false,
57151
+ };
57152
+ if (this[panel].isCollapsed) {
57153
+ this.toggleCollapsePanel(panel);
56272
57154
  }
56273
- this.componentTag = componentTag;
56274
- this.initialPanelProps = state.props ?? {};
56275
57155
  }
56276
57156
  toggle(componentTag, panelProps) {
56277
- if (this.isOpen && componentTag === this.componentTag) {
57157
+ const panel = this.mainPanel?.isPinned ? this.secondaryPanel : this.mainPanel;
57158
+ if (panel && componentTag === panel.componentTag) {
56278
57159
  this.close();
56279
57160
  }
56280
57161
  else {
@@ -56282,34 +57163,85 @@ stores.inject(MyMetaStore, storeInstance);
56282
57163
  }
56283
57164
  }
56284
57165
  close() {
56285
- this.initialPanelProps.onCloseSidePanel?.();
56286
- this.initialPanelProps = {};
56287
- this.componentTag = "";
57166
+ if (this.mainPanel?.isPinned) {
57167
+ if (this.secondaryPanel) {
57168
+ this.secondaryPanel.initialPanelProps.onCloseSidePanel?.();
57169
+ this.secondaryPanel = undefined;
57170
+ }
57171
+ return;
57172
+ }
57173
+ this.mainPanel?.initialPanelProps.onCloseSidePanel?.();
57174
+ this.mainPanel = undefined;
57175
+ }
57176
+ closeMainPanel() {
57177
+ this.mainPanel?.initialPanelProps.onCloseSidePanel?.();
57178
+ this.mainPanel = this.secondaryPanel || undefined;
57179
+ this.secondaryPanel = undefined;
56288
57180
  }
56289
- changePanelSize(size, spreadsheetElWidth) {
56290
- if (size < DEFAULT_SIDE_PANEL_SIZE) {
56291
- this.panelSize = DEFAULT_SIDE_PANEL_SIZE;
57181
+ changePanelSize(panel, size) {
57182
+ const panelInfo = this[panel];
57183
+ if (!panelInfo || ("isCollapsed" in panelInfo && panelInfo.isCollapsed)) {
57184
+ return;
56292
57185
  }
56293
- else if (size > spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH) {
56294
- this.panelSize = Math.max(spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH, DEFAULT_SIDE_PANEL_SIZE);
57186
+ size = Math.max(size, DEFAULT_SIDE_PANEL_SIZE);
57187
+ let otherPanelSize = panel === "mainPanel" ? this.secondaryPanel?.size || 0 : this.mainPanel?.size || 0;
57188
+ if (size > this.availableWidth - otherPanelSize) {
57189
+ if (panel === "mainPanel" && this.secondaryPanel) {
57190
+ // reduce the secondary panel size to fit the main panel
57191
+ this.secondaryPanel.size = Math.max(this.availableWidth - size, DEFAULT_SIDE_PANEL_SIZE);
57192
+ otherPanelSize = this.secondaryPanel.size;
57193
+ }
57194
+ size = Math.max(this.availableWidth - otherPanelSize, DEFAULT_SIDE_PANEL_SIZE);
56295
57195
  }
56296
- else {
56297
- this.panelSize = size;
57196
+ panelInfo.size = size;
57197
+ }
57198
+ resetPanelSize(panel) {
57199
+ const panelInfo = this[panel];
57200
+ if (!panelInfo) {
57201
+ return;
56298
57202
  }
57203
+ panelInfo.size = DEFAULT_SIDE_PANEL_SIZE;
56299
57204
  }
56300
- resetPanelSize() {
56301
- this.panelSize = DEFAULT_SIDE_PANEL_SIZE;
57205
+ togglePinPanel() {
57206
+ if (!this.mainPanel) {
57207
+ return;
57208
+ }
57209
+ this.mainPanel.isPinned = !this.mainPanel.isPinned;
57210
+ if (!this.mainPanel.isPinned && this.secondaryPanel) {
57211
+ this.secondaryPanel?.initialPanelProps.onCloseSidePanel?.();
57212
+ this.mainPanel = this.secondaryPanel;
57213
+ this.secondaryPanel = undefined;
57214
+ }
56302
57215
  }
56303
- computeState(componentTag, panelProps) {
56304
- const customComputeState = sidePanelRegistry.get(componentTag).computeState;
56305
- if (!customComputeState) {
56306
- return {
56307
- isOpen: true,
56308
- props: panelProps,
56309
- };
57216
+ toggleCollapsePanel(panel) {
57217
+ const panelInfo = this[panel];
57218
+ if (!panelInfo) {
57219
+ return;
57220
+ }
57221
+ if (panelInfo.isCollapsed) {
57222
+ panelInfo.isCollapsed = false;
57223
+ this.changePanelSize(panel, DEFAULT_SIDE_PANEL_SIZE);
56310
57224
  }
56311
57225
  else {
56312
- return customComputeState(this.getters, panelProps);
57226
+ panelInfo.isCollapsed = true;
57227
+ panelInfo.size = COLLAPSED_SIDE_PANEL_SIZE;
57228
+ }
57229
+ }
57230
+ computeState({ componentTag, initialPanelProps }) {
57231
+ const customComputeState = sidePanelRegistry.get(componentTag).computeState;
57232
+ const state = customComputeState
57233
+ ? customComputeState(this.getters, initialPanelProps)
57234
+ : { isOpen: true, props: initialPanelProps };
57235
+ return state.isOpen ? { ...state, key: state.key || componentTag } : state;
57236
+ }
57237
+ changeSpreadsheetWidth(width) {
57238
+ this.availableWidth = width - MIN_SHEET_VIEW_WIDTH;
57239
+ if (this.secondaryPanel && width - this.totalPanelSize < MIN_SHEET_VIEW_WIDTH) {
57240
+ this.secondaryPanel?.initialPanelProps.onCloseSidePanel?.();
57241
+ this.secondaryPanel = undefined;
57242
+ }
57243
+ if (this.mainPanel && width - this.totalPanelSize < MIN_SHEET_VIEW_WIDTH) {
57244
+ this.mainPanel.size = Math.max(width - MIN_SHEET_VIEW_WIDTH, DEFAULT_SIDE_PANEL_SIZE);
56313
57245
  }
56314
57246
  }
56315
57247
  }
@@ -56457,11 +57389,11 @@ stores.inject(MyMetaStore, storeInstance);
56457
57389
  this.hoveredCell.clear();
56458
57390
  });
56459
57391
  this.cellPopovers = useStore(CellPopoverStore);
56460
- owl.useEffect(() => {
56461
- if (!this.sidePanel.isOpen) {
57392
+ owl.useEffect((isMainPanelOpen, isSecondaryPanelOpen) => {
57393
+ if (!isMainPanelOpen && !isSecondaryPanelOpen) {
56462
57394
  this.DOMFocusableElementStore.focus();
56463
57395
  }
56464
- }, () => [this.sidePanel.isOpen]);
57396
+ }, () => [this.sidePanel.isMainPanelOpen, this.sidePanel.isSecondaryPanelOpen]);
56465
57397
  useTouchScroll(this.gridRef, this.moveCanvas.bind(this), () => {
56466
57398
  const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
56467
57399
  return scrollY > 0;
@@ -59543,7 +60475,7 @@ stores.inject(MyMetaStore, storeInstance);
59543
60475
  if (!rule)
59544
60476
  return false;
59545
60477
  return ((rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
59546
- rule.criterion.displayStyle === "arrow");
60478
+ (rule.criterion.displayStyle === "arrow" || rule.criterion.displayStyle === "chip"));
59547
60479
  }
59548
60480
  addDataValidationRule(sheetId, newRule) {
59549
60481
  const rules = this.rules[sheetId];
@@ -59553,7 +60485,7 @@ stores.inject(MyMetaStore, storeInstance);
59553
60485
  else if (newRule.criterion.type === "isValueInList") {
59554
60486
  newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
59555
60487
  }
59556
- const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
60488
+ const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules, newRule.id);
59557
60489
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
59558
60490
  if (ruleIndex !== -1) {
59559
60491
  adaptedRules[ruleIndex] = newRule;
@@ -59563,9 +60495,12 @@ stores.inject(MyMetaStore, storeInstance);
59563
60495
  this.history.update("rules", sheetId, [...adaptedRules, newRule]);
59564
60496
  }
59565
60497
  }
59566
- removeRangesFromRules(sheetId, ranges, rules) {
60498
+ removeRangesFromRules(sheetId, ranges, rules, editingRuleId) {
59567
60499
  rules = deepCopy(rules);
59568
60500
  for (const rule of rules) {
60501
+ if (rule.id === editingRuleId) {
60502
+ continue; // Skip the rule being edited to preserve its place in the list
60503
+ }
59569
60504
  rule.ranges = this.getters.recomputeRanges(rule.ranges, ranges);
59570
60505
  }
59571
60506
  return rules.filter((rule) => rule.ranges.length > 0);
@@ -62973,7 +63908,7 @@ stores.inject(MyMetaStore, storeInstance);
62973
63908
  break;
62974
63909
  }
62975
63910
  case "UPDATE_PIVOT": {
62976
- this.history.update("pivots", cmd.pivotId, "definition", this.repairSortedColumn(deepCopy(cmd.pivot)));
63911
+ this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
62977
63912
  this.compileCalculatedMeasures(cmd.pivot.measures);
62978
63913
  break;
62979
63914
  }
@@ -63044,10 +63979,7 @@ stores.inject(MyMetaStore, storeInstance);
63044
63979
  // Private
63045
63980
  // -------------------------------------------------------------------------
63046
63981
  addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {
63047
- this.history.update("pivots", pivotId, {
63048
- definition: this.repairSortedColumn(deepCopy(pivot)),
63049
- formulaId,
63050
- });
63982
+ this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
63051
63983
  this.compileCalculatedMeasures(pivot.measures);
63052
63984
  this.history.update("formulaIds", formulaId, pivotId);
63053
63985
  this.history.update("nextFormulaId", this.nextFormulaId + 1);
@@ -63136,7 +64068,6 @@ stores.inject(MyMetaStore, storeInstance);
63136
64068
  }
63137
64069
  }
63138
64070
  checkSortedColumnInMeasures(definition) {
63139
- definition = this.repairSortedColumn(definition);
63140
64071
  const measures = definition.measures.map((measure) => measure.id);
63141
64072
  if (definition.sortedColumn && !measures.includes(definition.sortedColumn.measure)) {
63142
64073
  return "InvalidDefinition" /* CommandResult.InvalidDefinition */;
@@ -63150,26 +64081,6 @@ stores.inject(MyMetaStore, storeInstance);
63150
64081
  }
63151
64082
  return "Success" /* CommandResult.Success */;
63152
64083
  }
63153
- repairSortedColumn(definition) {
63154
- if (definition.sortedColumn) {
63155
- // Fix for an upgrade issue: the sortedColumn measure was not updated
63156
- // from using fieldName to using id. If the sortedColumn measure matches
63157
- // a measure fieldName in the definition, update it to use the measure's id instead
63158
- // of its fieldName.
63159
- // TODO: add an upgrade step to fix this in master and remove this code
63160
- const sortedMeasure = definition.measures.find((measure) => measure.fieldName === definition.sortedColumn?.measure);
63161
- if (sortedMeasure) {
63162
- return {
63163
- ...definition,
63164
- sortedColumn: {
63165
- ...definition.sortedColumn,
63166
- measure: sortedMeasure.id,
63167
- },
63168
- };
63169
- }
63170
- }
63171
- return definition;
63172
- }
63173
64084
  // ---------------------------------------------------------------------
63174
64085
  // Import/Export
63175
64086
  // ---------------------------------------------------------------------
@@ -63251,9 +64162,7 @@ stores.inject(MyMetaStore, storeInstance);
63251
64162
  this.locale = data.settings?.locale ?? DEFAULT_LOCALE;
63252
64163
  }
63253
64164
  export(data) {
63254
- data.settings = {
63255
- locale: this.locale,
63256
- };
64165
+ data.settings = { locale: this.locale };
63257
64166
  }
63258
64167
  }
63259
64168
 
@@ -65909,7 +66818,10 @@ stores.inject(MyMetaStore, storeInstance);
65909
66818
  "getDataValidationInvalidCriterionValueMessage",
65910
66819
  "getInvalidDataValidationMessage",
65911
66820
  "getValidationResultForCellValue",
66821
+ "getDataValidationRangeValues",
65912
66822
  "isCellValidCheckbox",
66823
+ "getDataValidationCellStyle",
66824
+ "getDataValidationChipStyle",
65913
66825
  "isDataValidationInvalid",
65914
66826
  ];
65915
66827
  validationResults = {};
@@ -65930,6 +66842,18 @@ stores.inject(MyMetaStore, storeInstance);
65930
66842
  isDataValidationInvalid(cellPosition) {
65931
66843
  return !this.getValidationResultForCell(cellPosition).isValid;
65932
66844
  }
66845
+ getDataValidationCellStyle(position) {
66846
+ if (this.hasChip(position)) {
66847
+ return undefined; // The style is not applied on the cell if it's a chip
66848
+ }
66849
+ return this.getDataValidationStyle(position);
66850
+ }
66851
+ getDataValidationChipStyle(position) {
66852
+ if (this.hasChip(position)) {
66853
+ return this.getDataValidationStyle(position) ?? { fillColor: GRAY_200 };
66854
+ }
66855
+ return undefined;
66856
+ }
65933
66857
  getInvalidDataValidationMessage(cellPosition) {
65934
66858
  const validationResult = this.getValidationResultForCell(cellPosition);
65935
66859
  return validationResult.isValid ? undefined : validationResult.error;
@@ -65952,6 +66876,11 @@ stores.inject(MyMetaStore, storeInstance);
65952
66876
  }
65953
66877
  return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;
65954
66878
  }
66879
+ getDataValidationRangeValues(sheetId, criterion) {
66880
+ const range = this.getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
66881
+ const criterionValues = this.getters.getRangeValues(range);
66882
+ return criterionValues.map((value) => value?.toString()).filter(isDefined);
66883
+ }
65955
66884
  isCellValidCheckbox(cellPosition) {
65956
66885
  if (!this.getters.isMainCellPosition(cellPosition)) {
65957
66886
  return false;
@@ -65971,6 +66900,38 @@ stores.inject(MyMetaStore, storeInstance);
65971
66900
  const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
65972
66901
  return error ? { error, rule, isValid: false } : VALID_RESULT;
65973
66902
  }
66903
+ hasChip(position) {
66904
+ const rule = this.getters.getValidationRuleForCell(position);
66905
+ return ((rule?.criterion.type === "isValueInList" || rule?.criterion.type === "isValueInRange") &&
66906
+ rule.criterion.displayStyle === "chip");
66907
+ }
66908
+ getDataValidationStyle(position) {
66909
+ const rule = this.getters.getValidationRuleForCell(position);
66910
+ if (!rule || this.isDataValidationInvalid(position)) {
66911
+ return undefined;
66912
+ }
66913
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
66914
+ const color = this.getValueColor(rule, evaluatedCell.value);
66915
+ if (!color) {
66916
+ return undefined;
66917
+ }
66918
+ const style = {
66919
+ fillColor: color,
66920
+ textColor: chipTextColor(color),
66921
+ };
66922
+ return style;
66923
+ }
66924
+ getValueColor(rule, value) {
66925
+ if (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange") {
66926
+ return undefined;
66927
+ }
66928
+ for (const criterionValue in rule.criterion.colors) {
66929
+ if (criterionValue.toLowerCase() === String(value).toLowerCase()) {
66930
+ return rule.criterion.colors[criterionValue];
66931
+ }
66932
+ }
66933
+ return undefined;
66934
+ }
65974
66935
  isValidFormula(value) {
65975
66936
  return !compile(value).isBadExpression;
65976
66937
  }
@@ -66067,12 +67028,35 @@ stores.inject(MyMetaStore, storeInstance);
66067
67028
  }
66068
67029
  return undefined;
66069
67030
  });
67031
+ iconsOnCellRegistry.add("data_validation_chip_icon", (getters, position) => {
67032
+ const chipStyle = getters.getDataValidationChipStyle(position);
67033
+ if (chipStyle) {
67034
+ const cellStyle = getters.getCellComputedStyle(position);
67035
+ return {
67036
+ svg: getChipSvg(chipStyle),
67037
+ hoverSvg: getHoveredChipSvg(chipStyle),
67038
+ priority: 10,
67039
+ horizontalAlign: "right",
67040
+ size: computeTextFontSizeInPixels(cellStyle),
67041
+ margin: 4,
67042
+ position,
67043
+ onClick: (position, env) => {
67044
+ const { col, row } = position;
67045
+ env.model.selection.selectCell(col, row);
67046
+ env.startCellEdition();
67047
+ },
67048
+ type: "data_validation_chip_icon",
67049
+ };
67050
+ }
67051
+ return undefined;
67052
+ });
66070
67053
  iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
66071
67054
  const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
66072
67055
  if (hasIcon) {
67056
+ const cellStyle = getters.getCellComputedStyle(position);
66073
67057
  return {
66074
- svg: CARET_DOWN,
66075
- hoverSvg: HOVERED_CARET_DOWN,
67058
+ svg: getCaretDownSvg(cellStyle),
67059
+ hoverSvg: getHoveredCaretDownSvg(cellStyle),
66076
67060
  priority: 2,
66077
67061
  horizontalAlign: "right",
66078
67062
  size: GRID_ICON_EDGE_LENGTH,
@@ -66213,11 +67197,8 @@ stores.inject(MyMetaStore, storeInstance);
66213
67197
  }
66214
67198
  return this.cellIconsCache[position.sheetId][position.col][position.row];
66215
67199
  }
66216
- getCellIconRect(icon) {
67200
+ getCellIconRect(icon, cellRect) {
66217
67201
  const cellPosition = icon.position;
66218
- const merge = this.getters.getMerge(cellPosition);
66219
- const zone = merge || positionToZone(cellPosition);
66220
- const cellRect = this.getters.getRect(zone);
66221
67202
  const cell = this.getters.getCell(cellPosition);
66222
67203
  const x = this.getIconHorizontalPosition(cellRect, icon.horizontalAlign, icon);
66223
67204
  const y = this.getters.computeTextYCoordinate(cellRect, icon.size, cell?.style?.verticalAlign);
@@ -68614,11 +69595,11 @@ stores.inject(MyMetaStore, storeInstance);
68614
69595
  * transformation function given
68615
69596
  */
68616
69597
  addTransformation(executed, toTransforms, fn) {
69598
+ if (!this.content[executed]) {
69599
+ this.content[executed] = new Map();
69600
+ }
68617
69601
  for (const toTransform of toTransforms) {
68618
- if (!this.content[toTransform]) {
68619
- this.content[toTransform] = new Map();
68620
- }
68621
- this.content[toTransform].set(executed, fn);
69602
+ this.content[executed].set(toTransform, fn);
68622
69603
  }
68623
69604
  return this;
68624
69605
  }
@@ -68627,7 +69608,7 @@ stores.inject(MyMetaStore, storeInstance);
68627
69608
  * that the executed command happened.
68628
69609
  */
68629
69610
  getTransformation(toTransform, executed) {
68630
- return this.content[toTransform] && this.content[toTransform].get(executed);
69611
+ return this.content[executed] && this.content[executed].get(toTransform);
68631
69612
  }
68632
69613
  }
68633
69614
  const otRegistry = new OTRegistry();
@@ -68957,10 +69938,20 @@ stores.inject(MyMetaStore, storeInstance);
68957
69938
  */
68958
69939
  function transformAll(toTransform, executed) {
68959
69940
  let transformedCommands = [...toTransform];
69941
+ const possibleTransformations = new Set(otRegistry.getKeys());
68960
69942
  for (const executedCommand of executed) {
68961
- transformedCommands = transformedCommands
68962
- .map((cmd) => transform(cmd, executedCommand))
68963
- .filter(isDefined);
69943
+ // If the executed command is not in the registry, we skip it
69944
+ // because we know there won't be any transformation impacting the
69945
+ // commands to transform.
69946
+ if (possibleTransformations.has(executedCommand.type)) {
69947
+ transformedCommands = transformedCommands.reduce((acc, cmd) => {
69948
+ const transformed = transform(cmd, executedCommand);
69949
+ if (transformed) {
69950
+ acc.push(transformed);
69951
+ }
69952
+ return acc;
69953
+ }, []);
69954
+ }
68964
69955
  }
68965
69956
  return transformedCommands;
68966
69957
  }
@@ -70517,6 +71508,9 @@ stores.inject(MyMetaStore, storeInstance);
70517
71508
  for (const icon of this.getters.getCellIcons(position)) {
70518
71509
  contentWidth += icon.margin + icon.size;
70519
71510
  }
71511
+ if (this.getters.getDataValidationChipStyle(position)) {
71512
+ contentWidth += DATA_VALIDATION_CHIP_MARGIN * 2;
71513
+ }
70520
71514
  if (contentWidth === 0) {
70521
71515
  return 0;
70522
71516
  }
@@ -70674,7 +71668,7 @@ stores.inject(MyMetaStore, storeInstance);
70674
71668
  }
70675
71669
  const position = this.getters.getCellPosition(cell.id);
70676
71670
  const colSize = this.getters.getColSize(sheetId, position.col);
70677
- if (cell.isFormula) {
71671
+ if (cell.isFormula || this.getters.getArrayFormulaSpreadingOn(position)) {
70678
71672
  const content = this.getters.getEvaluatedCell(position).formattedValue;
70679
71673
  const evaluatedSize = getCellContentHeight(this.ctx, content, cell?.style, colSize);
70680
71674
  if (evaluatedSize > evaluatedRowSize && evaluatedSize > DEFAULT_CELL_HEIGHT) {
@@ -70877,6 +71871,8 @@ stores.inject(MyMetaStore, storeInstance);
70877
71871
  if (invalidateEvaluationCommands.has(cmd.type) ||
70878
71872
  cmd.type === "UPDATE_CELL" ||
70879
71873
  cmd.type === "SET_FORMATTING" ||
71874
+ cmd.type === "ADD_DATA_VALIDATION_RULE" ||
71875
+ cmd.type === "REMOVE_DATA_VALIDATION_RULE" ||
70880
71876
  cmd.type === "EVALUATE_CELLS") {
70881
71877
  this.styles = {};
70882
71878
  this.borders = {};
@@ -70948,8 +71944,10 @@ stores.inject(MyMetaStore, storeInstance);
70948
71944
  const cell = this.getters.getCell(position);
70949
71945
  const cfStyle = this.getters.getCellConditionalFormatStyle(position);
70950
71946
  const tableStyle = this.getters.getCellTableStyle(position);
71947
+ const dataValidationStyle = this.getters.getDataValidationCellStyle(position);
70951
71948
  const computedStyle = {
70952
71949
  ...removeFalsyAttributes(tableStyle),
71950
+ ...removeFalsyAttributes(dataValidationStyle),
70953
71951
  ...removeFalsyAttributes(cell?.style),
70954
71952
  ...removeFalsyAttributes(cfStyle),
70955
71953
  };
@@ -72033,49 +73031,17 @@ stores.inject(MyMetaStore, storeInstance);
72033
73031
  if (!copiedData) {
72034
73032
  return;
72035
73033
  }
72036
- let zone = undefined;
72037
- const selectedZones = [];
72038
73034
  const sheetId = this.getters.getActiveSheetId();
72039
- const target = {
72040
- sheetId,
72041
- zones,
72042
- };
72043
73035
  const handlers = this.selectClipboardHandlers(copiedData);
72044
- for (const { handlerName, handler } of handlers) {
72045
- const handlerData = copiedData[handlerName];
72046
- if (!handlerData) {
72047
- continue;
72048
- }
72049
- const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);
72050
- if (currentTarget.figureId) {
72051
- target.figureId = currentTarget.figureId;
72052
- }
72053
- for (const targetZone of currentTarget.zones) {
72054
- selectedZones.push(targetZone);
72055
- if (zone === undefined) {
72056
- zone = targetZone;
72057
- continue;
72058
- }
72059
- zone = union(zone, targetZone);
72060
- }
72061
- }
73036
+ const { target, zone, selectedZones } = getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options);
72062
73037
  if (zone !== undefined) {
72063
- this.addMissingDimensions(this.getters.getActiveSheetId(), zone.right - zone.left + 1, zone.bottom - zone.top + 1, zone.left, zone.top);
73038
+ this.addMissingDimensions(sheetId, zone.right - zone.left + 1, zone.bottom - zone.top + 1, zone.left, zone.top);
72064
73039
  }
72065
- handlers.forEach(({ handlerName, handler }) => {
72066
- const handlerData = copiedData[handlerName];
72067
- if (handlerData) {
72068
- handler.paste(target, handlerData, options);
72069
- }
72070
- });
73040
+ applyClipboardHandlersPaste(handlers, copiedData, target, options);
72071
73041
  if (!options?.selectTarget) {
72072
73042
  return;
72073
73043
  }
72074
- const selection = zones[0];
72075
- const col = selection.left;
72076
- const row = selection.top;
72077
- this.selection.getBackToDefault();
72078
- this.selection.selectZone({ cell: { col, row }, zone: union(...selectedZones) }, { scrollIntoView: false });
73044
+ selectPastedZone(this.selection, zones, selectedZones);
72079
73045
  }
72080
73046
  /**
72081
73047
  * Add columns and/or rows to ensure that col + width and row + height are still
@@ -74511,19 +75477,29 @@ stores.inject(MyMetaStore, storeInstance);
74511
75477
  (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange")) {
74512
75478
  return [];
74513
75479
  }
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 }));
75480
+ const sheetId = this.composer.currentEditedCell.sheetId;
75481
+ const values = rule.criterion.type === "isValueInRange"
75482
+ ? Array.from(new Set(this.getters.getDataValidationRangeValues(sheetId, rule.criterion)))
75483
+ : rule.criterion.values;
75484
+ const isChip = rule.criterion.displayStyle === "chip";
75485
+ if (!isChip) {
75486
+ return values.map((value) => ({ text: value }));
75487
+ }
75488
+ const colors = rule.criterion.colors;
75489
+ return values.map((value) => {
75490
+ const color = colors?.[value];
75491
+ return {
75492
+ text: value,
75493
+ htmlContent: [
75494
+ {
75495
+ value,
75496
+ color: color ? chipTextColor(color) : undefined,
75497
+ backgroundColor: color || GRAY_200,
75498
+ classes: ["badge rounded-pill fs-6 fw-normal w-100 mt-1 text-start"],
75499
+ },
75500
+ ],
75501
+ };
75502
+ });
74527
75503
  },
74528
75504
  selectProposal(tokenAtCursor, value) {
74529
75505
  this.composer.setCurrentContent(value);
@@ -74894,6 +75870,17 @@ stores.inject(MyMetaStore, storeInstance);
74894
75870
  return !!getters.getEvaluatedCell(position).link;
74895
75871
  },
74896
75872
  execute: (position, env, isMiddleClick) => openLink(env.model.getters.getEvaluatedCell(position).link, env, isMiddleClick),
75873
+ title: (position, getters) => {
75874
+ const link = getters.getEvaluatedCell(position).link;
75875
+ if (!link)
75876
+ return "";
75877
+ if (link.isExternal) {
75878
+ return _t("Go to url: %(url)s", { url: link.url });
75879
+ }
75880
+ else {
75881
+ return _t("Go to %(label)s", { label: link.label });
75882
+ }
75883
+ },
74897
75884
  sequence: 5,
74898
75885
  });
74899
75886
 
@@ -76605,12 +77592,13 @@ stores.inject(MyMetaStore, storeInstance);
76605
77592
  if (!item) {
76606
77593
  continue;
76607
77594
  }
77595
+ const title = typeof item.title === "function" ? item.title(position, getters) : item.title;
76608
77596
  const zone = getters.expandZone(sheetId, positionToZone(position));
76609
77597
  cells.push({
76610
77598
  coordinates: getters.getVisibleRect(zone),
76611
77599
  position,
76612
77600
  action: item.execute,
76613
- title: item.title || "",
77601
+ title: title || "",
76614
77602
  });
76615
77603
  }
76616
77604
  return cells;
@@ -76995,24 +77983,36 @@ stores.inject(MyMetaStore, storeInstance);
76995
77983
  user-select: none;
76996
77984
  color: ${TEXT_BODY};
76997
77985
 
77986
+ &.collapsed {
77987
+ padding: 8px;
77988
+ cursor: pointer;
77989
+
77990
+ .o-sidePanelTitle {
77991
+ writing-mode: vertical-rl;
77992
+ text-orientation: mixed;
77993
+ }
77994
+ }
77995
+
76998
77996
  .o-sidePanelTitle {
76999
77997
  line-height: 20px;
77000
77998
  font-size: 16px;
77001
77999
  }
77002
78000
 
77003
78001
  .o-sidePanelHeader {
77004
- padding: 8px 16px;
77005
- display: flex;
77006
- align-items: center;
77007
- justify-content: space-between;
78002
+ padding: 8px;
77008
78003
  border-bottom: 1px solid ${GRAY_300};
78004
+ }
77009
78005
 
77010
- .o-sidePanelClose {
77011
- padding: 5px 10px;
77012
- cursor: pointer;
77013
- &:hover {
77014
- background-color: WhiteSmoke;
77015
- }
78006
+ .o-sidePanelAction {
78007
+ padding: 5px 10px;
78008
+ cursor: pointer;
78009
+
78010
+ &.active {
78011
+ background-color: ${BUTTON_ACTIVE_BG};
78012
+ }
78013
+
78014
+ &:hover {
78015
+ background-color: ${BUTTON_HOVER_BG};
77016
78016
  }
77017
78017
  }
77018
78018
  .o-sidePanelBody-container {
@@ -77089,43 +78089,114 @@ stores.inject(MyMetaStore, storeInstance);
77089
78089
  `;
77090
78090
  class SidePanel extends owl.Component {
77091
78091
  static template = "o-spreadsheet-SidePanel";
78092
+ static props = {
78093
+ panelContent: Object,
78094
+ panelProps: Object,
78095
+ onCloseSidePanel: Function,
78096
+ onStartHandleDrag: Function,
78097
+ onResetPanelSize: Function,
78098
+ isPinned: { type: Boolean, optional: true },
78099
+ onTogglePinPanel: { type: Function, optional: true },
78100
+ onToggleCollapsePanel: { type: Function, optional: true },
78101
+ isCollapsed: { type: Boolean, optional: true },
78102
+ };
78103
+ spreadsheetRect = useSpreadsheetRect();
78104
+ getTitle() {
78105
+ const panel = this.props.panelContent;
78106
+ return typeof panel.title === "function"
78107
+ ? panel.title(this.env, this.props.panelProps)
78108
+ : panel.title;
78109
+ }
78110
+ get pinInfoMessage() {
78111
+ return _t("Pin this panel to allow to open another side panel beside it.");
78112
+ }
78113
+ }
78114
+
78115
+ class SidePanels extends owl.Component {
78116
+ static template = "o-spreadsheet-SidePanels";
77092
78117
  static props = {};
78118
+ static components = { SidePanel };
77093
78119
  sidePanelStore;
77094
78120
  spreadsheetRect = useSpreadsheetRect();
77095
78121
  setup() {
77096
78122
  this.sidePanelStore = useStore(SidePanelStore);
77097
- owl.useEffect((isOpen) => {
77098
- if (!isOpen) {
78123
+ owl.useEffect(() => {
78124
+ if (this.sidePanelStore.mainPanel && !this.sidePanelStore.isMainPanelOpen) {
78125
+ this.sidePanelStore.closeMainPanel();
78126
+ }
78127
+ if (this.sidePanelStore.secondaryPanel && !this.sidePanelStore.isSecondaryPanelOpen) {
77099
78128
  this.sidePanelStore.close();
77100
78129
  }
77101
- }, () => [this.sidePanelStore.isOpen]);
77102
- }
77103
- get panel() {
77104
- return sidePanelRegistry.get(this.sidePanelStore.componentTag);
77105
- }
77106
- close() {
77107
- this.sidePanelStore.close();
78130
+ }, () => [this.sidePanelStore.isMainPanelOpen, this.sidePanelStore.isSecondaryPanelOpen]);
77108
78131
  }
77109
- getTitle() {
77110
- const panel = this.panel;
77111
- return typeof panel.title === "function"
77112
- ? panel.title(this.env, this.sidePanelStore.panelProps)
77113
- : panel.title;
77114
- }
77115
- startHandleDrag(ev) {
78132
+ startHandleDrag(panel, ev) {
77116
78133
  const startingCursor = document.body.style.cursor;
77117
- const startSize = this.sidePanelStore.panelSize;
78134
+ const panelInfo = panel === "mainPanel" ? this.sidePanelStore.mainPanel : this.sidePanelStore.secondaryPanel;
78135
+ if (!panelInfo) {
78136
+ return;
78137
+ }
78138
+ const startSize = panelInfo.size;
77118
78139
  const startPosition = ev.clientX;
77119
78140
  const onMouseMove = (ev) => {
77120
78141
  document.body.style.cursor = "col-resize";
77121
78142
  const newSize = startSize + startPosition - ev.clientX;
77122
- this.sidePanelStore.changePanelSize(newSize, this.spreadsheetRect.width);
78143
+ this.sidePanelStore.changePanelSize(panel, newSize);
77123
78144
  };
77124
78145
  const cleanUp = () => {
77125
78146
  document.body.style.cursor = startingCursor;
77126
78147
  };
77127
78148
  startDnd(onMouseMove, cleanUp);
77128
78149
  }
78150
+ get mainPanelProps() {
78151
+ const panelProps = this.sidePanelStore.mainPanelProps;
78152
+ if (!this.sidePanelStore.mainPanel || !panelProps) {
78153
+ return undefined;
78154
+ }
78155
+ return {
78156
+ panelContent: sidePanelRegistry.get(this.sidePanelStore.mainPanel.componentTag),
78157
+ panelProps,
78158
+ onCloseSidePanel: () => this.sidePanelStore.closeMainPanel(),
78159
+ onTogglePinPanel: () => this.sidePanelStore.togglePinPanel(),
78160
+ onStartHandleDrag: (ev) => this.startHandleDrag("mainPanel", ev),
78161
+ onResetPanelSize: () => this.sidePanelStore.resetPanelSize("mainPanel"),
78162
+ isPinned: this.sidePanelStore.mainPanel?.isPinned,
78163
+ onToggleCollapsePanel: () => this.sidePanelStore.toggleCollapsePanel("mainPanel"),
78164
+ isCollapsed: this.sidePanelStore.mainPanel?.isCollapsed,
78165
+ };
78166
+ }
78167
+ get secondaryPanelProps() {
78168
+ const panelProps = this.sidePanelStore.secondaryPanelProps;
78169
+ if (!this.sidePanelStore.secondaryPanel || !panelProps) {
78170
+ return undefined;
78171
+ }
78172
+ return {
78173
+ panelContent: sidePanelRegistry.get(this.sidePanelStore.secondaryPanel.componentTag),
78174
+ panelProps,
78175
+ onCloseSidePanel: () => this.sidePanelStore.close(),
78176
+ onStartHandleDrag: (ev) => this.startHandleDrag("secondaryPanel", ev),
78177
+ onResetPanelSize: () => this.sidePanelStore.resetPanelSize("secondaryPanel"),
78178
+ onToggleCollapsePanel: () => this.sidePanelStore.toggleCollapsePanel("secondaryPanel"),
78179
+ isCollapsed: this.sidePanelStore.secondaryPanel?.isCollapsed,
78180
+ };
78181
+ }
78182
+ get panelList() {
78183
+ return [
78184
+ {
78185
+ key: this.sidePanelStore.secondaryPanelKey,
78186
+ props: this.secondaryPanelProps,
78187
+ style: this.sidePanelStore.secondaryPanel
78188
+ ? cssPropertiesToCss({ width: `${this.sidePanelStore.secondaryPanel.size}px` })
78189
+ : "",
78190
+ },
78191
+ {
78192
+ key: this.sidePanelStore.mainPanelKey,
78193
+ props: this.mainPanelProps,
78194
+ style: this.sidePanelStore.mainPanel
78195
+ ? cssPropertiesToCss({ width: `${this.sidePanelStore.mainPanel.size}px` })
78196
+ : "",
78197
+ },
78198
+ ].filter((panel) => panel.key && panel.props);
78199
+ }
77129
78200
  }
77130
78201
 
77131
78202
  class RibbonMenu extends owl.Component {
@@ -78687,7 +79758,7 @@ stores.inject(MyMetaStore, storeInstance);
78687
79758
  Grid,
78688
79759
  BottomBar,
78689
79760
  SmallBottomBar,
78690
- SidePanel,
79761
+ SidePanels,
78691
79762
  SpreadsheetDashboard,
78692
79763
  HeaderGroupContainer,
78693
79764
  FullScreenChart,
@@ -78710,7 +79781,9 @@ stores.inject(MyMetaStore, storeInstance);
78710
79781
  else {
78711
79782
  properties["grid-template-rows"] = `min-content auto min-content`;
78712
79783
  }
78713
- const columnWidth = this.sidePanel.isOpen ? `${this.sidePanel.panelSize}px` : "auto";
79784
+ const columnWidth = this.sidePanel.mainPanel
79785
+ ? `${this.sidePanel.totalPanelSize || DEFAULT_SIDE_PANEL_SIZE}px`
79786
+ : "auto";
78714
79787
  properties["grid-template-columns"] = `auto ${columnWidth}`;
78715
79788
  return cssPropertiesToCss(properties);
78716
79789
  }
@@ -78794,7 +79867,7 @@ stores.inject(MyMetaStore, storeInstance);
78794
79867
  this.checkViewportSize();
78795
79868
  });
78796
79869
  const resizeObserver = new ResizeObserver(() => {
78797
- this.sidePanel.changePanelSize(this.sidePanel.panelSize, this.spreadsheetRect.width);
79870
+ this.sidePanel.changeSpreadsheetWidth(this.spreadsheetRect.width);
78798
79871
  });
78799
79872
  }
78800
79873
  bindModelEvents() {
@@ -83391,9 +84464,9 @@ stores.inject(MyMetaStore, storeInstance);
83391
84464
  exports.tokenize = tokenize;
83392
84465
 
83393
84466
 
83394
- __info__.version = "18.4.0-alpha.8";
83395
- __info__.date = "2025-06-12T09:53:48.133Z";
83396
- __info__.hash = "9b7a8d0";
84467
+ __info__.version = "18.4.0";
84468
+ __info__.date = "2025-06-24T11:19:24.606Z";
84469
+ __info__.hash = "a5b7cad";
83397
84470
 
83398
84471
 
83399
84472
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);