@odoo/o-spreadsheet 18.4.0-alpha.9 → 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.9
6
- * @date 2025-06-19T18:23:22.025Z
7
- * @hash 6d4d685
5
+ * @version 18.4.0
6
+ * @date 2025-06-24T11:19:24.606Z
7
+ * @hash a5b7cad
8
8
  */
9
9
 
10
10
  'use strict';
@@ -164,6 +164,7 @@ const FROZEN_PANE_HEADER_BORDER_COLOR = "#BCBCBC";
164
164
  const FROZEN_PANE_BORDER_COLOR = "#DADFE8";
165
165
  const COMPOSER_ASSISTANT_COLOR = "#9B359B";
166
166
  const COLOR_TRANSPARENT = "#00000000";
167
+ const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414";
167
168
  const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2";
168
169
  const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175";
169
170
  const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA";
@@ -6849,19 +6850,17 @@ function getDefaultContextFont(fontSize, bold = false, italic = false) {
6849
6850
  const textWidthCache = {};
6850
6851
  function computeTextWidth(context, text, style, fontUnit = "pt") {
6851
6852
  const font = computeTextFont(style, fontUnit);
6852
- context.save();
6853
- context.font = font;
6854
- const width = computeCachedTextWidth(context, text);
6855
- context.restore();
6856
- return width;
6853
+ return computeCachedTextWidth(context, text, font);
6857
6854
  }
6858
- function computeCachedTextWidth(context, text) {
6859
- const font = context.font;
6855
+ function computeCachedTextWidth(context, text, font) {
6860
6856
  if (!textWidthCache[font]) {
6861
6857
  textWidthCache[font] = {};
6862
6858
  }
6863
6859
  if (textWidthCache[font][text] === undefined) {
6860
+ const oldFont = context.font;
6861
+ context.font = font;
6864
6862
  textWidthCache[font][text] = context.measureText(text).width;
6863
+ context.font = oldFont;
6865
6864
  }
6866
6865
  return textWidthCache[font][text];
6867
6866
  }
@@ -7026,19 +7025,19 @@ function getContextFontSize(font) {
7026
7025
  }
7027
7026
  // Inspired from https://stackoverflow.com/a/10511598
7028
7027
  function clipTextWithEllipsis(ctx, text, maxWidth) {
7029
- let width = computeCachedTextWidth(ctx, text);
7028
+ let width = computeCachedTextWidth(ctx, text, ctx.font);
7030
7029
  if (width <= maxWidth) {
7031
7030
  return text;
7032
7031
  }
7033
7032
  const ellipsis = "…";
7034
- const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis);
7033
+ const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis, ctx.font);
7035
7034
  if (width <= ellipsisWidth) {
7036
7035
  return text;
7037
7036
  }
7038
7037
  let len = text.length;
7039
7038
  while (width >= maxWidth - ellipsisWidth && len-- > 0) {
7040
7039
  text = text.substring(0, len);
7041
- width = computeCachedTextWidth(ctx, text);
7040
+ width = computeCachedTextWidth(ctx, text, ctx.font);
7042
7041
  }
7043
7042
  return text + ellipsis;
7044
7043
  }
@@ -7252,6 +7251,63 @@ function parseOSClipboardContent(content, clipboardId) {
7252
7251
  };
7253
7252
  return osClipboardContent;
7254
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
+ };
7255
7311
 
