@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
  (function (exports, owl) {
@@ -163,6 +163,7 @@
163
163
  const FROZEN_PANE_BORDER_COLOR = "#DADFE8";
164
164
  const COMPOSER_ASSISTANT_COLOR = "#9B359B";
165
165
  const COLOR_TRANSPARENT = "#00000000";
166
+ const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414";
166
167
  const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2";
167
168
  const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175";
168
169
  const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA";
@@ -6848,19 +6849,17 @@
6848
6849
  const textWidthCache = {};
6849
6850
  function computeTextWidth(context, text, style, fontUnit = "pt") {
6850
6851
  const font = computeTextFont(style, fontUnit);
6851
- context.save();
6852
- context.font = font;
6853
- const width = computeCachedTextWidth(context, text);
6854
- context.restore();
6855
- return width;
6852
+ return computeCachedTextWidth(context, text, font);
6856
6853
  }
6857
- function computeCachedTextWidth(context, text) {
6858
- const font = context.font;
6854
+ function computeCachedTextWidth(context, text, font) {
6859
6855
  if (!textWidthCache[font]) {
6860
6856
  textWidthCache[font] = {};
6861
6857
  }
6862
6858
  if (textWidthCache[font][text] === undefined) {
6859
+ const oldFont = context.font;
6860
+ context.font = font;
6863
6861
  textWidthCache[font][text] = context.measureText(text).width;
6862
+ context.font = oldFont;
6864
6863
  }
6865
6864
  return textWidthCache[font][text];
6866
6865
  }
@@ -7025,19 +7024,19 @@
7025
7024
  }
7026
7025
  // Inspired from https://stackoverflow.com/a/10511598
7027
7026
  function clipTextWithEllipsis(ctx, text, maxWidth) {
7028
- let width = computeCachedTextWidth(ctx, text);
7027
+ let width = computeCachedTextWidth(ctx, text, ctx.font);
7029
7028
  if (width <= maxWidth) {
7030
7029
  return text;
7031
7030
  }
7032
7031
  const ellipsis = "…";
7033
- const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis);
7032
+ const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis, ctx.font);
7034
7033
  if (width <= ellipsisWidth) {
7035
7034
  return text;
7036
7035
  }
7037
7036
  let len = text.length;
7038
7037
  while (width >= maxWidth - ellipsisWidth && len-- > 0) {
7039
7038
  text = text.substring(0, len);
7040
- width = computeCachedTextWidth(ctx, text);
7039
+ width = computeCachedTextWidth(ctx, text, ctx.font);
7041
7040
  }
7042
7041
  return text + ellipsis;
7043
7042
  }
@@ -7251,6 +7250,63 @@
7251
7250
  };
7252
7251
  return osClipboardContent;
7253
7252
  }
7253
+ /**
7254
+ * Applies each clipboard handler to paste its corresponding data into the target.
7255
+ */
7256
+ const applyClipboardHandlersPaste = (handlers, copiedData, target, options) => {
7257
+ handlers.forEach(({ handlerName, handler }) => {
7258
+ const data = copiedData[handlerName];
7259
+ if (data) {
7260
+ handler.paste(target, data, options);
7261
+ }
7262
+ });
7263
+ };
7264
+ /**
7265
+ * Returns the paste target based on clipboard handlers.
7266
+ * Also includes the full affected zone and the list of pasted zones for selection.
7267
+ */
7268
+ function getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options) {
7269
+ let zone = undefined;
7270
+ const selectedZones = [];
7271
+ const target = {
7272
+ sheetId,
7273
+ zones,
7274
+ };
7275
+ for (const { handlerName, handler } of handlers) {
7276
+ const handlerData = copiedData[handlerName];
7277
+ if (!handlerData) {
7278
+ continue;
7279
+ }
7280
+ const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);
7281
+ if (currentTarget.figureId) {
7282
+ target.figureId = currentTarget.figureId;
7283
+ }
7284
+ for (const targetZone of currentTarget.zones) {
7285
+ selectedZones.push(targetZone);
7286
+ if (zone === undefined) {
7287
+ zone = targetZone;
7288
+ continue;
7289
+ }
7290
+ zone = union(zone, targetZone);
7291
+ }
7292
+ }
7293
+ return {
7294
+ target,
7295
+ zone,
7296
+ selectedZones,
7297
+ };
7298
+ }
7299
+ /**
7300
+ * Updates the selection after a paste operation.
7301
+ */
7302
+ const selectPastedZone = (selection, sourceZones, pastedZones) => {
7303
+ const anchorCell = {
7304
+ col: sourceZones[0].left,
7305
+ row: sourceZones[0].top,
7306
+ };
7307
+ selection.getBackToDefault();
7308
+ selection.selectZone({ cell: anchorCell, zone: union(...pastedZones) }, { scrollIntoView: false });
7309
+ };
7254
7310
 
7255
7311
  class ClipboardHandler {
7256
7312
  getters;
@@ -9955,8 +10011,15 @@ stores.inject(MyMetaStore, storeInstance);
9955
10011
  const ModelStore = createAbstractStore("Model");
9956
10012
 
9957
10013
  class RendererStore {
9958
- mutators = ["register", "unRegister", "drawLayer"];
10014
+ mutators = ["register", "unRegister", "draw", "startAnimation", "stopAnimation"];
9959
10015
  renderers = {};
10016
+ model;
10017
+ context = undefined;
10018
+ animationFrameId = null;
10019
+ registeredAnimations = new Set();
10020
+ constructor(get) {
10021
+ this.model = get(ModelStore);
10022
+ }
9960
10023
  register(renderer) {
9961
10024
  if (!renderer.renderingLayers.length) {
9962
10025
  return;
@@ -9973,17 +10036,54 @@ stores.inject(MyMetaStore, storeInstance);
9973
10036
  this.renderers[layer] = this.renderers[layer].filter((r) => r !== renderer);
9974
10037
  }
9975
10038
  }
9976
- drawLayer(context, layer) {
10039
+ drawLayer(context, layer, timeStamp) {
9977
10040
  const renderers = this.renderers[layer];
9978
10041
  if (renderers) {
9979
10042
  for (const renderer of renderers) {
9980
10043
  context.ctx.save();
9981
- renderer.drawLayer(context, layer);
10044
+ renderer.drawLayer(context, layer, timeStamp);
9982
10045
  context.ctx.restore();
9983
10046
  }
9984
10047
  }
9985
10048
  return "noStateChange";
9986
10049
  }
10050
+ draw(context, timestamp) {
10051
+ context = context || this.context;
10052
+ if (!context) {
10053
+ throw new Error("Rendering context is not defined");
10054
+ }
10055
+ this.context = context;
10056
+ for (const layer of OrderedLayers()) {
10057
+ this.model.drawLayer(context, layer);
10058
+ this.drawLayer(context, layer, timestamp);
10059
+ }
10060
+ return "noStateChange";
10061
+ }
10062
+ startAnimation(animationId) {
10063
+ this.registeredAnimations.add(animationId);
10064
+ if (!this.animationFrameId) {
10065
+ const animationCallback = (timestamp) => {
10066
+ this.animationFrameId = requestAnimationFrame(animationCallback);
10067
+ this.draw(undefined, timestamp);
10068
+ };
10069
+ this.animationFrameId = requestAnimationFrame(animationCallback);
10070
+ }
10071
+ return "noStateChange";
10072
+ }
10073
+ stopAnimation(animationId) {
10074
+ this.registeredAnimations.delete(animationId);
10075
+ if (this.registeredAnimations.size === 0 && this.animationFrameId !== null) {
10076
+ cancelAnimationFrame(this.animationFrameId);
10077
+ this.animationFrameId = null;
10078
+ }
10079
+ return "noStateChange";
10080
+ }
10081
+ dispose() {
10082
+ if (this.animationFrameId) {
10083
+ cancelAnimationFrame(this.animationFrameId);
10084
+ this.animationFrameId = null;
10085
+ }
10086
+ }
9987
10087
  }
9988
10088
 
9989
10089
  class SpreadsheetStore extends DisposableStore {
@@ -10007,7 +10107,7 @@ stores.inject(MyMetaStore, storeInstance);
10007
10107
  }
10008
10108
  handle(cmd) { }
10009
10109
  finalize() { }
10010
- drawLayer(ctx, layer) { }
10110
+ drawLayer(ctx, layer, timestamp) { }
10011
10111
  }
10012
10112
 
10013
10113
  const VOID_COMPOSER = {
@@ -23729,11 +23829,11 @@ stores.inject(MyMetaStore, storeInstance);
23729
23829
  function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
23730
23830
  const maxValue = runtime.maxValue;
23731
23831
  const minValue = runtime.minValue;
23732
- const gaugeValue = runtime.gaugeValue;
23832
+ const gaugeValue = getGaugeValue(runtime, "animated");
23733
23833
  const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
23734
23834
  const gaugeArcWidth = gaugeRect.width / 6;
23735
23835
  const gaugePercentage = gaugeValue
23736
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
23836
+ ? (gaugeValue - minValue.value) / (maxValue.value - minValue.value)
23737
23837
  : 0;
23738
23838
  const gaugeValuePosition = {
23739
23839
  x: boundingRect.width / 2,
@@ -23746,7 +23846,7 @@ stores.inject(MyMetaStore, storeInstance);
23746
23846
  }
23747
23847
  // Scale down the font size if the text is too long
23748
23848
  const maxTextWidth = gaugeRect.width / 2;
23749
- const gaugeLabel = gaugeValue?.label || "-";
23849
+ const gaugeLabel = runtime.gaugeValue?.label || "-";
23750
23850
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
23751
23851
  gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
23752
23852
  }
@@ -23886,7 +23986,7 @@ stores.inject(MyMetaStore, storeInstance);
23886
23986
  return inflectionValues;
23887
23987
  }
23888
23988
  function getGaugeColor(runtime) {
23889
- const gaugeValue = runtime.gaugeValue?.value;
23989
+ const gaugeValue = getGaugeValue(runtime, "final");
23890
23990
  if (gaugeValue === undefined) {
23891
23991
  return GAUGE_BACKGROUND_COLOR;
23892
23992
  }
@@ -23974,6 +24074,11 @@ stores.inject(MyMetaStore, storeInstance);
23974
24074
  };
23975
24075
  return { bottomLeft, bottomRight, topRight, topLeft };
23976
24076
  }
24077
+ function getGaugeValue(runtime, mode) {
24078
+ return mode === "animated" && runtime.animationValue !== undefined
24079
+ ? runtime.animationValue
24080
+ : runtime.gaugeValue?.value;
24081
+ }
23977
24082
 
23978
24083
  const CHART_COMMON_OPTIONS = {
23979
24084
  // https://www.chartjs.org/docs/latest/general/responsive.html
@@ -26328,6 +26433,320 @@ stores.inject(MyMetaStore, storeInstance);
26328
26433
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
26329
26434
  }
26330
26435
 
