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