7256
7312
  class ClipboardHandler {
7257
7313
  getters;
@@ -9956,8 +10012,15 @@ function getDependencyContainer(env) {
9956
10012
  const ModelStore = createAbstractStore("Model");
9957
10013
 
9958
10014
  class RendererStore {
9959
- mutators = ["register", "unRegister", "drawLayer"];
10015
+ mutators = ["register", "unRegister", "draw", "startAnimation", "stopAnimation"];
9960
10016
  renderers = {};
10017
+ model;
10018
+ context = undefined;
10019
+ animationFrameId = null;
10020
+ registeredAnimations = new Set();
10021
+ constructor(get) {
10022
+ this.model = get(ModelStore);
10023
+ }
9961
10024
  register(renderer) {
9962
10025
  if (!renderer.renderingLayers.length) {
9963
10026
  return;
@@ -9974,17 +10037,54 @@ class RendererStore {
9974
10037
  this.renderers[layer] = this.renderers[layer].filter((r) => r !== renderer);
9975
10038
  }
9976
10039
  }
9977
- drawLayer(context, layer) {
10040
+ drawLayer(context, layer, timeStamp) {
9978
10041
  const renderers = this.renderers[layer];
9979
10042
  if (renderers) {
9980
10043
  for (const renderer of renderers) {
9981
10044
  context.ctx.save();
9982
- renderer.drawLayer(context, layer);
10045
+ renderer.drawLayer(context, layer, timeStamp);
9983
10046
  context.ctx.restore();
9984
10047
  }
9985
10048
  }
9986
10049
  return "noStateChange";
9987
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
+ }
9988
10088
  }
9989
10089
 
9990
10090
  class SpreadsheetStore extends DisposableStore {
@@ -10008,7 +10108,7 @@ class SpreadsheetStore extends DisposableStore {
10008
10108
  }
10009
10109
  handle(cmd) { }
10010
10110
  finalize() { }
10011
- drawLayer(ctx, layer) { }
10111
+ drawLayer(ctx, layer, timestamp) { }
10012
10112
  }
10013
10113
 
10014
10114
  const VOID_COMPOSER = {
@@ -23730,11 +23830,11 @@ function drawTitle(ctx, config) {
23730
23830
  function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
23731
23831
  const maxValue = runtime.maxValue;
23732
23832
  const minValue = runtime.minValue;
23733
- const gaugeValue = runtime.gaugeValue;
23833
+ const gaugeValue = getGaugeValue(runtime, "animated");
23734
23834
  const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
23735
23835
  const gaugeArcWidth = gaugeRect.width / 6;
23736
23836
  const gaugePercentage = gaugeValue
23737
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
23837
+ ? (gaugeValue - minValue.value) / (maxValue.value - minValue.value)
23738
23838
  : 0;
23739
23839
  const gaugeValuePosition = {
23740
23840
  x: boundingRect.width / 2,
@@ -23747,7 +23847,7 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
23747
23847
  }
23748
23848
  // Scale down the font size if the text is too long
23749
23849
  const maxTextWidth = gaugeRect.width / 2;
23750
- const gaugeLabel = gaugeValue?.label || "-";
23850
+ const gaugeLabel = runtime.gaugeValue?.label || "-";
23751
23851
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
23752
23852
  gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
23753
23853
  }
@@ -23887,7 +23987,7 @@ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
23887
23987
  return inflectionValues;
23888
23988
  }
23889
23989
  function getGaugeColor(runtime) {
23890
- const gaugeValue = runtime.gaugeValue?.value;
23990
+ const gaugeValue = getGaugeValue(runtime, "final");
23891
23991
  if (gaugeValue === undefined) {
23892
23992
  return GAUGE_BACKGROUND_COLOR;
23893
23993
  }
@@ -23975,6 +24075,11 @@ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY
23975
24075
  };
23976
24076
  return { bottomLeft, bottomRight, topRight, topLeft };
23977
24077
  }
24078
+ function getGaugeValue(runtime, mode) {
24079
+ return mode === "animated" && runtime.animationValue !== undefined
24080
+ ? runtime.animationValue
24081
+ : runtime.gaugeValue?.value;
24082
+ }
23978
24083
 
23979
24084
  const CHART_COMMON_OPTIONS = {
23980
24085
  // https://www.chartjs.org/docs/latest/general/responsive.html
@@ -26329,6 +26434,320 @@ function createBarChartRuntime(chart, getters) {
26329
26434
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
26330
26435
  }
26331
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;
26332
26751
  class GaugeChartComponent extends owl.Component {
26333
26752
  static template = "o-spreadsheet-GaugeChartComponent";
26334
26753
  static props = {
@@ -26336,16 +26755,101 @@ class GaugeChartComponent extends owl.Component {
26336
26755
  isFullScreen: { type: Boolean, optional: true },
26337
26756
  };
26338
26757
  canvas = owl.useRef("chartContainer");
26758
+ animationStore;
26339
26759
  get runtime() {
26340
26760
  return this.env.model.getters.getChartRuntime(this.props.figureUI.id);
26341
26761
  }
26342
26762
  setup() {
26343
- owl.useEffect(() => drawGaugeChart(this.canvas.el, this.runtime), () => {
26344
- const canvas = this.canvas.el;
26345
- 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();
26346
26788
  return [rect.width, rect.height, this.runtime, this.canvas.el, window.devicePixelRatio];
26347
26789
  });
26348
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
+ }
26349
26853
  }
26350
26854
 
26351
26855
  class ComboChart extends AbstractChart {
@@ -36538,6 +37042,7 @@ css /* scss */ `
36538
37042
  // We need here the svg of the icons that we need to convert to images for the renderer
36539
37043
  // -----------------------------------------------------------------------------
36540
37044
  const ARROW_DOWN = {
37045
+ name: "ARROW_DOWN",
36541
37046
  width: 448,
36542
37047
  height: 512,
36543
37048
  paths: [
@@ -36548,6 +37053,7 @@ const ARROW_DOWN = {
36548
37053
  ],
36549
37054
  };
36550
37055
  const ARROW_UP = {
37056
+ name: "ARROW_UP",
36551
37057
  width: 448,
36552
37058
  height: 512,
36553
37059
  paths: [
@@ -36558,6 +37064,7 @@ const ARROW_UP = {
36558
37064
  ],
36559
37065
  };
36560
37066
  const ARROW_RIGHT = {
37067
+ name: "ARROW_RIGHT",
36561
37068
  width: 448,
36562
37069
  height: 512,
36563
37070
  paths: [
@@ -36568,6 +37075,7 @@ const ARROW_RIGHT = {
36568
37075
  ],
36569
37076
  };
36570
37077
  const SMILE = {
37078
+ name: "SMILE",
36571
37079
  width: 496,
36572
37080
  height: 512,
36573
37081
  paths: [
@@ -36578,6 +37086,7 @@ const SMILE = {
36578
37086
  ],
36579
37087
  };
36580
37088
  const MEH = {
37089
+ name: "MEH",
36581
37090
  width: 496,
36582
37091
  height: 512,
36583
37092
  paths: [
@@ -36588,6 +37097,7 @@ const MEH = {
36588
37097
  ],
36589
37098
  };
36590
37099
  const FROWN = {
37100
+ name: "FROWN",
36591
37101
  width: 496,
36592
37102
  height: 512,
36593
37103
  paths: [
@@ -36599,22 +37109,26 @@ const FROWN = {
36599
37109
  };
36600
37110
  const DOT_PATH = "M256 9 a247 247 0 1 0.1 0 0";
36601
37111
  const GREEN_DOT = {
37112
+ name: "GREEN_DOT",
36602
37113
  width: 512,
36603
37114
  height: 512,
36604
37115
  paths: [{ fillColor: "#6AA84F", path: DOT_PATH }],
36605
37116
  };
36606
37117
  const YELLOW_DOT = {
37118
+ name: "YELLOW_DOT",
36607
37119
  width: 512,
36608
37120
  height: 512,
36609
37121
  paths: [{ fillColor: "#F0AD4E", path: DOT_PATH }],
36610
37122
  };
36611
37123
  const RED_DOT = {
37124
+ name: "RED_DOT",
36612
37125
  width: 512,
36613
37126
  height: 512,
36614
37127
  paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36615
37128
  };
36616
37129
  function getCaretDownSvg(color) {
36617
37130
  return {
37131
+ name: "CARET_DOWN",
36618
37132
  width: 512,
36619
37133
  height: 512,
36620
37134
  paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
@@ -36622,6 +37136,7 @@ function getCaretDownSvg(color) {
36622
37136
  }
36623
37137
  function getHoveredCaretDownSvg(color) {
36624
37138
  return {
37139
+ name: "CARET_DOWN",
36625
37140
  width: 512,
36626
37141
  height: 512,
36627
37142
  paths: [
@@ -36633,6 +37148,7 @@ function getHoveredCaretDownSvg(color) {
36633
37148
  const CHIP_CARET_DOWN_PATH = "M40 185 h270 l-135 128";
36634
37149
  function getChipSvg(chipStyle) {
36635
37150
  return {
37151
+ name: "CHIP",
36636
37152
  width: 512,
36637
37153
  height: 512,
36638
37154
  paths: [{ fillColor: chipStyle.textColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH }],
@@ -36640,6 +37156,7 @@ function getChipSvg(chipStyle) {
36640
37156
  }
36641
37157
  function getHoveredChipSvg(chipStyle) {
36642
37158
  return {
37159
+ name: "CHIP",
36643
37160
  width: 512,
36644
37161
  height: 512,
36645
37162
  paths: [
@@ -36652,16 +37169,19 @@ function getHoveredChipSvg(chipStyle) {
36652
37169
  };
36653
37170
  }
36654
37171
  const CHECKBOX_UNCHECKED = {
37172
+ name: "CHECKBOX_UNCHECKED",
36655
37173
  width: 512,
36656
37174
  height: 512,
36657
37175
  paths: [{ fillColor: GRAY_300, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36658
37176
  };
36659
37177
  const CHECKBOX_UNCHECKED_HOVERED = {
37178
+ name: "CHECKBOX_UNCHECKED",
36660
37179
  width: 512,
36661
37180
  height: 512,
36662
37181
  paths: [{ fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36663
37182
  };
36664
37183
  const CHECKBOX_CHECKED = {
37184
+ name: "CHECKBOX_CHECKED",
36665
37185
  width: 512,
36666
37186
  height: 512,
36667
37187
  paths: [
@@ -36674,6 +37194,7 @@ function getPivotIconSvg(isCollapsed, isHovered) {
36674
37194
  ? "M149,235 h213 v43 h-213 M235,149 h43 v213 h-43" // +
36675
37195
  : "M149,235 h213 v43 h-213"; // -
36676
37196
  return {
37197
+ name: "PIVOT_ICON",
36677
37198
  width: 512,
36678
37199
  height: 512,
36679
37200
  paths: [
@@ -36700,6 +37221,7 @@ function getDataFilterIcon(isActive, isHighContrast, isHovered) {
36700
37221
  colors.hoverBackgroundColor = "#fff";
36701
37222
  }
36702
37223
  return {
37224
+ name: "DATA_FILTER_ICON",
36703
37225
  width: isActive ? 24 : 850,
36704
37226
  height: isActive ? 24 : 850,
36705
37227
  paths: [
@@ -40895,6 +41417,23 @@ migrationStepRegistry
40895
41417
  }
40896
41418
  return data;
40897
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
+ },
40898
41437
  });
40899
41438
  function fixOverlappingFilters(data) {
40900
41439
  for (const sheet of data.sheets || []) {
@@ -45961,15 +46500,16 @@ function useHoveredElement(ref) {
45961
46500
  return state;
45962
46501
  }
45963
46502
 
46503
+ const PAINT_FORMAT_HANDLER_KEYS = [
46504
+ "cell",
46505
+ "border",
46506
+ "table",
46507
+ "conditionalFormat",
46508
+ "merge",
46509
+ ];
45964
46510
  class PaintFormatStore extends SpreadsheetStore {
45965
46511
  mutators = ["activate", "cancel", "pasteFormat"];
45966
46512
  highlightStore = this.get(HighlightStore);
45967
- clipboardHandlers = [
45968
- new CellClipboardHandler(this.getters, this.model.dispatch),
45969
- new BorderClipboardHandler(this.getters, this.model.dispatch),
45970
- new TableClipboardHandler(this.getters, this.model.dispatch),
45971
- new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
45972
- ];
45973
46513
  status = "inactive";
45974
46514
  copiedData;
45975
46515
  constructor(get) {
@@ -46000,24 +46540,38 @@ class PaintFormatStore extends SpreadsheetStore {
46000
46540
  get isActive() {
46001
46541
  return this.status !== "inactive";
46002
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
+ }
46003
46552
  copyFormats() {
46004
46553
  const sheetId = this.getters.getActiveSheetId();
46005
46554
  const zones = this.getters.getSelectedZones();
46006
- const copiedData = {};
46007
- for (const handler of this.clipboardHandlers) {
46008
- 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
+ }
46009
46561
  }
46010
46562
  return copiedData;
46011
46563
  }
46012
46564
  paintFormat(sheetId, target) {
46013
- if (this.copiedData) {
46014
- for (const handler of this.clipboardHandlers) {
46015
- handler.paste({ zones: target, sheetId }, this.copiedData, {
46016
- isCutOperation: false,
46017
- pasteOption: "onlyFormat",
46018
- });
46019
- }
46565
+ if (!this.copiedData) {
46566
+ return;
46020
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);
46021
46575
  if (this.status === "oneOff") {
46022
46576
  this.cancel();
46023
46577
  }
@@ -46064,12 +46618,8 @@ class HoveredTableStore extends SpreadsheetStore {
46064
46618
  this.row = undefined;
46065
46619
  }
46066
46620
  computeOverlay() {
46067
- if (!this.getters.isDashboard()) {
46068
- return;
46069
- }
46070
46621
  this.overlayColors = new PositionMap();
46071
- const col = this.col;
46072
- const row = this.row;
46622
+ const { col, row } = this;
46073
46623
  if (col === undefined || row === undefined) {
46074
46624
  return;
46075
46625
  }
@@ -46078,9 +46628,16 @@ class HoveredTableStore extends SpreadsheetStore {
46078
46628
  if (!table) {
46079
46629
  return;
46080
46630
  }
46081
- const { left, right } = table.range.zone;
46082
- for (let c = left; c <= right; c++) {
46083
- 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
+ }
46084
46641
  }
46085
46642
  }
46086
46643
  }
@@ -46328,7 +46885,10 @@ class GridOverlay extends owl.Component {
46328
46885
  }
46329
46886
  const icons = this.env.model.getters.getCellIcons(position);
46330
46887
  const icon = icons.find((icon) => {
46331
- 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));
46332
46892
  });
46333
46893
  return icon?.onClick ? icon : undefined;
46334
46894
  }
@@ -47065,19 +47625,56 @@ class HeadersOverlay extends owl.Component {
47065
47625
  }
47066
47626
  }
47067
47627
 
47068
- class GridRenderer {
47069
- getters;
47070
- renderer;
47628
+ const CELL_ANIMATION_DURATION = 200;
47629
+ class GridRenderer extends SpreadsheetStore {
47071
47630
  fingerprints;
47072
47631
  hoveredTables;
47073
47632
  hoveredIcon;
47633
+ lastRenderBoxes = new Map();
47634
+ preventNewAnimationsInNextFrame = false;
47635
+ zonesWithPreventedAnimationsInNextFrame = [];
47636
+ animations = new Map();
47074
47637
  constructor(get) {
47638
+ super(get);
47075
47639
  this.getters = get(ModelStore).getters;
47076
- this.renderer = get(RendererStore);
47077
47640
  this.fingerprints = get(FormulaFingerprintStore);
47078
47641
  this.hoveredTables = get(HoveredTableStore);
47079
47642
  this.hoveredIcon = get(HoveredIconStore);
47080
- 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
+ }
47081
47678
  }
47082
47679
  get renderingLayers() {
47083
47680
  return ["Background", "Headers"];
@@ -47085,17 +47682,20 @@ class GridRenderer {
47085
47682
  // ---------------------------------------------------------------------------
47086
47683
  // Grid rendering
47087
47684
  // ---------------------------------------------------------------------------
47088
- drawLayer(renderingContext, layer) {
47685
+ drawLayer(renderingContext, layer, timeStamp) {
47089
47686
  switch (layer) {
47090
47687
  case "Background":
47091
47688
  this.drawGlobalBackground(renderingContext);
47689
+ const oldBoxes = this.lastRenderBoxes;
47690
+ this.lastRenderBoxes = new Map();
47092
47691
  for (const { zone, rect } of this.getters.getAllActiveViewportsZonesAndRect()) {
47093
47692
  const { ctx } = renderingContext;
47094
47693
  ctx.save();
47095
47694
  ctx.beginPath();
47096
47695
  ctx.rect(rect.x, rect.y, rect.width, rect.height);
47097
47696
  ctx.clip();
47098
- const boxes = this.getGridBoxes(zone);
47697
+ const boxesWithoutAnimations = this.getGridBoxes(zone);
47698
+ const boxes = this.getBoxesWithAnimations(boxesWithoutAnimations, oldBoxes, timeStamp);
47099
47699
  this.drawBackground(renderingContext, boxes);
47100
47700
  this.drawOverflowingCellBackground(renderingContext, boxes);
47101
47701
  this.drawCellBackground(renderingContext, boxes);
@@ -47105,6 +47705,8 @@ class GridRenderer {
47105
47705
  ctx.restore();
47106
47706
  }
47107
47707
  this.drawFrozenPanes(renderingContext);
47708
+ this.preventNewAnimationsInNextFrame = false;
47709
+ this.zonesWithPreventedAnimationsInNextFrame = [];
47108
47710
  break;
47109
47711
  case "Headers":
47110
47712
  if (!this.getters.isDashboard()) {
@@ -47128,6 +47730,8 @@ class GridRenderer {
47128
47730
  const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;
47129
47731
  if (areGridLinesVisible) {
47130
47732
  for (const box of boxes) {
47733
+ if (box.skipCellGridLines)
47734
+ continue;
47131
47735
  ctx.strokeStyle = CELL_BORDER_COLOR;
47132
47736
  ctx.lineWidth = thinLineWidth;
47133
47737
  ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);
@@ -47235,7 +47839,8 @@ class GridRenderer {
47235
47839
  * each line and adding 1 pixel to the end of each line (depending on the direction of the
47236
47840
  * line).
47237
47841
  */
47238
- function drawBorder({ style, color }, x1, y1, x2, y2) {
47842
+ function drawBorder({ color, style, opacity }, x1, y1, x2, y2) {
47843
+ ctx.globalAlpha = opacity ?? 1;
47239
47844
  ctx.strokeStyle = color;
47240
47845
  switch (style) {
47241
47846
  case "medium":
@@ -47283,6 +47888,7 @@ class GridRenderer {
47283
47888
  ctx.stroke();
47284
47889
  ctx.lineWidth = 1;
47285
47890
  ctx.setLineDash([]);
47891
+ ctx.globalAlpha = 1;
47286
47892
  }
47287
47893
  }
47288
47894
  drawTexts(renderingContext, boxes) {
@@ -47291,6 +47897,7 @@ class GridRenderer {
47291
47897
  let currentFont;
47292
47898
  for (const box of boxes) {
47293
47899
  if (box.content) {
47900
+ ctx.globalAlpha = box.textOpacity ?? 1;
47294
47901
  const style = box.style || {};
47295
47902
  const align = box.content.align || "left";
47296
47903
  // compute font and textColor
@@ -47321,6 +47928,7 @@ class GridRenderer {
47321
47928
  if (box.clipRect) {
47322
47929
  ctx.restore();
47323
47930
  }
47931
+ ctx.globalAlpha = 1;
47324
47932
  }
47325
47933
  }
47326
47934
  }
@@ -47338,10 +47946,11 @@ class GridRenderer {
47338
47946
  }
47339
47947
  ctx.save();
47340
47948
  ctx.beginPath();
47341
- 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);
47342
47951
  ctx.clip();
47343
47952
  const iconSize = icon.size;
47344
- const { x, y } = this.getters.getCellIconRect(icon);
47953
+ const { x, y } = this.getters.getCellIconRect(icon, box);
47345
47954
  ctx.translate(x, y);
47346
47955
  ctx.scale(iconSize / svg.width, iconSize / svg.height);
47347
47956
  for (const path of svg.paths) {
@@ -47550,7 +48159,6 @@ class GridRenderer {
47550
48159
  const cell = this.getters.getEvaluatedCell(position);
47551
48160
  const showFormula = this.getters.shouldShowFormulas();
47552
48161
  const { x, y, width, height } = this.getters.getRect(zone);
47553
- const { verticalAlign } = this.getters.getCellStyle(position);
47554
48162
  const chipStyle = this.getters.getDataValidationChipStyle(position);
47555
48163
  let style = this.getters.getCellComputedStyle(position);
47556
48164
  if (this.fingerprints.isEnabled) {
@@ -47570,6 +48178,7 @@ class GridRenderer {
47570
48178
  center: iconsList.find((icon) => icon?.horizontalAlign === "center"),
47571
48179
  };
47572
48180
  const box = {
48181
+ id: zoneToXc(zone),
47573
48182
  x,
47574
48183
  y,
47575
48184
  width,
@@ -47577,11 +48186,11 @@ class GridRenderer {
47577
48186
  border: this.getters.getCellComputedBorder(position) || undefined,
47578
48187
  style,
47579
48188
  dataBarFill,
47580
- verticalAlign,
47581
48189
  overlayColor: this.hoveredTables.overlayColors.get(position),
47582
48190
  isError: (cell.type === CellValueType.error && !!cell.message) ||
47583
48191
  this.getters.isDataValidationInvalid(position),
47584
48192
  icons: cellIcons,
48193
+ disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame.some((z) => isZoneInside(zone, z) || overlap(zone, z)),
47585
48194
  };
47586
48195
  const fontSizePX = computeTextFontSizeInPixels(box.style);
47587
48196
  if (cell.type === CellValueType.empty || box.icons.center) {
@@ -47751,6 +48360,77 @@ class GridRenderer {
47751
48360
  }
47752
48361
  return boxes;
47753
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
+ }
47754
48434
  }
47755
48435
 
47756
48436
  function useGridDrawing(refName, model, canvasSize) {
@@ -47785,10 +48465,7 @@ function useGridDrawing(refName, model, canvasSize) {
47785
48465
  // http://diveintohtml5.info/canvas.html#pixel-madness
47786
48466
  ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT);
47787
48467
  ctx.scale(dpr, dpr);
47788
- for (const layer of OrderedLayers()) {
47789
- model.drawLayer(renderingContext, layer);
47790
- rendererStore.drawLayer(renderingContext, layer);
47791
- }
48468
+ rendererStore.draw(renderingContext);
47792
48469
  }
47793
48470
  }
47794
48471
 
@@ -54883,7 +55560,7 @@ class SpreadsheetPivot {
54883
55560
  }
54884
55561
  getTypeFromZone(sheetId, zone) {
54885
55562
  const cells = this.getters.getEvaluatedCellsInZone(sheetId, zone);
54886
- const nonEmptyCells = cells.filter((cell) => cell.type !== CellValueType.empty);
55563
+ const nonEmptyCells = cells.filter((cell) => !(cell.type === CellValueType.empty || cell.value === ""));
54887
55564
  if (nonEmptyCells.length === 0) {
54888
55565
  return "integer";
54889
55566
  }
@@ -55491,7 +56168,7 @@ css /* scss */ `
55491
56168
  `;
55492
56169
  class SettingsPanel extends owl.Component {
55493
56170
  static template = "o-spreadsheet-SettingsPanel";
55494
- static components = { Section, ValidationMessages };
56171
+ static components = { Section, ValidationMessages, BadgeSelection };
55495
56172
  static props = { onCloseSidePanel: Function };
55496
56173
  loadedLocales = [];
55497
56174
  setup() {
@@ -56375,49 +57052,111 @@ class ScreenWidthStore {
56375
57052
  }
56376
57053
 
56377
57054
  const DEFAULT_SIDE_PANEL_SIZE = 350;
57055
+ const COLLAPSED_SIDE_PANEL_SIZE = 45;
56378
57056
  const MIN_SHEET_VIEW_WIDTH = 150;
56379
57057
  class SidePanelStore extends SpreadsheetStore {
56380
- mutators = ["open", "toggle", "close", "changePanelSize", "resetPanelSize"];
56381
- initialPanelProps = {};
56382
- componentTag = "";
56383
- 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;
56384
57072
  screenWidthStore = this.get(ScreenWidthStore);
56385
- get isOpen() {
56386
- if (!this.componentTag) {
56387
- return false;
56388
- }
56389
- 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;
56390
57088
  }
56391
- get panelProps() {
56392
- 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);
56393
57100
  if (state.isOpen) {
56394
57101
  return state.props ?? {};
56395
57102
  }
56396
57103
  return {};
56397
57104
  }
56398
- get panelKey() {
56399
- const state = this.computeState(this.componentTag, this.initialPanelProps);
57105
+ getPanelKey(panelInfo) {
57106
+ const state = this.computeState(panelInfo);
56400
57107
  if (state.isOpen) {
56401
57108
  return state.key;
56402
57109
  }
56403
57110
  return undefined;
56404
57111
  }
56405
- open(componentTag, panelProps = {}) {
57112
+ open(componentTag, initialPanelProps = {}) {
56406
57113
  if (this.screenWidthStore.isSmall) {
56407
57114
  return;
56408
57115
  }
56409
- const state = this.computeState(componentTag, panelProps);
57116
+ const newPanelInfo = { initialPanelProps, componentTag, size: DEFAULT_SIDE_PANEL_SIZE };
57117
+ const state = this.computeState(newPanelInfo);
56410
57118
  if (!state.isOpen) {
56411
57119
  return;
56412
57120
  }
56413
- if (this.isOpen && componentTag !== this.componentTag) {
56414
- 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);
56415
57155
  }
56416
- this.componentTag = componentTag;
56417
- this.initialPanelProps = state.props ?? {};
56418
57156
  }
56419
57157
  toggle(componentTag, panelProps) {
56420
- if (this.isOpen && componentTag === this.componentTag) {
57158
+ const panel = this.mainPanel?.isPinned ? this.secondaryPanel : this.mainPanel;
57159
+ if (panel && componentTag === panel.componentTag) {
56421
57160
  this.close();
56422
57161
  }
56423
57162
  else {
@@ -56425,34 +57164,85 @@ class SidePanelStore extends SpreadsheetStore {
56425
57164
  }
56426
57165
  }
56427
57166
  close() {
56428
- this.initialPanelProps.onCloseSidePanel?.();
56429
- this.initialPanelProps = {};
56430
- 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;
56431
57176
  }
56432
- changePanelSize(size, spreadsheetElWidth) {
56433
- if (size < DEFAULT_SIDE_PANEL_SIZE) {
56434
- 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;
56435
57186
  }
56436
- else if (size > spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH) {
56437
- 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);
56438
57196
  }
56439
- else {
56440
- this.panelSize = size;
57197
+ panelInfo.size = size;
57198
+ }
57199
+ resetPanelSize(panel) {
57200
+ const panelInfo = this[panel];
57201
+ if (!panelInfo) {
57202
+ return;
56441
57203
  }
57204
+ panelInfo.size = DEFAULT_SIDE_PANEL_SIZE;
56442
57205
  }
56443
- resetPanelSize() {
56444
- 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
+ }
56445
57216
  }
56446
- computeState(componentTag, panelProps) {
56447
- const customComputeState = sidePanelRegistry.get(componentTag).computeState;
56448
- if (!customComputeState) {
56449
- return {
56450
- isOpen: true,
56451
- props: panelProps,
56452
- };
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);
56453
57225
  }
56454
57226
  else {
56455
- 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);
56456
57246
  }
56457
57247
  }
56458
57248
  }
@@ -56600,11 +57390,11 @@ class Grid extends owl.Component {
56600
57390
  this.hoveredCell.clear();
56601
57391
  });
56602
57392
  this.cellPopovers = useStore(CellPopoverStore);
56603
- owl.useEffect(() => {
56604
- if (!this.sidePanel.isOpen) {
57393
+ owl.useEffect((isMainPanelOpen, isSecondaryPanelOpen) => {
57394
+ if (!isMainPanelOpen && !isSecondaryPanelOpen) {
56605
57395
  this.DOMFocusableElementStore.focus();
56606
57396
  }
56607
- }, () => [this.sidePanel.isOpen]);
57397
+ }, () => [this.sidePanel.isMainPanelOpen, this.sidePanel.isSecondaryPanelOpen]);
56608
57398
  useTouchScroll(this.gridRef, this.moveCanvas.bind(this), () => {
56609
57399
  const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
56610
57400
  return scrollY > 0;
@@ -59696,7 +60486,7 @@ class DataValidationPlugin extends CorePlugin {
59696
60486
  else if (newRule.criterion.type === "isValueInList") {
59697
60487
  newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
59698
60488
  }
59699
- const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
60489
+ const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules, newRule.id);
59700
60490
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
59701
60491
  if (ruleIndex !== -1) {
59702
60492
  adaptedRules[ruleIndex] = newRule;
@@ -59706,9 +60496,12 @@ class DataValidationPlugin extends CorePlugin {
59706
60496
  this.history.update("rules", sheetId, [...adaptedRules, newRule]);
59707
60497
  }
59708
60498
  }
59709
- removeRangesFromRules(sheetId, ranges, rules) {
60499
+ removeRangesFromRules(sheetId, ranges, rules, editingRuleId) {
59710
60500
  rules = deepCopy(rules);
59711
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
+ }
59712
60505
  rule.ranges = this.getters.recomputeRanges(rule.ranges, ranges);
59713
60506
  }
59714
60507
  return rules.filter((rule) => rule.ranges.length > 0);
@@ -63116,7 +63909,7 @@ class PivotCorePlugin extends CorePlugin {
63116
63909
  break;
63117
63910
  }
63118
63911
  case "UPDATE_PIVOT": {
63119
- this.history.update("pivots", cmd.pivotId, "definition", this.repairSortedColumn(deepCopy(cmd.pivot)));
63912
+ this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
63120
63913
  this.compileCalculatedMeasures(cmd.pivot.measures);
63121
63914
  break;
63122
63915
  }
@@ -63187,10 +63980,7 @@ class PivotCorePlugin extends CorePlugin {
63187
63980
  // Private
63188
63981
  // -------------------------------------------------------------------------
63189
63982
  addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {
63190
- this.history.update("pivots", pivotId, {
63191
- definition: this.repairSortedColumn(deepCopy(pivot)),
63192
- formulaId,
63193
- });
63983
+ this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
63194
63984
  this.compileCalculatedMeasures(pivot.measures);
63195
63985
  this.history.update("formulaIds", formulaId, pivotId);
63196
63986
  this.history.update("nextFormulaId", this.nextFormulaId + 1);
@@ -63279,7 +64069,6 @@ class PivotCorePlugin extends CorePlugin {
63279
64069
  }
63280
64070
  }
63281
64071
  checkSortedColumnInMeasures(definition) {
63282
- definition = this.repairSortedColumn(definition);
63283
64072
  const measures = definition.measures.map((measure) => measure.id);
63284
64073
  if (definition.sortedColumn && !measures.includes(definition.sortedColumn.measure)) {
63285
64074
  return "InvalidDefinition" /* CommandResult.InvalidDefinition */;
@@ -63293,26 +64082,6 @@ class PivotCorePlugin extends CorePlugin {
63293
64082
  }
63294
64083
  return "Success" /* CommandResult.Success */;
63295
64084
  }
63296
- repairSortedColumn(definition) {
63297
- if (definition.sortedColumn) {
63298
- // Fix for an upgrade issue: the sortedColumn measure was not updated
63299
- // from using fieldName to using id. If the sortedColumn measure matches
63300
- // a measure fieldName in the definition, update it to use the measure's id instead
63301
- // of its fieldName.
63302
- // TODO: add an upgrade step to fix this in master and remove this code
63303
- const sortedMeasure = definition.measures.find((measure) => measure.fieldName === definition.sortedColumn?.measure);
63304
- if (sortedMeasure) {
63305
- return {
63306
- ...definition,
63307
- sortedColumn: {
63308
- ...definition.sortedColumn,
63309
- measure: sortedMeasure.id,
63310
- },
63311
- };
63312
- }
63313
- }
63314
- return definition;
63315
- }
63316
64085
  // ---------------------------------------------------------------------
63317
64086
  // Import/Export
63318
64087
  // ---------------------------------------------------------------------
@@ -63394,9 +64163,7 @@ class SettingsPlugin extends CorePlugin {
63394
64163
  this.locale = data.settings?.locale ?? DEFAULT_LOCALE;
63395
64164
  }
63396
64165
  export(data) {
63397
- data.settings = {
63398
- locale: this.locale,
63399
- };
64166
+ data.settings = { locale: this.locale };
63400
64167
  }
63401
64168
  }
63402
64169
 
@@ -66431,11 +67198,8 @@ class CellIconPlugin extends CoreViewPlugin {
66431
67198
  }
66432
67199
  return this.cellIconsCache[position.sheetId][position.col][position.row];
66433
67200
  }
66434
- getCellIconRect(icon) {
67201
+ getCellIconRect(icon, cellRect) {
66435
67202
  const cellPosition = icon.position;
66436
- const merge = this.getters.getMerge(cellPosition);
66437
- const zone = merge || positionToZone(cellPosition);
66438
- const cellRect = this.getters.getRect(zone);
66439
67203
  const cell = this.getters.getCell(cellPosition);
66440
67204
  const x = this.getIconHorizontalPosition(cellRect, icon.horizontalAlign, icon);
66441
67205
  const y = this.getters.computeTextYCoordinate(cellRect, icon.size, cell?.style?.verticalAlign);
@@ -72268,49 +73032,17 @@ class ClipboardPlugin extends UIPlugin {
72268
73032
  if (!copiedData) {
72269
73033
  return;
72270
73034
  }
72271
- let zone = undefined;
72272
- const selectedZones = [];
72273
73035
  const sheetId = this.getters.getActiveSheetId();
72274
- const target = {
72275
- sheetId,
72276
- zones,
72277
- };
72278
73036
  const handlers = this.selectClipboardHandlers(copiedData);
72279
- for (const { handlerName, handler } of handlers) {
72280
- const handlerData = copiedData[handlerName];
72281
- if (!handlerData) {
72282
- continue;
72283
- }
72284
- const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);
72285
- if (currentTarget.figureId) {
72286
- target.figureId = currentTarget.figureId;
72287
- }
72288
- for (const targetZone of currentTarget.zones) {
72289
- selectedZones.push(targetZone);
72290
- if (zone === undefined) {
72291
- zone = targetZone;
72292
- continue;
72293
- }
72294
- zone = union(zone, targetZone);
72295
- }
72296
- }
73037
+ const { target, zone, selectedZones } = getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options);
72297
73038
  if (zone !== undefined) {
72298
- 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);
72299
73040
  }
72300
- handlers.forEach(({ handlerName, handler }) => {
72301
- const handlerData = copiedData[handlerName];
72302
- if (handlerData) {
72303
- handler.paste(target, handlerData, options);
72304
- }
72305
- });
73041
+ applyClipboardHandlersPaste(handlers, copiedData, target, options);
72306
73042
  if (!options?.selectTarget) {
72307
73043
  return;
72308
73044
  }
72309
- const selection = zones[0];
72310
- const col = selection.left;
72311
- const row = selection.top;
72312
- this.selection.getBackToDefault();
72313
- this.selection.selectZone({ cell: { col, row }, zone: union(...selectedZones) }, { scrollIntoView: false });
73045
+ selectPastedZone(this.selection, zones, selectedZones);
72314
73046
  }
72315
73047
  /**
72316
73048
  * Add columns and/or rows to ensure that col + width and row + height are still
@@ -75139,6 +75871,17 @@ clickableCellRegistry.add("link", {
75139
75871
  return !!getters.getEvaluatedCell(position).link;
75140
75872
  },
75141
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
+ },
75142
75885
  sequence: 5,
75143
75886
  });
75144
75887
 
@@ -76850,12 +77593,13 @@ class ClickableCellsStore extends SpreadsheetStore {
76850
77593
  if (!item) {
76851
77594
  continue;
76852
77595
  }
77596
+ const title = typeof item.title === "function" ? item.title(position, getters) : item.title;
76853
77597
  const zone = getters.expandZone(sheetId, positionToZone(position));
76854
77598
  cells.push({
76855
77599
  coordinates: getters.getVisibleRect(zone),
76856
77600
  position,
76857
77601
  action: item.execute,
76858
- title: item.title || "",
77602
+ title: title || "",
76859
77603
  });
76860
77604
  }
76861
77605
  return cells;
@@ -77240,24 +77984,36 @@ css /* scss */ `
77240
77984
  user-select: none;
77241
77985
  color: ${TEXT_BODY};
77242
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
+
77243
77997
  .o-sidePanelTitle {
77244
77998
  line-height: 20px;
77245
77999
  font-size: 16px;
77246
78000
  }
77247
78001
 
77248
78002
  .o-sidePanelHeader {
77249
- padding: 8px 16px;
77250
- display: flex;
77251
- align-items: center;
77252
- justify-content: space-between;
78003
+ padding: 8px;
77253
78004
  border-bottom: 1px solid ${GRAY_300};
78005
+ }
77254
78006
 
77255
- .o-sidePanelClose {
77256
- padding: 5px 10px;
77257
- cursor: pointer;
77258
- &:hover {
77259
- background-color: WhiteSmoke;
77260
- }
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};
77261
78017
  }
77262
78018
  }
77263
78019
  .o-sidePanelBody-container {
@@ -77334,43 +78090,114 @@ css /* scss */ `
77334
78090
  `;
77335
78091
  class SidePanel extends owl.Component {
77336
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";
77337
78118
  static props = {};
78119
+ static components = { SidePanel };
77338
78120
  sidePanelStore;
77339
78121
  spreadsheetRect = useSpreadsheetRect();
77340
78122
  setup() {
77341
78123
  this.sidePanelStore = useStore(SidePanelStore);
77342
- owl.useEffect((isOpen) => {
77343
- 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) {
77344
78129
  this.sidePanelStore.close();
77345
78130
  }
77346
- }, () => [this.sidePanelStore.isOpen]);
77347
- }
77348
- get panel() {
77349
- return sidePanelRegistry.get(this.sidePanelStore.componentTag);
77350
- }
77351
- close() {
77352
- this.sidePanelStore.close();
78131
+ }, () => [this.sidePanelStore.isMainPanelOpen, this.sidePanelStore.isSecondaryPanelOpen]);
77353
78132
  }
77354
- getTitle() {
77355
- const panel = this.panel;
77356
- return typeof panel.title === "function"
77357
- ? panel.title(this.env, this.sidePanelStore.panelProps)
77358
- : panel.title;
77359
- }
77360
- startHandleDrag(ev) {
78133
+ startHandleDrag(panel, ev) {
77361
78134
  const startingCursor = document.body.style.cursor;
77362
- 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;
77363
78140
  const startPosition = ev.clientX;
77364
78141
  const onMouseMove = (ev) => {
77365
78142
  document.body.style.cursor = "col-resize";
77366
78143
  const newSize = startSize + startPosition - ev.clientX;
77367
- this.sidePanelStore.changePanelSize(newSize, this.spreadsheetRect.width);
78144
+ this.sidePanelStore.changePanelSize(panel, newSize);
77368
78145
  };
77369
78146
  const cleanUp = () => {
77370
78147
  document.body.style.cursor = startingCursor;
77371
78148
  };
77372
78149
  startDnd(onMouseMove, cleanUp);
77373
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
+ }
77374
78201
  }
77375
78202
 
77376
78203
  class RibbonMenu extends owl.Component {
@@ -78932,7 +79759,7 @@ class Spreadsheet extends owl.Component {
78932
79759
  Grid,
78933
79760
  BottomBar,
78934
79761
  SmallBottomBar,
78935
- SidePanel,
79762
+ SidePanels,
78936
79763
  SpreadsheetDashboard,
78937
79764
  HeaderGroupContainer,
78938
79765
  FullScreenChart,
@@ -78955,7 +79782,9 @@ class Spreadsheet extends owl.Component {
78955
79782
  else {
78956
79783
  properties["grid-template-rows"] = `min-content auto min-content`;
78957
79784
  }
78958
- 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";
78959
79788
  properties["grid-template-columns"] = `auto ${columnWidth}`;
78960
79789
  return cssPropertiesToCss(properties);
78961
79790
  }
@@ -79039,7 +79868,7 @@ class Spreadsheet extends owl.Component {
79039
79868
  this.checkViewportSize();
79040
79869
  });
79041
79870
  const resizeObserver = new ResizeObserver(() => {
79042
- this.sidePanel.changePanelSize(this.sidePanel.panelSize, this.spreadsheetRect.width);
79871
+ this.sidePanel.changeSpreadsheetWidth(this.spreadsheetRect.width);
79043
79872
  });
79044
79873
  }
79045
79874
  bindModelEvents() {
@@ -83636,6 +84465,6 @@ exports.tokenColors = tokenColors;
83636
84465
  exports.tokenize = tokenize;
83637
84466
 
83638
84467
 
83639
- __info__.version = "18.4.0-alpha.9";
83640
- __info__.date = "2025-06-19T18:23:22.025Z";
83641
- __info__.hash = "6d4d685";
84468
+ __info__.version = "18.4.0";
84469
+ __info__.date = "2025-06-24T11:19:24.606Z";
84470
+ __info__.hash = "a5b7cad";