26436
+ const cellAnimationRegistry = new Registry();
26437
+ cellAnimationRegistry.add("animatedBackgroundColorChange", {
26438
+ id: "animatedBackgroundColorChange",
26439
+ easingFn: "easeOutCubic",
26440
+ hasAnimation: (oldBox, newBox) => {
26441
+ return oldBox?.style?.fillColor !== newBox?.style?.fillColor;
26442
+ },
26443
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26444
+ const colorScale = getColorScale([
26445
+ { value: 0, color: oldBox.style.fillColor || "#ffffff" },
26446
+ { value: 1, color: newBox.style.fillColor || "#ffffff" },
26447
+ ]);
26448
+ animatedBox.style.fillColor = colorScale(EASING_FN[this.easingFn](progress));
26449
+ },
26450
+ });
26451
+ cellAnimationRegistry.add("animatedTextColorChange", {
26452
+ id: "animatedTextColorChange",
26453
+ easingFn: "easeOutCubic",
26454
+ hasAnimation: (oldBox, newBox) => {
26455
+ return oldBox?.style?.textColor !== newBox?.style?.textColor;
26456
+ },
26457
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26458
+ const colorScale = getColorScale([
26459
+ { value: 0, color: oldBox.style.textColor || "#000000" },
26460
+ { value: 1, color: newBox.style.textColor || "#000000" },
26461
+ ]);
26462
+ animatedBox.style.textColor = colorScale(EASING_FN[this.easingFn](progress));
26463
+ },
26464
+ });
26465
+ cellAnimationRegistry.add("animatedDataBar", {
26466
+ id: "animatedDataBar",
26467
+ easingFn: "easeOutCubic",
26468
+ hasAnimation: (oldBox, newBox) => {
26469
+ return oldBox?.dataBarFill?.percentage !== newBox?.dataBarFill?.percentage;
26470
+ },
26471
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26472
+ const startingPercentage = oldBox?.dataBarFill?.percentage || 0;
26473
+ const endingPercentage = newBox?.dataBarFill?.percentage || 0;
26474
+ const value = EASING_FN[this.easingFn](progress);
26475
+ const percentage = startingPercentage + (endingPercentage - startingPercentage) * value;
26476
+ animatedBox.dataBarFill = {
26477
+ color: newBox.dataBarFill?.color || oldBox.dataBarFill?.color || "#ffffff",
26478
+ percentage: percentage,
26479
+ };
26480
+ },
26481
+ });
26482
+ cellAnimationRegistry.add("textFadeIn", {
26483
+ id: "textFadeIn",
26484
+ easingFn: "easeInCubic",
26485
+ hasAnimation: (oldBox, newBox) => {
26486
+ const oldText = oldBox?.content?.textLines?.join("\n");
26487
+ const newText = newBox?.content?.textLines?.join("\n");
26488
+ return Boolean(!oldText && newText);
26489
+ },
26490
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26491
+ animatedBox.textOpacity = EASING_FN[this.easingFn](progress);
26492
+ },
26493
+ });
26494
+ cellAnimationRegistry.add("textFadeOut", {
26495
+ id: "textFadeOut",
26496
+ easingFn: "easeOutCubic",
26497
+ hasAnimation: (oldBox, newBox) => {
26498
+ const oldText = oldBox?.content?.textLines?.join("\n");
26499
+ const newText = newBox?.content?.textLines?.join("\n");
26500
+ return Boolean(oldText && !newText);
26501
+ },
26502
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26503
+ const textOpacity = 1 - EASING_FN[this.easingFn](progress);
26504
+ const style = { ...oldBox.style };
26505
+ delete style.fillColor;
26506
+ animatedBox.textOpacity = textOpacity;
26507
+ animatedBox.content = oldBox.content;
26508
+ animatedBox.clipRect = oldBox.clipRect;
26509
+ Object.assign(animatedBox.style, style);
26510
+ },
26511
+ });
26512
+ cellAnimationRegistry.add("textChange", {
26513
+ id: "textChange",
26514
+ easingFn: "easeOutCubic",
26515
+ hasAnimation: (oldBox, newBox) => {
26516
+ const oldText = oldBox?.content?.textLines?.join("\n");
26517
+ const newText = newBox?.content?.textLines?.join("\n");
26518
+ // Note: here, we also animate changes to icons layout (margins/size change, or icon appearing/disappearing)
26519
+ // because a change to the icon layout will impact where the text is positioned.
26520
+ return (Boolean(oldText && newText && oldText !== newText) || hasIconLayoutChange(newBox, oldBox));
26521
+ },
26522
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26523
+ const value = EASING_FN[this.easingFn](progress);
26524
+ const slideInY = newBox.y + (value - 1) * newBox.height;
26525
+ const slideOutY = newBox.y + value * newBox.height;
26526
+ const iconLayoutChange = hasIconLayoutChange(newBox, oldBox);
26527
+ const slideInBox = {
26528
+ id: newBox.id + "-text-slide-in",
26529
+ x: newBox.x,
26530
+ y: slideInY,
26531
+ width: newBox.width,
26532
+ height: newBox.height,
26533
+ style: { ...newBox.style },
26534
+ skipCellGridLines: true,
26535
+ content: newBox.content,
26536
+ clipRect: newBox.clipRect || {
26537
+ ...newBox,
26538
+ // large width to avoid clipping the text it it didn't have a clipRect before,
26539
+ // we mainly want to clip the Y for the animation
26540
+ x: Math.max(0, newBox.x - (newBox.content?.width || 0)),
26541
+ width: newBox.width + (newBox.content?.width || 0) * 2,
26542
+ },
26543
+ icons: iconLayoutChange
26544
+ ? addClipRectToIcons(newBox.icons, newBox)
26545
+ : makeIconsEmpty(newBox.icons),
26546
+ };
26547
+ const slideOutBox = {
26548
+ id: oldBox.id + "-text-slide-out",
26549
+ x: newBox.x,
26550
+ y: slideOutY,
26551
+ width: newBox.width,
26552
+ height: newBox.height,
26553
+ style: { ...oldBox.style },
26554
+ skipCellGridLines: true,
26555
+ content: oldBox.content,
26556
+ clipRect: oldBox.clipRect || {
26557
+ ...newBox,
26558
+ x: Math.max(0, newBox.x - (oldBox.content?.width || 0)),
26559
+ width: newBox.width + (oldBox.content?.width || 0) * 2,
26560
+ },
26561
+ icons: iconLayoutChange
26562
+ ? addClipRectToIcons(oldBox.icons, newBox)
26563
+ : makeIconsEmpty(oldBox.icons),
26564
+ };
26565
+ if (newBox.content && oldBox.content && slideInBox.content && slideOutBox.content) {
26566
+ const slideInContentY = newBox.content.y + (value - 1) * newBox.height;
26567
+ const slideOutContentY = newBox.content.y + value * newBox.height;
26568
+ slideInBox.content.y = slideInContentY;
26569
+ slideOutBox.content.y = slideOutContentY;
26570
+ }
26571
+ slideOutBox.style.fillColor = slideInBox.style.fillColor = undefined;
26572
+ animatedBox.content = undefined;
26573
+ animatedBox.icons = iconLayoutChange ? {} : animatedBox.icons;
26574
+ return { newBoxes: [slideInBox, slideOutBox] };
26575
+ },
26576
+ });
26577
+ cellAnimationRegistry.add("borderFadeIn", {
26578
+ id: "borderFadeIn",
26579
+ easingFn: "easeInCubic",
26580
+ hasAnimation: (oldBox, newBox) => {
26581
+ return Boolean((!oldBox?.border?.bottom && newBox?.border?.bottom) ||
26582
+ (!oldBox?.border?.top && newBox?.border?.top) ||
26583
+ (!oldBox?.border?.left && newBox?.border?.left) ||
26584
+ (!oldBox?.border?.right && newBox?.border?.right));
26585
+ },
26586
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26587
+ const borderOpacity = EASING_FN[this.easingFn](progress);
26588
+ if (animatedBox.border?.top && newBox.border?.top && !oldBox.border?.top) {
26589
+ animatedBox.border.top.opacity = borderOpacity;
26590
+ }
26591
+ if (animatedBox.border?.bottom && newBox.border?.bottom && !oldBox.border?.bottom) {
26592
+ animatedBox.border.bottom.opacity = borderOpacity;
26593
+ }
26594
+ if (animatedBox.border?.left && newBox.border?.left && !oldBox.border?.left) {
26595
+ animatedBox.border.left.opacity = borderOpacity;
26596
+ }
26597
+ if (animatedBox.border?.right && newBox.border?.right && !oldBox.border?.right) {
26598
+ animatedBox.border.right.opacity = borderOpacity;
26599
+ }
26600
+ },
26601
+ });
26602
+ cellAnimationRegistry.add("borderFadeOut", {
26603
+ id: "borderFadeOut",
26604
+ easingFn: "easeOutCubic",
26605
+ hasAnimation: (oldBox, newBox) => {
26606
+ return Boolean((oldBox?.border?.bottom && !newBox?.border?.bottom) ||
26607
+ (oldBox?.border?.top && !newBox?.border?.top) ||
26608
+ (oldBox?.border?.left && !newBox?.border?.left) ||
26609
+ (oldBox?.border?.right && !newBox?.border?.right));
26610
+ },
26611
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26612
+ const borderOpacity = 1 - EASING_FN[this.easingFn](progress);
26613
+ if (!animatedBox.border) {
26614
+ animatedBox.border = {};
26615
+ }
26616
+ if (oldBox.border?.top && !newBox.border?.top) {
26617
+ animatedBox.border.top = { ...oldBox.border.top, opacity: borderOpacity };
26618
+ }
26619
+ if (oldBox.border?.bottom && !newBox.border?.bottom) {
26620
+ animatedBox.border.bottom = { ...oldBox.border.bottom, opacity: borderOpacity };
26621
+ }
26622
+ if (oldBox.border?.left && !newBox.border?.left) {
26623
+ animatedBox.border.left = { ...oldBox.border.left, opacity: borderOpacity };
26624
+ }
26625
+ if (oldBox.border?.right && !newBox.border?.right) {
26626
+ animatedBox.border.right = { ...oldBox.border.right, opacity: borderOpacity };
26627
+ }
26628
+ },
26629
+ });
26630
+ cellAnimationRegistry.add("borderColorChange", {
26631
+ id: "borderColorChange",
26632
+ easingFn: "easeOutCubic",
26633
+ hasAnimation: (oldBox, newBox) => {
26634
+ const oldBorder = oldBox?.border;
26635
+ const newBorder = newBox?.border;
26636
+ if (!oldBorder || !newBorder) {
26637
+ return false;
26638
+ }
26639
+ return Boolean(oldBorder.bottom?.color !== newBorder.bottom?.color ||
26640
+ oldBorder.top?.color !== newBorder.top?.color ||
26641
+ oldBorder.left?.color !== newBorder.left?.color ||
26642
+ oldBorder.right?.color !== newBorder.right?.color);
26643
+ },
26644
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26645
+ const animateBorderColor = (side) => {
26646
+ const oldBorder = oldBox?.border?.[side];
26647
+ const newBorder = newBox?.border?.[side];
26648
+ const animatedBorder = animatedBox.border?.[side];
26649
+ if (oldBorder && newBorder && animatedBorder) {
26650
+ const colorScale = getColorScale([
26651
+ { value: 0, color: oldBorder.color || "#000000" },
26652
+ { value: 1, color: newBorder.color || "#000000" },
26653
+ ]);
26654
+ animatedBorder.color = colorScale(EASING_FN[this.easingFn](progress));
26655
+ }
26656
+ };
26657
+ animateBorderColor("top");
26658
+ animateBorderColor("bottom");
26659
+ animateBorderColor("left");
26660
+ animateBorderColor("right");
26661
+ },
26662
+ });
26663
+ cellAnimationRegistry.add("iconChange", {
26664
+ id: "iconChange",
26665
+ easingFn: "easeOutCubic",
26666
+ hasAnimation: (oldBox, newBox) => {
26667
+ return (!hasIconLayoutChange(newBox, oldBox) &&
26668
+ Boolean(oldBox?.icons?.center?.svg?.name !== newBox?.icons?.center?.svg?.name ||
26669
+ oldBox?.icons?.left?.svg?.name !== newBox?.icons?.left?.svg?.name ||
26670
+ oldBox?.icons?.right?.svg?.name !== newBox?.icons?.right?.svg?.name));
26671
+ },
26672
+ updateAnimation: function (progress, animatedBox, oldBox, newBox) {
26673
+ const value = EASING_FN[this.easingFn](progress);
26674
+ const slideInY = newBox.y + (value - 1) * newBox.height;
26675
+ const slideOutY = newBox.y + value * newBox.height;
26676
+ const newBoxes = [];
26677
+ const animateIconChange = (side) => {
26678
+ const oldIcon = oldBox.icons?.[side];
26679
+ const newIcon = newBox.icons?.[side];
26680
+ const slideInBox = {
26681
+ id: `${newBox.id}-icon-${side}-slide-in`,
26682
+ style: { verticalAlign: newBox.style.verticalAlign },
26683
+ x: newBox.x,
26684
+ y: slideInY,
26685
+ width: newBox.width,
26686
+ height: newBox.height,
26687
+ skipCellGridLines: true,
26688
+ icons: { [side]: { ...newIcon, clipRect: newBox } },
26689
+ };
26690
+ const slideOutBox = {
26691
+ id: `${newBox.id}-icon-${side}-slide-out`,
26692
+ style: { verticalAlign: oldBox.style.verticalAlign },
26693
+ x: newBox.x,
26694
+ y: slideOutY,
26695
+ width: newBox.width,
26696
+ height: newBox.height,
26697
+ skipCellGridLines: true,
26698
+ icons: { [side]: { ...oldIcon, clipRect: newBox } },
26699
+ };
26700
+ animatedBox.icons[side] = makeIconsEmpty(newBox.icons)[side];
26701
+ newBoxes.push(slideInBox, slideOutBox);
26702
+ };
26703
+ animateIconChange("left");
26704
+ animateIconChange("right");
26705
+ animateIconChange("center");
26706
+ return { newBoxes };
26707
+ },
26708
+ });
26709
+ const EASING_FN = {
26710
+ linear: (t) => t,
26711
+ easeInCubic: (t) => t * t * t,
26712
+ easeOutCubic: (t) => (t -= 1) * t * t + 1,
26713
+ easeInOutCubic: (t) => ((t /= 0.5) < 1 ? 0.5 * t * t * t : 0.5 * ((t -= 2) * t * t + 2)),
26714
+ easeOutQuart: (t) => -((t -= 1) * t * t * t - 1),
26715
+ };
26716
+ function makeIconsEmpty(icons) {
26717
+ return {
26718
+ left: icons.left ? { ...icons.left, svg: undefined } : undefined,
26719
+ right: icons.right ? { ...icons.right, svg: undefined } : undefined,
26720
+ center: icons.center ? { ...icons.center, svg: undefined } : undefined,
26721
+ };
26722
+ }
26723
+ function addClipRectToIcons(icons, clipRect) {
26724
+ return {
26725
+ left: icons.left ? { ...icons.left, clipRect } : undefined,
26726
+ right: icons.right ? { ...icons.right, clipRect } : undefined,
26727
+ center: icons.center ? { ...icons.center, clipRect } : undefined,
26728
+ };
26729
+ }
26730
+ /**
26731
+ * Check if the icons have appeared, disappeared or changed margin/size/align. Those changes affect where the text is positioned.
26732
+ */
26733
+ function hasIconLayoutChange(newBox, oldBox) {
26734
+ const hasLayoutChange = (newIcon, oldIcon) => {
26735
+ if (oldIcon && newIcon) {
26736
+ return !!(newIcon.horizontalAlign !== oldIcon.horizontalAlign ||
26737
+ newIcon.size !== oldIcon.size ||
26738
+ newIcon.margin !== oldIcon.margin ||
26739
+ (newIcon.svg && !oldIcon.svg) ||
26740
+ (!newIcon.svg && oldIcon.svg));
26741
+ }
26742
+ return !!((newIcon && !oldIcon) || (!newIcon && oldIcon));
26743
+ };
26744
+ return (hasLayoutChange(newBox?.icons.left, oldBox?.icons.left) ||
26745
+ hasLayoutChange(newBox?.icons.right, oldBox?.icons.right) ||
26746
+ hasLayoutChange(newBox?.icons.center, oldBox?.icons.center));
26747
+ }
26748
+
26749
+ const ANIMATION_DURATION = 1000;
26331
26750
  class GaugeChartComponent extends owl.Component {
26332
26751
  static template = "o-spreadsheet-GaugeChartComponent";
26333
26752
  static props = {
@@ -26335,16 +26754,101 @@ stores.inject(MyMetaStore, storeInstance);
26335
26754
  isFullScreen: { type: Boolean, optional: true },
26336
26755
  };
26337
26756
  canvas = owl.useRef("chartContainer");
26757
+ animationStore;
26338
26758
  get runtime() {
26339
26759
  return this.env.model.getters.getChartRuntime(this.props.figureUI.id);
26340
26760
  }
26341
26761
  setup() {
26342
- owl.useEffect(() => drawGaugeChart(this.canvas.el, this.runtime), () => {
26343
- const canvas = this.canvas.el;
26344
- const rect = canvas.getBoundingClientRect();
26762
+ if (this.env.model.getters.isDashboard()) {
26763
+ this.animationStore = useStore(ChartAnimationStore);
26764
+ }
26765
+ let animation = null;
26766
+ let lastRuntime = undefined;
26767
+ owl.useEffect(() => {
26768
+ if (this.env.isDashboard() &&
26769
+ lastRuntime === undefined && // first render
26770
+ this.animationStore?.animationPlayed[this.animationFigureId] !== "gauge") {
26771
+ animation = this.drawGaugeWithAnimation();
26772
+ this.animationStore?.disableAnimationForChart(this.animationFigureId, "gauge");
26773
+ }
26774
+ else if (this.env.isDashboard() &&
26775
+ lastRuntime !== undefined && // not first render
26776
+ !deepEquals(this.runtime, lastRuntime)) {
26777
+ animation = this.drawGaugeWithAnimation();
26778
+ this.animationStore?.disableAnimationForChart(this.animationFigureId, "gauge");
26779
+ }
26780
+ else {
26781
+ drawGaugeChart(this.canvasEl, this.runtime);
26782
+ }
26783
+ lastRuntime = this.runtime;
26784
+ return () => animation?.stop();
26785
+ }, () => {
26786
+ const rect = this.canvasEl.getBoundingClientRect();
26345
26787
  return [rect.width, rect.height, this.runtime, this.canvas.el, window.devicePixelRatio];
26346
26788
  });
26347
26789
  }
26790
+ drawGaugeWithAnimation() {
26791
+ drawGaugeChart(this.canvasEl, { ...this.runtime, animationValue: 0 });
26792
+ const gaugeValue = this.runtime.gaugeValue?.value || 0;
26793
+ const upperBound = this.runtime.maxValue.value;
26794
+ const finalValue = Math.sign(gaugeValue) * Math.min(Math.abs(gaugeValue), Math.abs(upperBound));
26795
+ if (finalValue === 0) {
26796
+ return null;
26797
+ }
26798
+ const lowerBound = this.runtime.minValue.value;
26799
+ const animation = new Animation(lowerBound, finalValue, ANIMATION_DURATION, (animationValue) => drawGaugeChart(this.canvasEl, { ...this.runtime, animationValue }));
26800
+ animation.start();
26801
+ return animation;
26802
+ }
26803
+ get canvasEl() {
26804
+ return this.canvas.el;
26805
+ }
26806
+ get animationFigureId() {
26807
+ return this.props.isFullScreen
26808
+ ? this.props.figureUI.id + "-fullscreen"
26809
+ : this.props.figureUI.id;
26810
+ }
26811
+ }
26812
+ /**
26813
+ * Animation interpolating values using the ease-out quartic curve function (chartJS default easing)
26814
+ */
26815
+ class Animation {
26816
+ startValue;
26817
+ endValue;
26818
+ duration;
26819
+ callback;
26820
+ startTime = undefined;
26821
+ animationFrameId = null;
26822
+ constructor(startValue, endValue, duration, callback) {
26823
+ this.startValue = startValue;
26824
+ this.endValue = endValue;
26825
+ this.duration = duration;
26826
+ this.callback = callback;
26827
+ }
26828
+ start() {
26829
+ this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
26830
+ }
26831
+ stop() {
26832
+ if (this.animationFrameId) {
26833
+ cancelAnimationFrame(this.animationFrameId);
26834
+ this.animationFrameId = null;
26835
+ }
26836
+ }
26837
+ animate(timestamp) {
26838
+ if (!this.startTime) {
26839
+ this.startTime = timestamp;
26840
+ }
26841
+ const elapsed = timestamp - this.startTime;
26842
+ const progress = Math.min(elapsed / this.duration, 1);
26843
+ const currentValue = this.startValue + (this.endValue - this.startValue) * EASING_FN.easeOutQuart(progress);
26844
+ this.callback(currentValue);
26845
+ if (progress < 1) {
26846
+ this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
26847
+ }
26848
+ else {
26849
+ this.stop();
26850
+ }
26851
+ }
26348
26852
  }
26349
26853
 
26350
26854
  class ComboChart extends AbstractChart {
@@ -36537,6 +37041,7 @@ stores.inject(MyMetaStore, storeInstance);
36537
37041
  // We need here the svg of the icons that we need to convert to images for the renderer
36538
37042
  // -----------------------------------------------------------------------------
36539
37043
  const ARROW_DOWN = {
37044
+ name: "ARROW_DOWN",
36540
37045
  width: 448,
36541
37046
  height: 512,
36542
37047
  paths: [
@@ -36547,6 +37052,7 @@ stores.inject(MyMetaStore, storeInstance);
36547
37052
  ],
36548
37053
  };
36549
37054
  const ARROW_UP = {
37055
+ name: "ARROW_UP",
36550
37056
  width: 448,
36551
37057
  height: 512,
36552
37058
  paths: [
@@ -36557,6 +37063,7 @@ stores.inject(MyMetaStore, storeInstance);
36557
37063
  ],
36558
37064
  };
36559
37065
  const ARROW_RIGHT = {
37066
+ name: "ARROW_RIGHT",
36560
37067
  width: 448,
36561
37068
  height: 512,
36562
37069
  paths: [
@@ -36567,6 +37074,7 @@ stores.inject(MyMetaStore, storeInstance);
36567
37074
  ],
36568
37075
  };
36569
37076
  const SMILE = {
37077
+ name: "SMILE",
36570
37078
  width: 496,
36571
37079
  height: 512,
36572
37080
  paths: [
@@ -36577,6 +37085,7 @@ stores.inject(MyMetaStore, storeInstance);
36577
37085
  ],
36578
37086
  };
36579
37087
  const MEH = {
37088
+ name: "MEH",
36580
37089
  width: 496,
36581
37090
  height: 512,
36582
37091
  paths: [
@@ -36587,6 +37096,7 @@ stores.inject(MyMetaStore, storeInstance);
36587
37096
  ],
36588
37097
  };
36589
37098
  const FROWN = {
37099
+ name: "FROWN",
36590
37100
  width: 496,
36591
37101
  height: 512,
36592
37102
  paths: [
@@ -36598,22 +37108,26 @@ stores.inject(MyMetaStore, storeInstance);
36598
37108
  };
36599
37109
  const DOT_PATH = "M256 9 a247 247 0 1 0.1 0 0";
36600
37110
  const GREEN_DOT = {
37111
+ name: "GREEN_DOT",
36601
37112
  width: 512,
36602
37113
  height: 512,
36603
37114
  paths: [{ fillColor: "#6AA84F", path: DOT_PATH }],
36604
37115
  };
36605
37116
  const YELLOW_DOT = {
37117
+ name: "YELLOW_DOT",
36606
37118
  width: 512,
36607
37119
  height: 512,
36608
37120
  paths: [{ fillColor: "#F0AD4E", path: DOT_PATH }],
36609
37121
  };
36610
37122
  const RED_DOT = {
37123
+ name: "RED_DOT",
36611
37124
  width: 512,
36612
37125
  height: 512,
36613
37126
  paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36614
37127
  };
36615
37128
  function getCaretDownSvg(color) {
36616
37129
  return {
37130
+ name: "CARET_DOWN",
36617
37131
  width: 512,
36618
37132
  height: 512,
36619
37133
  paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
@@ -36621,6 +37135,7 @@ stores.inject(MyMetaStore, storeInstance);
36621
37135
  }
36622
37136
  function getHoveredCaretDownSvg(color) {
36623
37137
  return {
37138
+ name: "CARET_DOWN",
36624
37139
  width: 512,
36625
37140
  height: 512,
36626
37141
  paths: [
@@ -36632,6 +37147,7 @@ stores.inject(MyMetaStore, storeInstance);
36632
37147
  const CHIP_CARET_DOWN_PATH = "M40 185 h270 l-135 128";
36633
37148
  function getChipSvg(chipStyle) {
36634
37149
  return {
37150
+ name: "CHIP",
36635
37151
  width: 512,
36636
37152
  height: 512,
36637
37153
  paths: [{ fillColor: chipStyle.textColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH }],
@@ -36639,6 +37155,7 @@ stores.inject(MyMetaStore, storeInstance);
36639
37155
  }
36640
37156
  function getHoveredChipSvg(chipStyle) {
36641
37157
  return {
37158
+ name: "CHIP",
36642
37159
  width: 512,
36643
37160
  height: 512,
36644
37161
  paths: [
@@ -36651,16 +37168,19 @@ stores.inject(MyMetaStore, storeInstance);
36651
37168
  };
36652
37169
  }
36653
37170
  const CHECKBOX_UNCHECKED = {
37171
+ name: "CHECKBOX_UNCHECKED",
36654
37172
  width: 512,
36655
37173
  height: 512,
36656
37174
  paths: [{ fillColor: GRAY_300, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36657
37175
  };
36658
37176
  const CHECKBOX_UNCHECKED_HOVERED = {
37177
+ name: "CHECKBOX_UNCHECKED",
36659
37178
  width: 512,
36660
37179
  height: 512,
36661
37180
  paths: [{ fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36662
37181
  };
36663
37182
  const CHECKBOX_CHECKED = {
37183
+ name: "CHECKBOX_CHECKED",
36664
37184
  width: 512,
36665
37185
  height: 512,
36666
37186
  paths: [
@@ -36673,6 +37193,7 @@ stores.inject(MyMetaStore, storeInstance);
36673
37193
  ? "M149,235 h213 v43 h-213 M235,149 h43 v213 h-43" // +
36674
37194
  : "M149,235 h213 v43 h-213"; // -
36675
37195
  return {
37196
+ name: "PIVOT_ICON",
36676
37197
  width: 512,
36677
37198
  height: 512,
36678
37199
  paths: [
@@ -36699,6 +37220,7 @@ stores.inject(MyMetaStore, storeInstance);
36699
37220
  colors.hoverBackgroundColor = "#fff";
36700
37221
  }
36701
37222
  return {
37223
+ name: "DATA_FILTER_ICON",
36702
37224
  width: isActive ? 24 : 850,
36703
37225
  height: isActive ? 24 : 850,
36704
37226
  paths: [
@@ -40894,6 +41416,23 @@ stores.inject(MyMetaStore, storeInstance);
40894
41416
  }
40895
41417
  return data;
40896
41418
  },
41419
+ })
41420
+ .add("18.4.3", {
41421
+ migrate(data) {
41422
+ if (!data.pivots) {
41423
+ return data;
41424
+ }
41425
+ for (const pivotId in data.pivots) {
41426
+ const pivot = data.pivots[pivotId];
41427
+ if (pivot.sortedColumn) {
41428
+ const measure = pivot.measures.find((measure) => measure.fieldName === pivot.sortedColumn?.measure);
41429
+ if (measure) {
41430
+ pivot.sortedColumn.measure = measure.id;
41431
+ }
41432
+ }
41433
+ }
41434
+ return data;
41435
+ },
40897
41436
  });
40898
41437
  function fixOverlappingFilters(data) {
40899
41438
  for (const sheet of data.sheets || []) {
@@ -45960,15 +46499,16 @@ stores.inject(MyMetaStore, storeInstance);
45960
46499
  return state;
45961
46500
  }
45962
46501
 
46502
+ const PAINT_FORMAT_HANDLER_KEYS = [
46503
+ "cell",
46504
+ "border",
46505
+ "table",
46506
+ "conditionalFormat",
46507
+ "merge",
46508
+ ];
45963
46509
  class PaintFormatStore extends SpreadsheetStore {
45964
46510
  mutators = ["activate", "cancel", "pasteFormat"];
45965
46511
  highlightStore = this.get(HighlightStore);
45966
- clipboardHandlers = [
45967
- new CellClipboardHandler(this.getters, this.model.dispatch),
45968
- new BorderClipboardHandler(this.getters, this.model.dispatch),
45969
- new TableClipboardHandler(this.getters, this.model.dispatch),
45970
- new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
45971
- ];
45972
46512
  status = "inactive";
45973
46513
  copiedData;
45974
46514
  constructor(get) {
@@ -45999,24 +46539,38 @@ stores.inject(MyMetaStore, storeInstance);
45999
46539
  get isActive() {
46000
46540
  return this.status !== "inactive";
46001
46541
  }
46542
+ get clipboardHandlers() {
46543
+ return PAINT_FORMAT_HANDLER_KEYS.map((handlerName) => {
46544
+ const HandlerClass = clipboardHandlersRegistries.cellHandlers.get(handlerName);
46545
+ return {
46546
+ handlerName,
46547
+ handler: new HandlerClass(this.getters, this.model.dispatch),
46548
+ };
46549
+ });
46550
+ }
46002
46551
  copyFormats() {
46003
46552
  const sheetId = this.getters.getActiveSheetId();
46004
46553
  const zones = this.getters.getSelectedZones();
46005
- const copiedData = {};
46006
- for (const handler of this.clipboardHandlers) {
46007
- Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones), false));
46554
+ const copiedData = { zones, sheetId };
46555
+ for (const { handlerName, handler } of this.clipboardHandlers) {
46556
+ const handlerResult = handler.copy(getClipboardDataPositions(sheetId, zones), false);
46557
+ if (handlerResult !== undefined) {
46558
+ copiedData[handlerName] = handlerResult;
46559
+ }
46008
46560
  }
46009
46561
  return copiedData;
46010
46562
  }
46011
46563
  paintFormat(sheetId, target) {
46012
- if (this.copiedData) {
46013
- for (const handler of this.clipboardHandlers) {
46014
- handler.paste({ zones: target, sheetId }, this.copiedData, {
46015
- isCutOperation: false,
46016
- pasteOption: "onlyFormat",
46017
- });
46018
- }
46564
+ if (!this.copiedData) {
46565
+ return;
46019
46566
  }
46567
+ const options = {
46568
+ isCutOperation: false,
46569
+ pasteOption: "onlyFormat",
46570
+ };
46571
+ const { target: pasteTarget, selectedZones } = getPasteTargetFromHandlers(sheetId, target, this.copiedData, this.clipboardHandlers, options);
46572
+ applyClipboardHandlersPaste(this.clipboardHandlers, this.copiedData, pasteTarget, options);
46573
+ selectPastedZone(this.model.selection, target, selectedZones);
46020
46574
  if (this.status === "oneOff") {
46021
46575
  this.cancel();
46022
46576
  }
@@ -46063,12 +46617,8 @@ stores.inject(MyMetaStore, storeInstance);
46063
46617
  this.row = undefined;
46064
46618
  }
46065
46619
  computeOverlay() {
46066
- if (!this.getters.isDashboard()) {
46067
- return;
46068
- }
46069
46620
  this.overlayColors = new PositionMap();
46070
- const col = this.col;
46071
- const row = this.row;
46621
+ const { col, row } = this;
46072
46622
  if (col === undefined || row === undefined) {
46073
46623
  return;
46074
46624
  }
@@ -46077,9 +46627,16 @@ stores.inject(MyMetaStore, storeInstance);
46077
46627
  if (!table) {
46078
46628
  return;
46079
46629
  }
46080
- const { left, right } = table.range.zone;
46081
- for (let c = left; c <= right; c++) {
46082
- this.overlayColors.set({ sheetId, col: c, row }, setColorAlpha("#017E84", 0.08));
46630
+ const { left, right, top } = table.range.zone;
46631
+ const isTableHeader = row < top + table.config.numberOfHeaders;
46632
+ const doesTableRowHaveContent = range(left, right + 1).some((col) => {
46633
+ return (!this.getters.isColHidden(sheetId, col) &&
46634
+ this.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue);
46635
+ });
46636
+ if (!isTableHeader && doesTableRowHaveContent) {
46637
+ for (let col = left; col <= right; col++) {
46638
+ this.overlayColors.set({ sheetId, col, row }, TABLE_HOVER_BACKGROUND_COLOR);
46639
+ }
46083
46640
  }
46084
46641
  }
46085
46642
  }
@@ -46327,7 +46884,10 @@ stores.inject(MyMetaStore, storeInstance);
46327
46884
  }
46328
46885
  const icons = this.env.model.getters.getCellIcons(position);
46329
46886
  const icon = icons.find((icon) => {
46330
- return isPointInsideRect(x, y, this.env.model.getters.getCellIconRect(icon));
46887
+ const merge = this.env.model.getters.getMerge(position);
46888
+ const zone = merge || positionToZone(position);
46889
+ const cellRect = this.env.model.getters.getRect(zone);
46890
+ return isPointInsideRect(x, y, this.env.model.getters.getCellIconRect(icon, cellRect));
46331
46891
  });
46332
46892
  return icon?.onClick ? icon : undefined;
46333
46893
  }
@@ -47064,19 +47624,56 @@ stores.inject(MyMetaStore, storeInstance);
47064
47624
  }
47065
47625
  }
47066
47626
 
47067
- class GridRenderer {
47068
- getters;
47069
- renderer;
47627
+ const CELL_ANIMATION_DURATION = 200;
47628
+ class GridRenderer extends SpreadsheetStore {
47070
47629
  fingerprints;
47071
47630
  hoveredTables;
47072
47631
  hoveredIcon;
47632
+ lastRenderBoxes = new Map();
47633
+ preventNewAnimationsInNextFrame = false;
47634
+ zonesWithPreventedAnimationsInNextFrame = [];
47635
+ animations = new Map();
47073
47636
  constructor(get) {
47637
+ super(get);
47074
47638
  this.getters = get(ModelStore).getters;
47075
- this.renderer = get(RendererStore);
47076
47639
  this.fingerprints = get(FormulaFingerprintStore);
47077
47640
  this.hoveredTables = get(HoveredTableStore);
47078
47641
  this.hoveredIcon = get(HoveredIconStore);
47079
- this.renderer.register(this);
47642
+ }
47643
+ handle(cmd) {
47644
+ switch (cmd.type) {
47645
+ case "START":
47646
+ case "ACTIVATE_SHEET":
47647
+ case "ADD_COLUMNS_ROWS":
47648
+ case "REMOVE_COLUMNS_ROWS":
47649
+ this.animations.clear();
47650
+ this.preventNewAnimationsInNextFrame = true;
47651
+ break;
47652
+ case "RESIZE_COLUMNS_ROWS":
47653
+ this.preventNewAnimationsInNextFrame = true;
47654
+ break;
47655
+ case "REDO":
47656
+ this.zonesWithPreventedAnimationsInNextFrame = [];
47657
+ break;
47658
+ case "UNDO":
47659
+ for (const command of cmd.commands) {
47660
+ if (command.type === "ADD_COLUMNS_ROWS" ||
47661
+ command.type === "REMOVE_COLUMNS_ROWS" ||
47662
+ command.type === "RESIZE_COLUMNS_ROWS") {
47663
+ this.animations.clear();
47664
+ this.preventNewAnimationsInNextFrame = true;
47665
+ break;
47666
+ }
47667
+ }
47668
+ break;
47669
+ case "PASTE":
47670
+ this.zonesWithPreventedAnimationsInNextFrame.push(...this.getters.getSelectedZones());
47671
+ break;
47672
+ case "UPDATE_CELL":
47673
+ const zones = this.getters.getCommandZones(cmd);
47674
+ this.zonesWithPreventedAnimationsInNextFrame.push(...zones);
47675
+ break;
47676
+ }
47080
47677
  }
47081
47678
  get renderingLayers() {
47082
47679
  return ["Background", "Headers"];
@@ -47084,17 +47681,20 @@ stores.inject(MyMetaStore, storeInstance);
47084
47681
  // ---------------------------------------------------------------------------
47085
47682
  // Grid rendering
47086
47683
  // ---------------------------------------------------------------------------
47087
- drawLayer(renderingContext, layer) {
47684
+ drawLayer(renderingContext, layer, timeStamp) {
47088
47685
  switch (layer) {
47089
47686
  case "Background":
47090
47687
  this.drawGlobalBackground(renderingContext);
47688
+ const oldBoxes = this.lastRenderBoxes;
47689
+ this.lastRenderBoxes = new Map();
47091
47690
  for (const { zone, rect } of this.getters.getAllActiveViewportsZonesAndRect()) {
47092
47691
  const { ctx } = renderingContext;
47093
47692
  ctx.save();
47094
47693
  ctx.beginPath();
47095
47694
  ctx.rect(rect.x, rect.y, rect.width, rect.height);
47096
47695
  ctx.clip();
47097
- const boxes = this.getGridBoxes(zone);
47696
+ const boxesWithoutAnimations = this.getGridBoxes(zone);
47697
+ const boxes = this.getBoxesWithAnimations(boxesWithoutAnimations, oldBoxes, timeStamp);
47098
47698
  this.drawBackground(renderingContext, boxes);
47099
47699
  this.drawOverflowingCellBackground(renderingContext, boxes);
47100
47700
  this.drawCellBackground(renderingContext, boxes);
@@ -47104,6 +47704,8 @@ stores.inject(MyMetaStore, storeInstance);
47104
47704
  ctx.restore();
47105
47705
  }
47106
47706
  this.drawFrozenPanes(renderingContext);
47707
+ this.preventNewAnimationsInNextFrame = false;
47708
+ this.zonesWithPreventedAnimationsInNextFrame = [];
47107
47709
  break;
47108
47710
  case "Headers":
47109
47711
  if (!this.getters.isDashboard()) {
@@ -47127,6 +47729,8 @@ stores.inject(MyMetaStore, storeInstance);
47127
47729
  const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;
47128
47730
  if (areGridLinesVisible) {
47129
47731
  for (const box of boxes) {
47732
+ if (box.skipCellGridLines)
47733
+ continue;
47130
47734
  ctx.strokeStyle = CELL_BORDER_COLOR;
47131
47735
  ctx.lineWidth = thinLineWidth;
47132
47736
  ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);
@@ -47234,7 +47838,8 @@ stores.inject(MyMetaStore, storeInstance);
47234
47838
  * each line and adding 1 pixel to the end of each line (depending on the direction of the
47235
47839
  * line).
47236
47840
  */
47237
- function drawBorder({ style, color }, x1, y1, x2, y2) {
47841
+ function drawBorder({ color, style, opacity }, x1, y1, x2, y2) {
47842
+ ctx.globalAlpha = opacity ?? 1;
47238
47843
  ctx.strokeStyle = color;
47239
47844
  switch (style) {
47240
47845
  case "medium":
@@ -47282,6 +47887,7 @@ stores.inject(MyMetaStore, storeInstance);
47282
47887
  ctx.stroke();
47283
47888
  ctx.lineWidth = 1;
47284
47889
  ctx.setLineDash([]);
47890
+ ctx.globalAlpha = 1;
47285
47891
  }
47286
47892
  }
47287
47893
  drawTexts(renderingContext, boxes) {
@@ -47290,6 +47896,7 @@ stores.inject(MyMetaStore, storeInstance);
47290
47896
  let currentFont;
47291
47897
  for (const box of boxes) {
47292
47898
  if (box.content) {
47899
+ ctx.globalAlpha = box.textOpacity ?? 1;
47293
47900
  const style = box.style || {};
47294
47901
  const align = box.content.align || "left";
47295
47902
  // compute font and textColor
@@ -47320,6 +47927,7 @@ stores.inject(MyMetaStore, storeInstance);
47320
47927
  if (box.clipRect) {
47321
47928
  ctx.restore();
47322
47929
  }
47930
+ ctx.globalAlpha = 1;
47323
47931
  }
47324
47932
  }
47325
47933
  }
@@ -47337,10 +47945,11 @@ stores.inject(MyMetaStore, storeInstance);
47337
47945
  }
47338
47946
  ctx.save();
47339
47947
  ctx.beginPath();
47340
- ctx.rect(box.x, box.y, box.width, box.height);
47948
+ const clipRect = icon.clipRect || box;
47949
+ ctx.rect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);
47341
47950
  ctx.clip();
47342
47951
  const iconSize = icon.size;
47343
- const { x, y } = this.getters.getCellIconRect(icon);
47952
+ const { x, y } = this.getters.getCellIconRect(icon, box);
47344
47953
  ctx.translate(x, y);
47345
47954
  ctx.scale(iconSize / svg.width, iconSize / svg.height);
47346
47955
  for (const path of svg.paths) {
@@ -47549,7 +48158,6 @@ stores.inject(MyMetaStore, storeInstance);
47549
48158
  const cell = this.getters.getEvaluatedCell(position);
47550
48159
  const showFormula = this.getters.shouldShowFormulas();
47551
48160
  const { x, y, width, height } = this.getters.getRect(zone);
47552
- const { verticalAlign } = this.getters.getCellStyle(position);
47553
48161
  const chipStyle = this.getters.getDataValidationChipStyle(position);
47554
48162
  let style = this.getters.getCellComputedStyle(position);
47555
48163
  if (this.fingerprints.isEnabled) {
@@ -47569,6 +48177,7 @@ stores.inject(MyMetaStore, storeInstance);
47569
48177
  center: iconsList.find((icon) => icon?.horizontalAlign === "center"),
47570
48178
  };
47571
48179
  const box = {
48180
+ id: zoneToXc(zone),
47572
48181
  x,
47573
48182
  y,
47574
48183
  width,
@@ -47576,11 +48185,11 @@ stores.inject(MyMetaStore, storeInstance);
47576
48185
  border: this.getters.getCellComputedBorder(position) || undefined,
47577
48186
  style,
47578
48187
  dataBarFill,
47579
- verticalAlign,
47580
48188
  overlayColor: this.hoveredTables.overlayColors.get(position),
47581
48189
  isError: (cell.type === CellValueType.error && !!cell.message) ||
47582
48190
  this.getters.isDataValidationInvalid(position),
47583
48191
  icons: cellIcons,
48192
+ disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame.some((z) => isZoneInside(zone, z) || overlap(zone, z)),
47584
48193
  };
47585
48194
  const fontSizePX = computeTextFontSizeInPixels(box.style);
47586
48195
  if (cell.type === CellValueType.empty || box.icons.center) {
@@ -47750,6 +48359,77 @@ stores.inject(MyMetaStore, storeInstance);
47750
48359
  }
47751
48360
  return boxes;
47752
48361
  }
48362
+ getBoxesWithAnimations(boxes, oldBoxes, timeStamp) {
48363
+ this.updateAnimationsProgress(timeStamp);
48364
+ this.addNewAnimations(boxes, oldBoxes, timeStamp);
48365
+ if (this.animations.size > 0) {
48366
+ this.renderer.startAnimation("grid_renderer_animation");
48367
+ return this.updateBoxesWithAnimations(boxes);
48368
+ }
48369
+ else {
48370
+ this.renderer.stopAnimation("grid_renderer_animation");
48371
+ return boxes;
48372
+ }
48373
+ }
48374
+ updateBoxesWithAnimations(boxes) {
48375
+ const boxesWithAnimations = [];
48376
+ for (const box of boxes) {
48377
+ const animation = this.animations.get(box.id);
48378
+ if (!animation) {
48379
+ boxesWithAnimations.push(box);
48380
+ continue;
48381
+ }
48382
+ const animatedBox = deepCopy(box);
48383
+ boxesWithAnimations.push(animatedBox);
48384
+ for (const animationId of animation.animationTypes) {
48385
+ const animationItem = cellAnimationRegistry.get(animationId);
48386
+ const newBoxes = animationItem.updateAnimation(animation.progress, animatedBox, animation.oldBox, box);
48387
+ if (newBoxes) {
48388
+ boxesWithAnimations.push(...newBoxes.newBoxes);
48389
+ }
48390
+ }
48391
+ }
48392
+ return boxesWithAnimations;
48393
+ }
48394
+ updateAnimationsProgress(timeStamp) {
48395
+ if (timeStamp === undefined) {
48396
+ return;
48397
+ }
48398
+ for (const boxId of this.animations.keys()) {
48399
+ const animation = this.animations.get(boxId);
48400
+ if (animation.startTime === undefined) {
48401
+ animation.startTime = timeStamp;
48402
+ continue;
48403
+ }
48404
+ const elapsedTime = timeStamp - animation.startTime;
48405
+ const progress = Math.min(1, elapsedTime / CELL_ANIMATION_DURATION);
48406
+ if (progress >= 1) {
48407
+ this.animations.delete(boxId);
48408
+ }
48409
+ animation.progress = progress;
48410
+ }
48411
+ }
48412
+ addNewAnimations(boxes, oldBoxes, timeStamp) {
48413
+ for (const box of boxes) {
48414
+ this.lastRenderBoxes.set(box.id, box);
48415
+ const oldBox = oldBoxes.get(box.id);
48416
+ if (this.preventNewAnimationsInNextFrame || !oldBox || box.disabledAnimation) {
48417
+ continue;
48418
+ }
48419
+ const animationTypes = [];
48420
+ for (const animationItem of cellAnimationRegistry.getAll()) {
48421
+ if (animationItem.hasAnimation(oldBox, box)) {
48422
+ animationTypes.push(animationItem.id);
48423
+ }
48424
+ }
48425
+ const animation = animationTypes.length > 0
48426
+ ? { animationTypes, oldBox, progress: 0, startTime: timeStamp }
48427
+ : undefined;
48428
+ if (animation) {
48429
+ this.animations.set(box.id, animation);
48430
+ }
48431
+ }
48432
+ }
47753
48433
  }
47754
48434
 
47755
48435
  function useGridDrawing(refName, model, canvasSize) {
@@ -47784,10 +48464,7 @@ stores.inject(MyMetaStore, storeInstance);
47784
48464
  // http://diveintohtml5.info/canvas.html#pixel-madness
47785
48465
  ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT);
47786
48466
  ctx.scale(dpr, dpr);
47787
- for (const layer of OrderedLayers()) {
47788
- model.drawLayer(renderingContext, layer);
47789
- rendererStore.drawLayer(renderingContext, layer);
47790
- }
48467
+ rendererStore.draw(renderingContext);
47791
48468
  }
47792
48469
  }
47793
48470
 
@@ -54882,7 +55559,7 @@ stores.inject(MyMetaStore, storeInstance);
54882
55559
  }
54883
55560
  getTypeFromZone(sheetId, zone) {
54884
55561
  const cells = this.getters.getEvaluatedCellsInZone(sheetId, zone);
54885
- const nonEmptyCells = cells.filter((cell) => cell.type !== CellValueType.empty);
55562
+ const nonEmptyCells = cells.filter((cell) => !(cell.type === CellValueType.empty || cell.value === ""));
54886
55563
  if (nonEmptyCells.length === 0) {
54887
55564
  return "integer";
54888
55565
  }
@@ -55490,7 +56167,7 @@ stores.inject(MyMetaStore, storeInstance);
55490
56167
  `;
55491
56168
  class SettingsPanel extends owl.Component {
55492
56169
  static template = "o-spreadsheet-SettingsPanel";
55493
- static components = { Section, ValidationMessages };
56170
+ static components = { Section, ValidationMessages, BadgeSelection };
55494
56171
  static props = { onCloseSidePanel: Function };
55495
56172
  loadedLocales = [];
55496
56173
  setup() {
@@ -56374,49 +57051,111 @@ stores.inject(MyMetaStore, storeInstance);
56374
57051
  }
56375
57052
 
56376
57053
  const DEFAULT_SIDE_PANEL_SIZE = 350;
57054
+ const COLLAPSED_SIDE_PANEL_SIZE = 45;
56377
57055
  const MIN_SHEET_VIEW_WIDTH = 150;
56378
57056
  class SidePanelStore extends SpreadsheetStore {
56379
- mutators = ["open", "toggle", "close", "changePanelSize", "resetPanelSize"];
56380
- initialPanelProps = {};
56381
- componentTag = "";
56382
- panelSize = DEFAULT_SIDE_PANEL_SIZE;
57057
+ mutators = [
57058
+ "open",
57059
+ "toggle",
57060
+ "close",
57061
+ "changePanelSize",
57062
+ "resetPanelSize",
57063
+ "togglePinPanel",
57064
+ "closeMainPanel",
57065
+ "changeSpreadsheetWidth",
57066
+ "toggleCollapsePanel",
57067
+ ];
57068
+ mainPanel = undefined;
57069
+ secondaryPanel;
57070
+ availableWidth = 0;
56383
57071
  screenWidthStore = this.get(ScreenWidthStore);
56384
- get isOpen() {
56385
- if (!this.componentTag) {
56386
- return false;
56387
- }
56388
- return this.computeState(this.componentTag, this.initialPanelProps).isOpen;
57072
+ get isMainPanelOpen() {
57073
+ return this.mainPanel && this.mainPanel.componentTag
57074
+ ? this.computeState(this.mainPanel).isOpen
57075
+ : false;
57076
+ }
57077
+ get isSecondaryPanelOpen() {
57078
+ return this.secondaryPanel && this.secondaryPanel.componentTag
57079
+ ? this.computeState(this.secondaryPanel).isOpen
57080
+ : false;
57081
+ }
57082
+ get mainPanelProps() {
57083
+ return this.mainPanel ? this.getPanelProps(this.mainPanel) : undefined;
57084
+ }
57085
+ get mainPanelKey() {
57086
+ return this.mainPanel ? this.getPanelKey(this.mainPanel) : undefined;
56389
57087
  }
56390
- get panelProps() {
56391
- const state = this.computeState(this.componentTag, this.initialPanelProps);
57088
+ get secondaryPanelProps() {
57089
+ return this.secondaryPanel ? this.getPanelProps(this.secondaryPanel) : undefined;
57090
+ }
57091
+ get secondaryPanelKey() {
57092
+ return this.secondaryPanel ? this.getPanelKey(this.secondaryPanel) : undefined;
57093
+ }
57094
+ get totalPanelSize() {
57095
+ return (this.mainPanel?.size || 0) + (this.secondaryPanel?.size ?? 0);
57096
+ }
57097
+ getPanelProps(panelInfo) {
57098
+ const state = this.computeState(panelInfo);
56392
57099
  if (state.isOpen) {
56393
57100
  return state.props ?? {};
56394
57101
  }
56395
57102
  return {};
56396
57103
  }
56397
- get panelKey() {
56398
- const state = this.computeState(this.componentTag, this.initialPanelProps);
57104
+ getPanelKey(panelInfo) {
57105
+ const state = this.computeState(panelInfo);
56399
57106
  if (state.isOpen) {
56400
57107
  return state.key;
56401
57108
  }
56402
57109
  return undefined;
56403
57110
  }
56404
- open(componentTag, panelProps = {}) {
57111
+ open(componentTag, initialPanelProps = {}) {
56405
57112
  if (this.screenWidthStore.isSmall) {
56406
57113
  return;
56407
57114
  }
56408
- const state = this.computeState(componentTag, panelProps);
57115
+ const newPanelInfo = { initialPanelProps, componentTag, size: DEFAULT_SIDE_PANEL_SIZE };
57116
+ const state = this.computeState(newPanelInfo);
56409
57117
  if (!state.isOpen) {
56410
57118
  return;
56411
57119
  }
56412
- if (this.isOpen && componentTag !== this.componentTag) {
56413
- this.initialPanelProps?.onCloseSidePanel?.();
57120
+ const mainPanelKey = this.mainPanel ? this.getPanelKey(this.mainPanel) : undefined;
57121
+ if (!this.mainPanel || !this.mainPanel.isPinned || mainPanelKey === state.key) {
57122
+ this._openPanel("mainPanel", newPanelInfo, state);
57123
+ return;
57124
+ }
57125
+ // Try to open secondary panel if main panel is pinned
57126
+ const nonCollapsedPanelSize = this.mainPanel.isCollapsed
57127
+ ? DEFAULT_SIDE_PANEL_SIZE
57128
+ : this.mainPanel.size;
57129
+ if (!this.secondaryPanel &&
57130
+ nonCollapsedPanelSize + DEFAULT_SIDE_PANEL_SIZE > this.availableWidth) {
57131
+ this.get(NotificationStore).notifyUser({
57132
+ sticky: false,
57133
+ type: "warning",
57134
+ text: _t("The window is too small to display multiple side panels."),
57135
+ });
57136
+ return;
57137
+ }
57138
+ this._openPanel("secondaryPanel", newPanelInfo, state);
57139
+ }
57140
+ _openPanel(panel, newPanel, state) {
57141
+ const currentPanel = this[panel];
57142
+ if (currentPanel && newPanel.componentTag !== currentPanel.componentTag) {
57143
+ currentPanel.initialPanelProps?.onCloseSidePanel?.();
57144
+ }
57145
+ this[panel] = {
57146
+ initialPanelProps: state.props ?? {},
57147
+ componentTag: newPanel.componentTag,
57148
+ size: currentPanel?.size || DEFAULT_SIDE_PANEL_SIZE,
57149
+ isCollapsed: currentPanel?.isCollapsed || false,
57150
+ isPinned: currentPanel && "isPinned" in currentPanel ? currentPanel.isPinned : false,
57151
+ };
57152
+ if (this[panel].isCollapsed) {
57153
+ this.toggleCollapsePanel(panel);
56414
57154
  }
56415
- this.componentTag = componentTag;
56416
- this.initialPanelProps = state.props ?? {};
56417
57155
  }
56418
57156
  toggle(componentTag, panelProps) {
56419
- if (this.isOpen && componentTag === this.componentTag) {
57157
+ const panel = this.mainPanel?.isPinned ? this.secondaryPanel : this.mainPanel;
57158
+ if (panel && componentTag === panel.componentTag) {
56420
57159
  this.close();
56421
57160
  }
56422
57161
  else {
@@ -56424,34 +57163,85 @@ stores.inject(MyMetaStore, storeInstance);
56424
57163
  }
56425
57164
  }
56426
57165
  close() {
56427
- this.initialPanelProps.onCloseSidePanel?.();
56428
- this.initialPanelProps = {};
56429
- this.componentTag = "";
57166
+ if (this.mainPanel?.isPinned) {
57167
+ if (this.secondaryPanel) {
57168
+ this.secondaryPanel.initialPanelProps.onCloseSidePanel?.();
57169
+ this.secondaryPanel = undefined;
57170
+ }
57171
+ return;
57172
+ }
57173
+ this.mainPanel?.initialPanelProps.onCloseSidePanel?.();
57174
+ this.mainPanel = undefined;
56430
57175
  }
56431
- changePanelSize(size, spreadsheetElWidth) {
56432
- if (size < DEFAULT_SIDE_PANEL_SIZE) {
56433
- this.panelSize = DEFAULT_SIDE_PANEL_SIZE;
57176
+ closeMainPanel() {
57177
+ this.mainPanel?.initialPanelProps.onCloseSidePanel?.();
57178
+ this.mainPanel = this.secondaryPanel || undefined;
57179
+ this.secondaryPanel = undefined;
57180
+ }
57181
+ changePanelSize(panel, size) {
57182
+ const panelInfo = this[panel];
57183
+ if (!panelInfo || ("isCollapsed" in panelInfo && panelInfo.isCollapsed)) {
57184
+ return;
56434
57185
  }
56435
- else if (size > spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH) {
56436
- this.panelSize = Math.max(spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH, DEFAULT_SIDE_PANEL_SIZE);
57186
+ size = Math.max(size, DEFAULT_SIDE_PANEL_SIZE);
57187
+ let otherPanelSize = panel === "mainPanel" ? this.secondaryPanel?.size || 0 : this.mainPanel?.size || 0;
57188
+ if (size > this.availableWidth - otherPanelSize) {
57189
+ if (panel === "mainPanel" && this.secondaryPanel) {
57190
+ // reduce the secondary panel size to fit the main panel
57191
+ this.secondaryPanel.size = Math.max(this.availableWidth - size, DEFAULT_SIDE_PANEL_SIZE);
57192
+ otherPanelSize = this.secondaryPanel.size;
57193
+ }
57194
+ size = Math.max(this.availableWidth - otherPanelSize, DEFAULT_SIDE_PANEL_SIZE);
56437
57195
  }
56438
- else {
56439
- this.panelSize = size;
57196
+ panelInfo.size = size;
57197
+ }
57198
+ resetPanelSize(panel) {
57199
+ const panelInfo = this[panel];
57200
+ if (!panelInfo) {
57201
+ return;
56440
57202
  }
57203
+ panelInfo.size = DEFAULT_SIDE_PANEL_SIZE;
56441
57204
  }
56442
- resetPanelSize() {
56443
- this.panelSize = DEFAULT_SIDE_PANEL_SIZE;
57205
+ togglePinPanel() {
57206
+ if (!this.mainPanel) {
57207
+ return;
57208
+ }
57209
+ this.mainPanel.isPinned = !this.mainPanel.isPinned;
57210
+ if (!this.mainPanel.isPinned && this.secondaryPanel) {
57211
+ this.secondaryPanel?.initialPanelProps.onCloseSidePanel?.();
57212
+ this.mainPanel = this.secondaryPanel;
57213
+ this.secondaryPanel = undefined;
57214
+ }
56444
57215
  }
56445
- computeState(componentTag, panelProps) {
56446
- const customComputeState = sidePanelRegistry.get(componentTag).computeState;
56447
- if (!customComputeState) {
56448
- return {
56449
- isOpen: true,
56450
- props: panelProps,
56451
- };
57216
+ toggleCollapsePanel(panel) {
57217
+ const panelInfo = this[panel];
57218
+ if (!panelInfo) {
57219
+ return;
57220
+ }
57221
+ if (panelInfo.isCollapsed) {
57222
+ panelInfo.isCollapsed = false;
57223
+ this.changePanelSize(panel, DEFAULT_SIDE_PANEL_SIZE);
56452
57224
  }
56453
57225
  else {
56454
- return customComputeState(this.getters, panelProps);
57226
+ panelInfo.isCollapsed = true;
57227
+ panelInfo.size = COLLAPSED_SIDE_PANEL_SIZE;
57228
+ }
57229
+ }
57230
+ computeState({ componentTag, initialPanelProps }) {
57231
+ const customComputeState = sidePanelRegistry.get(componentTag).computeState;
57232
+ const state = customComputeState
57233
+ ? customComputeState(this.getters, initialPanelProps)
57234
+ : { isOpen: true, props: initialPanelProps };
57235
+ return state.isOpen ? { ...state, key: state.key || componentTag } : state;
57236
+ }
57237
+ changeSpreadsheetWidth(width) {
57238
+ this.availableWidth = width - MIN_SHEET_VIEW_WIDTH;
57239
+ if (this.secondaryPanel && width - this.totalPanelSize < MIN_SHEET_VIEW_WIDTH) {
57240
+ this.secondaryPanel?.initialPanelProps.onCloseSidePanel?.();
57241
+ this.secondaryPanel = undefined;
57242
+ }
57243
+ if (this.mainPanel && width - this.totalPanelSize < MIN_SHEET_VIEW_WIDTH) {
57244
+ this.mainPanel.size = Math.max(width - MIN_SHEET_VIEW_WIDTH, DEFAULT_SIDE_PANEL_SIZE);
56455
57245
  }
56456
57246
  }
56457
57247
  }
@@ -56599,11 +57389,11 @@ stores.inject(MyMetaStore, storeInstance);
56599
57389
  this.hoveredCell.clear();
56600
57390
  });
56601
57391
  this.cellPopovers = useStore(CellPopoverStore);
56602
- owl.useEffect(() => {
56603
- if (!this.sidePanel.isOpen) {
57392
+ owl.useEffect((isMainPanelOpen, isSecondaryPanelOpen) => {
57393
+ if (!isMainPanelOpen && !isSecondaryPanelOpen) {
56604
57394
  this.DOMFocusableElementStore.focus();
56605
57395
  }
56606
- }, () => [this.sidePanel.isOpen]);
57396
+ }, () => [this.sidePanel.isMainPanelOpen, this.sidePanel.isSecondaryPanelOpen]);
56607
57397
  useTouchScroll(this.gridRef, this.moveCanvas.bind(this), () => {
56608
57398
  const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
56609
57399
  return scrollY > 0;
@@ -59695,7 +60485,7 @@ stores.inject(MyMetaStore, storeInstance);
59695
60485
  else if (newRule.criterion.type === "isValueInList") {
59696
60486
  newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
59697
60487
  }
59698
- const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
60488
+ const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules, newRule.id);
59699
60489
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
59700
60490
  if (ruleIndex !== -1) {
59701
60491
  adaptedRules[ruleIndex] = newRule;
@@ -59705,9 +60495,12 @@ stores.inject(MyMetaStore, storeInstance);
59705
60495
  this.history.update("rules", sheetId, [...adaptedRules, newRule]);
59706
60496
  }
59707
60497
  }
59708
- removeRangesFromRules(sheetId, ranges, rules) {
60498
+ removeRangesFromRules(sheetId, ranges, rules, editingRuleId) {
59709
60499
  rules = deepCopy(rules);
59710
60500
  for (const rule of rules) {
60501
+ if (rule.id === editingRuleId) {
60502
+ continue; // Skip the rule being edited to preserve its place in the list
60503
+ }
59711
60504
  rule.ranges = this.getters.recomputeRanges(rule.ranges, ranges);
59712
60505
  }
59713
60506
  return rules.filter((rule) => rule.ranges.length > 0);
@@ -63115,7 +63908,7 @@ stores.inject(MyMetaStore, storeInstance);
63115
63908
  break;
63116
63909
  }
63117
63910
  case "UPDATE_PIVOT": {
63118
- this.history.update("pivots", cmd.pivotId, "definition", this.repairSortedColumn(deepCopy(cmd.pivot)));
63911
+ this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
63119
63912
  this.compileCalculatedMeasures(cmd.pivot.measures);
63120
63913
  break;
63121
63914
  }
@@ -63186,10 +63979,7 @@ stores.inject(MyMetaStore, storeInstance);
63186
63979
  // Private
63187
63980
  // -------------------------------------------------------------------------
63188
63981
  addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {
63189
- this.history.update("pivots", pivotId, {
63190
- definition: this.repairSortedColumn(deepCopy(pivot)),
63191
- formulaId,
63192
- });
63982
+ this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
63193
63983
  this.compileCalculatedMeasures(pivot.measures);
63194
63984
  this.history.update("formulaIds", formulaId, pivotId);
63195
63985
  this.history.update("nextFormulaId", this.nextFormulaId + 1);
@@ -63278,7 +64068,6 @@ stores.inject(MyMetaStore, storeInstance);
63278
64068
  }
63279
64069
  }
63280
64070
  checkSortedColumnInMeasures(definition) {
63281
- definition = this.repairSortedColumn(definition);
63282
64071
  const measures = definition.measures.map((measure) => measure.id);
63283
64072
  if (definition.sortedColumn && !measures.includes(definition.sortedColumn.measure)) {
63284
64073
  return "InvalidDefinition" /* CommandResult.InvalidDefinition */;
@@ -63292,26 +64081,6 @@ stores.inject(MyMetaStore, storeInstance);
63292
64081
  }
63293
64082
  return "Success" /* CommandResult.Success */;
63294
64083
  }
63295
- repairSortedColumn(definition) {
63296
- if (definition.sortedColumn) {
63297
- // Fix for an upgrade issue: the sortedColumn measure was not updated
63298
- // from using fieldName to using id. If the sortedColumn measure matches
63299
- // a measure fieldName in the definition, update it to use the measure's id instead
63300
- // of its fieldName.
63301
- // TODO: add an upgrade step to fix this in master and remove this code
63302
- const sortedMeasure = definition.measures.find((measure) => measure.fieldName === definition.sortedColumn?.measure);
63303
- if (sortedMeasure) {
63304
- return {
63305
- ...definition,
63306
- sortedColumn: {
63307
- ...definition.sortedColumn,
63308
- measure: sortedMeasure.id,
63309
- },
63310
- };
63311
- }
63312
- }
63313
- return definition;
63314
- }
63315
64084
  // ---------------------------------------------------------------------
63316
64085
  // Import/Export
63317
64086
  // ---------------------------------------------------------------------
@@ -63393,9 +64162,7 @@ stores.inject(MyMetaStore, storeInstance);
63393
64162
  this.locale = data.settings?.locale ?? DEFAULT_LOCALE;
63394
64163
  }
63395
64164
  export(data) {
63396
- data.settings = {
63397
- locale: this.locale,
63398
- };
64165
+ data.settings = { locale: this.locale };
63399
64166
  }
63400
64167
  }
63401
64168
 
@@ -66430,11 +67197,8 @@ stores.inject(MyMetaStore, storeInstance);
66430
67197
  }
66431
67198
  return this.cellIconsCache[position.sheetId][position.col][position.row];
66432
67199
  }
66433
- getCellIconRect(icon) {
67200
+ getCellIconRect(icon, cellRect) {
66434
67201
  const cellPosition = icon.position;
66435
- const merge = this.getters.getMerge(cellPosition);
66436
- const zone = merge || positionToZone(cellPosition);
66437
- const cellRect = this.getters.getRect(zone);
66438
67202
  const cell = this.getters.getCell(cellPosition);
66439
67203
  const x = this.getIconHorizontalPosition(cellRect, icon.horizontalAlign, icon);
66440
67204
  const y = this.getters.computeTextYCoordinate(cellRect, icon.size, cell?.style?.verticalAlign);
@@ -72267,49 +73031,17 @@ stores.inject(MyMetaStore, storeInstance);
72267
73031
  if (!copiedData) {
72268
73032
  return;
72269
73033
  }
72270
- let zone = undefined;
72271
- const selectedZones = [];
72272
73034
  const sheetId = this.getters.getActiveSheetId();
72273
- const target = {
72274
- sheetId,
72275
- zones,
72276
- };
72277
73035
  const handlers = this.selectClipboardHandlers(copiedData);
72278
- for (const { handlerName, handler } of handlers) {
72279
- const handlerData = copiedData[handlerName];
72280
- if (!handlerData) {
72281
- continue;
72282
- }
72283
- const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);
72284
- if (currentTarget.figureId) {
72285
- target.figureId = currentTarget.figureId;
72286
- }
72287
- for (const targetZone of currentTarget.zones) {
72288
- selectedZones.push(targetZone);
72289
- if (zone === undefined) {
72290
- zone = targetZone;
72291
- continue;
72292
- }
72293
- zone = union(zone, targetZone);
72294
- }
72295
- }
73036
+ const { target, zone, selectedZones } = getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options);
72296
73037
  if (zone !== undefined) {
72297
- this.addMissingDimensions(this.getters.getActiveSheetId(), zone.right - zone.left + 1, zone.bottom - zone.top + 1, zone.left, zone.top);
73038
+ this.addMissingDimensions(sheetId, zone.right - zone.left + 1, zone.bottom - zone.top + 1, zone.left, zone.top);
72298
73039
  }
72299
- handlers.forEach(({ handlerName, handler }) => {
72300
- const handlerData = copiedData[handlerName];
72301
- if (handlerData) {
72302
- handler.paste(target, handlerData, options);
72303
- }
72304
- });
73040
+ applyClipboardHandlersPaste(handlers, copiedData, target, options);
72305
73041
  if (!options?.selectTarget) {
72306
73042
  return;
72307
73043
  }
72308
- const selection = zones[0];
72309
- const col = selection.left;
72310
- const row = selection.top;
72311
- this.selection.getBackToDefault();
72312
- this.selection.selectZone({ cell: { col, row }, zone: union(...selectedZones) }, { scrollIntoView: false });
73044
+ selectPastedZone(this.selection, zones, selectedZones);
72313
73045
  }
72314
73046
  /**
72315
73047
  * Add columns and/or rows to ensure that col + width and row + height are still
@@ -75138,6 +75870,17 @@ stores.inject(MyMetaStore, storeInstance);
75138
75870
  return !!getters.getEvaluatedCell(position).link;
75139
75871
  },
75140
75872
  execute: (position, env, isMiddleClick) => openLink(env.model.getters.getEvaluatedCell(position).link, env, isMiddleClick),
75873
+ title: (position, getters) => {
75874
+ const link = getters.getEvaluatedCell(position).link;
75875
+ if (!link)
75876
+ return "";
75877
+ if (link.isExternal) {
75878
+ return _t("Go to url: %(url)s", { url: link.url });
75879
+ }
75880
+ else {
75881
+ return _t("Go to %(label)s", { label: link.label });
75882
+ }
75883
+ },
75141
75884
  sequence: 5,
75142
75885
  });
75143
75886
 
@@ -76849,12 +77592,13 @@ stores.inject(MyMetaStore, storeInstance);
76849
77592
  if (!item) {
76850
77593
  continue;
76851
77594
  }
77595
+ const title = typeof item.title === "function" ? item.title(position, getters) : item.title;
76852
77596
  const zone = getters.expandZone(sheetId, positionToZone(position));
76853
77597
  cells.push({
76854
77598
  coordinates: getters.getVisibleRect(zone),
76855
77599
  position,
76856
77600
  action: item.execute,
76857
- title: item.title || "",
77601
+ title: title || "",
76858
77602
  });
76859
77603
  }
76860
77604
  return cells;
@@ -77239,24 +77983,36 @@ stores.inject(MyMetaStore, storeInstance);
77239
77983
  user-select: none;
77240
77984
  color: ${TEXT_BODY};
77241
77985
 
77986
+ &.collapsed {
77987
+ padding: 8px;
77988
+ cursor: pointer;
77989
+
77990
+ .o-sidePanelTitle {
77991
+ writing-mode: vertical-rl;
77992
+ text-orientation: mixed;
77993
+ }
77994
+ }
77995
+
77242
77996
  .o-sidePanelTitle {
77243
77997
  line-height: 20px;
77244
77998
  font-size: 16px;
77245
77999
  }
77246
78000
 
77247
78001
  .o-sidePanelHeader {
77248
- padding: 8px 16px;
77249
- display: flex;
77250
- align-items: center;
77251
- justify-content: space-between;
78002
+ padding: 8px;
77252
78003
  border-bottom: 1px solid ${GRAY_300};
78004
+ }
77253
78005
 
77254
- .o-sidePanelClose {
77255
- padding: 5px 10px;
77256
- cursor: pointer;
77257
- &:hover {
77258
- background-color: WhiteSmoke;
77259
- }
78006
+ .o-sidePanelAction {
78007
+ padding: 5px 10px;
78008
+ cursor: pointer;
78009
+
78010
+ &.active {
78011
+ background-color: ${BUTTON_ACTIVE_BG};
78012
+ }
78013
+
78014
+ &:hover {
78015
+ background-color: ${BUTTON_HOVER_BG};
77260
78016
  }
77261
78017
  }
77262
78018
  .o-sidePanelBody-container {
@@ -77333,43 +78089,114 @@ stores.inject(MyMetaStore, storeInstance);
77333
78089
  `;
77334
78090
  class SidePanel extends owl.Component {
77335
78091
  static template = "o-spreadsheet-SidePanel";
78092
+ static props = {
78093
+ panelContent: Object,
78094
+ panelProps: Object,
78095
+ onCloseSidePanel: Function,
78096
+ onStartHandleDrag: Function,
78097
+ onResetPanelSize: Function,
78098
+ isPinned: { type: Boolean, optional: true },
78099
+ onTogglePinPanel: { type: Function, optional: true },
78100
+ onToggleCollapsePanel: { type: Function, optional: true },
78101
+ isCollapsed: { type: Boolean, optional: true },
78102
+ };
78103
+ spreadsheetRect = useSpreadsheetRect();
78104
+ getTitle() {
78105
+ const panel = this.props.panelContent;
78106
+ return typeof panel.title === "function"
78107
+ ? panel.title(this.env, this.props.panelProps)
78108
+ : panel.title;
78109
+ }
78110
+ get pinInfoMessage() {
78111
+ return _t("Pin this panel to allow to open another side panel beside it.");
78112
+ }
78113
+ }
78114
+
78115
+ class SidePanels extends owl.Component {
78116
+ static template = "o-spreadsheet-SidePanels";
77336
78117
  static props = {};
78118
+ static components = { SidePanel };
77337
78119
  sidePanelStore;
77338
78120
  spreadsheetRect = useSpreadsheetRect();
77339
78121
  setup() {
77340
78122
  this.sidePanelStore = useStore(SidePanelStore);
77341
- owl.useEffect((isOpen) => {
77342
- if (!isOpen) {
78123
+ owl.useEffect(() => {
78124
+ if (this.sidePanelStore.mainPanel && !this.sidePanelStore.isMainPanelOpen) {
78125
+ this.sidePanelStore.closeMainPanel();
78126
+ }
78127
+ if (this.sidePanelStore.secondaryPanel && !this.sidePanelStore.isSecondaryPanelOpen) {
77343
78128
  this.sidePanelStore.close();
77344
78129
  }
77345
- }, () => [this.sidePanelStore.isOpen]);
77346
- }
77347
- get panel() {
77348
- return sidePanelRegistry.get(this.sidePanelStore.componentTag);
77349
- }
77350
- close() {
77351
- this.sidePanelStore.close();
78130
+ }, () => [this.sidePanelStore.isMainPanelOpen, this.sidePanelStore.isSecondaryPanelOpen]);
77352
78131
  }
77353
- getTitle() {
77354
- const panel = this.panel;
77355
- return typeof panel.title === "function"
77356
- ? panel.title(this.env, this.sidePanelStore.panelProps)
77357
- : panel.title;
77358
- }
77359
- startHandleDrag(ev) {
78132
+ startHandleDrag(panel, ev) {
77360
78133
  const startingCursor = document.body.style.cursor;
77361
- const startSize = this.sidePanelStore.panelSize;
78134
+ const panelInfo = panel === "mainPanel" ? this.sidePanelStore.mainPanel : this.sidePanelStore.secondaryPanel;
78135
+ if (!panelInfo) {
78136
+ return;
78137
+ }
78138
+ const startSize = panelInfo.size;
77362
78139
  const startPosition = ev.clientX;
77363
78140
  const onMouseMove = (ev) => {
77364
78141
  document.body.style.cursor = "col-resize";
77365
78142
  const newSize = startSize + startPosition - ev.clientX;
77366
- this.sidePanelStore.changePanelSize(newSize, this.spreadsheetRect.width);
78143
+ this.sidePanelStore.changePanelSize(panel, newSize);
77367
78144
  };
77368
78145
  const cleanUp = () => {
77369
78146
  document.body.style.cursor = startingCursor;
77370
78147
  };
77371
78148
  startDnd(onMouseMove, cleanUp);
77372
78149
  }
78150
+ get mainPanelProps() {
78151
+ const panelProps = this.sidePanelStore.mainPanelProps;
78152
+ if (!this.sidePanelStore.mainPanel || !panelProps) {
78153
+ return undefined;
78154
+ }
78155
+ return {
78156
+ panelContent: sidePanelRegistry.get(this.sidePanelStore.mainPanel.componentTag),
78157
+ panelProps,
78158
+ onCloseSidePanel: () => this.sidePanelStore.closeMainPanel(),
78159
+ onTogglePinPanel: () => this.sidePanelStore.togglePinPanel(),
78160
+ onStartHandleDrag: (ev) => this.startHandleDrag("mainPanel", ev),
78161
+ onResetPanelSize: () => this.sidePanelStore.resetPanelSize("mainPanel"),
78162
+ isPinned: this.sidePanelStore.mainPanel?.isPinned,
78163
+ onToggleCollapsePanel: () => this.sidePanelStore.toggleCollapsePanel("mainPanel"),
78164
+ isCollapsed: this.sidePanelStore.mainPanel?.isCollapsed,
78165
+ };
78166
+ }
78167
+ get secondaryPanelProps() {
78168
+ const panelProps = this.sidePanelStore.secondaryPanelProps;
78169
+ if (!this.sidePanelStore.secondaryPanel || !panelProps) {
78170
+ return undefined;
78171
+ }
78172
+ return {
78173
+ panelContent: sidePanelRegistry.get(this.sidePanelStore.secondaryPanel.componentTag),
78174
+ panelProps,
78175
+ onCloseSidePanel: () => this.sidePanelStore.close(),
78176
+ onStartHandleDrag: (ev) => this.startHandleDrag("secondaryPanel", ev),
78177
+ onResetPanelSize: () => this.sidePanelStore.resetPanelSize("secondaryPanel"),
78178
+ onToggleCollapsePanel: () => this.sidePanelStore.toggleCollapsePanel("secondaryPanel"),
78179
+ isCollapsed: this.sidePanelStore.secondaryPanel?.isCollapsed,
78180
+ };
78181
+ }
78182
+ get panelList() {
78183
+ return [
78184
+ {
78185
+ key: this.sidePanelStore.secondaryPanelKey,
78186
+ props: this.secondaryPanelProps,
78187
+ style: this.sidePanelStore.secondaryPanel
78188
+ ? cssPropertiesToCss({ width: `${this.sidePanelStore.secondaryPanel.size}px` })
78189
+ : "",
78190
+ },
78191
+ {
78192
+ key: this.sidePanelStore.mainPanelKey,
78193
+ props: this.mainPanelProps,
78194
+ style: this.sidePanelStore.mainPanel
78195
+ ? cssPropertiesToCss({ width: `${this.sidePanelStore.mainPanel.size}px` })
78196
+ : "",
78197
+ },
78198
+ ].filter((panel) => panel.key && panel.props);
78199
+ }
77373
78200
  }
77374
78201
 
77375
78202
  class RibbonMenu extends owl.Component {
@@ -78931,7 +79758,7 @@ stores.inject(MyMetaStore, storeInstance);
78931
79758
  Grid,
78932
79759
  BottomBar,
78933
79760
  SmallBottomBar,
78934
- SidePanel,
79761
+ SidePanels,
78935
79762
  SpreadsheetDashboard,
78936
79763
  HeaderGroupContainer,
78937
79764
  FullScreenChart,
@@ -78954,7 +79781,9 @@ stores.inject(MyMetaStore, storeInstance);
78954
79781
  else {
78955
79782
  properties["grid-template-rows"] = `min-content auto min-content`;
78956
79783
  }
78957
- const columnWidth = this.sidePanel.isOpen ? `${this.sidePanel.panelSize}px` : "auto";
79784
+ const columnWidth = this.sidePanel.mainPanel
79785
+ ? `${this.sidePanel.totalPanelSize || DEFAULT_SIDE_PANEL_SIZE}px`
79786
+ : "auto";
78958
79787
  properties["grid-template-columns"] = `auto ${columnWidth}`;
78959
79788
  return cssPropertiesToCss(properties);
78960
79789
  }
@@ -79038,7 +79867,7 @@ stores.inject(MyMetaStore, storeInstance);
79038
79867
  this.checkViewportSize();
79039
79868
  });
79040
79869
  const resizeObserver = new ResizeObserver(() => {
79041
- this.sidePanel.changePanelSize(this.sidePanel.panelSize, this.spreadsheetRect.width);
79870
+ this.sidePanel.changeSpreadsheetWidth(this.spreadsheetRect.width);
79042
79871
  });
79043
79872
  }
79044
79873
  bindModelEvents() {
@@ -83635,9 +84464,9 @@ stores.inject(MyMetaStore, storeInstance);
83635
84464
  exports.tokenize = tokenize;
83636
84465
 
83637
84466
 
83638
- __info__.version = "18.4.0-alpha.9";
83639
- __info__.date = "2025-06-19T18:23:22.025Z";
83640
- __info__.hash = "6d4d685";
84467
+ __info__.version = "18.4.0";
84468
+ __info__.date = "2025-06-24T11:19:24.606Z";
84469
+ __info__.hash = "a5b7cad";
83641
84470
 
83642
84471
 
83643
84472
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);