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