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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.4.0-alpha.8
6
- * @date 2025-06-12T09:53:48.133Z
7
- * @hash 9b7a8d0
5
+ * @version 18.4.0
6
+ * @date 2025-06-24T11:19:24.606Z
7
+ * @hash a5b7cad
8
8
  */
9
9
 
10
10
  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';
@@ -20,13 +20,21 @@ function createAction(item) {
20
20
  const icon = item.icon;
21
21
  const secondaryIcon = item.secondaryIcon;
22
22
  const itemId = item.id || nextItemId++;
23
+ const isEnabled = item.isEnabled ? item.isEnabled : () => true;
23
24
  return {
24
25
  id: itemId.toString(),
25
26
  name: typeof name === "function" ? name : () => name,
26
27
  isVisible: item.isVisible ? item.isVisible : () => true,
27
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
28
+ isEnabled: isEnabled,
28
29
  isActive: item.isActive,
29
- execute: item.execute,
30
+ execute: item.execute
31
+ ? (env, isMiddleClick) => {
32
+ if (isEnabled(env)) {
33
+ return item.execute(env, isMiddleClick);
34
+ }
35
+ return undefined;
36
+ }
37
+ : undefined,
30
38
  children: children
31
39
  ? (env) => {
32
40
  return children
@@ -154,6 +162,7 @@ const FROZEN_PANE_HEADER_BORDER_COLOR = "#BCBCBC";
154
162
  const FROZEN_PANE_BORDER_COLOR = "#DADFE8";
155
163
  const COMPOSER_ASSISTANT_COLOR = "#9B359B";
156
164
  const COLOR_TRANSPARENT = "#00000000";
165
+ const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414";
157
166
  const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2";
158
167
  const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175";
159
168
  const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA";
@@ -297,6 +306,7 @@ const GROUP_LAYER_WIDTH = 21;
297
306
  const GRID_ICON_MARGIN = 2;
298
307
  const GRID_ICON_EDGE_LENGTH = 17;
299
308
  const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT;
309
+ const DATA_VALIDATION_CHIP_MARGIN = 5;
300
310
  // 768px is a common breakpoint for small screens
301
311
  // Typically inside Odoo, it is the threshold for switching to mobile view
302
312
  const MOBILE_WIDTH_BREAKPOINT = 768;
@@ -642,9 +652,6 @@ function parseSheetUrl(sheetLink) {
642
652
  function isDefined(argument) {
643
653
  return argument !== undefined;
644
654
  }
645
- function isNotNull(argument) {
646
- return argument !== null;
647
- }
648
655
  /**
649
656
  * Check if all the values of an object, and all the values of the objects inside of it, are undefined.
650
657
  */
@@ -1355,9 +1362,16 @@ function darkenColor(color, percentage) {
1355
1362
  if (percentage === 1) {
1356
1363
  return "#000";
1357
1364
  }
1365
+ // increase saturation to compensate and make it more vivid
1366
+ hsla.s = Math.min(100, percentage * hsla.s + hsla.s);
1358
1367
  hsla.l = hsla.l - percentage * hsla.l;
1359
1368
  return hslaToHex(hsla);
1360
1369
  }
1370
+ function chipTextColor(chipBackgroundColor) {
1371
+ return relativeLuminance(chipBackgroundColor) < 0.6
1372
+ ? lightenColor(chipBackgroundColor, 0.9)
1373
+ : darkenColor(chipBackgroundColor, 0.75);
1374
+ }
1361
1375
  const COLORS_SM = [
1362
1376
  "#4EA7F2", // Blue
1363
1377
  "#EA6175", // Red
@@ -6834,19 +6848,17 @@ function getDefaultContextFont(fontSize, bold = false, italic = false) {
6834
6848
  const textWidthCache = {};
6835
6849
  function computeTextWidth(context, text, style, fontUnit = "pt") {
6836
6850
  const font = computeTextFont(style, fontUnit);
6837
- context.save();
6838
- context.font = font;
6839
- const width = computeCachedTextWidth(context, text);
6840
- context.restore();
6841
- return width;
6851
+ return computeCachedTextWidth(context, text, font);
6842
6852
  }
6843
- function computeCachedTextWidth(context, text) {
6844
- const font = context.font;
6853
+ function computeCachedTextWidth(context, text, font) {
6845
6854
  if (!textWidthCache[font]) {
6846
6855
  textWidthCache[font] = {};
6847
6856
  }
6848
6857
  if (textWidthCache[font][text] === undefined) {
6858
+ const oldFont = context.font;
6859
+ context.font = font;
6849
6860
  textWidthCache[font][text] = context.measureText(text).width;
6861
+ context.font = oldFont;
6850
6862
  }
6851
6863
  return textWidthCache[font][text];
6852
6864
  }
@@ -7011,19 +7023,19 @@ function getContextFontSize(font) {
7011
7023
  }
7012
7024
  // Inspired from https://stackoverflow.com/a/10511598
7013
7025
  function clipTextWithEllipsis(ctx, text, maxWidth) {
7014
- let width = computeCachedTextWidth(ctx, text);
7026
+ let width = computeCachedTextWidth(ctx, text, ctx.font);
7015
7027
  if (width <= maxWidth) {
7016
7028
  return text;
7017
7029
  }
7018
7030
  const ellipsis = "…";
7019
- const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis);
7031
+ const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis, ctx.font);
7020
7032
  if (width <= ellipsisWidth) {
7021
7033
  return text;
7022
7034
  }
7023
7035
  let len = text.length;
7024
7036
  while (width >= maxWidth - ellipsisWidth && len-- > 0) {
7025
7037
  text = text.substring(0, len);
7026
- width = computeCachedTextWidth(ctx, text);
7038
+ width = computeCachedTextWidth(ctx, text, ctx.font);
7027
7039
  }
7028
7040
  return text + ellipsis;
7029
7041
  }
@@ -7237,6 +7249,63 @@ function parseOSClipboardContent(content, clipboardId) {
7237
7249
  };
7238
7250
  return osClipboardContent;
7239
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
+ };
7240
7309
 
7241
7310
  class ClipboardHandler {
7242
7311
  getters;
@@ -9941,8 +10010,15 @@ function getDependencyContainer(env) {
9941
10010
  const ModelStore = createAbstractStore("Model");
9942
10011
 
9943
10012
  class RendererStore {
9944
- mutators = ["register", "unRegister", "drawLayer"];
10013
+ mutators = ["register", "unRegister", "draw", "startAnimation", "stopAnimation"];
9945
10014
  renderers = {};
10015
+ model;
10016
+ context = undefined;
10017
+ animationFrameId = null;
10018
+ registeredAnimations = new Set();
10019
+ constructor(get) {
10020
+ this.model = get(ModelStore);
10021
+ }
9946
10022
  register(renderer) {
9947
10023
  if (!renderer.renderingLayers.length) {
9948
10024
  return;
@@ -9959,17 +10035,54 @@ class RendererStore {
9959
10035
  this.renderers[layer] = this.renderers[layer].filter((r) => r !== renderer);
9960
10036
  }
9961
10037
  }
9962
- drawLayer(context, layer) {
10038
+ drawLayer(context, layer, timeStamp) {
9963
10039
  const renderers = this.renderers[layer];
9964
10040
  if (renderers) {
9965
10041
  for (const renderer of renderers) {
9966
10042
  context.ctx.save();
9967
- renderer.drawLayer(context, layer);
10043
+ renderer.drawLayer(context, layer, timeStamp);
9968
10044
  context.ctx.restore();
9969
10045
  }
9970
10046
  }
9971
10047
  return "noStateChange";
9972
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
+ }
9973
10086
  }
9974
10087
 
9975
10088
  class SpreadsheetStore extends DisposableStore {
@@ -9993,7 +10106,7 @@ class SpreadsheetStore extends DisposableStore {
9993
10106
  }
9994
10107
  handle(cmd) { }
9995
10108
  finalize() { }
9996
- drawLayer(ctx, layer) { }
10109
+ drawLayer(ctx, layer, timestamp) { }
9997
10110
  }
9998
10111
 
9999
10112
  const VOID_COMPOSER = {
@@ -21686,6 +21799,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21686
21799
  if (isTrendLineAxis(dataset.xAxisID) || dataset.hidden) {
21687
21800
  continue;
21688
21801
  }
21802
+ const yAxisScale = chart.scales[dataset.yAxisID];
21689
21803
  for (let i = 0; i < dataset._parsed.length; i++) {
21690
21804
  const parsedValue = dataset._parsed[i];
21691
21805
  const value = Number(chart.config.type === "radar" ? parsedValue.r : parsedValue.y);
@@ -21696,10 +21810,18 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21696
21810
  const xPosition = point.x;
21697
21811
  let yPosition = 0;
21698
21812
  if (chart.config.type === "line" || chart.config.type === "radar") {
21699
- yPosition = point.y - 10;
21813
+ yPosition = value < 0 ? point.y + 10 : point.y - 10;
21700
21814
  }
21701
21815
  else {
21702
- yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21816
+ const yZeroLine = yAxisScale.getPixelForValue(0);
21817
+ const distanceFromAxisOrigin = Math.abs(yZeroLine - point.y);
21818
+ const textHeight = 12; // ChartJS default text height
21819
+ if (distanceFromAxisOrigin < textHeight) {
21820
+ yPosition = value < 0 ? yZeroLine + textHeight / 2 : yZeroLine - textHeight / 2;
21821
+ }
21822
+ else {
21823
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
21824
+ }
21703
21825
  }
21704
21826
  yPosition = Math.min(yPosition, yMax);
21705
21827
  yPosition = Math.max(yPosition, yMin);
@@ -21709,7 +21831,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
21709
21831
  }
21710
21832
  for (const otherPosition of textsPositions[xPosition] || []) {
21711
21833
  if (Math.abs(otherPosition - yPosition) < 13) {
21712
- yPosition = otherPosition - 13;
21834
+ yPosition = value < 0 ? otherPosition + 13 : otherPosition - 13;
21713
21835
  }
21714
21836
  }
21715
21837
  textsPositions[xPosition].push(yPosition);
@@ -21728,6 +21850,8 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21728
21850
  if (isTrendLineAxis(dataset.xAxisID)) {
21729
21851
  return; // ignore trend lines
21730
21852
  }
21853
+ const xAxisScale = chart.scales[dataset.xAxisID];
21854
+ const xZeroLine = xAxisScale.getPixelForValue(0);
21731
21855
  for (let i = 0; i < dataset._parsed.length; i++) {
21732
21856
  const value = Number(dataset._parsed[i].x);
21733
21857
  if (isNaN(value)) {
@@ -21736,17 +21860,27 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
21736
21860
  const displayValue = options.callback(value, dataset, i);
21737
21861
  const point = dataset.data[i];
21738
21862
  const yPosition = point.y;
21739
- let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21740
- xPosition = Math.min(xPosition, xMax);
21741
- xPosition = Math.max(xPosition, xMin);
21863
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21864
+ const distanceFromAxisOrigin = Math.abs(point.x - xZeroLine);
21865
+ const PADDING = 3;
21866
+ let xPosition;
21867
+ if (distanceFromAxisOrigin < textWidth) {
21868
+ xPosition =
21869
+ value < 0 ? xZeroLine - textWidth / 2 - PADDING : xZeroLine + textWidth / 2 + PADDING;
21870
+ }
21871
+ else {
21872
+ xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
21873
+ xPosition = Math.min(xPosition, xMax);
21874
+ xPosition = Math.max(xPosition, xMin);
21875
+ }
21742
21876
  // Avoid overlapping texts with same Y
21743
21877
  if (!textsPositions[yPosition]) {
21744
21878
  textsPositions[yPosition] = [];
21745
21879
  }
21746
- const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
21747
21880
  for (const otherPosition of textsPositions[yPosition]) {
21748
21881
  if (Math.abs(otherPosition - xPosition) < textWidth) {
21749
- xPosition = otherPosition + textWidth + 3;
21882
+ xPosition =
21883
+ value < 0 ? otherPosition - textWidth - PADDING : otherPosition + textWidth + PADDING;
21750
21884
  }
21751
21885
  }
21752
21886
  textsPositions[yPosition].push(xPosition);
@@ -23694,11 +23828,11 @@ function drawTitle(ctx, config) {
23694
23828
  function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
23695
23829
  const maxValue = runtime.maxValue;
23696
23830
  const minValue = runtime.minValue;
23697
- const gaugeValue = runtime.gaugeValue;
23831
+ const gaugeValue = getGaugeValue(runtime, "animated");
23698
23832
  const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
23699
23833
  const gaugeArcWidth = gaugeRect.width / 6;
23700
23834
  const gaugePercentage = gaugeValue
23701
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
23835
+ ? (gaugeValue - minValue.value) / (maxValue.value - minValue.value)
23702
23836
  : 0;
23703
23837
  const gaugeValuePosition = {
23704
23838
  x: boundingRect.width / 2,
@@ -23711,7 +23845,7 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
23711
23845
  }
23712
23846
  // Scale down the font size if the text is too long
23713
23847
  const maxTextWidth = gaugeRect.width / 2;
23714
- const gaugeLabel = gaugeValue?.label || "-";
23848
+ const gaugeLabel = runtime.gaugeValue?.label || "-";
23715
23849
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
23716
23850
  gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
23717
23851
  }
@@ -23851,7 +23985,7 @@ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
23851
23985
  return inflectionValues;
23852
23986
  }
23853
23987
  function getGaugeColor(runtime) {
23854
- const gaugeValue = runtime.gaugeValue?.value;
23988
+ const gaugeValue = getGaugeValue(runtime, "final");
23855
23989
  if (gaugeValue === undefined) {
23856
23990
  return GAUGE_BACKGROUND_COLOR;
23857
23991
  }
@@ -23939,6 +24073,11 @@ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY
23939
24073
  };
23940
24074
  return { bottomLeft, bottomRight, topRight, topLeft };
23941
24075
  }
24076
+ function getGaugeValue(runtime, mode) {
24077
+ return mode === "animated" && runtime.animationValue !== undefined
24078
+ ? runtime.animationValue
24079
+ : runtime.gaugeValue?.value;
24080
+ }
23942
24081
 
23943
24082
  const CHART_COMMON_OPTIONS = {
23944
24083
  // https://www.chartjs.org/docs/latest/general/responsive.html
@@ -25680,7 +25819,9 @@ function getPyramidChartShowValues(definition, args) {
25680
25819
  background: definition.background,
25681
25820
  callback: (value, dataset) => {
25682
25821
  value = Math.abs(Number(value));
25683
- return formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25822
+ return value === 0
25823
+ ? ""
25824
+ : formatChartDatasetValue(axisFormats, locale)(value, dataset.xAxisID || "x");
25684
25825
  },
25685
25826
  };
25686
25827
  }
@@ -26291,6 +26432,320 @@ function createBarChartRuntime(chart, getters) {
26291
26432
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
26292
26433
  }
26293
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;
26294
26749
  class GaugeChartComponent extends Component {
26295
26750
  static template = "o-spreadsheet-GaugeChartComponent";
26296
26751
  static props = {
@@ -26298,16 +26753,101 @@ class GaugeChartComponent extends Component {
26298
26753
  isFullScreen: { type: Boolean, optional: true },
26299
26754
  };
26300
26755
  canvas = useRef("chartContainer");
26756
+ animationStore;
26301
26757
  get runtime() {
26302
26758
  return this.env.model.getters.getChartRuntime(this.props.figureUI.id);
26303
26759
  }
26304
26760
  setup() {
26305
- useEffect(() => drawGaugeChart(this.canvas.el, this.runtime), () => {
26306
- const canvas = this.canvas.el;
26307
- 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();
26308
26786
  return [rect.width, rect.height, this.runtime, this.canvas.el, window.devicePixelRatio];
26309
26787
  });
26310
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
+ }
26311
26851
  }
26312
26852
 
26313
26853
  class ComboChart extends AbstractChart {
@@ -29341,6 +29881,12 @@ class Menu extends Component {
29341
29881
  menu.onStopHover?.(this.env);
29342
29882
  this.props.onMouseLeave?.(menu, ev);
29343
29883
  }
29884
+ onClickMenu(menu, ev) {
29885
+ if (!this.isEnabled(menu)) {
29886
+ return;
29887
+ }
29888
+ this.props.onClickMenu?.(menu, ev);
29889
+ }
29344
29890
  }
29345
29891
 
29346
29892
  /**
@@ -29839,9 +30385,6 @@ class MenuPopover extends Component {
29839
30385
  this.subMenu.parentMenu = undefined;
29840
30386
  }
29841
30387
  onClickMenu(menu, ev) {
29842
- if (!this.isEnabled(menu)) {
29843
- return;
29844
- }
29845
30388
  if (this.isRoot(menu)) {
29846
30389
  this.openSubMenu(menu, ev.currentTarget);
29847
30390
  }
@@ -31092,11 +31635,9 @@ criterionEvaluatorRegistry.add("isValueInRange", {
31092
31635
  if (!value) {
31093
31636
  return false;
31094
31637
  }
31095
- const range = getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
31096
- const criterionValues = getters.getRangeValues(range);
31638
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
31097
31639
  return criterionValues
31098
- .filter(isNotNull)
31099
- .map((value) => value.toString().toLowerCase())
31640
+ .map((value) => value.toLowerCase())
31100
31641
  .includes(value.toString().toLowerCase());
31101
31642
  },
31102
31643
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
@@ -31268,6 +31809,12 @@ class TextValueProvider extends Component {
31268
31809
  selectedElement?.scrollIntoView?.({ block: "nearest" });
31269
31810
  }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
31270
31811
  }
31812
+ getCss(html) {
31813
+ return cssPropertiesToCss({
31814
+ color: html.color || "#000000",
31815
+ background: html.backgroundColor,
31816
+ });
31817
+ }
31271
31818
  }
31272
31819
 
31273
31820
  class ContentEditableHelper {
@@ -31406,7 +31953,6 @@ class ContentEditableHelper {
31406
31953
  // We can only modify a node in place if it has the same type as the content
31407
31954
  // that we would insert, which are spans.
31408
31955
  // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
31409
- // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)
31410
31956
  const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
31411
31957
  if (childIsSpan && compareContentToSpanElement(content, child)) {
31412
31958
  continue;
@@ -31441,9 +31987,7 @@ class ContentEditableHelper {
31441
31987
  }
31442
31988
  // Empty line
31443
31989
  if (!p.hasChildNodes()) {
31444
- const span = document.createElement("span");
31445
- span.appendChild(document.createElement("br"));
31446
- p.appendChild(span);
31990
+ p.appendChild(document.createElement("span"));
31447
31991
  }
31448
31992
  // replace p if necessary
31449
31993
  if (newChild) {
@@ -31490,13 +32034,10 @@ class ContentEditableHelper {
31490
32034
  }
31491
32035
  getText() {
31492
32036
  let text = "";
31493
- const it = iterateChildren(this.el);
31494
- let current = it.next();
31495
32037
  let isFirstParagraph = true;
31496
- while (!current.done) {
31497
- if (!current.value.hasChildNodes()) {
31498
- text += current.value.textContent;
31499
- }
32038
+ let emptyParagraph = false;
32039
+ const it = iterateChildren(this.el);
32040
+ for (let current = it.next(); !current.done; current = it.next()) {
31500
32041
  if (current.value.nodeName === "P" ||
31501
32042
  (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
31502
32043
  ) {
@@ -31506,8 +32047,15 @@ class ContentEditableHelper {
31506
32047
  else {
31507
32048
  text += NEWLINE;
31508
32049
  }
32050
+ emptyParagraph = ["<br>", "<span><br></span>"].includes(current.value.innerHTML);
32051
+ continue;
32052
+ }
32053
+ if (!current.value.hasChildNodes()) {
32054
+ if (current.value.nodeName === "BR" && !emptyParagraph) {
32055
+ text += NEWLINE;
32056
+ }
32057
+ text += current.value.textContent;
31509
32058
  }
31510
- current = it.next();
31511
32059
  }
31512
32060
  return text;
31513
32061
  }
@@ -32857,6 +33405,12 @@ class Composer extends Component {
32857
33405
  useEffect(() => {
32858
33406
  this.processTokenAtCursor();
32859
33407
  }, () => [this.props.composerStore.editionMode !== "inactive"]);
33408
+ useEffect(() => {
33409
+ this.contentHelper.scrollSelectionIntoView();
33410
+ }, () => [
33411
+ this.props.composerStore.composerSelection.start,
33412
+ this.props.composerStore.composerSelection.end,
33413
+ ]);
32860
33414
  }
32861
33415
  // ---------------------------------------------------------------------------
32862
33416
  // Handlers
@@ -33067,6 +33621,7 @@ class Composer extends Component {
33067
33621
  if (this.env.isMobile() && !isIOS()) {
33068
33622
  return;
33069
33623
  }
33624
+ this.debouncedHover.stopDebounce();
33070
33625
  this.contentHelper.removeSelection();
33071
33626
  }
33072
33627
  onMouseup() {
@@ -33145,7 +33700,6 @@ class Composer extends Component {
33145
33700
  const { start, end } = this.props.composerStore.composerSelection;
33146
33701
  this.contentHelper.selectRange(start, end);
33147
33702
  }
33148
- this.contentHelper.scrollSelectionIntoView();
33149
33703
  }
33150
33704
  this.shouldProcessInputEvents = true;
33151
33705
  }
@@ -33621,68 +34175,6 @@ class SingleInputCriterionForm extends CriterionForm {
33621
34175
  }
33622
34176
  }
33623
34177
 
33624
- css /* scss */ `
33625
- .o-dv-list-item-delete {
33626
- color: #666666;
33627
- cursor: pointer;
33628
- }
33629
- `;
33630
- class ListCriterionForm extends CriterionForm {
33631
- static template = "o-spreadsheet-ListCriterionForm";
33632
- static components = { CriterionInput };
33633
- state = useState({
33634
- numberOfValues: Math.max(this.props.criterion.values.length, 2),
33635
- });
33636
- setup() {
33637
- super.setup();
33638
- const setupDefault = (props) => {
33639
- if (props.criterion.displayStyle === undefined) {
33640
- this.updateCriterion({ displayStyle: "arrow" });
33641
- }
33642
- };
33643
- onWillUpdateProps(setupDefault);
33644
- onWillStart(() => setupDefault(this.props));
33645
- }
33646
- onValueChanged(value, index) {
33647
- const values = [...this.displayedValues];
33648
- values[index] = value;
33649
- this.updateCriterion({ values });
33650
- }
33651
- onAddAnotherValue() {
33652
- this.state.numberOfValues++;
33653
- }
33654
- removeItem(index) {
33655
- const values = [...this.displayedValues];
33656
- values.splice(index, 1);
33657
- this.state.numberOfValues--;
33658
- this.updateCriterion({ values });
33659
- }
33660
- onChangedDisplayStyle(ev) {
33661
- const displayStyle = ev.target.value;
33662
- this.updateCriterion({ displayStyle });
33663
- }
33664
- onKeyDown(ev, index) {
33665
- if ((ev.key === "Enter" || ev.key === "Tab") && index === this.state.numberOfValues - 1) {
33666
- this.onAddAnotherValue();
33667
- this.state.focusedValueIndex = index + 1;
33668
- ev.preventDefault();
33669
- }
33670
- else if (ev.key === "Enter") {
33671
- this.state.focusedValueIndex = index + 1;
33672
- }
33673
- }
33674
- onBlurInput() {
33675
- this.state.focusedValueIndex = undefined;
33676
- }
33677
- get displayedValues() {
33678
- const values = [];
33679
- for (let i = 0; i < this.state.numberOfValues; i++) {
33680
- values.push(this.props.criterion.values[i] || "");
33681
- }
33682
- return values;
33683
- }
33684
- }
33685
-
33686
34178
  /**
33687
34179
  * Start listening to pointer events and apply the given callbacks.
33688
34180
  *
@@ -33712,207 +34204,653 @@ function startDnd(onPointerMove, onPointerUp) {
33712
34204
  return removeListeners;
33713
34205
  }
33714
34206
 
33715
- function useDragAndDropListItems() {
33716
- let dndHelper;
33717
- const previousCursor = document.body.style.cursor;
33718
- let cleanupFns = [];
33719
- const cleanUp = () => {
33720
- dndHelper = undefined;
33721
- document.body.style.cursor = previousCursor;
33722
- cleanupFns.forEach((fn) => fn());
33723
- cleanupFns = [];
33724
- };
33725
- const start = (direction, args) => {
33726
- const onChange = () => {
33727
- document.body.style.cursor = "move";
33728
- if (!dndHelper)
33729
- return;
33730
- Object.assign(state.itemsStyle, dndHelper.getItemStyles());
33731
- args.onChange?.();
33732
- };
33733
- state.cancel = () => {
33734
- state.draggedItemId = undefined;
33735
- state.itemsStyle = {};
33736
- document.body.style.cursor = previousCursor;
33737
- args.onCancel?.();
33738
- cleanUp();
33739
- };
33740
- const onDragEnd = (itemId, indexAtEnd) => {
33741
- state.draggedItemId = undefined;
33742
- state.itemsStyle = {};
33743
- document.body.style.cursor = previousCursor;
33744
- args.onDragEnd?.(itemId, indexAtEnd);
33745
- cleanUp();
33746
- };
33747
- document.body.style.cursor = "move";
33748
- state.draggedItemId = args.draggedItemId;
33749
- const container = direction === "horizontal"
33750
- ? new HorizontalContainer(args.scrollableContainerEl)
33751
- : new VerticalContainer(args.scrollableContainerEl);
33752
- dndHelper = new DOMDndHelper({
33753
- ...args,
33754
- container,
33755
- onChange,
33756
- onDragEnd,
33757
- onCancel: state.cancel,
33758
- });
33759
- const stopListening = startDnd(dndHelper.onMouseMove.bind(dndHelper), dndHelper.onMouseUp.bind(dndHelper));
33760
- cleanupFns.push(stopListening);
33761
- const onScroll = dndHelper.onScroll.bind(dndHelper);
33762
- args.scrollableContainerEl.addEventListener("scroll", onScroll);
33763
- cleanupFns.push(() => args.scrollableContainerEl.removeEventListener("scroll", onScroll));
33764
- cleanupFns.push(dndHelper.destroy.bind(dndHelper));
33765
- };
33766
- onWillUnmount(() => {
33767
- cleanUp();
33768
- });
33769
- const state = useState({
33770
- itemsStyle: {},
33771
- draggedItemId: undefined,
33772
- start,
33773
- cancel: () => { },
33774
- });
33775
- return state;
33776
- }
33777
- class DOMDndHelper {
33778
- draggedItemId;
33779
- items;
33780
- container;
33781
- initialMousePosition;
33782
- currentMousePosition;
33783
- initialScroll;
33784
- minPosition;
33785
- maxPosition;
33786
- edgeScrollIntervalId;
33787
- onChange;
33788
- onCancel;
33789
- onDragEnd;
33790
- /**
33791
- * The dead zone is an area in which the pointermove events are ignored.
33792
- *
33793
- * This is useful when swapping the dragged item with a larger item. After the swap,
33794
- * the mouse is still hovering on the item we just swapped with. In this case, we don't want
33795
- * a mouse move to trigger another swap the other way around, so we create a dead zone. We will clear
33796
- * the dead zone when the mouse leaves the swapped item.
33797
- */
33798
- deadZone;
33799
- constructor(args) {
33800
- this.items = args.items.map((item) => ({ ...item, positionAtStart: item.position }));
33801
- this.draggedItemId = args.draggedItemId;
33802
- this.container = args.container;
33803
- this.onChange = args.onChange;
33804
- this.onCancel = args.onCancel;
33805
- this.onDragEnd = args.onDragEnd;
33806
- this.initialMousePosition = args.initialMousePosition;
33807
- this.currentMousePosition = args.initialMousePosition;
33808
- this.initialScroll = this.container.scroll;
33809
- this.minPosition = this.items[0].position;
33810
- this.maxPosition =
33811
- this.items[this.items.length - 1].position + this.items[this.items.length - 1].size;
33812
- }
33813
- getItemStyles() {
33814
- const styles = {};
33815
- for (const item of this.items) {
33816
- styles[item.id] = this.getItemStyle(item.id);
33817
- }
33818
- return styles;
33819
- }
33820
- getItemStyle(itemId) {
33821
- const position = this.container.cssPositionProperty;
33822
- const style = {};
33823
- style.position = "relative";
33824
- style[position] = (this.getItemsPositions()[itemId] || 0) + "px";
33825
- style.transition = `${position} 0.5s`;
33826
- style["pointer-events"] = "none";
33827
- if (this.draggedItemId === itemId) {
33828
- style.transition = `${position} 0s`;
33829
- style["z-index"] = "1000";
33830
- }
33831
- return cssPropertiesToCss(style);
34207
+ const LINE_VERTICAL_PADDING = 1;
34208
+ const PICKER_PADDING = 8;
34209
+ const ITEM_BORDER_WIDTH = 1;
34210
+ const ITEM_EDGE_LENGTH = 18;
34211
+ const ITEMS_PER_LINE = 10;
34212
+ const MAGNIFIER_EDGE = 16;
34213
+ const ITEM_GAP = 2;
34214
+ const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
34215
+ const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
34216
+ const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
34217
+ const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
34218
+ css /* scss */ `
34219
+ .o-color-picker {
34220
+ padding: ${PICKER_PADDING}px 0;
34221
+ /* FIXME: this is useless, overiden by the popover container */
34222
+ box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
34223
+ background-color: white;
34224
+ line-height: 1.2;
34225
+ overflow-y: auto;
34226
+ overflow-x: hidden;
34227
+ width: ${CONTAINER_WIDTH}px;
34228
+
34229
+ .o-color-picker-section-name {
34230
+ margin: 0px ${ITEM_BORDER_WIDTH}px;
34231
+ padding: 4px ${PICKER_PADDING}px;
33832
34232
  }
33833
- onScroll() {
33834
- this.moveDraggedItemToPosition(this.currentMousePosition + this.scrollOffset);
34233
+ .colors-grid {
34234
+ display: grid;
34235
+ padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
34236
+ grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
34237
+ grid-gap: ${ITEM_GAP}px;
33835
34238
  }
33836
- onMouseMove(ev) {
33837
- if (ev.button > 1) {
33838
- this.onCancel();
33839
- return;
33840
- }
33841
- const mousePosition = this.container.getMousePosition(ev);
33842
- this.currentMousePosition = mousePosition;
33843
- if (mousePosition < this.container.start || mousePosition > this.container.end) {
33844
- this.startEdgeScroll(mousePosition < this.container.start ? -1 : 1);
33845
- return;
33846
- }
33847
- else {
33848
- this.stopEdgeScroll();
34239
+ .o-color-picker-toggler-button {
34240
+ display: flex;
34241
+ .o-color-picker-toggler-sign {
34242
+ display: flex;
34243
+ margin: auto auto;
34244
+ width: 55%;
34245
+ height: 55%;
34246
+ .o-icon {
34247
+ width: 100%;
34248
+ height: 100%;
33849
34249
  }
33850
- this.moveDraggedItemToPosition(mousePosition + this.scrollOffset);
34250
+ }
33851
34251
  }
33852
- moveDraggedItemToPosition(position) {
33853
- const hoveredItemIndex = this.getHoveredItemIndex(position, this.items);
33854
- const draggedItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
33855
- const draggedItem = this.items[draggedItemIndex];
33856
- if (this.deadZone && this.isInZone(position, this.deadZone)) {
33857
- this.onChange(this.getItemsPositions());
33858
- return;
33859
- }
33860
- else if (this.isInZone(position, {
33861
- start: draggedItem.position,
33862
- end: draggedItem.position + draggedItem.size,
33863
- })) {
33864
- this.deadZone = undefined;
33865
- }
33866
- if (draggedItemIndex === hoveredItemIndex) {
33867
- this.onChange(this.getItemsPositions());
33868
- return;
33869
- }
33870
- const startIndex = Math.min(draggedItemIndex, hoveredItemIndex);
33871
- const endIndex = Math.max(draggedItemIndex, hoveredItemIndex);
33872
- const direction = Math.sign(hoveredItemIndex - draggedItemIndex);
33873
- let draggedItemMoveSize = 0;
33874
- for (let i = startIndex; i <= endIndex; i++) {
33875
- if (i === draggedItemIndex) {
33876
- continue;
33877
- }
33878
- this.items[i].position -= direction * draggedItem.size;
33879
- draggedItemMoveSize += this.items[i].size;
33880
- }
33881
- draggedItem.position += direction * draggedItemMoveSize;
33882
- this.items.sort((item1, item2) => item1.position - item2.position);
33883
- this.deadZone =
33884
- direction > 0
33885
- ? { start: position, end: draggedItem.position }
33886
- : { start: draggedItem.position + draggedItem.size, end: position };
33887
- this.onChange(this.getItemsPositions());
34252
+ .o-color-picker-line-item {
34253
+ width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
34254
+ height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
34255
+ margin: 0px;
34256
+ border-radius: 50px;
34257
+ border: ${ITEM_BORDER_WIDTH}px solid #666666;
34258
+ padding: 0px;
34259
+ font-size: 16px;
34260
+ background: white;
34261
+ &:hover {
34262
+ background-color: rgba(0, 0, 0, 0.08);
34263
+ outline: 1px solid gray;
34264
+ cursor: pointer;
34265
+ }
33888
34266
  }
33889
- onMouseUp(ev) {
33890
- if (ev.button !== 0) {
33891
- this.onCancel();
34267
+ .o-buttons {
34268
+ padding: ${PICKER_PADDING}px;
34269
+ display: flex;
34270
+ .o-cancel {
34271
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34272
+ width: 100%;
34273
+ padding: 5px;
34274
+ font-size: 14px;
34275
+ background: white;
34276
+ border-radius: 4px;
34277
+ &:hover:enabled {
34278
+ background-color: rgba(0, 0, 0, 0.08);
33892
34279
  }
33893
- ev.stopPropagation();
33894
- ev.preventDefault();
33895
- const targetItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
33896
- this.onDragEnd(this.draggedItemId, targetItemIndex);
33897
- this.stopEdgeScroll();
33898
- return false;
34280
+ }
33899
34281
  }
33900
- startEdgeScroll(direction) {
33901
- if (this.edgeScrollIntervalId)
33902
- return;
33903
- this.edgeScrollIntervalId = window.setInterval(() => {
33904
- const offset = direction * 3;
33905
- this.container.scroll += offset;
33906
- }, 5);
34282
+ .o-add-button {
34283
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34284
+ padding: 4px;
34285
+ background: white;
34286
+ border-radius: 4px;
34287
+ &:hover:enabled {
34288
+ background-color: rgba(0, 0, 0, 0.08);
34289
+ }
33907
34290
  }
33908
- stopEdgeScroll() {
33909
- window.clearInterval(this.edgeScrollIntervalId);
33910
- this.edgeScrollIntervalId = undefined;
34291
+ .o-separator {
34292
+ border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
34293
+ margin-top: ${MENU_SEPARATOR_PADDING}px;
34294
+ margin-bottom: ${MENU_SEPARATOR_PADDING}px;
33911
34295
  }
33912
- /**
33913
- * Get the index of the item the given mouse position is inside.
33914
- * If the mouse is outside the container, return the first or last item index.
33915
- */
34296
+
34297
+ .o-custom-selector {
34298
+ padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
34299
+ position: relative;
34300
+ .o-gradient {
34301
+ margin-bottom: ${MAGNIFIER_EDGE / 2}px;
34302
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34303
+ width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
34304
+ height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
34305
+ position: relative;
34306
+ }
34307
+
34308
+ .magnifier {
34309
+ height: ${MAGNIFIER_EDGE}px;
34310
+ width: ${MAGNIFIER_EDGE}px;
34311
+ border-radius: 50%;
34312
+ border: 2px solid #fff;
34313
+ box-shadow: 0px 0px 3px #c0c0c0;
34314
+ position: absolute;
34315
+ z-index: 2;
34316
+ }
34317
+ .saturation {
34318
+ background: linear-gradient(to right, #fff 0%, transparent 100%);
34319
+ }
34320
+ .lightness {
34321
+ background: linear-gradient(to top, #000 0%, transparent 100%);
34322
+ }
34323
+ .o-hue-picker {
34324
+ border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
34325
+ width: 100%;
34326
+ height: 12px;
34327
+ border-radius: 4px;
34328
+ background: linear-gradient(
34329
+ to right,
34330
+ hsl(0 100% 50%) 0%,
34331
+ hsl(0.2turn 100% 50%) 20%,
34332
+ hsl(0.3turn 100% 50%) 30%,
34333
+ hsl(0.4turn 100% 50%) 40%,
34334
+ hsl(0.5turn 100% 50%) 50%,
34335
+ hsl(0.6turn 100% 50%) 60%,
34336
+ hsl(0.7turn 100% 50%) 70%,
34337
+ hsl(0.8turn 100% 50%) 80%,
34338
+ hsl(0.9turn 100% 50%) 90%,
34339
+ hsl(1turn 100% 50%) 100%
34340
+ );
34341
+ position: relative;
34342
+ cursor: crosshair;
34343
+ }
34344
+ .o-hue-slider {
34345
+ margin-top: -3px;
34346
+ }
34347
+ .o-custom-input-preview {
34348
+ padding: 2px 0px;
34349
+ display: flex;
34350
+ input {
34351
+ width: 50%;
34352
+ border-radius: 4px;
34353
+ padding: 4px 23px 4px 10px;
34354
+ height: 24px;
34355
+ border: 1px solid #c0c0c0;
34356
+ margin-right: 2px;
34357
+ }
34358
+ .o-wrong-color {
34359
+ /* FIXME bootstrap class instead? */
34360
+ outline-color: red;
34361
+ border-color: red;
34362
+ &:focus {
34363
+ outline-style: solid;
34364
+ outline-width: 1px;
34365
+ }
34366
+ }
34367
+ }
34368
+ .o-custom-input-buttons {
34369
+ padding: 2px 0px;
34370
+ display: flex;
34371
+ justify-content: end;
34372
+ }
34373
+ .o-color-preview {
34374
+ border: 1px solid #c0c0c0;
34375
+ border-radius: 4px;
34376
+ width: 50%;
34377
+ }
34378
+ }
34379
+ }
34380
+ `;
34381
+ class ColorPicker extends Component {
34382
+ static template = "o-spreadsheet-ColorPicker";
34383
+ static props = {
34384
+ onColorPicked: Function,
34385
+ currentColor: { type: String, optional: true },
34386
+ maxHeight: { type: Number, optional: true },
34387
+ anchorRect: Object,
34388
+ disableNoColor: { type: Boolean, optional: true },
34389
+ };
34390
+ static defaultProps = { currentColor: "" };
34391
+ static components = { Popover };
34392
+ COLORS = COLOR_PICKER_DEFAULTS;
34393
+ state = useState({
34394
+ showGradient: false,
34395
+ currentHslaColor: isColorValid(this.props.currentColor)
34396
+ ? { ...hexToHSLA(this.props.currentColor), a: 1 }
34397
+ : { h: 0, s: 100, l: 100, a: 1 },
34398
+ customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
34399
+ });
34400
+ get colorPickerStyle() {
34401
+ if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
34402
+ return cssPropertiesToCss({ display: "none" });
34403
+ }
34404
+ return "";
34405
+ }
34406
+ get popoverProps() {
34407
+ return {
34408
+ anchorRect: this.props.anchorRect,
34409
+ maxHeight: this.props.maxHeight,
34410
+ positioning: "bottom-left",
34411
+ verticalOffset: 0,
34412
+ };
34413
+ }
34414
+ get gradientHueStyle() {
34415
+ const hue = this.state.currentHslaColor?.h || 0;
34416
+ return cssPropertiesToCss({
34417
+ background: `hsl(${hue} 100% 50%)`,
34418
+ });
34419
+ }
34420
+ get sliderStyle() {
34421
+ const hue = this.state.currentHslaColor?.h || 0;
34422
+ const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
34423
+ const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
34424
+ return cssPropertiesToCss({
34425
+ "margin-left": `${left}px`,
34426
+ });
34427
+ }
34428
+ get pointerStyle() {
34429
+ const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
34430
+ const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
34431
+ const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
34432
+ return cssPropertiesToCss({
34433
+ left: `${-MAGNIFIER_EDGE / 2 + left}px`,
34434
+ top: `${-MAGNIFIER_EDGE / 2 + top}px`,
34435
+ background: hslaToHex(this.state.currentHslaColor),
34436
+ });
34437
+ }
34438
+ get colorPreviewStyle() {
34439
+ return cssPropertiesToCss({
34440
+ "background-color": hslaToHex(this.state.currentHslaColor),
34441
+ });
34442
+ }
34443
+ get checkmarkColor() {
34444
+ return chartFontColor(this.props.currentColor);
34445
+ }
34446
+ get isHexColorInputValid() {
34447
+ return !this.state.customHexColor || isColorValid(this.state.customHexColor);
34448
+ }
34449
+ setCustomGradient({ x, y }) {
34450
+ const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
34451
+ const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
34452
+ const deltaX = offsetX / INNER_GRADIENT_WIDTH;
34453
+ const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
34454
+ const s = 100 * deltaX;
34455
+ const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
34456
+ this.updateColor({ s, l });
34457
+ }
34458
+ setCustomHue(x) {
34459
+ // needs to be capped such that h is in [0°, 359°]
34460
+ const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
34461
+ this.updateColor({ h });
34462
+ }
34463
+ updateColor(newHsl) {
34464
+ this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
34465
+ this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
34466
+ }
34467
+ onColorClick(color) {
34468
+ if (color) {
34469
+ this.props.onColorPicked(toHex(color));
34470
+ }
34471
+ }
34472
+ resetColor() {
34473
+ this.props.onColorPicked("");
34474
+ }
34475
+ toggleColorPicker() {
34476
+ this.state.showGradient = !this.state.showGradient;
34477
+ }
34478
+ dragGradientPointer(ev) {
34479
+ const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
34480
+ this.setCustomGradient(initialGradientCoordinates);
34481
+ const initialMousePosition = { x: ev.clientX, y: ev.clientY };
34482
+ const onMouseMove = (ev) => {
34483
+ const currentMousePosition = { x: ev.clientX, y: ev.clientY };
34484
+ const deltaX = currentMousePosition.x - initialMousePosition.x;
34485
+ const deltaY = currentMousePosition.y - initialMousePosition.y;
34486
+ const currentGradientCoordinates = {
34487
+ x: initialGradientCoordinates.x + deltaX,
34488
+ y: initialGradientCoordinates.y + deltaY,
34489
+ };
34490
+ this.setCustomGradient(currentGradientCoordinates);
34491
+ };
34492
+ startDnd(onMouseMove, () => { });
34493
+ }
34494
+ dragHuePointer(ev) {
34495
+ const initialX = ev.offsetX;
34496
+ const initialMouseX = ev.clientX;
34497
+ this.setCustomHue(initialX);
34498
+ const onMouseMove = (ev) => {
34499
+ const currentMouseX = ev.clientX;
34500
+ const deltaX = currentMouseX - initialMouseX;
34501
+ const x = initialX + deltaX;
34502
+ this.setCustomHue(x);
34503
+ };
34504
+ startDnd(onMouseMove, () => { });
34505
+ }
34506
+ setHexColor(ev) {
34507
+ // only support HEX code input
34508
+ const val = ev.target.value.replace("##", "#").slice(0, 7);
34509
+ this.state.customHexColor = val;
34510
+ if (!isColorValid(val)) ;
34511
+ else {
34512
+ this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
34513
+ }
34514
+ }
34515
+ addCustomColor(ev) {
34516
+ if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
34517
+ return;
34518
+ }
34519
+ this.props.onColorPicked(toHex(this.state.customHexColor));
34520
+ }
34521
+ isSameColor(color1, color2) {
34522
+ return isSameColor(color1, color2);
34523
+ }
34524
+ }
34525
+
34526
+ class Section extends Component {
34527
+ static template = "o_spreadsheet.Section";
34528
+ static props = {
34529
+ class: { type: String, optional: true },
34530
+ title: { type: String, optional: true },
34531
+ slots: Object,
34532
+ };
34533
+ }
34534
+
34535
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
34536
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
34537
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
34538
+ </svg>
34539
+ `;
34540
+ css /* scss */ `
34541
+ .o-round-color-picker-button {
34542
+ width: 20px;
34543
+ height: 20px;
34544
+ cursor: pointer;
34545
+ border: 1px solid ${GRAY_300};
34546
+ background-position: 1px 1px;
34547
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
34548
+ }
34549
+ `;
34550
+ class RoundColorPicker extends Component {
34551
+ static template = "o-spreadsheet.RoundColorPicker";
34552
+ static components = { Section, ColorPicker };
34553
+ static props = {
34554
+ currentColor: { type: String, optional: true },
34555
+ title: { type: String, optional: true },
34556
+ onColorPicked: Function,
34557
+ disableNoColor: { type: Boolean, optional: true },
34558
+ };
34559
+ colorPickerButtonRef = useRef("colorPickerButton");
34560
+ state;
34561
+ setup() {
34562
+ this.state = useState({ pickerOpened: false });
34563
+ useExternalListener(window, "click", this.closePicker);
34564
+ }
34565
+ closePicker() {
34566
+ this.state.pickerOpened = false;
34567
+ }
34568
+ togglePicker() {
34569
+ this.state.pickerOpened = !this.state.pickerOpened;
34570
+ }
34571
+ onColorPicked(color) {
34572
+ this.props.onColorPicked(color);
34573
+ this.state.pickerOpened = false;
34574
+ }
34575
+ get colorPickerAnchorRect() {
34576
+ const button = this.colorPickerButtonRef.el;
34577
+ return getBoundingRectAsPOJO(button);
34578
+ }
34579
+ get buttonStyle() {
34580
+ return cssPropertiesToCss({
34581
+ background: this.props.currentColor,
34582
+ });
34583
+ }
34584
+ }
34585
+
34586
+ css /* scss */ `
34587
+ .o-dv-list-item-delete {
34588
+ color: #666666;
34589
+ cursor: pointer;
34590
+ }
34591
+ `;
34592
+ class ListCriterionForm extends CriterionForm {
34593
+ static template = "o-spreadsheet-ListCriterionForm";
34594
+ static components = { CriterionInput, RoundColorPicker };
34595
+ state = useState({
34596
+ numberOfValues: Math.max(this.props.criterion.values.length, 2),
34597
+ });
34598
+ setup() {
34599
+ super.setup();
34600
+ const setupDefault = (props) => {
34601
+ if (props.criterion.displayStyle === undefined) {
34602
+ this.updateCriterion({ displayStyle: "chip" });
34603
+ }
34604
+ };
34605
+ onWillUpdateProps(setupDefault);
34606
+ onWillStart(() => setupDefault(this.props));
34607
+ }
34608
+ onValueChanged(value, index) {
34609
+ const values = [...this.displayedValues];
34610
+ values[index] = value;
34611
+ this.updateCriterion({ values });
34612
+ }
34613
+ onColorChanged(color, value) {
34614
+ const colors = { ...this.props.criterion.colors };
34615
+ colors[value] = color || undefined;
34616
+ this.updateCriterion({ colors });
34617
+ }
34618
+ onAddAnotherValue() {
34619
+ this.state.numberOfValues++;
34620
+ }
34621
+ removeItem(index) {
34622
+ const values = [...this.displayedValues];
34623
+ values.splice(index, 1);
34624
+ this.state.numberOfValues--;
34625
+ this.updateCriterion({ values });
34626
+ }
34627
+ onChangedDisplayStyle(ev) {
34628
+ const displayStyle = ev.target.value;
34629
+ this.updateCriterion({ displayStyle });
34630
+ }
34631
+ onKeyDown(ev, index) {
34632
+ if ((ev.key === "Enter" || ev.key === "Tab") && index === this.state.numberOfValues - 1) {
34633
+ this.onAddAnotherValue();
34634
+ this.state.focusedValueIndex = index + 1;
34635
+ ev.preventDefault();
34636
+ }
34637
+ else if (ev.key === "Enter") {
34638
+ this.state.focusedValueIndex = index + 1;
34639
+ }
34640
+ }
34641
+ onBlurInput() {
34642
+ this.state.focusedValueIndex = undefined;
34643
+ }
34644
+ get displayedValues() {
34645
+ const values = [];
34646
+ for (let i = 0; i < this.state.numberOfValues; i++) {
34647
+ values.push(this.props.criterion.values[i] || "");
34648
+ }
34649
+ return values;
34650
+ }
34651
+ }
34652
+
34653
+ function useDragAndDropListItems() {
34654
+ let dndHelper;
34655
+ const previousCursor = document.body.style.cursor;
34656
+ let cleanupFns = [];
34657
+ const cleanUp = () => {
34658
+ dndHelper = undefined;
34659
+ document.body.style.cursor = previousCursor;
34660
+ cleanupFns.forEach((fn) => fn());
34661
+ cleanupFns = [];
34662
+ };
34663
+ const start = (direction, args) => {
34664
+ const onChange = () => {
34665
+ document.body.style.cursor = "move";
34666
+ if (!dndHelper)
34667
+ return;
34668
+ Object.assign(state.itemsStyle, dndHelper.getItemStyles());
34669
+ args.onChange?.();
34670
+ };
34671
+ state.cancel = () => {
34672
+ state.draggedItemId = undefined;
34673
+ state.itemsStyle = {};
34674
+ document.body.style.cursor = previousCursor;
34675
+ args.onCancel?.();
34676
+ cleanUp();
34677
+ };
34678
+ const onDragEnd = (itemId, indexAtEnd) => {
34679
+ state.draggedItemId = undefined;
34680
+ state.itemsStyle = {};
34681
+ document.body.style.cursor = previousCursor;
34682
+ args.onDragEnd?.(itemId, indexAtEnd);
34683
+ cleanUp();
34684
+ };
34685
+ document.body.style.cursor = "move";
34686
+ state.draggedItemId = args.draggedItemId;
34687
+ const container = direction === "horizontal"
34688
+ ? new HorizontalContainer(args.scrollableContainerEl)
34689
+ : new VerticalContainer(args.scrollableContainerEl);
34690
+ dndHelper = new DOMDndHelper({
34691
+ ...args,
34692
+ container,
34693
+ onChange,
34694
+ onDragEnd,
34695
+ onCancel: state.cancel,
34696
+ });
34697
+ const stopListening = startDnd(dndHelper.onMouseMove.bind(dndHelper), dndHelper.onMouseUp.bind(dndHelper));
34698
+ cleanupFns.push(stopListening);
34699
+ const onScroll = dndHelper.onScroll.bind(dndHelper);
34700
+ args.scrollableContainerEl.addEventListener("scroll", onScroll);
34701
+ cleanupFns.push(() => args.scrollableContainerEl.removeEventListener("scroll", onScroll));
34702
+ cleanupFns.push(dndHelper.destroy.bind(dndHelper));
34703
+ };
34704
+ onWillUnmount(() => {
34705
+ cleanUp();
34706
+ });
34707
+ const state = useState({
34708
+ itemsStyle: {},
34709
+ draggedItemId: undefined,
34710
+ start,
34711
+ cancel: () => { },
34712
+ });
34713
+ return state;
34714
+ }
34715
+ class DOMDndHelper {
34716
+ draggedItemId;
34717
+ items;
34718
+ container;
34719
+ initialMousePosition;
34720
+ currentMousePosition;
34721
+ initialScroll;
34722
+ minPosition;
34723
+ maxPosition;
34724
+ edgeScrollIntervalId;
34725
+ onChange;
34726
+ onCancel;
34727
+ onDragEnd;
34728
+ /**
34729
+ * The dead zone is an area in which the pointermove events are ignored.
34730
+ *
34731
+ * This is useful when swapping the dragged item with a larger item. After the swap,
34732
+ * the mouse is still hovering on the item we just swapped with. In this case, we don't want
34733
+ * a mouse move to trigger another swap the other way around, so we create a dead zone. We will clear
34734
+ * the dead zone when the mouse leaves the swapped item.
34735
+ */
34736
+ deadZone;
34737
+ constructor(args) {
34738
+ this.items = args.items.map((item) => ({ ...item, positionAtStart: item.position }));
34739
+ this.draggedItemId = args.draggedItemId;
34740
+ this.container = args.container;
34741
+ this.onChange = args.onChange;
34742
+ this.onCancel = args.onCancel;
34743
+ this.onDragEnd = args.onDragEnd;
34744
+ this.initialMousePosition = args.initialMousePosition;
34745
+ this.currentMousePosition = args.initialMousePosition;
34746
+ this.initialScroll = this.container.scroll;
34747
+ this.minPosition = this.items[0].position;
34748
+ this.maxPosition =
34749
+ this.items[this.items.length - 1].position + this.items[this.items.length - 1].size;
34750
+ }
34751
+ getItemStyles() {
34752
+ const styles = {};
34753
+ for (const item of this.items) {
34754
+ styles[item.id] = this.getItemStyle(item.id);
34755
+ }
34756
+ return styles;
34757
+ }
34758
+ getItemStyle(itemId) {
34759
+ const position = this.container.cssPositionProperty;
34760
+ const style = {};
34761
+ style.position = "relative";
34762
+ style[position] = (this.getItemsPositions()[itemId] || 0) + "px";
34763
+ style.transition = `${position} 0.5s`;
34764
+ style["pointer-events"] = "none";
34765
+ if (this.draggedItemId === itemId) {
34766
+ style.transition = `${position} 0s`;
34767
+ style["z-index"] = "1000";
34768
+ }
34769
+ return cssPropertiesToCss(style);
34770
+ }
34771
+ onScroll() {
34772
+ this.moveDraggedItemToPosition(this.currentMousePosition + this.scrollOffset);
34773
+ }
34774
+ onMouseMove(ev) {
34775
+ if (ev.button > 1) {
34776
+ this.onCancel();
34777
+ return;
34778
+ }
34779
+ const mousePosition = this.container.getMousePosition(ev);
34780
+ this.currentMousePosition = mousePosition;
34781
+ if (mousePosition < this.container.start || mousePosition > this.container.end) {
34782
+ this.startEdgeScroll(mousePosition < this.container.start ? -1 : 1);
34783
+ return;
34784
+ }
34785
+ else {
34786
+ this.stopEdgeScroll();
34787
+ }
34788
+ this.moveDraggedItemToPosition(mousePosition + this.scrollOffset);
34789
+ }
34790
+ moveDraggedItemToPosition(position) {
34791
+ const hoveredItemIndex = this.getHoveredItemIndex(position, this.items);
34792
+ const draggedItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
34793
+ const draggedItem = this.items[draggedItemIndex];
34794
+ if (this.deadZone && this.isInZone(position, this.deadZone)) {
34795
+ this.onChange(this.getItemsPositions());
34796
+ return;
34797
+ }
34798
+ else if (this.isInZone(position, {
34799
+ start: draggedItem.position,
34800
+ end: draggedItem.position + draggedItem.size,
34801
+ })) {
34802
+ this.deadZone = undefined;
34803
+ }
34804
+ if (draggedItemIndex === hoveredItemIndex) {
34805
+ this.onChange(this.getItemsPositions());
34806
+ return;
34807
+ }
34808
+ const startIndex = Math.min(draggedItemIndex, hoveredItemIndex);
34809
+ const endIndex = Math.max(draggedItemIndex, hoveredItemIndex);
34810
+ const direction = Math.sign(hoveredItemIndex - draggedItemIndex);
34811
+ let draggedItemMoveSize = 0;
34812
+ for (let i = startIndex; i <= endIndex; i++) {
34813
+ if (i === draggedItemIndex) {
34814
+ continue;
34815
+ }
34816
+ this.items[i].position -= direction * draggedItem.size;
34817
+ draggedItemMoveSize += this.items[i].size;
34818
+ }
34819
+ draggedItem.position += direction * draggedItemMoveSize;
34820
+ this.items.sort((item1, item2) => item1.position - item2.position);
34821
+ this.deadZone =
34822
+ direction > 0
34823
+ ? { start: position, end: draggedItem.position }
34824
+ : { start: draggedItem.position + draggedItem.size, end: position };
34825
+ this.onChange(this.getItemsPositions());
34826
+ }
34827
+ onMouseUp(ev) {
34828
+ if (ev.button !== 0) {
34829
+ this.onCancel();
34830
+ }
34831
+ ev.stopPropagation();
34832
+ ev.preventDefault();
34833
+ const targetItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
34834
+ this.onDragEnd(this.draggedItemId, targetItemIndex);
34835
+ this.stopEdgeScroll();
34836
+ return false;
34837
+ }
34838
+ startEdgeScroll(direction) {
34839
+ if (this.edgeScrollIntervalId)
34840
+ return;
34841
+ this.edgeScrollIntervalId = window.setInterval(() => {
34842
+ const offset = direction * 3;
34843
+ this.container.scroll += offset;
34844
+ }, 5);
34845
+ }
34846
+ stopEdgeScroll() {
34847
+ window.clearInterval(this.edgeScrollIntervalId);
34848
+ this.edgeScrollIntervalId = undefined;
34849
+ }
34850
+ /**
34851
+ * Get the index of the item the given mouse position is inside.
34852
+ * If the mouse is outside the container, return the first or last item index.
34853
+ */
33916
34854
  getHoveredItemIndex(mousePosition, items) {
33917
34855
  if (mousePosition <= this.minPosition)
33918
34856
  return 0;
@@ -34571,12 +35509,12 @@ class SelectionInput extends Component {
34571
35509
 
34572
35510
  class ValueInRangeCriterionForm extends CriterionForm {
34573
35511
  static template = "o-spreadsheet-ValueInRangeCriterionForm";
34574
- static components = { SelectionInput };
35512
+ static components = { RoundColorPicker, SelectionInput };
34575
35513
  setup() {
34576
35514
  super.setup();
34577
35515
  const setupDefault = (props) => {
34578
35516
  if (props.criterion.displayStyle === undefined) {
34579
- this.updateCriterion({ displayStyle: "arrow" });
35517
+ this.updateCriterion({ displayStyle: "chip" });
34580
35518
  }
34581
35519
  };
34582
35520
  onWillUpdateProps(setupDefault);
@@ -34589,6 +35527,16 @@ class ValueInRangeCriterionForm extends CriterionForm {
34589
35527
  const displayStyle = ev.target.value;
34590
35528
  this.updateCriterion({ displayStyle });
34591
35529
  }
35530
+ onColorChanged(color, value) {
35531
+ const colors = { ...this.props.criterion.colors };
35532
+ colors[value] = color || undefined;
35533
+ this.updateCriterion({ colors });
35534
+ }
35535
+ get values() {
35536
+ const sheetId = this.env.model.getters.getActiveSheetId();
35537
+ const values = this.env.model.getters.getDataValidationRangeValues(sheetId, this.props.criterion);
35538
+ return new Set(values);
35539
+ }
34592
35540
  }
34593
35541
 
34594
35542
  const criterionCategoriesSequences = {
@@ -36092,6 +37040,7 @@ css /* scss */ `
36092
37040
  // We need here the svg of the icons that we need to convert to images for the renderer
36093
37041
  // -----------------------------------------------------------------------------
36094
37042
  const ARROW_DOWN = {
37043
+ name: "ARROW_DOWN",
36095
37044
  width: 448,
36096
37045
  height: 512,
36097
37046
  paths: [
@@ -36102,6 +37051,7 @@ const ARROW_DOWN = {
36102
37051
  ],
36103
37052
  };
36104
37053
  const ARROW_UP = {
37054
+ name: "ARROW_UP",
36105
37055
  width: 448,
36106
37056
  height: 512,
36107
37057
  paths: [
@@ -36112,6 +37062,7 @@ const ARROW_UP = {
36112
37062
  ],
36113
37063
  };
36114
37064
  const ARROW_RIGHT = {
37065
+ name: "ARROW_RIGHT",
36115
37066
  width: 448,
36116
37067
  height: 512,
36117
37068
  paths: [
@@ -36122,6 +37073,7 @@ const ARROW_RIGHT = {
36122
37073
  ],
36123
37074
  };
36124
37075
  const SMILE = {
37076
+ name: "SMILE",
36125
37077
  width: 496,
36126
37078
  height: 512,
36127
37079
  paths: [
@@ -36132,6 +37084,7 @@ const SMILE = {
36132
37084
  ],
36133
37085
  };
36134
37086
  const MEH = {
37087
+ name: "MEH",
36135
37088
  width: 496,
36136
37089
  height: 512,
36137
37090
  paths: [
@@ -36142,6 +37095,7 @@ const MEH = {
36142
37095
  ],
36143
37096
  };
36144
37097
  const FROWN = {
37098
+ name: "FROWN",
36145
37099
  width: 496,
36146
37100
  height: 512,
36147
37101
  paths: [
@@ -36153,44 +37107,79 @@ const FROWN = {
36153
37107
  };
36154
37108
  const DOT_PATH = "M256 9 a247 247 0 1 0.1 0 0";
36155
37109
  const GREEN_DOT = {
37110
+ name: "GREEN_DOT",
36156
37111
  width: 512,
36157
37112
  height: 512,
36158
37113
  paths: [{ fillColor: "#6AA84F", path: DOT_PATH }],
36159
37114
  };
36160
37115
  const YELLOW_DOT = {
37116
+ name: "YELLOW_DOT",
36161
37117
  width: 512,
36162
37118
  height: 512,
36163
37119
  paths: [{ fillColor: "#F0AD4E", path: DOT_PATH }],
36164
37120
  };
36165
37121
  const RED_DOT = {
37122
+ name: "RED_DOT",
36166
37123
  width: 512,
36167
37124
  height: 512,
36168
37125
  paths: [{ fillColor: "#E06666", path: DOT_PATH }],
36169
37126
  };
36170
- const CARET_DOWN = {
36171
- width: 512,
36172
- height: 512,
36173
- paths: [{ fillColor: TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
36174
- };
36175
- const HOVERED_CARET_DOWN = {
36176
- width: 512,
36177
- height: 512,
36178
- paths: [
36179
- { fillColor: TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
36180
- { fillColor: "#fff", path: "M120 195 h270 l-135 130" },
36181
- ],
36182
- };
37127
+ function getCaretDownSvg(color) {
37128
+ return {
37129
+ name: "CARET_DOWN",
37130
+ width: 512,
37131
+ height: 512,
37132
+ paths: [{ fillColor: color.textColor || TEXT_BODY_MUTED, path: "M120 195 h270 l-135 130" }],
37133
+ };
37134
+ }
37135
+ function getHoveredCaretDownSvg(color) {
37136
+ return {
37137
+ name: "CARET_DOWN",
37138
+ width: 512,
37139
+ height: 512,
37140
+ paths: [
37141
+ { fillColor: color.textColor || TEXT_BODY_MUTED, path: "M15 15 h482 v482 h-482" },
37142
+ { fillColor: color.fillColor || "#fff", path: "M120 195 h270 l-135 130" },
37143
+ ],
37144
+ };
37145
+ }
37146
+ const CHIP_CARET_DOWN_PATH = "M40 185 h270 l-135 128";
37147
+ function getChipSvg(chipStyle) {
37148
+ return {
37149
+ name: "CHIP",
37150
+ width: 512,
37151
+ height: 512,
37152
+ paths: [{ fillColor: chipStyle.textColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH }],
37153
+ };
37154
+ }
37155
+ function getHoveredChipSvg(chipStyle) {
37156
+ return {
37157
+ name: "CHIP",
37158
+ width: 512,
37159
+ height: 512,
37160
+ paths: [
37161
+ {
37162
+ fillColor: chipStyle.textColor || TEXT_BODY_MUTED,
37163
+ path: "M0,225 A175,175 0 1,0 350,225 A175,175 0 1,0 0,225",
37164
+ },
37165
+ { fillColor: chipStyle.fillColor || TEXT_BODY_MUTED, path: CHIP_CARET_DOWN_PATH },
37166
+ ],
37167
+ };
37168
+ }
36183
37169
  const CHECKBOX_UNCHECKED = {
37170
+ name: "CHECKBOX_UNCHECKED",
36184
37171
  width: 512,
36185
37172
  height: 512,
36186
37173
  paths: [{ fillColor: GRAY_300, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36187
37174
  };
36188
37175
  const CHECKBOX_UNCHECKED_HOVERED = {
37176
+ name: "CHECKBOX_UNCHECKED",
36189
37177
  width: 512,
36190
37178
  height: 512,
36191
37179
  paths: [{ fillColor: ACTION_COLOR, path: "M45,45 h422 v422 h-422 v-422 m30,30 v362 h362 v-362" }],
36192
37180
  };
36193
37181
  const CHECKBOX_CHECKED = {
37182
+ name: "CHECKBOX_CHECKED",
36194
37183
  width: 512,
36195
37184
  height: 512,
36196
37185
  paths: [
@@ -36203,6 +37192,7 @@ function getPivotIconSvg(isCollapsed, isHovered) {
36203
37192
  ? "M149,235 h213 v43 h-213 M235,149 h43 v213 h-43" // +
36204
37193
  : "M149,235 h213 v43 h-213"; // -
36205
37194
  return {
37195
+ name: "PIVOT_ICON",
36206
37196
  width: 512,
36207
37197
  height: 512,
36208
37198
  paths: [
@@ -36229,6 +37219,7 @@ function getDataFilterIcon(isActive, isHighContrast, isHovered) {
36229
37219
  colors.hoverBackgroundColor = "#fff";
36230
37220
  }
36231
37221
  return {
37222
+ name: "DATA_FILTER_ICON",
36232
37223
  width: isActive ? 24 : 850,
36233
37224
  height: isActive ? 24 : 850,
36234
37225
  paths: [
@@ -40424,6 +41415,23 @@ migrationStepRegistry
40424
41415
  }
40425
41416
  return data;
40426
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
+ },
40427
41435
  });
40428
41436
  function fixOverlappingFilters(data) {
40429
41437
  for (const sheet of data.sheets || []) {
@@ -41087,6 +42095,10 @@ const REMOVE_ROWS_ACTION = (env) => {
41087
42095
  });
41088
42096
  };
41089
42097
  const CAN_REMOVE_COLUMNS_ROWS = (dimension, env) => {
42098
+ if ((dimension === "COL" && env.model.getters.getActiveRows().size > 0) ||
42099
+ (dimension === "ROW" && env.model.getters.getActiveCols().size > 0)) {
42100
+ return false;
42101
+ }
41090
42102
  const sheetId = env.model.getters.getActiveSheetId();
41091
42103
  const selectedElements = env.model.getters.getElementsFromSelection(dimension);
41092
42104
  const includesAllVisibleHeaders = env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, dimension, selectedElements);
@@ -42641,7 +43653,7 @@ const insertDropdown = {
42641
43653
  criterion: {
42642
43654
  type: "isValueInList",
42643
43655
  values: [],
42644
- displayStyle: "arrow",
43656
+ displayStyle: "chip",
42645
43657
  },
42646
43658
  },
42647
43659
  });
@@ -44556,6 +45568,11 @@ class GridComposer extends Component {
44556
45568
  rect = this.defaultRect;
44557
45569
  isEditing = false;
44558
45570
  isCellReferenceVisible = false;
45571
+ currentEditedCell = {
45572
+ col: 0,
45573
+ row: 0,
45574
+ sheetId: this.env.model.getters.getActiveSheetId(),
45575
+ };
44559
45576
  composerStore;
44560
45577
  composerFocusStore;
44561
45578
  composerInterface;
@@ -44680,12 +45697,17 @@ class GridComposer extends Component {
44680
45697
  if (!isEditing && this.composerFocusStore.activeComposer !== this.composerInterface) {
44681
45698
  this.composerFocusStore.focusComposer(this.composerInterface, { focusMode: "inactive" });
44682
45699
  }
45700
+ let shouldRecomputeRect = !deepEquals(this.currentEditedCell, this.composerStore.currentEditedCell);
44683
45701
  if (this.isEditing !== isEditing) {
44684
45702
  this.isEditing = isEditing;
44685
45703
  if (!isEditing) {
44686
45704
  this.rect = this.defaultRect;
44687
45705
  return;
44688
45706
  }
45707
+ this.currentEditedCell = this.composerStore.currentEditedCell;
45708
+ shouldRecomputeRect = true;
45709
+ }
45710
+ if (shouldRecomputeRect) {
44689
45711
  const position = this.env.model.getters.getActivePosition();
44690
45712
  const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
44691
45713
  this.rect = this.env.model.getters.getVisibleRect(zone);
@@ -45476,15 +46498,16 @@ function useHoveredElement(ref) {
45476
46498
  return state;
45477
46499
  }
45478
46500
 
46501
+ const PAINT_FORMAT_HANDLER_KEYS = [
46502
+ "cell",
46503
+ "border",
46504
+ "table",
46505
+ "conditionalFormat",
46506
+ "merge",
46507
+ ];
45479
46508
  class PaintFormatStore extends SpreadsheetStore {
45480
46509
  mutators = ["activate", "cancel", "pasteFormat"];
45481
46510
  highlightStore = this.get(HighlightStore);
45482
- clipboardHandlers = [
45483
- new CellClipboardHandler(this.getters, this.model.dispatch),
45484
- new BorderClipboardHandler(this.getters, this.model.dispatch),
45485
- new TableClipboardHandler(this.getters, this.model.dispatch),
45486
- new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
45487
- ];
45488
46511
  status = "inactive";
45489
46512
  copiedData;
45490
46513
  constructor(get) {
@@ -45515,24 +46538,38 @@ class PaintFormatStore extends SpreadsheetStore {
45515
46538
  get isActive() {
45516
46539
  return this.status !== "inactive";
45517
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
+ }
45518
46550
  copyFormats() {
45519
46551
  const sheetId = this.getters.getActiveSheetId();
45520
46552
  const zones = this.getters.getSelectedZones();
45521
- const copiedData = {};
45522
- for (const handler of this.clipboardHandlers) {
45523
- 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
+ }
45524
46559
  }
45525
46560
  return copiedData;
45526
46561
  }
45527
46562
  paintFormat(sheetId, target) {
45528
- if (this.copiedData) {
45529
- for (const handler of this.clipboardHandlers) {
45530
- handler.paste({ zones: target, sheetId }, this.copiedData, {
45531
- isCutOperation: false,
45532
- pasteOption: "onlyFormat",
45533
- });
45534
- }
46563
+ if (!this.copiedData) {
46564
+ return;
45535
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);
45536
46573
  if (this.status === "oneOff") {
45537
46574
  this.cancel();
45538
46575
  }
@@ -45579,12 +46616,8 @@ class HoveredTableStore extends SpreadsheetStore {
45579
46616
  this.row = undefined;
45580
46617
  }
45581
46618
  computeOverlay() {
45582
- if (!this.getters.isDashboard()) {
45583
- return;
45584
- }
45585
46619
  this.overlayColors = new PositionMap();
45586
- const col = this.col;
45587
- const row = this.row;
46620
+ const { col, row } = this;
45588
46621
  if (col === undefined || row === undefined) {
45589
46622
  return;
45590
46623
  }
@@ -45593,9 +46626,16 @@ class HoveredTableStore extends SpreadsheetStore {
45593
46626
  if (!table) {
45594
46627
  return;
45595
46628
  }
45596
- const { left, right } = table.range.zone;
45597
- for (let c = left; c <= right; c++) {
45598
- 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
+ }
45599
46639
  }
45600
46640
  }
45601
46641
  }
@@ -45791,11 +46831,14 @@ class GridOverlay extends Component {
45791
46831
  onCellClicked(ev) {
45792
46832
  const openedPopover = this.cellPopovers.persistentCellPopover;
45793
46833
  const [col, row] = this.getCartesianCoordinates(ev);
46834
+ const clickedIcon = this.getInteractiveIconAtEvent(ev);
46835
+ if (clickedIcon) {
46836
+ this.env.model.selection.getBackToDefault();
46837
+ }
45794
46838
  this.props.onCellClicked(col, row, {
45795
46839
  expandZone: ev.shiftKey,
45796
46840
  addZone: isCtrlKey(ev),
45797
46841
  }, ev);
45798
- const clickedIcon = this.getInteractiveIconAtEvent(ev);
45799
46842
  if (clickedIcon?.onClick) {
45800
46843
  clickedIcon.onClick(clickedIcon.position, this.env);
45801
46844
  }
@@ -45840,7 +46883,10 @@ class GridOverlay extends Component {
45840
46883
  }
45841
46884
  const icons = this.env.model.getters.getCellIcons(position);
45842
46885
  const icon = icons.find((icon) => {
45843
- 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));
45844
46890
  });
45845
46891
  return icon?.onClick ? icon : undefined;
45846
46892
  }
@@ -46577,19 +47623,56 @@ class HeadersOverlay extends Component {
46577
47623
  }
46578
47624
  }
46579
47625
 
46580
- class GridRenderer {
46581
- getters;
46582
- renderer;
47626
+ const CELL_ANIMATION_DURATION = 200;
47627
+ class GridRenderer extends SpreadsheetStore {
46583
47628
  fingerprints;
46584
47629
  hoveredTables;
46585
47630
  hoveredIcon;
47631
+ lastRenderBoxes = new Map();
47632
+ preventNewAnimationsInNextFrame = false;
47633
+ zonesWithPreventedAnimationsInNextFrame = [];
47634
+ animations = new Map();
46586
47635
  constructor(get) {
47636
+ super(get);
46587
47637
  this.getters = get(ModelStore).getters;
46588
- this.renderer = get(RendererStore);
46589
47638
  this.fingerprints = get(FormulaFingerprintStore);
46590
47639
  this.hoveredTables = get(HoveredTableStore);
46591
47640
  this.hoveredIcon = get(HoveredIconStore);
46592
- 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
+ }
46593
47676
  }
46594
47677
  get renderingLayers() {
46595
47678
  return ["Background", "Headers"];
@@ -46597,17 +47680,20 @@ class GridRenderer {
46597
47680
  // ---------------------------------------------------------------------------
46598
47681
  // Grid rendering
46599
47682
  // ---------------------------------------------------------------------------
46600
- drawLayer(renderingContext, layer) {
47683
+ drawLayer(renderingContext, layer, timeStamp) {
46601
47684
  switch (layer) {
46602
47685
  case "Background":
46603
47686
  this.drawGlobalBackground(renderingContext);
47687
+ const oldBoxes = this.lastRenderBoxes;
47688
+ this.lastRenderBoxes = new Map();
46604
47689
  for (const { zone, rect } of this.getters.getAllActiveViewportsZonesAndRect()) {
46605
47690
  const { ctx } = renderingContext;
46606
47691
  ctx.save();
46607
47692
  ctx.beginPath();
46608
47693
  ctx.rect(rect.x, rect.y, rect.width, rect.height);
46609
47694
  ctx.clip();
46610
- const boxes = this.getGridBoxes(zone);
47695
+ const boxesWithoutAnimations = this.getGridBoxes(zone);
47696
+ const boxes = this.getBoxesWithAnimations(boxesWithoutAnimations, oldBoxes, timeStamp);
46611
47697
  this.drawBackground(renderingContext, boxes);
46612
47698
  this.drawOverflowingCellBackground(renderingContext, boxes);
46613
47699
  this.drawCellBackground(renderingContext, boxes);
@@ -46617,6 +47703,8 @@ class GridRenderer {
46617
47703
  ctx.restore();
46618
47704
  }
46619
47705
  this.drawFrozenPanes(renderingContext);
47706
+ this.preventNewAnimationsInNextFrame = false;
47707
+ this.zonesWithPreventedAnimationsInNextFrame = [];
46620
47708
  break;
46621
47709
  case "Headers":
46622
47710
  if (!this.getters.isDashboard()) {
@@ -46640,6 +47728,8 @@ class GridRenderer {
46640
47728
  const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;
46641
47729
  if (areGridLinesVisible) {
46642
47730
  for (const box of boxes) {
47731
+ if (box.skipCellGridLines)
47732
+ continue;
46643
47733
  ctx.strokeStyle = CELL_BORDER_COLOR;
46644
47734
  ctx.lineWidth = thinLineWidth;
46645
47735
  ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);
@@ -46660,6 +47750,19 @@ class GridRenderer {
46660
47750
  const width = box.width * (percentage / 100);
46661
47751
  ctx.fillRect(box.x, box.y, width, box.height);
46662
47752
  }
47753
+ if (box?.chip) {
47754
+ ctx.save();
47755
+ ctx.beginPath();
47756
+ ctx.rect(box.x, box.y, box.width, box.height);
47757
+ ctx.clip();
47758
+ const chip = box.chip;
47759
+ ctx.fillStyle = chip.color;
47760
+ const radius = 10;
47761
+ ctx.beginPath();
47762
+ ctx.roundRect(chip.x, chip.y, chip.width, chip.height, radius);
47763
+ ctx.fill();
47764
+ ctx.restore();
47765
+ }
46663
47766
  if (box.overlayColor) {
46664
47767
  ctx.fillStyle = box.overlayColor;
46665
47768
  ctx.fillRect(box.x, box.y, box.width, box.height);
@@ -46734,7 +47837,8 @@ class GridRenderer {
46734
47837
  * each line and adding 1 pixel to the end of each line (depending on the direction of the
46735
47838
  * line).
46736
47839
  */
46737
- function drawBorder({ style, color }, x1, y1, x2, y2) {
47840
+ function drawBorder({ color, style, opacity }, x1, y1, x2, y2) {
47841
+ ctx.globalAlpha = opacity ?? 1;
46738
47842
  ctx.strokeStyle = color;
46739
47843
  switch (style) {
46740
47844
  case "medium":
@@ -46782,6 +47886,7 @@ class GridRenderer {
46782
47886
  ctx.stroke();
46783
47887
  ctx.lineWidth = 1;
46784
47888
  ctx.setLineDash([]);
47889
+ ctx.globalAlpha = 1;
46785
47890
  }
46786
47891
  }
46787
47892
  drawTexts(renderingContext, boxes) {
@@ -46790,6 +47895,7 @@ class GridRenderer {
46790
47895
  let currentFont;
46791
47896
  for (const box of boxes) {
46792
47897
  if (box.content) {
47898
+ ctx.globalAlpha = box.textOpacity ?? 1;
46793
47899
  const style = box.style || {};
46794
47900
  const align = box.content.align || "left";
46795
47901
  // compute font and textColor
@@ -46799,19 +47905,6 @@ class GridRenderer {
46799
47905
  ctx.font = font;
46800
47906
  }
46801
47907
  ctx.fillStyle = style.textColor || "#000";
46802
- // compute horizontal align start point parameter
46803
- let x = box.x;
46804
- if (align === "left") {
46805
- const leftIconSize = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
46806
- x += MIN_CELL_TEXT_MARGIN + leftIconSize;
46807
- }
46808
- else if (align === "right") {
46809
- const rightIconSize = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
46810
- x += box.width - MIN_CELL_TEXT_MARGIN - rightIconSize;
46811
- }
46812
- else {
46813
- x += box.width / 2;
46814
- }
46815
47908
  // horizontal align text direction
46816
47909
  ctx.textAlign = align;
46817
47910
  // clip rect if needed
@@ -46822,19 +47915,18 @@ class GridRenderer {
46822
47915
  ctx.rect(x, y, width, height);
46823
47916
  ctx.clip();
46824
47917
  }
46825
- // compute vertical align start point parameter:
46826
- const textLineHeight = computeTextFontSizeInPixels(style);
46827
- const numberOfLines = box.content.textLines.length;
46828
- let y = this.getters.computeTextYCoordinate(box, textLineHeight, style.verticalAlign, numberOfLines);
47918
+ const x = box.content.x;
47919
+ let y = box.content.y;
46829
47920
  // use the horizontal and the vertical start points to:
46830
47921
  // fill text / fill strikethrough / fill underline
46831
47922
  for (const brokenLine of box.content.textLines) {
46832
- drawDecoratedText(ctx, brokenLine, { x: Math.round(x), y: Math.round(y) }, style.underline, style.strikethrough);
46833
- y += MIN_CELL_TEXT_MARGIN + textLineHeight;
47923
+ drawDecoratedText(ctx, brokenLine, { x, y }, style.underline, style.strikethrough);
47924
+ y += MIN_CELL_TEXT_MARGIN + box.content.fontSizePx;
46834
47925
  }
46835
47926
  if (box.clipRect) {
46836
47927
  ctx.restore();
46837
47928
  }
47929
+ ctx.globalAlpha = 1;
46838
47930
  }
46839
47931
  }
46840
47932
  }
@@ -46852,10 +47944,11 @@ class GridRenderer {
46852
47944
  }
46853
47945
  ctx.save();
46854
47946
  ctx.beginPath();
46855
- 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);
46856
47949
  ctx.clip();
46857
47950
  const iconSize = icon.size;
46858
- const { x, y } = this.getters.getCellIconRect(icon);
47951
+ const { x, y } = this.getters.getCellIconRect(icon, box);
46859
47952
  ctx.translate(x, y);
46860
47953
  ctx.scale(iconSize / svg.width, iconSize / svg.height);
46861
47954
  for (const path of svg.paths) {
@@ -47064,12 +48157,15 @@ class GridRenderer {
47064
48157
  const cell = this.getters.getEvaluatedCell(position);
47065
48158
  const showFormula = this.getters.shouldShowFormulas();
47066
48159
  const { x, y, width, height } = this.getters.getRect(zone);
47067
- const { verticalAlign } = this.getters.getCellStyle(position);
48160
+ const chipStyle = this.getters.getDataValidationChipStyle(position);
47068
48161
  let style = this.getters.getCellComputedStyle(position);
47069
48162
  if (this.fingerprints.isEnabled) {
47070
48163
  const fingerprintColor = this.fingerprints.colors.get(position);
47071
48164
  style = { ...style, fillColor: fingerprintColor };
47072
48165
  }
48166
+ if (chipStyle?.textColor) {
48167
+ style = { ...style, textColor: chipStyle.textColor };
48168
+ }
47073
48169
  const dataBarFill = this.fingerprints.isEnabled
47074
48170
  ? undefined
47075
48171
  : this.getters.getConditionalDataBar(position);
@@ -47080,6 +48176,7 @@ class GridRenderer {
47080
48176
  center: iconsList.find((icon) => icon?.horizontalAlign === "center"),
47081
48177
  };
47082
48178
  const box = {
48179
+ id: zoneToXc(zone),
47083
48180
  x,
47084
48181
  y,
47085
48182
  width,
@@ -47087,11 +48184,11 @@ class GridRenderer {
47087
48184
  border: this.getters.getCellComputedBorder(position) || undefined,
47088
48185
  style,
47089
48186
  dataBarFill,
47090
- verticalAlign,
47091
48187
  overlayColor: this.hoveredTables.overlayColors.get(position),
47092
48188
  isError: (cell.type === CellValueType.error && !!cell.message) ||
47093
48189
  this.getters.isDataValidationInvalid(position),
47094
48190
  icons: cellIcons,
48191
+ disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame.some((z) => isZoneInside(zone, z) || overlap(zone, z)),
47095
48192
  };
47096
48193
  const fontSizePX = computeTextFontSizeInPixels(box.style);
47097
48194
  if (cell.type === CellValueType.empty || box.icons.center) {
@@ -47103,22 +48200,55 @@ class GridRenderer {
47103
48200
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
47104
48201
  const multiLineText = this.getters.getCellMultiLineText(position, { maxWidth, wrapText });
47105
48202
  const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
48203
+ const chipMargin = chipStyle ? DATA_VALIDATION_CHIP_MARGIN : 0;
47106
48204
  const leftIconWidth = box.icons.left ? box.icons.left.size + box.icons.left.margin : 0;
48205
+ const leftMargin = leftIconWidth + chipMargin;
47107
48206
  const rightIconWidth = box.icons.right ? box.icons.right.size + box.icons.right.margin : 0;
47108
- const contentWidth = leftIconWidth + textWidth + rightIconWidth;
48207
+ const rightMargin = rightIconWidth + chipMargin;
48208
+ const contentWidth = leftMargin + textWidth + rightMargin;
47109
48209
  const align = this.computeCellAlignment(position, contentWidth > width);
48210
+ // compute vertical align start point parameter:
48211
+ const numberOfLines = multiLineText.length;
48212
+ const contentY = Math.round(this.getters.computeTextYCoordinate(box, fontSizePX, style.verticalAlign, numberOfLines));
48213
+ // compute horizontal align start point parameter
48214
+ let contentX = box.x;
48215
+ if (align === "left") {
48216
+ contentX += MIN_CELL_TEXT_MARGIN + leftMargin;
48217
+ }
48218
+ else if (align === "right") {
48219
+ contentX += box.width - MIN_CELL_TEXT_MARGIN - rightMargin;
48220
+ }
48221
+ else {
48222
+ contentX += box.width / 2;
48223
+ }
48224
+ contentX = Math.round(contentX);
48225
+ const textHeight = computeTextLinesHeight(fontSizePX, numberOfLines);
47110
48226
  box.content = {
47111
48227
  textLines: multiLineText,
47112
48228
  width: wrapping === "overflow" ? textWidth : width,
47113
48229
  align,
48230
+ x: contentX,
48231
+ y: contentY,
48232
+ fontSizePx: fontSizePX,
47114
48233
  };
48234
+ if (chipStyle?.fillColor) {
48235
+ const chipMarginLeft = leftMargin;
48236
+ const chipMarginRight = DATA_VALIDATION_CHIP_MARGIN;
48237
+ box.chip = {
48238
+ color: chipStyle.fillColor,
48239
+ width: box.width - chipMarginLeft - chipMarginRight,
48240
+ height: textHeight + 2,
48241
+ x: box.x + chipMarginLeft,
48242
+ y: contentY - 2,
48243
+ };
48244
+ }
47115
48245
  /** ClipRect */
47116
48246
  const isOverflowing = contentWidth > width || fontSizePX > height;
47117
- if (box.icons.left || box.icons.right) {
48247
+ if (box.icons.left || box.icons.right || box.chip) {
47118
48248
  box.clipRect = {
47119
- x: box.x + leftIconWidth,
48249
+ x: box.x + leftMargin,
47120
48250
  y: box.y,
47121
- width: Math.max(0, width - leftIconWidth - rightIconWidth),
48251
+ width: Math.max(0, width - leftMargin - rightMargin),
47122
48252
  height,
47123
48253
  };
47124
48254
  }
@@ -47228,6 +48358,77 @@ class GridRenderer {
47228
48358
  }
47229
48359
  return boxes;
47230
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
+ }
47231
48432
  }
47232
48433
 
47233
48434
  function useGridDrawing(refName, model, canvasSize) {
@@ -47262,10 +48463,7 @@ function useGridDrawing(refName, model, canvasSize) {
47262
48463
  // http://diveintohtml5.info/canvas.html#pixel-madness
47263
48464
  ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT);
47264
48465
  ctx.scale(dpr, dpr);
47265
- for (const layer of OrderedLayers()) {
47266
- model.drawLayer(renderingContext, layer);
47267
- rendererStore.drawLayer(renderingContext, layer);
47268
- }
48466
+ rendererStore.draw(renderingContext);
47269
48467
  }
47270
48468
  }
47271
48469
 
@@ -47818,15 +49016,6 @@ class Selection extends Component {
47818
49016
  }
47819
49017
  }
47820
49018
 
47821
- class Section extends Component {
47822
- static template = "o_spreadsheet.Section";
47823
- static props = {
47824
- class: { type: String, optional: true },
47825
- title: { type: String, optional: true },
47826
- slots: Object,
47827
- };
47828
- }
47829
-
47830
49019
  class ChartDataSeries extends Component {
47831
49020
  static template = "o-spreadsheet.ChartDataSeries";
47832
49021
  static components = { SelectionInput, Section };
@@ -48165,532 +49354,213 @@ class GenericChartConfigPanel extends Component {
48165
49354
  * Change the local labelRange. The model should be updated when the
48166
49355
  * button "confirm" is clicked
48167
49356
  */
48168
- onLabelRangeChanged(ranges) {
48169
- this.labelRange = ranges[0];
48170
- this.state.labelsDispatchResult = this.props.canUpdateChart(this.props.figureId, {
48171
- labelRange: this.labelRange,
48172
- });
48173
- }
48174
- onLabelRangeConfirmed() {
48175
- this.state.labelsDispatchResult = this.props.updateChart(this.props.figureId, {
48176
- labelRange: this.labelRange,
48177
- });
48178
- }
48179
- getLabelRange() {
48180
- return this.labelRange || "";
48181
- }
48182
- onUpdateAggregated(aggregated) {
48183
- this.props.updateChart(this.props.figureId, {
48184
- aggregated,
48185
- });
48186
- }
48187
- calculateHeaderPosition() {
48188
- if (this.isDatasetInvalid || this.isLabelInvalid) {
48189
- return undefined;
48190
- }
48191
- const getters = this.env.model.getters;
48192
- const sheetId = getters.getActiveSheetId();
48193
- const labelRange = createValidRange(getters, sheetId, this.labelRange);
48194
- const dataSets = createDataSets(getters, this.dataSets, sheetId, this.props.definition.dataSetsHaveTitle);
48195
- if (dataSets.length) {
48196
- return this.datasetOrientation === "rows"
48197
- ? dataSets[0].dataRange.zone.left
48198
- : dataSets[0].dataRange.zone.top + 1;
48199
- }
48200
- else if (labelRange) {
48201
- return labelRange.zone.top + 1;
48202
- }
48203
- return undefined;
48204
- }
48205
- get maxNumberOfUsedRanges() {
48206
- return chartRegistry.get(this.props.definition.type).dataSeriesLimit;
48207
- }
48208
- transposeDataSet(dataRanges, datasetOrientation) {
48209
- const getters = this.env.model.getters;
48210
- if (datasetOrientation === undefined) {
48211
- return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange }));
48212
- }
48213
- const zonesBySheetName = {};
48214
- const transposedDatasets = [];
48215
- const figureSheetId = getters.getFigureSheetId(this.props.figureId);
48216
- let name = getters.getActiveSheet().name;
48217
- if (figureSheetId) {
48218
- name = getters.getSheet(figureSheetId).name;
48219
- }
48220
- for (const dataRange of dataRanges) {
48221
- if (!dataRange) {
48222
- continue;
48223
- }
48224
- if (!isXcRepresentation(dataRange)) {
48225
- return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange }));
48226
- }
48227
- let { sheetName, xc } = splitReference(dataRange);
48228
- sheetName = sheetName ?? name;
48229
- if (!zonesBySheetName[sheetName]) {
48230
- zonesBySheetName[sheetName] = [];
48231
- }
48232
- zonesBySheetName[sheetName].push(toZone(xc));
48233
- }
48234
- for (const sheetName in zonesBySheetName) {
48235
- const zones = zonesBySheetName[sheetName];
48236
- const contiguousZones = mergeContiguousZones(zones);
48237
- if (datasetOrientation === "columns") {
48238
- for (const zone of contiguousZones) {
48239
- for (let col = zone.left; col <= zone.right; col++) {
48240
- const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
48241
- ...zone,
48242
- left: col,
48243
- right: col,
48244
- })}`;
48245
- transposedDatasets.push({ dataRange });
48246
- }
48247
- }
48248
- }
48249
- else {
48250
- for (const zone of contiguousZones) {
48251
- for (let row = zone.top; row <= zone.bottom; row++) {
48252
- const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
48253
- ...zone,
48254
- top: row,
48255
- bottom: row,
48256
- })}`;
48257
- transposedDatasets.push({ dataRange });
48258
- }
48259
- }
48260
- }
48261
- }
48262
- return transposedDatasets;
48263
- }
48264
- }
48265
-
48266
- class BarConfigPanel extends GenericChartConfigPanel {
48267
- static template = "o-spreadsheet-BarConfigPanel";
48268
- get stackedLabel() {
48269
- const definition = this.props.definition;
48270
- return definition.horizontal
48271
- ? this.chartTerms.StackedBarChart
48272
- : this.chartTerms.StackedColumnChart;
48273
- }
48274
- onUpdateStacked(stacked) {
48275
- this.props.updateChart(this.props.figureId, {
48276
- stacked,
48277
- });
48278
- }
48279
- }
48280
-
48281
- css /* scss */ `
48282
- .o-badge-selection {
48283
- gap: 1px;
48284
- button.o-button {
48285
- border-radius: 0;
48286
- &.selected {
48287
- color: ${GRAY_900};
48288
- border-color: ${ACTION_COLOR};
48289
- background: ${BADGE_SELECTED_COLOR};
48290
- font-weight: 600;
48291
- }
48292
-
48293
- &:first-child {
48294
- border-radius: 4px 0 0 4px;
48295
- }
48296
- &:last-child {
48297
- border-radius: 0 4px 4px 0;
48298
- }
48299
- }
48300
- }
48301
- `;
48302
- class BadgeSelection extends Component {
48303
- static template = "o-spreadsheet.BadgeSelection";
48304
- static props = {
48305
- choices: Array,
48306
- onChange: Function,
48307
- selectedValue: String,
48308
- };
48309
- }
48310
-
48311
- css /* scss */ `
48312
- .o-menu-item-button {
48313
- display: flex;
48314
- justify-content: center;
48315
- align-items: center;
48316
- margin: 2px 1px;
48317
- padding: 0px 1px;
48318
- border-radius: 2px;
48319
- min-width: 22px;
48320
- }
48321
- .o-disabled {
48322
- opacity: 0.6;
48323
- cursor: default;
48324
- }
48325
- `;
48326
- class ActionButton extends Component {
48327
- static template = "o-spreadsheet-ActionButton";
48328
- static props = {
48329
- action: Object,
48330
- hasTriangleDownIcon: { type: Boolean, optional: true },
48331
- selectedColor: { type: String, optional: true },
48332
- class: { type: String, optional: true },
48333
- onClick: { type: Function, optional: true },
48334
- };
48335
- actionButton = createAction(this.props.action);
48336
- setup() {
48337
- onWillUpdateProps((nextProps) => {
48338
- if (nextProps.action !== this.props.action) {
48339
- this.actionButton = createAction(nextProps.action);
48340
- }
48341
- });
48342
- }
48343
- get isVisible() {
48344
- return this.actionButton.isVisible(this.env);
48345
- }
48346
- get isEnabled() {
48347
- return this.actionButton.isEnabled(this.env);
48348
- }
48349
- get isActive() {
48350
- return this.actionButton.isActive?.(this.env);
48351
- }
48352
- get title() {
48353
- const name = this.actionButton.name(this.env);
48354
- const description = this.actionButton.description(this.env);
48355
- return name + (description ? ` (${description})` : "");
48356
- }
48357
- get iconTitle() {
48358
- return this.actionButton.icon(this.env);
48359
- }
48360
- onClick(ev) {
48361
- if (this.isEnabled) {
48362
- this.props.onClick?.(ev);
48363
- this.actionButton.execute?.(this.env);
48364
- }
48365
- }
48366
- get buttonStyle() {
48367
- if (this.props.selectedColor) {
48368
- return cssPropertiesToCss({
48369
- "border-bottom": `4px solid ${this.props.selectedColor}`,
48370
- height: "16px",
48371
- "margin-top": "2px",
48372
- });
48373
- }
48374
- return "";
48375
- }
48376
- }
48377
-
48378
- const LINE_VERTICAL_PADDING = 1;
48379
- const PICKER_PADDING = 8;
48380
- const ITEM_BORDER_WIDTH = 1;
48381
- const ITEM_EDGE_LENGTH = 18;
48382
- const ITEMS_PER_LINE = 10;
48383
- const MAGNIFIER_EDGE = 16;
48384
- const ITEM_GAP = 2;
48385
- const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
48386
- const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
48387
- const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
48388
- const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
48389
- css /* scss */ `
48390
- .o-color-picker {
48391
- padding: ${PICKER_PADDING}px 0;
48392
- /* FIXME: this is useless, overiden by the popover container */
48393
- box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
48394
- background-color: white;
48395
- line-height: 1.2;
48396
- overflow-y: auto;
48397
- overflow-x: hidden;
48398
- width: ${CONTAINER_WIDTH}px;
48399
-
48400
- .o-color-picker-section-name {
48401
- margin: 0px ${ITEM_BORDER_WIDTH}px;
48402
- padding: 4px ${PICKER_PADDING}px;
48403
- }
48404
- .colors-grid {
48405
- display: grid;
48406
- padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
48407
- grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
48408
- grid-gap: ${ITEM_GAP}px;
48409
- }
48410
- .o-color-picker-toggler-button {
48411
- display: flex;
48412
- .o-color-picker-toggler-sign {
48413
- display: flex;
48414
- margin: auto auto;
48415
- width: 55%;
48416
- height: 55%;
48417
- .o-icon {
48418
- width: 100%;
48419
- height: 100%;
48420
- }
48421
- }
48422
- }
48423
- .o-color-picker-line-item {
48424
- width: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48425
- height: ${ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH}px;
48426
- margin: 0px;
48427
- border-radius: 50px;
48428
- border: ${ITEM_BORDER_WIDTH}px solid #666666;
48429
- padding: 0px;
48430
- font-size: 16px;
48431
- background: white;
48432
- &:hover {
48433
- background-color: rgba(0, 0, 0, 0.08);
48434
- outline: 1px solid gray;
48435
- cursor: pointer;
48436
- }
48437
- }
48438
- .o-buttons {
48439
- padding: ${PICKER_PADDING}px;
48440
- display: flex;
48441
- .o-cancel {
48442
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48443
- width: 100%;
48444
- padding: 5px;
48445
- font-size: 14px;
48446
- background: white;
48447
- border-radius: 4px;
48448
- &:hover:enabled {
48449
- background-color: rgba(0, 0, 0, 0.08);
48450
- }
48451
- }
48452
- }
48453
- .o-add-button {
48454
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48455
- padding: 4px;
48456
- background: white;
48457
- border-radius: 4px;
48458
- &:hover:enabled {
48459
- background-color: rgba(0, 0, 0, 0.08);
48460
- }
48461
- }
48462
- .o-separator {
48463
- border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
48464
- margin-top: ${MENU_SEPARATOR_PADDING}px;
48465
- margin-bottom: ${MENU_SEPARATOR_PADDING}px;
48466
- }
48467
-
48468
- .o-custom-selector {
48469
- padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
48470
- position: relative;
48471
- .o-gradient {
48472
- margin-bottom: ${MAGNIFIER_EDGE / 2}px;
48473
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48474
- width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
48475
- height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
48476
- position: relative;
48477
- }
48478
-
48479
- .magnifier {
48480
- height: ${MAGNIFIER_EDGE}px;
48481
- width: ${MAGNIFIER_EDGE}px;
48482
- border-radius: 50%;
48483
- border: 2px solid #fff;
48484
- box-shadow: 0px 0px 3px #c0c0c0;
48485
- position: absolute;
48486
- z-index: 2;
48487
- }
48488
- .saturation {
48489
- background: linear-gradient(to right, #fff 0%, transparent 100%);
48490
- }
48491
- .lightness {
48492
- background: linear-gradient(to top, #000 0%, transparent 100%);
48493
- }
48494
- .o-hue-picker {
48495
- border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
48496
- width: 100%;
48497
- height: 12px;
48498
- border-radius: 4px;
48499
- background: linear-gradient(
48500
- to right,
48501
- hsl(0 100% 50%) 0%,
48502
- hsl(0.2turn 100% 50%) 20%,
48503
- hsl(0.3turn 100% 50%) 30%,
48504
- hsl(0.4turn 100% 50%) 40%,
48505
- hsl(0.5turn 100% 50%) 50%,
48506
- hsl(0.6turn 100% 50%) 60%,
48507
- hsl(0.7turn 100% 50%) 70%,
48508
- hsl(0.8turn 100% 50%) 80%,
48509
- hsl(0.9turn 100% 50%) 90%,
48510
- hsl(1turn 100% 50%) 100%
48511
- );
48512
- position: relative;
48513
- cursor: crosshair;
48514
- }
48515
- .o-hue-slider {
48516
- margin-top: -3px;
48517
- }
48518
- .o-custom-input-preview {
48519
- padding: 2px 0px;
48520
- display: flex;
48521
- input {
48522
- width: 50%;
48523
- border-radius: 4px;
48524
- padding: 4px 23px 4px 10px;
48525
- height: 24px;
48526
- border: 1px solid #c0c0c0;
48527
- margin-right: 2px;
48528
- }
48529
- .o-wrong-color {
48530
- /* FIXME bootstrap class instead? */
48531
- outline-color: red;
48532
- border-color: red;
48533
- &:focus {
48534
- outline-style: solid;
48535
- outline-width: 1px;
48536
- }
48537
- }
48538
- }
48539
- .o-custom-input-buttons {
48540
- padding: 2px 0px;
48541
- display: flex;
48542
- justify-content: end;
48543
- }
48544
- .o-color-preview {
48545
- border: 1px solid #c0c0c0;
48546
- border-radius: 4px;
48547
- width: 50%;
48548
- }
48549
- }
48550
- }
48551
- `;
48552
- class ColorPicker extends Component {
48553
- static template = "o-spreadsheet-ColorPicker";
48554
- static props = {
48555
- onColorPicked: Function,
48556
- currentColor: { type: String, optional: true },
48557
- maxHeight: { type: Number, optional: true },
48558
- anchorRect: Object,
48559
- disableNoColor: { type: Boolean, optional: true },
48560
- };
48561
- static defaultProps = { currentColor: "" };
48562
- static components = { Popover };
48563
- COLORS = COLOR_PICKER_DEFAULTS;
48564
- state = useState({
48565
- showGradient: false,
48566
- currentHslaColor: isColorValid(this.props.currentColor)
48567
- ? { ...hexToHSLA(this.props.currentColor), a: 1 }
48568
- : { h: 0, s: 100, l: 100, a: 1 },
48569
- customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
48570
- });
48571
- get colorPickerStyle() {
48572
- if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
48573
- return cssPropertiesToCss({ display: "none" });
48574
- }
48575
- return "";
48576
- }
48577
- get popoverProps() {
48578
- return {
48579
- anchorRect: this.props.anchorRect,
48580
- maxHeight: this.props.maxHeight,
48581
- positioning: "bottom-left",
48582
- verticalOffset: 0,
48583
- };
48584
- }
48585
- get gradientHueStyle() {
48586
- const hue = this.state.currentHslaColor?.h || 0;
48587
- return cssPropertiesToCss({
48588
- background: `hsl(${hue} 100% 50%)`,
49357
+ onLabelRangeChanged(ranges) {
49358
+ this.labelRange = ranges[0];
49359
+ this.state.labelsDispatchResult = this.props.canUpdateChart(this.props.figureId, {
49360
+ labelRange: this.labelRange,
48589
49361
  });
48590
49362
  }
48591
- get sliderStyle() {
48592
- const hue = this.state.currentHslaColor?.h || 0;
48593
- const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
48594
- const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
48595
- return cssPropertiesToCss({
48596
- "margin-left": `${left}px`,
49363
+ onLabelRangeConfirmed() {
49364
+ this.state.labelsDispatchResult = this.props.updateChart(this.props.figureId, {
49365
+ labelRange: this.labelRange,
48597
49366
  });
48598
49367
  }
48599
- get pointerStyle() {
48600
- const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
48601
- const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
48602
- const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
48603
- return cssPropertiesToCss({
48604
- left: `${-MAGNIFIER_EDGE / 2 + left}px`,
48605
- top: `${-MAGNIFIER_EDGE / 2 + top}px`,
48606
- background: hslaToHex(this.state.currentHslaColor),
48607
- });
49368
+ getLabelRange() {
49369
+ return this.labelRange || "";
48608
49370
  }
48609
- get colorPreviewStyle() {
48610
- return cssPropertiesToCss({
48611
- "background-color": hslaToHex(this.state.currentHslaColor),
49371
+ onUpdateAggregated(aggregated) {
49372
+ this.props.updateChart(this.props.figureId, {
49373
+ aggregated,
48612
49374
  });
48613
49375
  }
48614
- get checkmarkColor() {
48615
- return chartFontColor(this.props.currentColor);
49376
+ calculateHeaderPosition() {
49377
+ if (this.isDatasetInvalid || this.isLabelInvalid) {
49378
+ return undefined;
49379
+ }
49380
+ const getters = this.env.model.getters;
49381
+ const sheetId = getters.getActiveSheetId();
49382
+ const labelRange = createValidRange(getters, sheetId, this.labelRange);
49383
+ const dataSets = createDataSets(getters, this.dataSets, sheetId, this.props.definition.dataSetsHaveTitle);
49384
+ if (dataSets.length) {
49385
+ return this.datasetOrientation === "rows"
49386
+ ? dataSets[0].dataRange.zone.left
49387
+ : dataSets[0].dataRange.zone.top + 1;
49388
+ }
49389
+ else if (labelRange) {
49390
+ return labelRange.zone.top + 1;
49391
+ }
49392
+ return undefined;
48616
49393
  }
48617
- get isHexColorInputValid() {
48618
- return !this.state.customHexColor || isColorValid(this.state.customHexColor);
49394
+ get maxNumberOfUsedRanges() {
49395
+ return chartRegistry.get(this.props.definition.type).dataSeriesLimit;
48619
49396
  }
48620
- setCustomGradient({ x, y }) {
48621
- const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
48622
- const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
48623
- const deltaX = offsetX / INNER_GRADIENT_WIDTH;
48624
- const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
48625
- const s = 100 * deltaX;
48626
- const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
48627
- this.updateColor({ s, l });
49397
+ transposeDataSet(dataRanges, datasetOrientation) {
49398
+ const getters = this.env.model.getters;
49399
+ if (datasetOrientation === undefined) {
49400
+ return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange }));
49401
+ }
49402
+ const zonesBySheetName = {};
49403
+ const transposedDatasets = [];
49404
+ const figureSheetId = getters.getFigureSheetId(this.props.figureId);
49405
+ let name = getters.getActiveSheet().name;
49406
+ if (figureSheetId) {
49407
+ name = getters.getSheet(figureSheetId).name;
49408
+ }
49409
+ for (const dataRange of dataRanges) {
49410
+ if (!dataRange) {
49411
+ continue;
49412
+ }
49413
+ if (!isXcRepresentation(dataRange)) {
49414
+ return dataRanges.filter(isDefined).map((dataRange) => ({ dataRange }));
49415
+ }
49416
+ let { sheetName, xc } = splitReference(dataRange);
49417
+ sheetName = sheetName ?? name;
49418
+ if (!zonesBySheetName[sheetName]) {
49419
+ zonesBySheetName[sheetName] = [];
49420
+ }
49421
+ zonesBySheetName[sheetName].push(toZone(xc));
49422
+ }
49423
+ for (const sheetName in zonesBySheetName) {
49424
+ const zones = zonesBySheetName[sheetName];
49425
+ const contiguousZones = mergeContiguousZones(zones);
49426
+ if (datasetOrientation === "columns") {
49427
+ for (const zone of contiguousZones) {
49428
+ for (let col = zone.left; col <= zone.right; col++) {
49429
+ const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
49430
+ ...zone,
49431
+ left: col,
49432
+ right: col,
49433
+ })}`;
49434
+ transposedDatasets.push({ dataRange });
49435
+ }
49436
+ }
49437
+ }
49438
+ else {
49439
+ for (const zone of contiguousZones) {
49440
+ for (let row = zone.top; row <= zone.bottom; row++) {
49441
+ const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
49442
+ ...zone,
49443
+ top: row,
49444
+ bottom: row,
49445
+ })}`;
49446
+ transposedDatasets.push({ dataRange });
49447
+ }
49448
+ }
49449
+ }
49450
+ }
49451
+ return transposedDatasets;
48628
49452
  }
48629
- setCustomHue(x) {
48630
- // needs to be capped such that h is in [0°, 359°]
48631
- const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
48632
- this.updateColor({ h });
49453
+ }
49454
+
49455
+ class BarConfigPanel extends GenericChartConfigPanel {
49456
+ static template = "o-spreadsheet-BarConfigPanel";
49457
+ get stackedLabel() {
49458
+ const definition = this.props.definition;
49459
+ return definition.horizontal
49460
+ ? this.chartTerms.StackedBarChart
49461
+ : this.chartTerms.StackedColumnChart;
48633
49462
  }
48634
- updateColor(newHsl) {
48635
- this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
48636
- this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
49463
+ onUpdateStacked(stacked) {
49464
+ this.props.updateChart(this.props.figureId, {
49465
+ stacked,
49466
+ });
48637
49467
  }
48638
- onColorClick(color) {
48639
- if (color) {
48640
- this.props.onColorPicked(toHex(color));
48641
- }
49468
+ }
49469
+
49470
+ css /* scss */ `
49471
+ .o-badge-selection {
49472
+ gap: 1px;
49473
+ button.o-button {
49474
+ border-radius: 0;
49475
+ &.selected {
49476
+ color: ${GRAY_900};
49477
+ border-color: ${ACTION_COLOR};
49478
+ background: ${BADGE_SELECTED_COLOR};
49479
+ font-weight: 600;
49480
+ }
49481
+
49482
+ &:first-child {
49483
+ border-radius: 4px 0 0 4px;
49484
+ }
49485
+ &:last-child {
49486
+ border-radius: 0 4px 4px 0;
49487
+ }
48642
49488
  }
48643
- resetColor() {
48644
- this.props.onColorPicked("");
49489
+ }
49490
+ `;
49491
+ class BadgeSelection extends Component {
49492
+ static template = "o-spreadsheet.BadgeSelection";
49493
+ static props = {
49494
+ choices: Array,
49495
+ onChange: Function,
49496
+ selectedValue: String,
49497
+ };
49498
+ }
49499
+
49500
+ css /* scss */ `
49501
+ .o-menu-item-button {
49502
+ display: flex;
49503
+ justify-content: center;
49504
+ align-items: center;
49505
+ margin: 2px 1px;
49506
+ padding: 0px 1px;
49507
+ border-radius: 2px;
49508
+ min-width: 22px;
49509
+ }
49510
+ .o-disabled {
49511
+ opacity: 0.6;
49512
+ cursor: default;
49513
+ }
49514
+ `;
49515
+ class ActionButton extends Component {
49516
+ static template = "o-spreadsheet-ActionButton";
49517
+ static props = {
49518
+ action: Object,
49519
+ hasTriangleDownIcon: { type: Boolean, optional: true },
49520
+ selectedColor: { type: String, optional: true },
49521
+ class: { type: String, optional: true },
49522
+ onClick: { type: Function, optional: true },
49523
+ };
49524
+ actionButton = createAction(this.props.action);
49525
+ setup() {
49526
+ onWillUpdateProps((nextProps) => {
49527
+ if (nextProps.action !== this.props.action) {
49528
+ this.actionButton = createAction(nextProps.action);
49529
+ }
49530
+ });
48645
49531
  }
48646
- toggleColorPicker() {
48647
- this.state.showGradient = !this.state.showGradient;
49532
+ get isVisible() {
49533
+ return this.actionButton.isVisible(this.env);
48648
49534
  }
48649
- dragGradientPointer(ev) {
48650
- const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
48651
- this.setCustomGradient(initialGradientCoordinates);
48652
- const initialMousePosition = { x: ev.clientX, y: ev.clientY };
48653
- const onMouseMove = (ev) => {
48654
- const currentMousePosition = { x: ev.clientX, y: ev.clientY };
48655
- const deltaX = currentMousePosition.x - initialMousePosition.x;
48656
- const deltaY = currentMousePosition.y - initialMousePosition.y;
48657
- const currentGradientCoordinates = {
48658
- x: initialGradientCoordinates.x + deltaX,
48659
- y: initialGradientCoordinates.y + deltaY,
48660
- };
48661
- this.setCustomGradient(currentGradientCoordinates);
48662
- };
48663
- startDnd(onMouseMove, () => { });
49535
+ get isEnabled() {
49536
+ return this.actionButton.isEnabled(this.env);
48664
49537
  }
48665
- dragHuePointer(ev) {
48666
- const initialX = ev.offsetX;
48667
- const initialMouseX = ev.clientX;
48668
- this.setCustomHue(initialX);
48669
- const onMouseMove = (ev) => {
48670
- const currentMouseX = ev.clientX;
48671
- const deltaX = currentMouseX - initialMouseX;
48672
- const x = initialX + deltaX;
48673
- this.setCustomHue(x);
48674
- };
48675
- startDnd(onMouseMove, () => { });
49538
+ get isActive() {
49539
+ return this.actionButton.isActive?.(this.env);
48676
49540
  }
48677
- setHexColor(ev) {
48678
- // only support HEX code input
48679
- const val = ev.target.value.replace("##", "#").slice(0, 7);
48680
- this.state.customHexColor = val;
48681
- if (!isColorValid(val)) ;
48682
- else {
48683
- this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
48684
- }
49541
+ get title() {
49542
+ const name = this.actionButton.name(this.env);
49543
+ const description = this.actionButton.description(this.env);
49544
+ return name + (description ? ` (${description})` : "");
48685
49545
  }
48686
- addCustomColor(ev) {
48687
- if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
48688
- return;
49546
+ get iconTitle() {
49547
+ return this.actionButton.icon(this.env);
49548
+ }
49549
+ onClick(ev) {
49550
+ if (this.isEnabled) {
49551
+ this.props.onClick?.(ev);
49552
+ this.actionButton.execute?.(this.env);
48689
49553
  }
48690
- this.props.onColorPicked(toHex(this.state.customHexColor));
48691
49554
  }
48692
- isSameColor(color1, color2) {
48693
- return isSameColor(color1, color2);
49555
+ get buttonStyle() {
49556
+ if (this.props.selectedColor) {
49557
+ return cssPropertiesToCss({
49558
+ "border-bottom": `4px solid ${this.props.selectedColor}`,
49559
+ height: "16px",
49560
+ "margin-top": "2px",
49561
+ });
49562
+ }
49563
+ return "";
48694
49564
  }
48695
49565
  }
48696
49566
 
@@ -49161,57 +50031,6 @@ class RadioSelection extends Component {
49161
50031
  };
49162
50032
  }
49163
50033
 
49164
- const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
49165
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
49166
- <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
49167
- </svg>
49168
- `;
49169
- css /* scss */ `
49170
- .o-round-color-picker-button {
49171
- width: 20px;
49172
- height: 20px;
49173
- cursor: pointer;
49174
- border: 1px solid ${GRAY_300};
49175
- background-position: 1px 1px;
49176
- background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
49177
- }
49178
- `;
49179
- class RoundColorPicker extends Component {
49180
- static template = "o-spreadsheet.RoundColorPicker";
49181
- static components = { Section, ColorPicker };
49182
- static props = {
49183
- currentColor: { type: String, optional: true },
49184
- title: { type: String, optional: true },
49185
- onColorPicked: Function,
49186
- disableNoColor: { type: Boolean, optional: true },
49187
- };
49188
- colorPickerButtonRef = useRef("colorPickerButton");
49189
- state;
49190
- setup() {
49191
- this.state = useState({ pickerOpened: false });
49192
- useExternalListener(window, "click", this.closePicker);
49193
- }
49194
- closePicker() {
49195
- this.state.pickerOpened = false;
49196
- }
49197
- togglePicker() {
49198
- this.state.pickerOpened = !this.state.pickerOpened;
49199
- }
49200
- onColorPicked(color) {
49201
- this.props.onColorPicked(color);
49202
- this.state.pickerOpened = false;
49203
- }
49204
- get colorPickerAnchorRect() {
49205
- const button = this.colorPickerButtonRef.el;
49206
- return getBoundingRectAsPOJO(button);
49207
- }
49208
- get buttonStyle() {
49209
- return cssPropertiesToCss({
49210
- background: this.props.currentColor,
49211
- });
49212
- }
49213
- }
49214
-
49215
50034
  class GeneralDesignEditor extends Component {
49216
50035
  static template = "o-spreadsheet-GeneralDesignEditor";
49217
50036
  static components = {
@@ -54739,7 +55558,7 @@ class SpreadsheetPivot {
54739
55558
  }
54740
55559
  getTypeFromZone(sheetId, zone) {
54741
55560
  const cells = this.getters.getEvaluatedCellsInZone(sheetId, zone);
54742
- const nonEmptyCells = cells.filter((cell) => cell.type !== CellValueType.empty);
55561
+ const nonEmptyCells = cells.filter((cell) => !(cell.type === CellValueType.empty || cell.value === ""));
54743
55562
  if (nonEmptyCells.length === 0) {
54744
55563
  return "integer";
54745
55564
  }
@@ -55347,7 +56166,7 @@ css /* scss */ `
55347
56166
  `;
55348
56167
  class SettingsPanel extends Component {
55349
56168
  static template = "o-spreadsheet-SettingsPanel";
55350
- static components = { Section, ValidationMessages };
56169
+ static components = { Section, ValidationMessages, BadgeSelection };
55351
56170
  static props = { onCloseSidePanel: Function };
55352
56171
  loadedLocales = [];
55353
56172
  setup() {
@@ -56231,49 +57050,111 @@ class ScreenWidthStore {
56231
57050
  }
56232
57051
 
56233
57052
  const DEFAULT_SIDE_PANEL_SIZE = 350;
57053
+ const COLLAPSED_SIDE_PANEL_SIZE = 45;
56234
57054
  const MIN_SHEET_VIEW_WIDTH = 150;
56235
57055
  class SidePanelStore extends SpreadsheetStore {
56236
- mutators = ["open", "toggle", "close", "changePanelSize", "resetPanelSize"];
56237
- initialPanelProps = {};
56238
- componentTag = "";
56239
- 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;
56240
57070
  screenWidthStore = this.get(ScreenWidthStore);
56241
- get isOpen() {
56242
- if (!this.componentTag) {
56243
- return false;
56244
- }
56245
- 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;
56246
57086
  }
56247
- get panelProps() {
56248
- 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);
56249
57098
  if (state.isOpen) {
56250
57099
  return state.props ?? {};
56251
57100
  }
56252
57101
  return {};
56253
57102
  }
56254
- get panelKey() {
56255
- const state = this.computeState(this.componentTag, this.initialPanelProps);
57103
+ getPanelKey(panelInfo) {
57104
+ const state = this.computeState(panelInfo);
56256
57105
  if (state.isOpen) {
56257
57106
  return state.key;
56258
57107
  }
56259
57108
  return undefined;
56260
57109
  }
56261
- open(componentTag, panelProps = {}) {
57110
+ open(componentTag, initialPanelProps = {}) {
56262
57111
  if (this.screenWidthStore.isSmall) {
56263
57112
  return;
56264
57113
  }
56265
- const state = this.computeState(componentTag, panelProps);
57114
+ const newPanelInfo = { initialPanelProps, componentTag, size: DEFAULT_SIDE_PANEL_SIZE };
57115
+ const state = this.computeState(newPanelInfo);
56266
57116
  if (!state.isOpen) {
56267
57117
  return;
56268
57118
  }
56269
- if (this.isOpen && componentTag !== this.componentTag) {
56270
- 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);
56271
57153
  }
56272
- this.componentTag = componentTag;
56273
- this.initialPanelProps = state.props ?? {};
56274
57154
  }
56275
57155
  toggle(componentTag, panelProps) {
56276
- if (this.isOpen && componentTag === this.componentTag) {
57156
+ const panel = this.mainPanel?.isPinned ? this.secondaryPanel : this.mainPanel;
57157
+ if (panel && componentTag === panel.componentTag) {
56277
57158
  this.close();
56278
57159
  }
56279
57160
  else {
@@ -56281,34 +57162,85 @@ class SidePanelStore extends SpreadsheetStore {
56281
57162
  }
56282
57163
  }
56283
57164
  close() {
56284
- this.initialPanelProps.onCloseSidePanel?.();
56285
- this.initialPanelProps = {};
56286
- 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;
56287
57174
  }
56288
- changePanelSize(size, spreadsheetElWidth) {
56289
- if (size < DEFAULT_SIDE_PANEL_SIZE) {
56290
- 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;
56291
57184
  }
56292
- else if (size > spreadsheetElWidth - MIN_SHEET_VIEW_WIDTH) {
56293
- 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);
56294
57194
  }
56295
- else {
56296
- this.panelSize = size;
57195
+ panelInfo.size = size;
57196
+ }
57197
+ resetPanelSize(panel) {
57198
+ const panelInfo = this[panel];
57199
+ if (!panelInfo) {
57200
+ return;
56297
57201
  }
57202
+ panelInfo.size = DEFAULT_SIDE_PANEL_SIZE;
56298
57203
  }
56299
- resetPanelSize() {
56300
- 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
+ }
56301
57214
  }
56302
- computeState(componentTag, panelProps) {
56303
- const customComputeState = sidePanelRegistry.get(componentTag).computeState;
56304
- if (!customComputeState) {
56305
- return {
56306
- isOpen: true,
56307
- props: panelProps,
56308
- };
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);
56309
57223
  }
56310
57224
  else {
56311
- 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);
56312
57244
  }
56313
57245
  }
56314
57246
  }
@@ -56456,11 +57388,11 @@ class Grid extends Component {
56456
57388
  this.hoveredCell.clear();
56457
57389
  });
56458
57390
  this.cellPopovers = useStore(CellPopoverStore);
56459
- useEffect(() => {
56460
- if (!this.sidePanel.isOpen) {
57391
+ useEffect((isMainPanelOpen, isSecondaryPanelOpen) => {
57392
+ if (!isMainPanelOpen && !isSecondaryPanelOpen) {
56461
57393
  this.DOMFocusableElementStore.focus();
56462
57394
  }
56463
- }, () => [this.sidePanel.isOpen]);
57395
+ }, () => [this.sidePanel.isMainPanelOpen, this.sidePanel.isSecondaryPanelOpen]);
56464
57396
  useTouchScroll(this.gridRef, this.moveCanvas.bind(this), () => {
56465
57397
  const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
56466
57398
  return scrollY > 0;
@@ -59542,7 +60474,7 @@ class DataValidationPlugin extends CorePlugin {
59542
60474
  if (!rule)
59543
60475
  return false;
59544
60476
  return ((rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
59545
- rule.criterion.displayStyle === "arrow");
60477
+ (rule.criterion.displayStyle === "arrow" || rule.criterion.displayStyle === "chip"));
59546
60478
  }
59547
60479
  addDataValidationRule(sheetId, newRule) {
59548
60480
  const rules = this.rules[sheetId];
@@ -59552,7 +60484,7 @@ class DataValidationPlugin extends CorePlugin {
59552
60484
  else if (newRule.criterion.type === "isValueInList") {
59553
60485
  newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
59554
60486
  }
59555
- const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
60487
+ const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules, newRule.id);
59556
60488
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
59557
60489
  if (ruleIndex !== -1) {
59558
60490
  adaptedRules[ruleIndex] = newRule;
@@ -59562,9 +60494,12 @@ class DataValidationPlugin extends CorePlugin {
59562
60494
  this.history.update("rules", sheetId, [...adaptedRules, newRule]);
59563
60495
  }
59564
60496
  }
59565
- removeRangesFromRules(sheetId, ranges, rules) {
60497
+ removeRangesFromRules(sheetId, ranges, rules, editingRuleId) {
59566
60498
  rules = deepCopy(rules);
59567
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
+ }
59568
60503
  rule.ranges = this.getters.recomputeRanges(rule.ranges, ranges);
59569
60504
  }
59570
60505
  return rules.filter((rule) => rule.ranges.length > 0);
@@ -62972,7 +63907,7 @@ class PivotCorePlugin extends CorePlugin {
62972
63907
  break;
62973
63908
  }
62974
63909
  case "UPDATE_PIVOT": {
62975
- this.history.update("pivots", cmd.pivotId, "definition", this.repairSortedColumn(deepCopy(cmd.pivot)));
63910
+ this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
62976
63911
  this.compileCalculatedMeasures(cmd.pivot.measures);
62977
63912
  break;
62978
63913
  }
@@ -63043,10 +63978,7 @@ class PivotCorePlugin extends CorePlugin {
63043
63978
  // Private
63044
63979
  // -------------------------------------------------------------------------
63045
63980
  addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {
63046
- this.history.update("pivots", pivotId, {
63047
- definition: this.repairSortedColumn(deepCopy(pivot)),
63048
- formulaId,
63049
- });
63981
+ this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
63050
63982
  this.compileCalculatedMeasures(pivot.measures);
63051
63983
  this.history.update("formulaIds", formulaId, pivotId);
63052
63984
  this.history.update("nextFormulaId", this.nextFormulaId + 1);
@@ -63135,7 +64067,6 @@ class PivotCorePlugin extends CorePlugin {
63135
64067
  }
63136
64068
  }
63137
64069
  checkSortedColumnInMeasures(definition) {
63138
- definition = this.repairSortedColumn(definition);
63139
64070
  const measures = definition.measures.map((measure) => measure.id);
63140
64071
  if (definition.sortedColumn && !measures.includes(definition.sortedColumn.measure)) {
63141
64072
  return "InvalidDefinition" /* CommandResult.InvalidDefinition */;
@@ -63149,26 +64080,6 @@ class PivotCorePlugin extends CorePlugin {
63149
64080
  }
63150
64081
  return "Success" /* CommandResult.Success */;
63151
64082
  }
63152
- repairSortedColumn(definition) {
63153
- if (definition.sortedColumn) {
63154
- // Fix for an upgrade issue: the sortedColumn measure was not updated
63155
- // from using fieldName to using id. If the sortedColumn measure matches
63156
- // a measure fieldName in the definition, update it to use the measure's id instead
63157
- // of its fieldName.
63158
- // TODO: add an upgrade step to fix this in master and remove this code
63159
- const sortedMeasure = definition.measures.find((measure) => measure.fieldName === definition.sortedColumn?.measure);
63160
- if (sortedMeasure) {
63161
- return {
63162
- ...definition,
63163
- sortedColumn: {
63164
- ...definition.sortedColumn,
63165
- measure: sortedMeasure.id,
63166
- },
63167
- };
63168
- }
63169
- }
63170
- return definition;
63171
- }
63172
64083
  // ---------------------------------------------------------------------
63173
64084
  // Import/Export
63174
64085
  // ---------------------------------------------------------------------
@@ -63250,9 +64161,7 @@ class SettingsPlugin extends CorePlugin {
63250
64161
  this.locale = data.settings?.locale ?? DEFAULT_LOCALE;
63251
64162
  }
63252
64163
  export(data) {
63253
- data.settings = {
63254
- locale: this.locale,
63255
- };
64164
+ data.settings = { locale: this.locale };
63256
64165
  }
63257
64166
  }
63258
64167
 
@@ -65908,7 +66817,10 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65908
66817
  "getDataValidationInvalidCriterionValueMessage",
65909
66818
  "getInvalidDataValidationMessage",
65910
66819
  "getValidationResultForCellValue",
66820
+ "getDataValidationRangeValues",
65911
66821
  "isCellValidCheckbox",
66822
+ "getDataValidationCellStyle",
66823
+ "getDataValidationChipStyle",
65912
66824
  "isDataValidationInvalid",
65913
66825
  ];
65914
66826
  validationResults = {};
@@ -65929,6 +66841,18 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65929
66841
  isDataValidationInvalid(cellPosition) {
65930
66842
  return !this.getValidationResultForCell(cellPosition).isValid;
65931
66843
  }
66844
+ getDataValidationCellStyle(position) {
66845
+ if (this.hasChip(position)) {
66846
+ return undefined; // The style is not applied on the cell if it's a chip
66847
+ }
66848
+ return this.getDataValidationStyle(position);
66849
+ }
66850
+ getDataValidationChipStyle(position) {
66851
+ if (this.hasChip(position)) {
66852
+ return this.getDataValidationStyle(position) ?? { fillColor: GRAY_200 };
66853
+ }
66854
+ return undefined;
66855
+ }
65932
66856
  getInvalidDataValidationMessage(cellPosition) {
65933
66857
  const validationResult = this.getValidationResultForCell(cellPosition);
65934
66858
  return validationResult.isValid ? undefined : validationResult.error;
@@ -65951,6 +66875,11 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65951
66875
  }
65952
66876
  return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;
65953
66877
  }
66878
+ getDataValidationRangeValues(sheetId, criterion) {
66879
+ const range = this.getters.getRangeFromSheetXC(sheetId, String(criterion.values[0]));
66880
+ const criterionValues = this.getters.getRangeValues(range);
66881
+ return criterionValues.map((value) => value?.toString()).filter(isDefined);
66882
+ }
65954
66883
  isCellValidCheckbox(cellPosition) {
65955
66884
  if (!this.getters.isMainCellPosition(cellPosition)) {
65956
66885
  return false;
@@ -65970,6 +66899,38 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
65970
66899
  const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
65971
66900
  return error ? { error, rule, isValid: false } : VALID_RESULT;
65972
66901
  }
66902
+ hasChip(position) {
66903
+ const rule = this.getters.getValidationRuleForCell(position);
66904
+ return ((rule?.criterion.type === "isValueInList" || rule?.criterion.type === "isValueInRange") &&
66905
+ rule.criterion.displayStyle === "chip");
66906
+ }
66907
+ getDataValidationStyle(position) {
66908
+ const rule = this.getters.getValidationRuleForCell(position);
66909
+ if (!rule || this.isDataValidationInvalid(position)) {
66910
+ return undefined;
66911
+ }
66912
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
66913
+ const color = this.getValueColor(rule, evaluatedCell.value);
66914
+ if (!color) {
66915
+ return undefined;
66916
+ }
66917
+ const style = {
66918
+ fillColor: color,
66919
+ textColor: chipTextColor(color),
66920
+ };
66921
+ return style;
66922
+ }
66923
+ getValueColor(rule, value) {
66924
+ if (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange") {
66925
+ return undefined;
66926
+ }
66927
+ for (const criterionValue in rule.criterion.colors) {
66928
+ if (criterionValue.toLowerCase() === String(value).toLowerCase()) {
66929
+ return rule.criterion.colors[criterionValue];
66930
+ }
66931
+ }
66932
+ return undefined;
66933
+ }
65973
66934
  isValidFormula(value) {
65974
66935
  return !compile(value).isBadExpression;
65975
66936
  }
@@ -66066,12 +67027,35 @@ iconsOnCellRegistry.add("data_validation_checkbox", (getters, position) => {
66066
67027
  }
66067
67028
  return undefined;
66068
67029
  });
67030
+ iconsOnCellRegistry.add("data_validation_chip_icon", (getters, position) => {
67031
+ const chipStyle = getters.getDataValidationChipStyle(position);
67032
+ if (chipStyle) {
67033
+ const cellStyle = getters.getCellComputedStyle(position);
67034
+ return {
67035
+ svg: getChipSvg(chipStyle),
67036
+ hoverSvg: getHoveredChipSvg(chipStyle),
67037
+ priority: 10,
67038
+ horizontalAlign: "right",
67039
+ size: computeTextFontSizeInPixels(cellStyle),
67040
+ margin: 4,
67041
+ position,
67042
+ onClick: (position, env) => {
67043
+ const { col, row } = position;
67044
+ env.model.selection.selectCell(col, row);
67045
+ env.startCellEdition();
67046
+ },
67047
+ type: "data_validation_chip_icon",
67048
+ };
67049
+ }
67050
+ return undefined;
67051
+ });
66069
67052
  iconsOnCellRegistry.add("data_validation_list_icon", (getters, position) => {
66070
67053
  const hasIcon = !getters.isReadonly() && getters.cellHasListDataValidationIcon(position);
66071
67054
  if (hasIcon) {
67055
+ const cellStyle = getters.getCellComputedStyle(position);
66072
67056
  return {
66073
- svg: CARET_DOWN,
66074
- hoverSvg: HOVERED_CARET_DOWN,
67057
+ svg: getCaretDownSvg(cellStyle),
67058
+ hoverSvg: getHoveredCaretDownSvg(cellStyle),
66075
67059
  priority: 2,
66076
67060
  horizontalAlign: "right",
66077
67061
  size: GRID_ICON_EDGE_LENGTH,
@@ -66212,11 +67196,8 @@ class CellIconPlugin extends CoreViewPlugin {
66212
67196
  }
66213
67197
  return this.cellIconsCache[position.sheetId][position.col][position.row];
66214
67198
  }
66215
- getCellIconRect(icon) {
67199
+ getCellIconRect(icon, cellRect) {
66216
67200
  const cellPosition = icon.position;
66217
- const merge = this.getters.getMerge(cellPosition);
66218
- const zone = merge || positionToZone(cellPosition);
66219
- const cellRect = this.getters.getRect(zone);
66220
67201
  const cell = this.getters.getCell(cellPosition);
66221
67202
  const x = this.getIconHorizontalPosition(cellRect, icon.horizontalAlign, icon);
66222
67203
  const y = this.getters.computeTextYCoordinate(cellRect, icon.size, cell?.style?.verticalAlign);
@@ -68613,11 +69594,11 @@ class OTRegistry extends Registry {
68613
69594
  * transformation function given
68614
69595
  */
68615
69596
  addTransformation(executed, toTransforms, fn) {
69597
+ if (!this.content[executed]) {
69598
+ this.content[executed] = new Map();
69599
+ }
68616
69600
  for (const toTransform of toTransforms) {
68617
- if (!this.content[toTransform]) {
68618
- this.content[toTransform] = new Map();
68619
- }
68620
- this.content[toTransform].set(executed, fn);
69601
+ this.content[executed].set(toTransform, fn);
68621
69602
  }
68622
69603
  return this;
68623
69604
  }
@@ -68626,7 +69607,7 @@ class OTRegistry extends Registry {
68626
69607
  * that the executed command happened.
68627
69608
  */
68628
69609
  getTransformation(toTransform, executed) {
68629
- return this.content[toTransform] && this.content[toTransform].get(executed);
69610
+ return this.content[executed] && this.content[executed].get(toTransform);
68630
69611
  }
68631
69612
  }
68632
69613
  const otRegistry = new OTRegistry();
@@ -68956,10 +69937,20 @@ function adaptTransform(toTransform, executed) {
68956
69937
  */
68957
69938
  function transformAll(toTransform, executed) {
68958
69939
  let transformedCommands = [...toTransform];
69940
+ const possibleTransformations = new Set(otRegistry.getKeys());
68959
69941
  for (const executedCommand of executed) {
68960
- transformedCommands = transformedCommands
68961
- .map((cmd) => transform(cmd, executedCommand))
68962
- .filter(isDefined);
69942
+ // If the executed command is not in the registry, we skip it
69943
+ // because we know there won't be any transformation impacting the
69944
+ // commands to transform.
69945
+ if (possibleTransformations.has(executedCommand.type)) {
69946
+ transformedCommands = transformedCommands.reduce((acc, cmd) => {
69947
+ const transformed = transform(cmd, executedCommand);
69948
+ if (transformed) {
69949
+ acc.push(transformed);
69950
+ }
69951
+ return acc;
69952
+ }, []);
69953
+ }
68963
69954
  }
68964
69955
  return transformedCommands;
68965
69956
  }
@@ -70516,6 +71507,9 @@ class SheetUIPlugin extends UIPlugin {
70516
71507
  for (const icon of this.getters.getCellIcons(position)) {
70517
71508
  contentWidth += icon.margin + icon.size;
70518
71509
  }
71510
+ if (this.getters.getDataValidationChipStyle(position)) {
71511
+ contentWidth += DATA_VALIDATION_CHIP_MARGIN * 2;
71512
+ }
70519
71513
  if (contentWidth === 0) {
70520
71514
  return 0;
70521
71515
  }
@@ -70673,7 +71667,7 @@ class SheetUIPlugin extends UIPlugin {
70673
71667
  }
70674
71668
  const position = this.getters.getCellPosition(cell.id);
70675
71669
  const colSize = this.getters.getColSize(sheetId, position.col);
70676
- if (cell.isFormula) {
71670
+ if (cell.isFormula || this.getters.getArrayFormulaSpreadingOn(position)) {
70677
71671
  const content = this.getters.getEvaluatedCell(position).formattedValue;
70678
71672
  const evaluatedSize = getCellContentHeight(this.ctx, content, cell?.style, colSize);
70679
71673
  if (evaluatedSize > evaluatedRowSize && evaluatedSize > DEFAULT_CELL_HEIGHT) {
@@ -70876,6 +71870,8 @@ class CellComputedStylePlugin extends UIPlugin {
70876
71870
  if (invalidateEvaluationCommands.has(cmd.type) ||
70877
71871
  cmd.type === "UPDATE_CELL" ||
70878
71872
  cmd.type === "SET_FORMATTING" ||
71873
+ cmd.type === "ADD_DATA_VALIDATION_RULE" ||
71874
+ cmd.type === "REMOVE_DATA_VALIDATION_RULE" ||
70879
71875
  cmd.type === "EVALUATE_CELLS") {
70880
71876
  this.styles = {};
70881
71877
  this.borders = {};
@@ -70947,8 +71943,10 @@ class CellComputedStylePlugin extends UIPlugin {
70947
71943
  const cell = this.getters.getCell(position);
70948
71944
  const cfStyle = this.getters.getCellConditionalFormatStyle(position);
70949
71945
  const tableStyle = this.getters.getCellTableStyle(position);
71946
+ const dataValidationStyle = this.getters.getDataValidationCellStyle(position);
70950
71947
  const computedStyle = {
70951
71948
  ...removeFalsyAttributes(tableStyle),
71949
+ ...removeFalsyAttributes(dataValidationStyle),
70952
71950
  ...removeFalsyAttributes(cell?.style),
70953
71951
  ...removeFalsyAttributes(cfStyle),
70954
71952
  };
@@ -72032,49 +73030,17 @@ class ClipboardPlugin extends UIPlugin {
72032
73030
  if (!copiedData) {
72033
73031
  return;
72034
73032
  }
72035
- let zone = undefined;
72036
- const selectedZones = [];
72037
73033
  const sheetId = this.getters.getActiveSheetId();
72038
- const target = {
72039
- sheetId,
72040
- zones,
72041
- };
72042
73034
  const handlers = this.selectClipboardHandlers(copiedData);
72043
- for (const { handlerName, handler } of handlers) {
72044
- const handlerData = copiedData[handlerName];
72045
- if (!handlerData) {
72046
- continue;
72047
- }
72048
- const currentTarget = handler.getPasteTarget(sheetId, zones, handlerData, options);
72049
- if (currentTarget.figureId) {
72050
- target.figureId = currentTarget.figureId;
72051
- }
72052
- for (const targetZone of currentTarget.zones) {
72053
- selectedZones.push(targetZone);
72054
- if (zone === undefined) {
72055
- zone = targetZone;
72056
- continue;
72057
- }
72058
- zone = union(zone, targetZone);
72059
- }
72060
- }
73035
+ const { target, zone, selectedZones } = getPasteTargetFromHandlers(sheetId, zones, copiedData, handlers, options);
72061
73036
  if (zone !== undefined) {
72062
- 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);
72063
73038
  }
72064
- handlers.forEach(({ handlerName, handler }) => {
72065
- const handlerData = copiedData[handlerName];
72066
- if (handlerData) {
72067
- handler.paste(target, handlerData, options);
72068
- }
72069
- });
73039
+ applyClipboardHandlersPaste(handlers, copiedData, target, options);
72070
73040
  if (!options?.selectTarget) {
72071
73041
  return;
72072
73042
  }
72073
- const selection = zones[0];
72074
- const col = selection.left;
72075
- const row = selection.top;
72076
- this.selection.getBackToDefault();
72077
- this.selection.selectZone({ cell: { col, row }, zone: union(...selectedZones) }, { scrollIntoView: false });
73043
+ selectPastedZone(this.selection, zones, selectedZones);
72078
73044
  }
72079
73045
  /**
72080
73046
  * Add columns and/or rows to ensure that col + width and row + height are still
@@ -74510,19 +75476,29 @@ autoCompleteProviders.add("dataValidation", {
74510
75476
  (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange")) {
74511
75477
  return [];
74512
75478
  }
74513
- let values;
74514
- if (rule.criterion.type === "isValueInList") {
74515
- values = rule.criterion.values;
74516
- }
74517
- else {
74518
- const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
74519
- values = Array.from(new Set(this.getters
74520
- .getRangeValues(range)
74521
- .filter(isNotNull)
74522
- .map((value) => value.toString())
74523
- .filter((val) => val !== "")));
74524
- }
74525
- return values.map((value) => ({ text: value }));
75479
+ const sheetId = this.composer.currentEditedCell.sheetId;
75480
+ const values = rule.criterion.type === "isValueInRange"
75481
+ ? Array.from(new Set(this.getters.getDataValidationRangeValues(sheetId, rule.criterion)))
75482
+ : rule.criterion.values;
75483
+ const isChip = rule.criterion.displayStyle === "chip";
75484
+ if (!isChip) {
75485
+ return values.map((value) => ({ text: value }));
75486
+ }
75487
+ const colors = rule.criterion.colors;
75488
+ return values.map((value) => {
75489
+ const color = colors?.[value];
75490
+ return {
75491
+ text: value,
75492
+ htmlContent: [
75493
+ {
75494
+ value,
75495
+ color: color ? chipTextColor(color) : undefined,
75496
+ backgroundColor: color || GRAY_200,
75497
+ classes: ["badge rounded-pill fs-6 fw-normal w-100 mt-1 text-start"],
75498
+ },
75499
+ ],
75500
+ };
75501
+ });
74526
75502
  },
74527
75503
  selectProposal(tokenAtCursor, value) {
74528
75504
  this.composer.setCurrentContent(value);
@@ -74893,6 +75869,17 @@ clickableCellRegistry.add("link", {
74893
75869
  return !!getters.getEvaluatedCell(position).link;
74894
75870
  },
74895
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
+ },
74896
75883
  sequence: 5,
74897
75884
  });
74898
75885
 
@@ -76604,12 +77591,13 @@ class ClickableCellsStore extends SpreadsheetStore {
76604
77591
  if (!item) {
76605
77592
  continue;
76606
77593
  }
77594
+ const title = typeof item.title === "function" ? item.title(position, getters) : item.title;
76607
77595
  const zone = getters.expandZone(sheetId, positionToZone(position));
76608
77596
  cells.push({
76609
77597
  coordinates: getters.getVisibleRect(zone),
76610
77598
  position,
76611
77599
  action: item.execute,
76612
- title: item.title || "",
77600
+ title: title || "",
76613
77601
  });
76614
77602
  }
76615
77603
  return cells;
@@ -76994,24 +77982,36 @@ css /* scss */ `
76994
77982
  user-select: none;
76995
77983
  color: ${TEXT_BODY};
76996
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
+
76997
77995
  .o-sidePanelTitle {
76998
77996
  line-height: 20px;
76999
77997
  font-size: 16px;
77000
77998
  }
77001
77999
 
77002
78000
  .o-sidePanelHeader {
77003
- padding: 8px 16px;
77004
- display: flex;
77005
- align-items: center;
77006
- justify-content: space-between;
78001
+ padding: 8px;
77007
78002
  border-bottom: 1px solid ${GRAY_300};
78003
+ }
77008
78004
 
77009
- .o-sidePanelClose {
77010
- padding: 5px 10px;
77011
- cursor: pointer;
77012
- &:hover {
77013
- background-color: WhiteSmoke;
77014
- }
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};
77015
78015
  }
77016
78016
  }
77017
78017
  .o-sidePanelBody-container {
@@ -77088,43 +78088,114 @@ css /* scss */ `
77088
78088
  `;
77089
78089
  class SidePanel extends Component {
77090
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";
77091
78116
  static props = {};
78117
+ static components = { SidePanel };
77092
78118
  sidePanelStore;
77093
78119
  spreadsheetRect = useSpreadsheetRect();
77094
78120
  setup() {
77095
78121
  this.sidePanelStore = useStore(SidePanelStore);
77096
- useEffect((isOpen) => {
77097
- 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) {
77098
78127
  this.sidePanelStore.close();
77099
78128
  }
77100
- }, () => [this.sidePanelStore.isOpen]);
77101
- }
77102
- get panel() {
77103
- return sidePanelRegistry.get(this.sidePanelStore.componentTag);
78129
+ }, () => [this.sidePanelStore.isMainPanelOpen, this.sidePanelStore.isSecondaryPanelOpen]);
77104
78130
  }
77105
- close() {
77106
- this.sidePanelStore.close();
77107
- }
77108
- getTitle() {
77109
- const panel = this.panel;
77110
- return typeof panel.title === "function"
77111
- ? panel.title(this.env, this.sidePanelStore.panelProps)
77112
- : panel.title;
77113
- }
77114
- startHandleDrag(ev) {
78131
+ startHandleDrag(panel, ev) {
77115
78132
  const startingCursor = document.body.style.cursor;
77116
- 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;
77117
78138
  const startPosition = ev.clientX;
77118
78139
  const onMouseMove = (ev) => {
77119
78140
  document.body.style.cursor = "col-resize";
77120
78141
  const newSize = startSize + startPosition - ev.clientX;
77121
- this.sidePanelStore.changePanelSize(newSize, this.spreadsheetRect.width);
78142
+ this.sidePanelStore.changePanelSize(panel, newSize);
77122
78143
  };
77123
78144
  const cleanUp = () => {
77124
78145
  document.body.style.cursor = startingCursor;
77125
78146
  };
77126
78147
  startDnd(onMouseMove, cleanUp);
77127
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
+ }
77128
78199
  }
77129
78200
 
77130
78201
  class RibbonMenu extends Component {
@@ -78686,7 +79757,7 @@ class Spreadsheet extends Component {
78686
79757
  Grid,
78687
79758
  BottomBar,
78688
79759
  SmallBottomBar,
78689
- SidePanel,
79760
+ SidePanels,
78690
79761
  SpreadsheetDashboard,
78691
79762
  HeaderGroupContainer,
78692
79763
  FullScreenChart,
@@ -78709,7 +79780,9 @@ class Spreadsheet extends Component {
78709
79780
  else {
78710
79781
  properties["grid-template-rows"] = `min-content auto min-content`;
78711
79782
  }
78712
- 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";
78713
79786
  properties["grid-template-columns"] = `auto ${columnWidth}`;
78714
79787
  return cssPropertiesToCss(properties);
78715
79788
  }
@@ -78793,7 +79866,7 @@ class Spreadsheet extends Component {
78793
79866
  this.checkViewportSize();
78794
79867
  });
78795
79868
  const resizeObserver = new ResizeObserver(() => {
78796
- this.sidePanel.changePanelSize(this.sidePanel.panelSize, this.spreadsheetRect.width);
79869
+ this.sidePanel.changeSpreadsheetWidth(this.spreadsheetRect.width);
78797
79870
  });
78798
79871
  }
78799
79872
  bindModelEvents() {
@@ -83342,6 +84415,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
83342
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 };
83343
84416
 
83344
84417
 
83345
- __info__.version = "18.4.0-alpha.8";
83346
- __info__.date = "2025-06-12T09:53:48.133Z";
83347
- __info__.hash = "9b7a8d0";
84418
+ __info__.version = "18.4.0";
84419
+ __info__.date = "2025-06-24T11:19:24.606Z";
84420
+ __info__.hash = "a5b7cad";