@odoo/o-spreadsheet 18.1.8 → 18.1.10

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.1.8
6
- * @date 2025-02-14T08:42:08.322Z
7
- * @hash 02682f4
5
+ * @version 18.1.10
6
+ * @date 2025-03-07T10:34:41.861Z
7
+ * @hash 31e4526
8
8
  */
9
9
 
10
10
  'use strict';
@@ -2220,17 +2220,7 @@ function toZoneWithoutBoundaryChanges(xc) {
2220
2220
  */
2221
2221
  function toUnboundedZone(xc) {
2222
2222
  const zone = toZoneWithoutBoundaryChanges(xc);
2223
- if (zone.right !== undefined && zone.right < zone.left) {
2224
- const tmp = zone.left;
2225
- zone.left = zone.right;
2226
- zone.right = tmp;
2227
- }
2228
- if (zone.bottom !== undefined && zone.bottom < zone.top) {
2229
- const tmp = zone.top;
2230
- zone.top = zone.bottom;
2231
- zone.bottom = tmp;
2232
- }
2233
- return zone;
2223
+ return reorderZone(zone);
2234
2224
  }
2235
2225
  /**
2236
2226
  * Convert from a cartesian reference to a Zone.
@@ -2504,11 +2494,11 @@ function positions(zone) {
2504
2494
  return positions;
2505
2495
  }
2506
2496
  function reorderZone(zone) {
2507
- if (zone.left > zone.right) {
2508
- zone = { left: zone.right, right: zone.left, top: zone.top, bottom: zone.bottom };
2497
+ if (zone.right !== undefined && zone.left > zone.right) {
2498
+ zone = { ...zone, left: zone.right, right: zone.left };
2509
2499
  }
2510
- if (zone.top > zone.bottom) {
2511
- zone = { left: zone.left, right: zone.right, top: zone.bottom, bottom: zone.top };
2500
+ if (zone.bottom !== undefined && zone.top > zone.bottom) {
2501
+ zone = { ...zone, top: zone.bottom, bottom: zone.top };
2512
2502
  }
2513
2503
  return zone;
2514
2504
  }
@@ -3413,12 +3403,12 @@ function isTargetDependent(cmd) {
3413
3403
  function isRangeDependant(cmd) {
3414
3404
  return "ranges" in cmd;
3415
3405
  }
3416
- function isZoneDependent(cmd) {
3417
- return "zone" in cmd;
3418
- }
3419
3406
  function isPositionDependent(cmd) {
3420
3407
  return "col" in cmd && "row" in cmd && "sheetId" in cmd;
3421
3408
  }
3409
+ function isZoneDependent(cmd) {
3410
+ return "sheetId" in cmd && "zone" in cmd;
3411
+ }
3422
3412
  const invalidateEvaluationCommands = new Set([
3423
3413
  "RENAME_SHEET",
3424
3414
  "DELETE_SHEET",
@@ -3430,6 +3420,7 @@ const invalidateEvaluationCommands = new Set([
3430
3420
  "REDO",
3431
3421
  "ADD_MERGE",
3432
3422
  "REMOVE_MERGE",
3423
+ "DUPLICATE_SHEET",
3433
3424
  "UPDATE_LOCALE",
3434
3425
  "ADD_PIVOT",
3435
3426
  "UPDATE_PIVOT",
@@ -3459,7 +3450,6 @@ const invalidateChartEvaluationCommands = new Set([
3459
3450
  ]);
3460
3451
  const invalidateDependenciesCommands = new Set(["MOVE_RANGES"]);
3461
3452
  const invalidateCFEvaluationCommands = new Set([
3462
- "DUPLICATE_SHEET",
3463
3453
  "EVALUATE_CELLS",
3464
3454
  "ADD_CONDITIONAL_FORMAT",
3465
3455
  "REMOVE_CONDITIONAL_FORMAT",
@@ -3629,6 +3619,7 @@ exports.CommandResult = void 0;
3629
3619
  CommandResult["InvalidRange"] = "InvalidRange";
3630
3620
  CommandResult["InvalidZones"] = "InvalidZones";
3631
3621
  CommandResult["InvalidSheetId"] = "InvalidSheetId";
3622
+ CommandResult["InvalidCellId"] = "InvalidCellId";
3632
3623
  CommandResult["InvalidFigureId"] = "InvalidFigureId";
3633
3624
  CommandResult["InputAlreadyFocused"] = "InputAlreadyFocused";
3634
3625
  CommandResult["MaximumRangesReached"] = "MaximumRangesReached";
@@ -4460,7 +4451,7 @@ function dichotomicSearch(data, target, mode, sortOrder, rangeLength, getValueIn
4460
4451
  * @param reverseSearch if true, search in the array starting from the end.
4461
4452
 
4462
4453
  */
4463
- function linearSearch(data, target, mode, numberOfValues, getValueInData, reverseSearch = false) {
4454
+ function linearSearch(data, target, mode, numberOfValues, getValueInData, lookupCaches, reverseSearch = false) {
4464
4455
  if (target === undefined || target.value === null) {
4465
4456
  return -1;
4466
4457
  }
@@ -4469,17 +4460,48 @@ function linearSearch(data, target, mode, numberOfValues, getValueInData, revers
4469
4460
  }
4470
4461
  const _target = normalizeValue(target.value);
4471
4462
  const getValue = reverseSearch
4472
- ? (data, i) => getValueInData(data, numberOfValues - i - 1)
4473
- : getValueInData;
4463
+ ? (data, i) => normalizeValue(getValueInData(data, numberOfValues - i - 1))
4464
+ : (data, i) => normalizeValue(getValueInData(data, i));
4465
+ // first check if the target is in the cache
4466
+ const isNotWildcardTarget = mode !== "wildcard" ||
4467
+ typeof _target !== "string" ||
4468
+ !(_target.includes("*") || _target.includes("?"));
4469
+ if (lookupCaches && isNotWildcardTarget) {
4470
+ const searchMode = reverseSearch ? "reverseSearch" : "forwardSearch";
4471
+ let cache = lookupCaches[searchMode].get(data);
4472
+ if (cache === undefined) {
4473
+ // build the cache for all the values
4474
+ cache = new Map();
4475
+ for (let i = 0; i < numberOfValues; i++) {
4476
+ const value = getValue(data, i) ?? null;
4477
+ if (!cache.has(value)) {
4478
+ cache.set(value, i);
4479
+ }
4480
+ }
4481
+ lookupCaches[searchMode].set(data, cache);
4482
+ }
4483
+ if (cache.has(_target)) {
4484
+ const resultIndex = cache.get(_target);
4485
+ return reverseSearch ? numberOfValues - resultIndex - 1 : resultIndex;
4486
+ }
4487
+ if (mode === "strict") {
4488
+ return -1;
4489
+ }
4490
+ }
4491
+ // else perform the linear search
4492
+ const resultIndex = _linearSearch(data, _target, mode, numberOfValues, getValue);
4493
+ return reverseSearch && resultIndex !== -1 ? numberOfValues - resultIndex - 1 : resultIndex;
4494
+ }
4495
+ function _linearSearch(data, _target, mode, numberOfValues, getNormalizeValue) {
4474
4496
  let indexMatchTarget = (i) => {
4475
- return normalizeValue(getValue(data, i)) === _target;
4497
+ return getNormalizeValue(data, i) === _target;
4476
4498
  };
4477
4499
  if (mode === "wildcard" &&
4478
4500
  typeof _target === "string" &&
4479
4501
  (_target.includes("*") || _target.includes("?"))) {
4480
4502
  const regExp = wildcardToRegExp(_target);
4481
4503
  indexMatchTarget = (i) => {
4482
- const value = normalizeValue(getValue(data, i));
4504
+ const value = getNormalizeValue(data, i);
4483
4505
  if (typeof value === "string") {
4484
4506
  return regExp.test(value);
4485
4507
  }
@@ -4490,7 +4512,7 @@ function linearSearch(data, target, mode, numberOfValues, getValueInData, revers
4490
4512
  let closestMatchIndex = -1;
4491
4513
  if (mode === "nextSmaller") {
4492
4514
  indexMatchTarget = (i) => {
4493
- const value = normalizeValue(getValue(data, i));
4515
+ const value = getNormalizeValue(data, i);
4494
4516
  if ((!closestMatch && compareCellValues(_target, value) >= 0) ||
4495
4517
  (compareCellValues(_target, value) >= 0 && compareCellValues(value, closestMatch) > 0)) {
4496
4518
  closestMatch = value;
@@ -4501,7 +4523,7 @@ function linearSearch(data, target, mode, numberOfValues, getValueInData, revers
4501
4523
  }
4502
4524
  if (mode === "nextGreater") {
4503
4525
  indexMatchTarget = (i) => {
4504
- const value = normalizeValue(getValue(data, i));
4526
+ const value = getNormalizeValue(data, i);
4505
4527
  if ((!closestMatch && compareCellValues(_target, value) <= 0) ||
4506
4528
  (compareCellValues(_target, value) <= 0 && compareCellValues(value, closestMatch) < 0)) {
4507
4529
  closestMatch = value;
@@ -4512,12 +4534,10 @@ function linearSearch(data, target, mode, numberOfValues, getValueInData, revers
4512
4534
  }
4513
4535
  for (let i = 0; i < numberOfValues; i++) {
4514
4536
  if (indexMatchTarget(i)) {
4515
- return reverseSearch ? numberOfValues - i - 1 : i;
4537
+ return i;
4516
4538
  }
4517
4539
  }
4518
- return reverseSearch && closestMatchIndex !== -1
4519
- ? numberOfValues - closestMatchIndex - 1
4520
- : closestMatchIndex;
4540
+ return closestMatchIndex;
4521
4541
  }
4522
4542
  /**
4523
4543
  * Normalize a value.
@@ -6073,8 +6093,9 @@ function spreadRange(getters, dataSets) {
6073
6093
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6074
6094
  if (zone.right) {
6075
6095
  for (let j = zone.left; j <= zone.right; ++j) {
6096
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6076
6097
  postProcessedRanges.push({
6077
- ...dataSet,
6098
+ ...datasetOptions,
6078
6099
  dataRange: `${sheetPrefix}${zoneToXc({
6079
6100
  left: j,
6080
6101
  right: j,
@@ -6086,8 +6107,9 @@ function spreadRange(getters, dataSets) {
6086
6107
  }
6087
6108
  else {
6088
6109
  for (let j = zone.top; j <= zone.bottom; ++j) {
6110
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6089
6111
  postProcessedRanges.push({
6090
- ...dataSet,
6112
+ ...datasetOptions,
6091
6113
  dataRange: `${sheetPrefix}${zoneToXc({
6092
6114
  left: zone.left,
6093
6115
  right: zone.right,
@@ -6491,10 +6513,11 @@ class UuidGenerator {
6491
6513
  *
6492
6514
  */
6493
6515
  smallUuid() {
6494
- //@ts-ignore
6495
- if (window.crypto && window.crypto.getRandomValues) {
6496
- //@ts-ignore
6497
- return ([1e7] + -1e3).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
6516
+ if (window.crypto) {
6517
+ return "10000000-1000".replace(/[01]/g, (c) => {
6518
+ const n = Number(c);
6519
+ return (n ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (n / 4)))).toString(16);
6520
+ });
6498
6521
  }
6499
6522
  else {
6500
6523
  // mainly for jest and other browsers that do not have the crypto functionality
@@ -6509,10 +6532,11 @@ class UuidGenerator {
6509
6532
  * This method should be used when you need to avoid collisions at all costs, like the id of a revision.
6510
6533
  */
6511
6534
  uuidv4() {
6512
- //@ts-ignore
6513
- if (window.crypto && window.crypto.getRandomValues) {
6514
- //@ts-ignore
6515
- return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
6535
+ if (window.crypto) {
6536
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => {
6537
+ const n = Number(c);
6538
+ return (n ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (n / 4)))).toString(16);
6539
+ });
6516
6540
  }
6517
6541
  else {
6518
6542
  // mainly for jest and other browsers that do not have the crypto functionality
@@ -10036,70 +10060,341 @@ function getNextNonEmptyBar(bars, startIndex) {
10036
10060
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10037
10061
  }
10038
10062
 
10039
- window.Chart?.register(waterfallLinesPlugin);
10040
- window.Chart?.register(chartShowValuesPlugin);
10041
- class ChartJsComponent extends owl.Component {
10042
- static template = "o-spreadsheet-ChartJsComponent";
10043
- static props = {
10044
- figure: Object,
10063
+ const GAUGE_PADDING_SIDE = 30;
10064
+ const GAUGE_PADDING_TOP = 10;
10065
+ const GAUGE_PADDING_BOTTOM = 20;
10066
+ const GAUGE_LABELS_FONT_SIZE = 12;
10067
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10068
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10069
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10070
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10071
+ function drawGaugeChart(canvas, runtime) {
10072
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10073
+ canvas.width = canvasBoundingRect.width;
10074
+ canvas.height = canvasBoundingRect.height;
10075
+ const ctx = canvas.getContext("2d");
10076
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10077
+ drawBackground(ctx, config);
10078
+ drawGauge(ctx, config);
10079
+ drawInflectionValues(ctx, config);
10080
+ drawLabels(ctx, config);
10081
+ drawTitle(ctx, config);
10082
+ }
10083
+ function drawGauge(ctx, config) {
10084
+ ctx.save();
10085
+ const gauge = config.gauge;
10086
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10087
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10088
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10089
+ if (arcRadius < 0) {
10090
+ return;
10091
+ }
10092
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10093
+ // Gauge background
10094
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10095
+ ctx.beginPath();
10096
+ ctx.lineWidth = gauge.arcWidth;
10097
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10098
+ ctx.stroke();
10099
+ // Gauge value
10100
+ ctx.strokeStyle = gauge.color;
10101
+ ctx.beginPath();
10102
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10103
+ ctx.stroke();
10104
+ ctx.restore();
10105
+ }
10106
+ function drawBackground(ctx, config) {
10107
+ ctx.save();
10108
+ ctx.fillStyle = config.backgroundColor;
10109
+ ctx.fillRect(0, 0, config.width, config.height);
10110
+ ctx.restore();
10111
+ }
10112
+ function drawLabels(ctx, config) {
10113
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10114
+ ctx.save();
10115
+ ctx.textAlign = "center";
10116
+ ctx.fillStyle = label.color;
10117
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10118
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10119
+ ctx.restore();
10120
+ }
10121
+ }
10122
+ function drawInflectionValues(ctx, config) {
10123
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10124
+ for (const inflectionValue of config.inflectionValues) {
10125
+ ctx.save();
10126
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10127
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10128
+ ctx.lineWidth = 2;
10129
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10130
+ ctx.beginPath();
10131
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10132
+ ctx.lineTo(0, -height - 3);
10133
+ ctx.stroke();
10134
+ ctx.textAlign = "center";
10135
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10136
+ ctx.fillStyle = inflectionValue.color;
10137
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10138
+ ctx.fillText(inflectionValue.label, 0, textY);
10139
+ ctx.restore();
10140
+ }
10141
+ }
10142
+ function drawTitle(ctx, config) {
10143
+ ctx.save();
10144
+ const title = config.title;
10145
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10146
+ ctx.textBaseline = "middle";
10147
+ ctx.fillStyle = title.color;
10148
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10149
+ ctx.restore();
10150
+ }
10151
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10152
+ const maxValue = runtime.maxValue;
10153
+ const minValue = runtime.minValue;
10154
+ const gaugeValue = runtime.gaugeValue;
10155
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10156
+ const gaugeArcWidth = gaugeRect.width / 6;
10157
+ const gaugePercentage = gaugeValue
10158
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10159
+ : 0;
10160
+ const gaugeValuePosition = {
10161
+ x: boundingRect.width / 2,
10162
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10045
10163
  };
10046
- canvas = owl.useRef("graphContainer");
10047
- chart;
10048
- currentRuntime;
10049
- get background() {
10050
- return this.chartRuntime.background;
10164
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10165
+ // Scale down the font size if the gaugeRect is too small
10166
+ if (gaugeRect.height < 300) {
10167
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10051
10168
  }
10052
- get canvasStyle() {
10053
- return `background-color: ${this.background}`;
10169
+ // Scale down the font size if the text is too long
10170
+ const maxTextWidth = gaugeRect.width / 2;
10171
+ const gaugeLabel = gaugeValue?.label || "-";
10172
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10173
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10054
10174
  }
10055
- get chartRuntime() {
10056
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10057
- if (!("chartJsConfig" in runtime)) {
10058
- throw new Error("Unsupported chart runtime");
10059
- }
10060
- return runtime;
10175
+ const minLabelPosition = {
10176
+ x: gaugeRect.x + gaugeArcWidth / 2,
10177
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10178
+ };
10179
+ const maxLabelPosition = {
10180
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10181
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10182
+ };
10183
+ const textColor = chartMutedFontColor(runtime.background);
10184
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10185
+ let x = 0, titleWidth = 0, titleHeight = 0;
10186
+ if (runtime.title.text) {
10187
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10061
10188
  }
10062
- setup() {
10063
- owl.onMounted(() => {
10064
- const runtime = this.chartRuntime;
10065
- this.currentRuntime = runtime;
10066
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10067
- this.createChart(deepCopy(runtime.chartJsConfig));
10068
- });
10069
- owl.onWillUnmount(() => this.chart?.destroy());
10070
- owl.useEffect(() => {
10071
- const runtime = this.chartRuntime;
10072
- if (runtime !== this.currentRuntime) {
10073
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10074
- this.chart?.destroy();
10075
- this.createChart(deepCopy(runtime.chartJsConfig));
10076
- }
10077
- else {
10078
- this.updateChartJs(deepCopy(runtime));
10079
- }
10080
- this.currentRuntime = runtime;
10081
- }
10189
+ switch (runtime.title.align) {
10190
+ case "right":
10191
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10192
+ break;
10193
+ case "center":
10194
+ x = (boundingRect.width - titleWidth) / 2;
10195
+ break;
10196
+ case "left":
10197
+ default:
10198
+ x = CHART_PADDING$1;
10199
+ break;
10200
+ }
10201
+ return {
10202
+ width: boundingRect.width,
10203
+ height: boundingRect.height,
10204
+ title: {
10205
+ label: runtime.title.text ?? "",
10206
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10207
+ textPosition: {
10208
+ x,
10209
+ y: CHART_PADDING_TOP + titleHeight / 2,
10210
+ },
10211
+ color: runtime.title.color ?? textColor,
10212
+ bold: runtime.title.bold,
10213
+ italic: runtime.title.italic,
10214
+ },
10215
+ backgroundColor: runtime.background,
10216
+ gauge: {
10217
+ rect: gaugeRect,
10218
+ arcWidth: gaugeArcWidth,
10219
+ percentage: clip(gaugePercentage, 0, 1),
10220
+ color: getGaugeColor(runtime),
10221
+ },
10222
+ inflectionValues,
10223
+ gaugeValue: {
10224
+ label: gaugeLabel,
10225
+ textPosition: gaugeValuePosition,
10226
+ fontSize: gaugeValueFontSize,
10227
+ color: textColor,
10228
+ },
10229
+ minLabel: {
10230
+ label: runtime.minValue.label,
10231
+ textPosition: minLabelPosition,
10232
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10233
+ color: textColor,
10234
+ },
10235
+ maxLabel: {
10236
+ label: runtime.maxValue.label,
10237
+ textPosition: maxLabelPosition,
10238
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10239
+ color: textColor,
10240
+ },
10241
+ };
10242
+ }
10243
+ /**
10244
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10245
+ * space for the title and labels.
10246
+ */
10247
+ function getGaugeRect(boundingRect, title) {
10248
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10249
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10250
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10251
+ let gaugeWidth;
10252
+ let gaugeHeight;
10253
+ if (drawWidth > 2 * drawHeight) {
10254
+ gaugeWidth = 2 * drawHeight;
10255
+ gaugeHeight = drawHeight;
10256
+ }
10257
+ else {
10258
+ gaugeWidth = drawWidth;
10259
+ gaugeHeight = drawWidth / 2;
10260
+ }
10261
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10262
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10263
+ return {
10264
+ x: gaugeX,
10265
+ y: gaugeY,
10266
+ width: gaugeWidth,
10267
+ height: gaugeHeight,
10268
+ };
10269
+ }
10270
+ /**
10271
+ * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
10272
+ *
10273
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10274
+ */
10275
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10276
+ const maxValue = runtime.maxValue;
10277
+ const minValue = runtime.minValue;
10278
+ const gaugeCircleCenter = {
10279
+ x: gaugeRect.x + gaugeRect.width / 2,
10280
+ y: gaugeRect.y + gaugeRect.height,
10281
+ };
10282
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10283
+ const inflectionValues = [];
10284
+ const inflectionValuesTextRects = [];
10285
+ for (const inflectionValue of runtime.inflectionValues) {
10286
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10287
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10288
+ const angle = Math.PI - Math.PI * percentage;
10289
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10290
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10291
+ gaugeCircleCenter.x, // center of the gauge circle
10292
+ gaugeCircleCenter.y, // center of the gauge circle
10293
+ labelWidth + 2, // width of the text + some margin
10294
+ GAUGE_LABELS_FONT_SIZE // height of the text
10295
+ );
10296
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10297
+ ? GAUGE_LABELS_FONT_SIZE
10298
+ : 0;
10299
+ inflectionValuesTextRects.push(textRect);
10300
+ inflectionValues.push({
10301
+ rotation: angle,
10302
+ label: inflectionValue.label,
10303
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10304
+ color: textColor,
10305
+ offset,
10082
10306
  });
10083
10307
  }
10084
- createChart(chartData) {
10085
- const canvas = this.canvas.el;
10086
- const ctx = canvas.getContext("2d");
10087
- this.chart = new window.Chart(ctx, chartData);
10308
+ return inflectionValues;
10309
+ }
10310
+ function getGaugeColor(runtime) {
10311
+ const gaugeValue = runtime.gaugeValue?.value;
10312
+ if (gaugeValue === undefined) {
10313
+ return GAUGE_BACKGROUND_COLOR;
10088
10314
  }
10089
- updateChartJs(chartRuntime) {
10090
- const chartData = chartRuntime.chartJsConfig;
10091
- if (chartData.data && chartData.data.datasets) {
10092
- this.chart.data = chartData.data;
10093
- if (chartData.options?.plugins?.title) {
10094
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10095
- }
10315
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10316
+ const inflectionValue = runtime.inflectionValues[i];
10317
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10318
+ return runtime.colors[i];
10096
10319
  }
10097
- else {
10098
- this.chart.data.datasets = [];
10320
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10321
+ return runtime.colors[i];
10322
+ }
10323
+ }
10324
+ return runtime.colors.at(-1);
10325
+ }
10326
+ function getSegmentsOfRectangle(rectangle) {
10327
+ return [
10328
+ { start: rectangle.topLeft, end: rectangle.topRight },
10329
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10330
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10331
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10332
+ ];
10333
+ }
10334
+ /**
10335
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10336
+ * is not handled.
10337
+ */
10338
+ function doSegmentIntersect(segment1, segment2) {
10339
+ const A = segment1.start;
10340
+ const B = segment1.end;
10341
+ const C = segment2.start;
10342
+ const D = segment2.end;
10343
+ /**
10344
+ * Line segment intersection algorithm
10345
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10346
+ */
10347
+ function ccw(a, b, c) {
10348
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10349
+ }
10350
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10351
+ }
10352
+ function doRectanglesIntersect(rect1, rect2) {
10353
+ const segments1 = getSegmentsOfRectangle(rect1);
10354
+ const segments2 = getSegmentsOfRectangle(rect2);
10355
+ for (const segment1 of segments1) {
10356
+ for (const segment2 of segments2) {
10357
+ if (doSegmentIntersect(segment1, segment2)) {
10358
+ return true;
10359
+ }
10099
10360
  }
10100
- this.chart.config.options = chartData.options;
10101
- this.chart.update();
10102
10361
  }
10362
+ return false;
10363
+ }
10364
+ /**
10365
+ * Get the rectangle that is tangent to a circle at a given angle.
10366
+ *
10367
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10368
+ */
10369
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10370
+ const cos = Math.cos(angle);
10371
+ const sin = Math.sin(angle);
10372
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10373
+ const x = cos * radius;
10374
+ const y = sin * radius;
10375
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10376
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10377
+ const y2 = cos * (rectWidth / 2);
10378
+ const bottomRight = {
10379
+ x: x + x2 + circleCenterX,
10380
+ y: circleCenterY - (y - y2),
10381
+ };
10382
+ const bottomLeft = {
10383
+ x: x - x2 + circleCenterX,
10384
+ y: circleCenterY - (y + y2),
10385
+ };
10386
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10387
+ const xp = cos * (radius + rectHeight);
10388
+ const yp = sin * (radius + rectHeight);
10389
+ const topLeft = {
10390
+ x: xp - x2 + circleCenterX,
10391
+ y: circleCenterY - (yp + y2),
10392
+ };
10393
+ const topRight = {
10394
+ x: xp + x2 + circleCenterX,
10395
+ y: circleCenterY - (yp - y2),
10396
+ };
10397
+ return { bottomLeft, bottomRight, topRight, topLeft };
10103
10398
  }
10104
10399
 
10105
10400
  /**
@@ -10681,6 +10976,155 @@ class ScorecardChartConfigBuilder {
10681
10976
  }
10682
10977
  }
10683
10978
 
10979
+ const CHART_COMMON_OPTIONS = {
10980
+ // https://www.chartjs.org/docs/latest/general/responsive.html
10981
+ responsive: true, // will resize when its container is resized
10982
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
10983
+ elements: {
10984
+ line: {
10985
+ fill: false, // do not fill the area under line charts
10986
+ },
10987
+ point: {
10988
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
10989
+ },
10990
+ },
10991
+ animation: false,
10992
+ };
10993
+ function truncateLabel(label) {
10994
+ if (!label) {
10995
+ return "";
10996
+ }
10997
+ if (label.length > MAX_CHAR_LABEL) {
10998
+ return label.substring(0, MAX_CHAR_LABEL) + "…";
10999
+ }
11000
+ return label;
11001
+ }
11002
+ function chartToImage(runtime, figure, type) {
11003
+ // wrap the canvas in a div with a fixed size because chart.js would
11004
+ // fill the whole page otherwise
11005
+ const div = document.createElement("div");
11006
+ div.style.width = `${figure.width}px`;
11007
+ div.style.height = `${figure.height}px`;
11008
+ const canvas = document.createElement("canvas");
11009
+ div.append(canvas);
11010
+ canvas.setAttribute("width", figure.width.toString());
11011
+ canvas.setAttribute("height", figure.height.toString());
11012
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11013
+ document.body.append(div);
11014
+ if ("chartJsConfig" in runtime) {
11015
+ const config = deepCopy(runtime.chartJsConfig);
11016
+ config.plugins = [backgroundColorChartJSPlugin];
11017
+ const Chart = getChartJSConstructor();
11018
+ const chart = new Chart(canvas, config);
11019
+ const imgContent = chart.toBase64Image();
11020
+ chart.destroy();
11021
+ div.remove();
11022
+ return imgContent;
11023
+ }
11024
+ else if (type === "scorecard") {
11025
+ const design = getScorecardConfiguration(figure, runtime);
11026
+ drawScoreChart(design, canvas);
11027
+ const imgContent = canvas.toDataURL();
11028
+ div.remove();
11029
+ return imgContent;
11030
+ }
11031
+ else if (type === "gauge") {
11032
+ drawGaugeChart(canvas, runtime);
11033
+ const imgContent = canvas.toDataURL();
11034
+ div.remove();
11035
+ return imgContent;
11036
+ }
11037
+ return undefined;
11038
+ }
11039
+ /**
11040
+ * Custom chart.js plugin to set the background color of the canvas
11041
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11042
+ */
11043
+ const backgroundColorChartJSPlugin = {
11044
+ id: "customCanvasBackgroundColor",
11045
+ beforeDraw: (chart) => {
11046
+ const { ctx } = chart;
11047
+ ctx.save();
11048
+ ctx.globalCompositeOperation = "destination-over";
11049
+ ctx.fillStyle = "#ffffff";
11050
+ ctx.fillRect(0, 0, chart.width, chart.height);
11051
+ ctx.restore();
11052
+ },
11053
+ };
11054
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11055
+ function getChartJSConstructor() {
11056
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11057
+ window.Chart.register(chartShowValuesPlugin);
11058
+ window.Chart.register(waterfallLinesPlugin);
11059
+ }
11060
+ return window.Chart;
11061
+ }
11062
+
11063
+ class ChartJsComponent extends owl.Component {
11064
+ static template = "o-spreadsheet-ChartJsComponent";
11065
+ static props = {
11066
+ figure: Object,
11067
+ };
11068
+ canvas = owl.useRef("graphContainer");
11069
+ chart;
11070
+ currentRuntime;
11071
+ get background() {
11072
+ return this.chartRuntime.background;
11073
+ }
11074
+ get canvasStyle() {
11075
+ return `background-color: ${this.background}`;
11076
+ }
11077
+ get chartRuntime() {
11078
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11079
+ if (!("chartJsConfig" in runtime)) {
11080
+ throw new Error("Unsupported chart runtime");
11081
+ }
11082
+ return runtime;
11083
+ }
11084
+ setup() {
11085
+ owl.onMounted(() => {
11086
+ const runtime = this.chartRuntime;
11087
+ this.currentRuntime = runtime;
11088
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11089
+ this.createChart(deepCopy(runtime.chartJsConfig));
11090
+ });
11091
+ owl.onWillUnmount(() => this.chart?.destroy());
11092
+ owl.useEffect(() => {
11093
+ const runtime = this.chartRuntime;
11094
+ if (runtime !== this.currentRuntime) {
11095
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11096
+ this.chart?.destroy();
11097
+ this.createChart(deepCopy(runtime.chartJsConfig));
11098
+ }
11099
+ else {
11100
+ this.updateChartJs(deepCopy(runtime));
11101
+ }
11102
+ this.currentRuntime = runtime;
11103
+ }
11104
+ });
11105
+ }
11106
+ createChart(chartData) {
11107
+ const canvas = this.canvas.el;
11108
+ const ctx = canvas.getContext("2d");
11109
+ const Chart = getChartJSConstructor();
11110
+ this.chart = new Chart(ctx, chartData);
11111
+ }
11112
+ updateChartJs(chartRuntime) {
11113
+ const chartData = chartRuntime.chartJsConfig;
11114
+ if (chartData.data && chartData.data.datasets) {
11115
+ this.chart.data = chartData.data;
11116
+ if (chartData.options?.plugins?.title) {
11117
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11118
+ }
11119
+ }
11120
+ else {
11121
+ this.chart.data.datasets = [];
11122
+ }
11123
+ this.chart.config.options = chartData.options;
11124
+ this.chart.update();
11125
+ }
11126
+ }
11127
+
10684
11128
  class ScorecardChart extends owl.Component {
10685
11129
  static template = "o-spreadsheet-ScorecardChart";
10686
11130
  static props = {
@@ -12065,6 +12509,25 @@ const LN = {
12065
12509
  isExported: true,
12066
12510
  };
12067
12511
  // -----------------------------------------------------------------------------
12512
+ // LOG
12513
+ // -----------------------------------------------------------------------------
12514
+ const LOG = {
12515
+ description: _t("The logarithm of a number, for a given base."),
12516
+ args: [
12517
+ arg("value (number)", _t("The value for which to calculate the logarithm.")),
12518
+ arg("base (number, default=10)", _t("The base of the logarithm.")),
12519
+ ],
12520
+ compute: function (value, base = { value: 10 }) {
12521
+ const _value = toNumber(value, this.locale);
12522
+ const _base = toNumber(base, this.locale);
12523
+ assert(() => _value > 0, _t("The value (%s) must be strictly positive.", _value.toString()));
12524
+ assert(() => _base > 0, _t("The base (%s) must be strictly positive.", _base.toString()));
12525
+ assert(() => _base !== 1, _t("The base must be different from 1."));
12526
+ return Math.log10(_value) / Math.log10(_base);
12527
+ },
12528
+ isExported: true,
12529
+ };
12530
+ // -----------------------------------------------------------------------------
12068
12531
  // MOD
12069
12532
  // -----------------------------------------------------------------------------
12070
12533
  function mod(dividend, divisor) {
@@ -12604,6 +13067,7 @@ var math = /*#__PURE__*/Object.freeze({
12604
13067
  ISODD: ISODD,
12605
13068
  ISO_CEILING: ISO_CEILING,
12606
13069
  LN: LN,
13070
+ LOG: LOG,
12607
13071
  MOD: MOD,
12608
13072
  MUNIT: MUNIT,
12609
13073
  ODD: ODD,
@@ -18502,7 +18966,7 @@ const HLOOKUP = {
18502
18966
  const _isSorted = toBoolean(isSorted.value);
18503
18967
  const colIndex = _isSorted
18504
18968
  ? dichotomicSearch(_range, searchKey, "nextSmaller", "asc", _range.length, getValueFromRange)
18505
- : linearSearch(_range, searchKey, "wildcard", _range.length, getValueFromRange);
18969
+ : linearSearch(_range, searchKey, "wildcard", _range.length, getValueFromRange, this.lookupCaches);
18506
18970
  const col = _range[colIndex];
18507
18971
  if (col === undefined) {
18508
18972
  return valueNotAvailable(searchKey);
@@ -18657,7 +19121,7 @@ const MATCH = {
18657
19121
  index = dichotomicSearch(_range, searchKey, "nextSmaller", "asc", rangeLen, getElement);
18658
19122
  break;
18659
19123
  case 0:
18660
- index = linearSearch(_range, searchKey, "wildcard", rangeLen, getElement);
19124
+ index = linearSearch(_range, searchKey, "wildcard", rangeLen, getElement, this.lookupCaches);
18661
19125
  break;
18662
19126
  case -1:
18663
19127
  index = dichotomicSearch(_range, searchKey, "nextGreater", "desc", rangeLen, getElement);
@@ -18725,7 +19189,7 @@ const VLOOKUP = {
18725
19189
  const _isSorted = toBoolean(isSorted.value);
18726
19190
  const rowIndex = _isSorted
18727
19191
  ? dichotomicSearch(_range, searchKey, "nextSmaller", "asc", _range[0].length, getValueFromRange)
18728
- : linearSearch(_range, searchKey, "wildcard", _range[0].length, getValueFromRange);
19192
+ : linearSearch(_range, searchKey, "wildcard", _range[0].length, getValueFromRange, this.lookupCaches);
18729
19193
  const value = _range[_index - 1][rowIndex];
18730
19194
  if (value === undefined) {
18731
19195
  return valueNotAvailable(searchKey);
@@ -18781,7 +19245,7 @@ const XLOOKUP = {
18781
19245
  const reverseSearch = _searchMode === -1;
18782
19246
  const index = _searchMode === 2 || _searchMode === -2
18783
19247
  ? dichotomicSearch(_lookupRange, searchKey, mode, _searchMode === 2 ? "asc" : "desc", rangeLen, getElement)
18784
- : linearSearch(_lookupRange, searchKey, mode, rangeLen, getElement, reverseSearch);
19248
+ : linearSearch(_lookupRange, searchKey, mode, rangeLen, getElement, this.lookupCaches, reverseSearch);
18785
19249
  if (index !== -1) {
18786
19250
  return lookupDirection === "col"
18787
19251
  ? _returnRange.map((col) => [col[index]])
@@ -22113,7 +22577,7 @@ autofillRulesRegistry
22113
22577
  condition: (cell) => !cell.isFormula &&
22114
22578
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22115
22579
  alphaNumericValueRegExp.test(cell.content),
22116
- generateRule: (cell, cells) => {
22580
+ generateRule: (cell, cells, direction) => {
22117
22581
  const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22118
22582
  const prefix = cell.content.match(stringPrefixRegExp)[0];
22119
22583
  const numberPostfixLength = cell.content.length - prefix.length;
@@ -22121,7 +22585,10 @@ autofillRulesRegistry
22121
22585
  alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22122
22586
  .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22123
22587
  .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22124
- const increment = calculateIncrementBasedOnGroup(group);
22588
+ let increment = calculateIncrementBasedOnGroup(group);
22589
+ if (["up", "left"].includes(direction) && group.length === 1) {
22590
+ increment = -increment;
22591
+ }
22125
22592
  return {
22126
22593
  type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22127
22594
  prefix,
@@ -22184,10 +22651,13 @@ autofillRulesRegistry
22184
22651
  .add("increment_number", {
22185
22652
  condition: (cell) => !cell.isFormula &&
22186
22653
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22187
- generateRule: (cell, cells) => {
22654
+ generateRule: (cell, cells, direction) => {
22188
22655
  const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22189
22656
  !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22190
- const increment = calculateIncrementBasedOnGroup(group);
22657
+ let increment = calculateIncrementBasedOnGroup(group);
22658
+ if (["up", "left"].includes(direction) && group.length === 1) {
22659
+ increment = -increment;
22660
+ }
22191
22661
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22192
22662
  return {
22193
22663
  type: "INCREMENT_MODIFIER",
@@ -22231,343 +22701,6 @@ function getDateIntervals(dates) {
22231
22701
 
22232
22702
  const cellPopoverRegistry = new Registry();
22233
22703
 
22234
- const GAUGE_PADDING_SIDE = 30;
22235
- const GAUGE_PADDING_TOP = 10;
22236
- const GAUGE_PADDING_BOTTOM = 20;
22237
- const GAUGE_LABELS_FONT_SIZE = 12;
22238
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22239
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22240
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22241
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22242
- function drawGaugeChart(canvas, runtime) {
22243
- const canvasBoundingRect = canvas.getBoundingClientRect();
22244
- canvas.width = canvasBoundingRect.width;
22245
- canvas.height = canvasBoundingRect.height;
22246
- const ctx = canvas.getContext("2d");
22247
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22248
- drawBackground(ctx, config);
22249
- drawGauge(ctx, config);
22250
- drawInflectionValues(ctx, config);
22251
- drawLabels(ctx, config);
22252
- drawTitle(ctx, config);
22253
- }
22254
- function drawGauge(ctx, config) {
22255
- ctx.save();
22256
- const gauge = config.gauge;
22257
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22258
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22259
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22260
- if (arcRadius < 0) {
22261
- return;
22262
- }
22263
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22264
- // Gauge background
22265
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22266
- ctx.beginPath();
22267
- ctx.lineWidth = gauge.arcWidth;
22268
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22269
- ctx.stroke();
22270
- // Gauge value
22271
- ctx.strokeStyle = gauge.color;
22272
- ctx.beginPath();
22273
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22274
- ctx.stroke();
22275
- ctx.restore();
22276
- }
22277
- function drawBackground(ctx, config) {
22278
- ctx.save();
22279
- ctx.fillStyle = config.backgroundColor;
22280
- ctx.fillRect(0, 0, config.width, config.height);
22281
- ctx.restore();
22282
- }
22283
- function drawLabels(ctx, config) {
22284
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22285
- ctx.save();
22286
- ctx.textAlign = "center";
22287
- ctx.fillStyle = label.color;
22288
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22289
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22290
- ctx.restore();
22291
- }
22292
- }
22293
- function drawInflectionValues(ctx, config) {
22294
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22295
- for (const inflectionValue of config.inflectionValues) {
22296
- ctx.save();
22297
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22298
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22299
- ctx.lineWidth = 2;
22300
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22301
- ctx.beginPath();
22302
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22303
- ctx.lineTo(0, -height - 3);
22304
- ctx.stroke();
22305
- ctx.textAlign = "center";
22306
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22307
- ctx.fillStyle = inflectionValue.color;
22308
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22309
- ctx.fillText(inflectionValue.label, 0, textY);
22310
- ctx.restore();
22311
- }
22312
- }
22313
- function drawTitle(ctx, config) {
22314
- ctx.save();
22315
- const title = config.title;
22316
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22317
- ctx.textBaseline = "middle";
22318
- ctx.fillStyle = title.color;
22319
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22320
- ctx.restore();
22321
- }
22322
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22323
- const maxValue = runtime.maxValue;
22324
- const minValue = runtime.minValue;
22325
- const gaugeValue = runtime.gaugeValue;
22326
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22327
- const gaugeArcWidth = gaugeRect.width / 6;
22328
- const gaugePercentage = gaugeValue
22329
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22330
- : 0;
22331
- const gaugeValuePosition = {
22332
- x: boundingRect.width / 2,
22333
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22334
- };
22335
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22336
- // Scale down the font size if the gaugeRect is too small
22337
- if (gaugeRect.height < 300) {
22338
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22339
- }
22340
- // Scale down the font size if the text is too long
22341
- const maxTextWidth = gaugeRect.width / 2;
22342
- const gaugeLabel = gaugeValue?.label || "-";
22343
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22344
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22345
- }
22346
- const minLabelPosition = {
22347
- x: gaugeRect.x + gaugeArcWidth / 2,
22348
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22349
- };
22350
- const maxLabelPosition = {
22351
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22352
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22353
- };
22354
- const textColor = chartMutedFontColor(runtime.background);
22355
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22356
- let x = 0, titleWidth = 0, titleHeight = 0;
22357
- if (runtime.title.text) {
22358
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22359
- }
22360
- switch (runtime.title.align) {
22361
- case "right":
22362
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22363
- break;
22364
- case "center":
22365
- x = (boundingRect.width - titleWidth) / 2;
22366
- break;
22367
- case "left":
22368
- default:
22369
- x = CHART_PADDING$1;
22370
- break;
22371
- }
22372
- return {
22373
- width: boundingRect.width,
22374
- height: boundingRect.height,
22375
- title: {
22376
- label: runtime.title.text ?? "",
22377
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22378
- textPosition: {
22379
- x,
22380
- y: CHART_PADDING_TOP + titleHeight / 2,
22381
- },
22382
- color: runtime.title.color ?? textColor,
22383
- bold: runtime.title.bold,
22384
- italic: runtime.title.italic,
22385
- },
22386
- backgroundColor: runtime.background,
22387
- gauge: {
22388
- rect: gaugeRect,
22389
- arcWidth: gaugeArcWidth,
22390
- percentage: clip(gaugePercentage, 0, 1),
22391
- color: getGaugeColor(runtime),
22392
- },
22393
- inflectionValues,
22394
- gaugeValue: {
22395
- label: gaugeLabel,
22396
- textPosition: gaugeValuePosition,
22397
- fontSize: gaugeValueFontSize,
22398
- color: textColor,
22399
- },
22400
- minLabel: {
22401
- label: runtime.minValue.label,
22402
- textPosition: minLabelPosition,
22403
- fontSize: GAUGE_LABELS_FONT_SIZE,
22404
- color: textColor,
22405
- },
22406
- maxLabel: {
22407
- label: runtime.maxValue.label,
22408
- textPosition: maxLabelPosition,
22409
- fontSize: GAUGE_LABELS_FONT_SIZE,
22410
- color: textColor,
22411
- },
22412
- };
22413
- }
22414
- /**
22415
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22416
- * space for the title and labels.
22417
- */
22418
- function getGaugeRect(boundingRect, title) {
22419
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22420
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22421
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22422
- let gaugeWidth;
22423
- let gaugeHeight;
22424
- if (drawWidth > 2 * drawHeight) {
22425
- gaugeWidth = 2 * drawHeight;
22426
- gaugeHeight = drawHeight;
22427
- }
22428
- else {
22429
- gaugeWidth = drawWidth;
22430
- gaugeHeight = drawWidth / 2;
22431
- }
22432
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22433
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22434
- return {
22435
- x: gaugeX,
22436
- y: gaugeY,
22437
- width: gaugeWidth,
22438
- height: gaugeHeight,
22439
- };
22440
- }
22441
- /**
22442
- * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
22443
- *
22444
- * Also compute an offset for the text so that it doesn't overlap with other text.
22445
- */
22446
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22447
- const maxValue = runtime.maxValue;
22448
- const minValue = runtime.minValue;
22449
- const gaugeCircleCenter = {
22450
- x: gaugeRect.x + gaugeRect.width / 2,
22451
- y: gaugeRect.y + gaugeRect.height,
22452
- };
22453
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22454
- const inflectionValues = [];
22455
- const inflectionValuesTextRects = [];
22456
- for (const inflectionValue of runtime.inflectionValues) {
22457
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22458
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22459
- const angle = Math.PI - Math.PI * percentage;
22460
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22461
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22462
- gaugeCircleCenter.x, // center of the gauge circle
22463
- gaugeCircleCenter.y, // center of the gauge circle
22464
- labelWidth + 2, // width of the text + some margin
22465
- GAUGE_LABELS_FONT_SIZE // height of the text
22466
- );
22467
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22468
- ? GAUGE_LABELS_FONT_SIZE
22469
- : 0;
22470
- inflectionValuesTextRects.push(textRect);
22471
- inflectionValues.push({
22472
- rotation: angle,
22473
- label: inflectionValue.label,
22474
- fontSize: GAUGE_LABELS_FONT_SIZE,
22475
- color: textColor,
22476
- offset,
22477
- });
22478
- }
22479
- return inflectionValues;
22480
- }
22481
- function getGaugeColor(runtime) {
22482
- const gaugeValue = runtime.gaugeValue?.value;
22483
- if (gaugeValue === undefined) {
22484
- return GAUGE_BACKGROUND_COLOR;
22485
- }
22486
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22487
- const inflectionValue = runtime.inflectionValues[i];
22488
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22489
- return runtime.colors[i];
22490
- }
22491
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22492
- return runtime.colors[i];
22493
- }
22494
- }
22495
- return runtime.colors.at(-1);
22496
- }
22497
- function getSegmentsOfRectangle(rectangle) {
22498
- return [
22499
- { start: rectangle.topLeft, end: rectangle.topRight },
22500
- { start: rectangle.topRight, end: rectangle.bottomRight },
22501
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22502
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22503
- ];
22504
- }
22505
- /**
22506
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22507
- * is not handled.
22508
- */
22509
- function doSegmentIntersect(segment1, segment2) {
22510
- const A = segment1.start;
22511
- const B = segment1.end;
22512
- const C = segment2.start;
22513
- const D = segment2.end;
22514
- /**
22515
- * Line segment intersection algorithm
22516
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22517
- */
22518
- function ccw(a, b, c) {
22519
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22520
- }
22521
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22522
- }
22523
- function doRectanglesIntersect(rect1, rect2) {
22524
- const segments1 = getSegmentsOfRectangle(rect1);
22525
- const segments2 = getSegmentsOfRectangle(rect2);
22526
- for (const segment1 of segments1) {
22527
- for (const segment2 of segments2) {
22528
- if (doSegmentIntersect(segment1, segment2)) {
22529
- return true;
22530
- }
22531
- }
22532
- }
22533
- return false;
22534
- }
22535
- /**
22536
- * Get the rectangle that is tangent to a circle at a given angle.
22537
- *
22538
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22539
- */
22540
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22541
- const cos = Math.cos(angle);
22542
- const sin = Math.sin(angle);
22543
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22544
- const x = cos * radius;
22545
- const y = sin * radius;
22546
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22547
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22548
- const y2 = cos * (rectWidth / 2);
22549
- const bottomRight = {
22550
- x: x + x2 + circleCenterX,
22551
- y: circleCenterY - (y - y2),
22552
- };
22553
- const bottomLeft = {
22554
- x: x - x2 + circleCenterX,
22555
- y: circleCenterY - (y + y2),
22556
- };
22557
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22558
- const xp = cos * (radius + rectHeight);
22559
- const yp = sin * (radius + rectHeight);
22560
- const topLeft = {
22561
- x: xp - x2 + circleCenterX,
22562
- y: circleCenterY - (yp + y2),
22563
- };
22564
- const topRight = {
22565
- x: xp + x2 + circleCenterX,
22566
- y: circleCenterY - (yp - y2),
22567
- };
22568
- return { bottomLeft, bottomRight, topRight, topLeft };
22569
- }
22570
-
22571
22704
  class GaugeChartComponent extends owl.Component {
22572
22705
  static template = "o-spreadsheet-GaugeChartComponent";
22573
22706
  canvas = owl.useRef("chartContainer");
@@ -22600,81 +22733,6 @@ function toXlsxHexColor(color) {
22600
22733
  return color;
22601
22734
  }
22602
22735
 
22603
- const CHART_COMMON_OPTIONS = {
22604
- // https://www.chartjs.org/docs/latest/general/responsive.html
22605
- responsive: true, // will resize when its container is resized
22606
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22607
- elements: {
22608
- line: {
22609
- fill: false, // do not fill the area under line charts
22610
- },
22611
- point: {
22612
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22613
- },
22614
- },
22615
- animation: false,
22616
- };
22617
- function truncateLabel(label) {
22618
- if (!label) {
22619
- return "";
22620
- }
22621
- if (label.length > MAX_CHAR_LABEL) {
22622
- return label.substring(0, MAX_CHAR_LABEL) + "…";
22623
- }
22624
- return label;
22625
- }
22626
- function chartToImage(runtime, figure, type) {
22627
- // wrap the canvas in a div with a fixed size because chart.js would
22628
- // fill the whole page otherwise
22629
- const div = document.createElement("div");
22630
- div.style.width = `${figure.width}px`;
22631
- div.style.height = `${figure.height}px`;
22632
- const canvas = document.createElement("canvas");
22633
- div.append(canvas);
22634
- canvas.setAttribute("width", figure.width.toString());
22635
- canvas.setAttribute("height", figure.height.toString());
22636
- // we have to add the canvas to the DOM otherwise it won't be rendered
22637
- document.body.append(div);
22638
- if ("chartJsConfig" in runtime) {
22639
- const config = deepCopy(runtime.chartJsConfig);
22640
- config.plugins = [backgroundColorChartJSPlugin];
22641
- const chart = new window.Chart(canvas, config);
22642
- const imgContent = chart.toBase64Image();
22643
- chart.destroy();
22644
- div.remove();
22645
- return imgContent;
22646
- }
22647
- else if (type === "scorecard") {
22648
- const design = getScorecardConfiguration(figure, runtime);
22649
- drawScoreChart(design, canvas);
22650
- const imgContent = canvas.toDataURL();
22651
- div.remove();
22652
- return imgContent;
22653
- }
22654
- else if (type === "gauge") {
22655
- drawGaugeChart(canvas, runtime);
22656
- const imgContent = canvas.toDataURL();
22657
- div.remove();
22658
- return imgContent;
22659
- }
22660
- return undefined;
22661
- }
22662
- /**
22663
- * Custom chart.js plugin to set the background color of the canvas
22664
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22665
- */
22666
- const backgroundColorChartJSPlugin = {
22667
- id: "customCanvasBackgroundColor",
22668
- beforeDraw: (chart) => {
22669
- const { ctx } = chart;
22670
- ctx.save();
22671
- ctx.globalCompositeOperation = "destination-over";
22672
- ctx.fillStyle = "#ffffff";
22673
- ctx.fillRect(0, 0, chart.width, chart.height);
22674
- ctx.restore();
22675
- },
22676
- };
22677
-
22678
22736
  /**
22679
22737
  * Represent a raw XML string
22680
22738
  */
@@ -22736,6 +22794,7 @@ const DRAWING_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
22736
22794
  const CONTENT_TYPES = {
22737
22795
  workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
22738
22796
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
22797
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22739
22798
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22740
22799
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22741
22800
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22748,6 +22807,7 @@ const CONTENT_TYPES = {
22748
22807
  const XLSX_RELATION_TYPE = {
22749
22808
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22750
22809
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
22810
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22751
22811
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22752
22812
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22753
22813
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22757,6 +22817,7 @@ const XLSX_RELATION_TYPE = {
22757
22817
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22758
22818
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22759
22819
  };
22820
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22760
22821
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22761
22822
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22762
22823
  /**
@@ -25322,29 +25383,34 @@ function convertPivotTableConfig(pivotTable) {
25322
25383
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25323
25384
  */
25324
25385
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25325
- for (let sheet of convertedSheets) {
25326
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25386
+ for (let tableSheet of convertedSheets) {
25387
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25327
25388
  for (let table of tables) {
25328
25389
  const tabRef = table.name + "[";
25329
- for (let position of positions(toZone(table.ref))) {
25330
- const xc = toXC(position.col, position.row);
25331
- let cellContent = sheet.cells[xc];
25332
- if (cellContent?.startsWith("=")) {
25333
- let refIndex;
25334
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25335
- let reference = cellContent.slice(refIndex + tabRef.length);
25336
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25337
- let endIndex = reference.indexOf("]");
25338
- if (reference.startsWith(`[`)) {
25339
- endIndex = reference.indexOf("]", endIndex + 1);
25340
- endIndex = reference.indexOf("]", endIndex + 1);
25390
+ for (let sheet of convertedSheets) {
25391
+ for (let xc in sheet.cells) {
25392
+ const cell = sheet.cells[xc];
25393
+ let cellContent = sheet.cells[xc];
25394
+ if (cell && cellContent && cellContent.startsWith("=")) {
25395
+ let refIndex;
25396
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25397
+ let endIndex = refIndex + tabRef.length;
25398
+ let openBrackets = 1;
25399
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25400
+ if (cellContent[endIndex] === "[") {
25401
+ openBrackets++;
25402
+ }
25403
+ else if (cellContent[endIndex] === "]") {
25404
+ openBrackets--;
25405
+ }
25406
+ endIndex++;
25407
+ }
25408
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25409
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25410
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25411
+ cellContent =
25412
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25341
25413
  }
25342
- reference = reference.slice(0, endIndex);
25343
- const convertedRef = convertTableReference(reference, table, xc);
25344
- cellContent =
25345
- cellContent.slice(0, refIndex) +
25346
- convertedRef +
25347
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25348
25414
  }
25349
25415
  sheet.cells[xc] = cellContent;
25350
25416
  }
@@ -25353,11 +25419,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25353
25419
  }
25354
25420
  }
25355
25421
  /**
25356
- * Convert table-specific references in formulas into standard references.
25422
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25423
+ * and of keywords determining the rows of the table to reference.
25357
25424
  *
25358
25425
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25359
25426
  * - tableName[colName] : reference to the whole column "colName"
25427
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25360
25428
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25429
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25430
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25431
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25432
+ *
25361
25433
  *
25362
25434
  * The available keywords are :
25363
25435
  * - #All : all the column (including totals)
@@ -25365,58 +25437,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25365
25437
  * - #Headers : only the header of the column
25366
25438
  * - #Totals : only the totals of the column
25367
25439
  * - #This Row : only the element in the same row as the cell
25440
+ *
25441
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25368
25442
  */
25369
- function convertTableReference(expr, table, cellXc) {
25370
- const refElements = expr.split(",");
25443
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25444
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25445
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25446
+ const parts = expr.split(",").map((part) => part.trim());
25371
25447
  const tableZone = toZone(table.ref);
25372
- const refZone = { ...tableZone };
25373
- let isReferencedZoneValid = true;
25374
- // Single column reference
25375
- if (refElements.length === 1) {
25376
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25377
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25378
- if (table.headerRowCount) {
25379
- refZone.top += table.headerRowCount;
25380
- }
25381
- if (table.totalsRowCount) {
25382
- refZone.bottom -= 1;
25448
+ const colIndexes = [];
25449
+ const rowIndexes = [];
25450
+ const foundKeywords = [];
25451
+ for (const part of parts) {
25452
+ if (removeBrackets(part).startsWith("#")) {
25453
+ const keyWord = removeBrackets(part);
25454
+ foundKeywords.push(keyWord);
25455
+ switch (keyWord) {
25456
+ case "#All":
25457
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25458
+ break;
25459
+ case "#Data":
25460
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25461
+ const bottom = table.totalsRowCount
25462
+ ? tableZone.bottom - table.totalsRowCount
25463
+ : tableZone.bottom;
25464
+ rowIndexes.push(top, bottom);
25465
+ break;
25466
+ case "#This Row":
25467
+ rowIndexes.push(toCartesian(cellXc).row);
25468
+ break;
25469
+ case "#Headers":
25470
+ if (!table.headerRowCount) {
25471
+ return CellErrorType.InvalidReference;
25472
+ }
25473
+ rowIndexes.push(tableZone.top);
25474
+ break;
25475
+ case "#Totals":
25476
+ if (!table.totalsRowCount) {
25477
+ return CellErrorType.InvalidReference;
25478
+ }
25479
+ rowIndexes.push(tableZone.bottom);
25480
+ break;
25481
+ }
25383
25482
  }
25384
- }
25385
- // Other references
25386
- else {
25387
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25388
- case "#All":
25389
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25390
- refZone.bottom = tableZone.bottom;
25391
- break;
25392
- case "#Data":
25393
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25394
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25395
- break;
25396
- case "#This Row":
25397
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25398
- break;
25399
- case "#Headers":
25400
- refZone.top = refZone.bottom = tableZone.top;
25401
- if (!table.headerRowCount) {
25402
- isReferencedZoneValid = false;
25403
- }
25404
- break;
25405
- case "#Totals":
25406
- refZone.top = refZone.bottom = tableZone.bottom;
25407
- if (!table.totalsRowCount) {
25408
- isReferencedZoneValid = false;
25483
+ else {
25484
+ const columns = part
25485
+ .split(":")
25486
+ .map((part) => part.trim())
25487
+ .map(removeBrackets);
25488
+ if (colIndexes.length) {
25489
+ return CellErrorType.InvalidReference;
25490
+ }
25491
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25492
+ if (colRelativeIndex === -1) {
25493
+ return CellErrorType.InvalidReference;
25494
+ }
25495
+ colIndexes.push(colRelativeIndex + tableZone.left);
25496
+ if (columns[1]) {
25497
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25498
+ if (colRelativeIndex2 === -1) {
25499
+ return CellErrorType.InvalidReference;
25409
25500
  }
25410
- break;
25501
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25502
+ }
25411
25503
  }
25412
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25413
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25414
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25415
25504
  }
25416
- if (!isReferencedZoneValid) {
25505
+ if (!areKeywordsCompatible(foundKeywords)) {
25417
25506
  return CellErrorType.InvalidReference;
25418
25507
  }
25419
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25508
+ if (rowIndexes.length === 0) {
25509
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25510
+ const bottom = table.totalsRowCount
25511
+ ? tableZone.bottom - table.totalsRowCount
25512
+ : tableZone.bottom;
25513
+ rowIndexes.push(top, bottom);
25514
+ }
25515
+ if (colIndexes.length === 0) {
25516
+ colIndexes.push(tableZone.left, tableZone.right);
25517
+ }
25518
+ const refZone = {
25519
+ top: Math.min(...rowIndexes),
25520
+ left: Math.min(...colIndexes),
25521
+ bottom: Math.max(...rowIndexes),
25522
+ right: Math.max(...colIndexes),
25523
+ };
25524
+ return sheetPrefix + zoneToXc(refZone);
25525
+ }
25526
+ function removeBrackets(str) {
25527
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25528
+ }
25529
+ function areKeywordsCompatible(keywords) {
25530
+ if (keywords.length < 2) {
25531
+ return true;
25532
+ }
25533
+ else if (keywords.length > 2) {
25534
+ return false;
25535
+ }
25536
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25537
+ return true;
25538
+ }
25539
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25540
+ return true;
25541
+ }
25542
+ return false;
25420
25543
  }
25421
25544
 
25422
25545
  // -------------------------------------
@@ -28269,7 +28392,7 @@ function getBarChartData(definition, dataSets, labelRange, getters) {
28269
28392
  }
28270
28393
  function getPyramidChartData(definition, dataSets, labelRange, getters) {
28271
28394
  const barChartData = getBarChartData(definition, dataSets, labelRange, getters);
28272
- const barDataset = barChartData.dataSetsValues;
28395
+ const barDataset = barChartData.dataSetsValues.filter((ds) => !ds.hidden);
28273
28396
  const pyramidDatasetValues = [];
28274
28397
  if (barDataset[0]) {
28275
28398
  const pyramidData = barDataset[0].data.map((value) => (value > 0 ? value : 0));
@@ -28584,11 +28707,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
28584
28707
  }
28585
28708
  let missingTimeAdapterAlreadyWarned = false;
28586
28709
  function isLuxonTimeAdapterInstalled() {
28587
- if (!window.Chart) {
28710
+ const Chart = getChartJSConstructor();
28711
+ if (!Chart) {
28588
28712
  return false;
28589
28713
  }
28590
28714
  // @ts-ignore
28591
- const adapter = new window.Chart._adapters._date({});
28715
+ const adapter = new Chart._adapters._date({});
28592
28716
  const isInstalled = adapter._id === "luxon";
28593
28717
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28594
28718
  missingTimeAdapterAlreadyWarned = true;
@@ -28746,10 +28870,8 @@ function getChartDatasetFormat(getters, allDataSets, axis) {
28746
28870
  function getChartDatasetValues(getters, dataSets) {
28747
28871
  const datasetValues = [];
28748
28872
  for (const [dsIndex, ds] of Object.entries(dataSets)) {
28749
- if (getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left)) {
28750
- continue;
28751
- }
28752
28873
  let label;
28874
+ let hidden = getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left);
28753
28875
  if (ds.labelCell) {
28754
28876
  const labelRange = ds.labelCell;
28755
28877
  const cell = labelRange
@@ -28776,9 +28898,9 @@ function getChartDatasetValues(getters, dataSets) {
28776
28898
  data.fill(1);
28777
28899
  }
28778
28900
  else if (data.every((cell) => cell === undefined || cell === null || !isNumber(cell.toString(), DEFAULT_LOCALE))) {
28779
- continue;
28901
+ hidden = true;
28780
28902
  }
28781
- datasetValues.push({ data, label });
28903
+ datasetValues.push({ data, label, hidden });
28782
28904
  }
28783
28905
  return datasetValues;
28784
28906
  }
@@ -28789,12 +28911,13 @@ function getBarChartDatasets(definition, args) {
28789
28911
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28790
28912
  const trendDatasets = [];
28791
28913
  for (const index in dataSetsValues) {
28792
- let { label, data } = dataSetsValues[index];
28914
+ let { label, data, hidden } = dataSetsValues[index];
28793
28915
  label = definition.dataSets?.[index].label || label;
28794
28916
  const backgroundColor = colors.next();
28795
28917
  const dataset = {
28796
28918
  label,
28797
28919
  data,
28920
+ hidden,
28798
28921
  borderColor: definition.background || BACKGROUND_CHART_COLOR,
28799
28922
  borderWidth: definition.stacked ? 1 : 0,
28800
28923
  backgroundColor,
@@ -28827,6 +28950,9 @@ function getWaterfallDatasetAndLabels(definition, args) {
28827
28950
  const labelsWithSubTotals = [];
28828
28951
  let lastValue = 0;
28829
28952
  for (const dataSetsValue of dataSetsValues) {
28953
+ if (dataSetsValue.hidden) {
28954
+ continue;
28955
+ }
28830
28956
  for (let i = 0; i < dataSetsValue.data.length; i++) {
28831
28957
  const data = dataSetsValue.data[i];
28832
28958
  labelsWithSubTotals.push(labels[i]);
@@ -28862,7 +28988,7 @@ function getLineChartDatasets(definition, args) {
28862
28988
  const trendDatasets = [];
28863
28989
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28864
28990
  for (let index = 0; index < dataSetsValues.length; index++) {
28865
- let { label, data } = dataSetsValues[index];
28991
+ let { label, data, hidden } = dataSetsValues[index];
28866
28992
  label = definition.dataSets?.[index].label || label;
28867
28993
  const color = colors.next();
28868
28994
  if (axisType && ["linear", "time"].includes(axisType)) {
@@ -28872,6 +28998,7 @@ function getLineChartDatasets(definition, args) {
28872
28998
  const dataset = {
28873
28999
  label,
28874
29000
  data,
29001
+ hidden,
28875
29002
  tension: 0, // 0 -> render straight lines, which is much faster
28876
29003
  borderColor: color,
28877
29004
  backgroundColor: areaChart ? setColorAlpha(color, LINE_FILL_TRANSPARENCY) : color,
@@ -28904,11 +29031,13 @@ function getPieChartDatasets(definition, args) {
28904
29031
  const dataSets = [];
28905
29032
  const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
28906
29033
  const backgroundColor = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);
28907
- for (const { label, data } of dataSetsValues) {
29034
+ for (const { label, data, hidden } of dataSetsValues) {
29035
+ if (hidden)
29036
+ continue;
28908
29037
  const dataset = {
28909
29038
  label,
28910
29039
  data,
28911
- borderColor: BACKGROUND_CHART_COLOR,
29040
+ borderColor: definition.background || "#FFFFFF",
28912
29041
  backgroundColor,
28913
29042
  hoverOffset: 30,
28914
29043
  };
@@ -28922,7 +29051,7 @@ function getComboChartDatasets(definition, args) {
28922
29051
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28923
29052
  const trendDatasets = [];
28924
29053
  for (let index = 0; index < dataSetsValues.length; index++) {
28925
- let { label, data } = dataSetsValues[index];
29054
+ let { label, data, hidden } = dataSetsValues[index];
28926
29055
  label = definition.dataSets?.[index].label || label;
28927
29056
  const design = definition.dataSets?.[index];
28928
29057
  const color = colors.next();
@@ -28930,6 +29059,7 @@ function getComboChartDatasets(definition, args) {
28930
29059
  const dataset = {
28931
29060
  label: label,
28932
29061
  data,
29062
+ hidden,
28933
29063
  borderColor: color,
28934
29064
  backgroundColor: color,
28935
29065
  yAxisID: definition.dataSets?.[index].yAxisId || "y",
@@ -28954,7 +29084,7 @@ function getRadarChartDatasets(definition, args) {
28954
29084
  const fill = definition.fillArea ?? false;
28955
29085
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28956
29086
  for (let i = 0; i < dataSetsValues.length; i++) {
28957
- let { label, data } = dataSetsValues[i];
29087
+ let { label, data, hidden } = dataSetsValues[i];
28958
29088
  if (definition.dataSets?.[i]?.label) {
28959
29089
  label = definition.dataSets[i].label;
28960
29090
  }
@@ -28962,6 +29092,7 @@ function getRadarChartDatasets(definition, args) {
28962
29092
  const dataset = {
28963
29093
  label,
28964
29094
  data,
29095
+ hidden,
28965
29096
  borderColor,
28966
29097
  backgroundColor: borderColor,
28967
29098
  };
@@ -29107,6 +29238,11 @@ function getPieChartLegend(definition, args) {
29107
29238
  hidden: false,
29108
29239
  lineWidth: 2,
29109
29240
  })),
29241
+ filter: (legendItem, data) => {
29242
+ return "datasetIndex" in legendItem
29243
+ ? !data.datasets[legendItem.datasetIndex].hidden
29244
+ : true;
29245
+ },
29110
29246
  },
29111
29247
  };
29112
29248
  }
@@ -29168,6 +29304,11 @@ function getWaterfallChartLegend(definition, args) {
29168
29304
  }
29169
29305
  return legendValues;
29170
29306
  },
29307
+ filter: (legendItem, data) => {
29308
+ return "datasetIndex" in legendItem
29309
+ ? !data.datasets[legendItem.datasetIndex].hidden
29310
+ : true;
29311
+ },
29171
29312
  },
29172
29313
  onClick: () => { }, // Disables click interaction with the waterfall chart legend items
29173
29314
  };
@@ -29251,6 +29392,11 @@ function getCustomLegendLabels(fontColor, legendLabelConfig) {
29251
29392
  ...legendLabelConfig,
29252
29393
  };
29253
29394
  }),
29395
+ filter: (legendItem, data) => {
29396
+ return "datasetIndex" in legendItem
29397
+ ? !data.datasets[legendItem.datasetIndex].hidden
29398
+ : true;
29399
+ },
29254
29400
  },
29255
29401
  };
29256
29402
  }
@@ -32263,10 +32409,6 @@ class Popover extends owl.Component {
32263
32409
  this.currentDisplayValue = newDisplay;
32264
32410
  if (!anchor)
32265
32411
  return;
32266
- el.style.top = "";
32267
- el.style.left = "";
32268
- el.style["max-height"] = "";
32269
- el.style["max-width"] = "";
32270
32412
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32271
32413
  let elDims = {
32272
32414
  width: el.getBoundingClientRect().width,
@@ -33040,6 +33182,100 @@ function* iterateChildren(el) {
33040
33182
  function getOpenedMenus() {
33041
33183
  return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
33042
33184
  }
33185
+ function getCurrentSelection(el) {
33186
+ let { startElement, endElement, startSelectionOffset, endSelectionOffset } = getStartAndEndSelection(el);
33187
+ let startSizeBefore = findSelectionIndex(el, startElement, startSelectionOffset);
33188
+ let endSizeBefore = findSelectionIndex(el, endElement, endSelectionOffset);
33189
+ return {
33190
+ start: startSizeBefore,
33191
+ end: endSizeBefore,
33192
+ };
33193
+ }
33194
+ function getStartAndEndSelection(el) {
33195
+ const selection = document.getSelection();
33196
+ return {
33197
+ startElement: selection.anchorNode || el,
33198
+ startSelectionOffset: selection.anchorOffset,
33199
+ endElement: selection.focusNode || el,
33200
+ endSelectionOffset: selection.focusOffset,
33201
+ };
33202
+ }
33203
+ /**
33204
+ * Computes the text 'index' inside this.el based on the currently selected node and its offset.
33205
+ * The selected node is either a Text node or an Element node.
33206
+ *
33207
+ * case 1 -Text node:
33208
+ * the offset is the number of characters from the start of the node. We have to add this offset to the
33209
+ * content length of all previous nodes.
33210
+ *
33211
+ * case 2 - Element node:
33212
+ * the offset is the number of child nodes before the selected node. We have to add the content length of
33213
+ * all the nodes prior to the selected node as well as the content of the child node before the offset.
33214
+ *
33215
+ * See the MDN documentation for more details.
33216
+ * https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset
33217
+ * https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset
33218
+ *
33219
+ */
33220
+ function findSelectionIndex(el, nodeToFind, nodeOffset) {
33221
+ let usedCharacters = 0;
33222
+ let it = iterateChildren(el);
33223
+ let current = it.next();
33224
+ let isFirstParagraph = true;
33225
+ while (!current.done && current.value !== nodeToFind) {
33226
+ if (!current.value.hasChildNodes()) {
33227
+ if (current.value.textContent) {
33228
+ usedCharacters += current.value.textContent.length;
33229
+ }
33230
+ }
33231
+ // One new paragraph = one new line character, except for the first paragraph
33232
+ if (current.value.nodeName === "P" ||
33233
+ (current.value.nodeName === "DIV" && current.value !== el) // On paste, the HTML may contain <div> instead of <p>
33234
+ ) {
33235
+ if (isFirstParagraph) {
33236
+ isFirstParagraph = false;
33237
+ }
33238
+ else {
33239
+ usedCharacters++;
33240
+ }
33241
+ }
33242
+ current = it.next();
33243
+ }
33244
+ if (current.value !== nodeToFind) {
33245
+ /** This situation can happen if the code is called while the selection is not currently on the element.
33246
+ * In this case, we return 0 because we don't know the size of the text before the selection.
33247
+ *
33248
+ * A known occurrence is triggered since the introduction of commit d4663158 (PR #2038).
33249
+ */
33250
+ return 0;
33251
+ }
33252
+ else {
33253
+ if (!current.value.hasChildNodes()) {
33254
+ usedCharacters += nodeOffset;
33255
+ }
33256
+ else {
33257
+ const children = [...current.value.childNodes].slice(0, nodeOffset);
33258
+ usedCharacters += children.reduce((acc, child, index) => {
33259
+ if (child.textContent !== null) {
33260
+ // need to account for paragraph nodes that implicitly add a new line
33261
+ // except for the last paragraph
33262
+ let chars = child.textContent.length;
33263
+ if (child.nodeName === "P" && index !== children.length - 1) {
33264
+ chars++;
33265
+ }
33266
+ return acc + chars;
33267
+ }
33268
+ else {
33269
+ return acc;
33270
+ }
33271
+ }, 0);
33272
+ }
33273
+ }
33274
+ if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
33275
+ usedCharacters++;
33276
+ }
33277
+ return usedCharacters;
33278
+ }
33043
33279
  const letterRegex = /^[a-zA-Z]$/;
33044
33280
  /**
33045
33281
  * Transform a keyboard event into a shortcut string that represent this event. The letters keys will be uppercased.
@@ -33710,6 +33946,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
33710
33946
  drawScoreChart: drawScoreChart,
33711
33947
  formatChartDatasetValue: formatChartDatasetValue,
33712
33948
  formatTickValue: formatTickValue,
33949
+ getChartJSConstructor: getChartJSConstructor,
33713
33950
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
33714
33951
  getDefinedAxis: getDefinedAxis,
33715
33952
  getPieColors: getPieColors,
@@ -37593,6 +37830,9 @@ class GenericChartConfigPanel extends owl.Component {
37593
37830
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
37594
37831
  dataSets: this.dataSeriesRanges,
37595
37832
  });
37833
+ if (this.state.datasetDispatchResult.isSuccessful) {
37834
+ this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
37835
+ }
37596
37836
  }
37597
37837
  getDataSeriesRanges() {
37598
37838
  return this.dataSeriesRanges;
@@ -38275,7 +38515,7 @@ css /* scss */ `
38275
38515
  .o-font-size-editor {
38276
38516
  height: calc(100% - 4px);
38277
38517
  input.o-font-size {
38278
- outline-color: ${SELECTION_BORDER_COLOR};
38518
+ outline: none;
38279
38519
  height: 20px;
38280
38520
  width: 23px;
38281
38521
  }
@@ -39888,6 +40128,10 @@ class ContentEditableHelper {
39888
40128
  if (currentStart === start && currentEnd === end) {
39889
40129
  return;
39890
40130
  }
40131
+ if (selection.rangeCount === 0) {
40132
+ const range = document.createRange();
40133
+ selection.addRange(range);
40134
+ }
39891
40135
  const currentRange = selection.getRangeAt(0);
39892
40136
  let range;
39893
40137
  if (this.el.contains(currentRange.startContainer)) {
@@ -39915,8 +40159,16 @@ class ContentEditableHelper {
39915
40159
  }
39916
40160
  let startNode = this.findChildAtCharacterIndex(start);
39917
40161
  let endNode = this.findChildAtCharacterIndex(end);
39918
- range.setStart(startNode.node, startNode.offset);
39919
- range.setEnd(endNode.node, endNode.offset);
40162
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
40163
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
40164
+ if (start <= end) {
40165
+ range.setStart(startNode.node, startNode.offset);
40166
+ range.setEnd(endNode.node, endNode.offset);
40167
+ }
40168
+ else {
40169
+ range.setStart(endNode.node, endNode.offset);
40170
+ range.setEnd(startNode.node, startNode.offset);
40171
+ }
39920
40172
  }
39921
40173
  }
39922
40174
  /**
@@ -40050,7 +40302,7 @@ class ContentEditableHelper {
40050
40302
  if (!focusedNode || !this.el.contains(focusedNode))
40051
40303
  return;
40052
40304
  const element = focusedNode instanceof HTMLElement ? focusedNode : focusedNode.parentElement;
40053
- element?.scrollIntoView({ block: "nearest" });
40305
+ element?.scrollIntoView?.({ block: "nearest" });
40054
40306
  }
40055
40307
  /**
40056
40308
  * remove the current selection of the user
@@ -40070,100 +40322,7 @@ class ContentEditableHelper {
40070
40322
  * finds the indexes of the current selection.
40071
40323
  * */
40072
40324
  getCurrentSelection() {
40073
- let { startElement, endElement, startSelectionOffset, endSelectionOffset } = this.getStartAndEndSelection();
40074
- let startSizeBefore = this.findSelectionIndex(startElement, startSelectionOffset);
40075
- let endSizeBefore = this.findSelectionIndex(endElement, endSelectionOffset);
40076
- return {
40077
- start: startSizeBefore,
40078
- end: endSizeBefore,
40079
- };
40080
- }
40081
- /**
40082
- * Computes the text 'index' inside this.el based on the currently selected node and its offset.
40083
- * The selected node is either a Text node or an Element node.
40084
- *
40085
- * case 1 -Text node:
40086
- * the offset is the number of characters from the start of the node. We have to add this offset to the
40087
- * content length of all previous nodes.
40088
- *
40089
- * case 2 - Element node:
40090
- * the offset is the number of child nodes before the selected node. We have to add the content length of
40091
- * all the bnodes prior to the selected node as well as the content of the child node before the offset.
40092
- *
40093
- * See the MDN documentation for more details.
40094
- * https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset
40095
- * https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset
40096
- *
40097
- */
40098
- findSelectionIndex(nodeToFind, nodeOffset) {
40099
- let usedCharacters = 0;
40100
- let it = iterateChildren(this.el);
40101
- let current = it.next();
40102
- let isFirstParagraph = true;
40103
- while (!current.done && current.value !== nodeToFind) {
40104
- if (!current.value.hasChildNodes()) {
40105
- if (current.value.textContent) {
40106
- usedCharacters += current.value.textContent.length;
40107
- }
40108
- }
40109
- // One new paragraph = one new line character, except for the first paragraph
40110
- if (current.value.nodeName === "P" ||
40111
- (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
40112
- ) {
40113
- if (isFirstParagraph) {
40114
- isFirstParagraph = false;
40115
- }
40116
- else {
40117
- usedCharacters++;
40118
- }
40119
- }
40120
- current = it.next();
40121
- }
40122
- if (current.value !== nodeToFind) {
40123
- /** This situation can happen if the code is called while the selection is not currently on the ContentEditableHelper.
40124
- * In this case, we return 0 because we don't know the size of the text before the selection.
40125
- *
40126
- * A known occurence is triggered since the introduction of commit d4663158 (PR #2038).
40127
- *
40128
- * FIXME: find a way to test eventhough the selection API is not available in jsDOM.
40129
- */
40130
- return 0;
40131
- }
40132
- else {
40133
- if (!current.value.hasChildNodes()) {
40134
- usedCharacters += nodeOffset;
40135
- }
40136
- else {
40137
- const children = [...current.value.childNodes].slice(0, nodeOffset);
40138
- usedCharacters += children.reduce((acc, child, index) => {
40139
- if (child.textContent !== null) {
40140
- // need to account for paragraph nodes that implicitely add a new line
40141
- // except for the last paragraph
40142
- let chars = child.textContent.length;
40143
- if (child.nodeName === "P" && index !== children.length - 1) {
40144
- chars++;
40145
- }
40146
- return acc + chars;
40147
- }
40148
- else {
40149
- return acc;
40150
- }
40151
- }, 0);
40152
- }
40153
- }
40154
- if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
40155
- usedCharacters++;
40156
- }
40157
- return usedCharacters;
40158
- }
40159
- getStartAndEndSelection() {
40160
- const selection = document.getSelection();
40161
- return {
40162
- startElement: selection.anchorNode || this.el,
40163
- startSelectionOffset: selection.anchorOffset,
40164
- endElement: selection.focusNode || this.el,
40165
- endSelectionOffset: selection.focusOffset,
40166
- };
40325
+ return getCurrentSelection(this.el);
40167
40326
  }
40168
40327
  getText() {
40169
40328
  let text = "";
@@ -40313,8 +40472,7 @@ css /* scss */ `
40313
40472
  }
40314
40473
 
40315
40474
  .o-composer-assistant {
40316
- position: absolute;
40317
- margin: 1px 4px;
40475
+ margin-top: 1px;
40318
40476
 
40319
40477
  .o-semi-bold {
40320
40478
  /* FIXME: to remove in favor of Bootstrap
@@ -40365,10 +40523,11 @@ class Composer extends owl.Component {
40365
40523
  });
40366
40524
  compositionActive = false;
40367
40525
  spreadsheetRect = useSpreadsheetRect();
40368
- get assistantStyle() {
40526
+ get assistantStyleProperties() {
40369
40527
  const composerRect = this.composerRef.el.getBoundingClientRect();
40370
40528
  const assistantStyle = {};
40371
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40529
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40530
+ assistantStyle["min-width"] = `${minWidth}px`;
40372
40531
  const proposals = this.autoCompleteState.provider?.proposals;
40373
40532
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40374
40533
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40392,13 +40551,29 @@ class Composer extends owl.Component {
40392
40551
  }
40393
40552
  }
40394
40553
  else {
40395
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40554
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40396
40555
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40397
40556
  this.spreadsheetRect.width) {
40398
40557
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40399
40558
  }
40400
40559
  }
40401
- return cssPropertiesToCss(assistantStyle);
40560
+ return assistantStyle;
40561
+ }
40562
+ get assistantStyle() {
40563
+ const allProperties = this.assistantStyleProperties;
40564
+ return cssPropertiesToCss({
40565
+ "max-height": allProperties["max-height"],
40566
+ width: allProperties["width"],
40567
+ "min-width": allProperties["min-width"],
40568
+ });
40569
+ }
40570
+ get assistantContainerStyle() {
40571
+ const allProperties = this.assistantStyleProperties;
40572
+ return cssPropertiesToCss({
40573
+ top: allProperties["top"],
40574
+ right: allProperties["right"],
40575
+ transform: allProperties["transform"],
40576
+ });
40402
40577
  }
40403
40578
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40404
40579
  shouldProcessInputEvents = false;
@@ -46335,9 +46510,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46335
46510
  pivot: this.draft,
46336
46511
  });
46337
46512
  this.draft = null;
46338
- if (!this.alreadyNotified &&
46339
- !this.isDynamicPivotInViewport() &&
46340
- this.isStaticPivotInViewport()) {
46513
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46341
46514
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46342
46515
  const pivotExample = `=PIVOT(${formulaId})`;
46343
46516
  this.alreadyNotified = true;
@@ -46393,29 +46566,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
46393
46566
  this.applyUpdate();
46394
46567
  }
46395
46568
  }
46396
- isDynamicPivotInViewport() {
46397
- const sheetId = this.getters.getActiveSheetId();
46398
- for (const col of this.getters.getSheetViewVisibleCols()) {
46399
- for (const row of this.getters.getSheetViewVisibleRows()) {
46400
- const isDynamicPivot = this.getters.isSpillPivotFormula({ sheetId, col, row });
46401
- if (isDynamicPivot) {
46402
- return true;
46403
- }
46404
- }
46405
- }
46406
- return false;
46407
- }
46408
- isStaticPivotInViewport() {
46569
+ /**
46570
+ * @returns true if the updated pivot is visible in the viewport only as a
46571
+ * static pivot and not as a dynamic pivot
46572
+ */
46573
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46574
+ let staticPivotCount = 0;
46575
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46409
46576
  for (const position of this.getters.getVisibleCellPositions()) {
46410
46577
  const cell = this.getters.getCell(position);
46411
46578
  if (cell?.isFormula) {
46412
46579
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46413
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46414
- return true;
46580
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46581
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46582
+ if (pivotFunction.functionName === "PIVOT") {
46583
+ // if we have at least one dynamic pivot visible inserted the viewport
46584
+ // we return false
46585
+ return false;
46586
+ }
46587
+ else {
46588
+ staticPivotCount++;
46589
+ }
46415
46590
  }
46416
46591
  }
46417
46592
  }
46418
- return false;
46593
+ // we return true if there are only static pivots visible inserted the viewport,
46594
+ // otherwise false
46595
+ return staticPivotCount > 0;
46419
46596
  }
46420
46597
  addDefaultDateTimeGranularity(fields, definition) {
46421
46598
  const { columns, rows } = definition;
@@ -53118,6 +53295,10 @@ class CellPlugin extends CorePlugin {
53118
53295
  return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessUpdateCell);
53119
53296
  case "CLEAR_CELL":
53120
53297
  return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessClearCell);
53298
+ case "UPDATE_CELL_POSITION":
53299
+ return !cmd.cellId || this.cells[cmd.sheetId]?.[cmd.cellId]
53300
+ ? "Success" /* CommandResult.Success */
53301
+ : "InvalidCellId" /* CommandResult.InvalidCellId */;
53121
53302
  default:
53122
53303
  return "Success" /* CommandResult.Success */;
53123
53304
  }
@@ -53162,6 +53343,9 @@ class CellPlugin extends CorePlugin {
53162
53343
  case "DELETE_CONTENT":
53163
53344
  this.clearZones(cmd.sheetId, cmd.target);
53164
53345
  break;
53346
+ case "DELETE_SHEET": {
53347
+ this.history.update("cells", cmd.sheetId, undefined);
53348
+ }
53165
53349
  }
53166
53350
  }
53167
53351
  clearZones(sheetId, zones) {
@@ -53952,6 +54136,9 @@ class ConditionalFormatPlugin extends CorePlugin {
53952
54136
  allowDispatch(cmd) {
53953
54137
  switch (cmd.type) {
53954
54138
  case "ADD_CONDITIONAL_FORMAT":
54139
+ if (cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId))) {
54140
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
54141
+ }
53955
54142
  return this.checkValidations(cmd, this.checkCFRule, this.checkEmptyRange, this.checkCFHasChanged);
53956
54143
  case "CHANGE_CONDITIONAL_FORMAT_PRIORITY":
53957
54144
  return this.checkValidPriorityChange(cmd.cfId, cmd.delta, cmd.sheetId);
@@ -54368,8 +54555,17 @@ class DataValidationPlugin extends CorePlugin {
54368
54555
  allowDispatch(cmd) {
54369
54556
  switch (cmd.type) {
54370
54557
  case "ADD_DATA_VALIDATION_RULE":
54558
+ if (!this.getters.tryGetSheet(cmd.sheetId)) {
54559
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
54560
+ }
54561
+ if (cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId))) {
54562
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
54563
+ }
54371
54564
  return this.checkValidations(cmd, this.chainValidations(this.checkEmptyRange, this.checkValidRange, this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));
54372
54565
  case "REMOVE_DATA_VALIDATION_RULE":
54566
+ if (!this.getters.tryGetSheet(cmd.sheetId)) {
54567
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
54568
+ }
54373
54569
  if (!this.rules[cmd.sheetId].find((rule) => rule.id === cmd.id)) {
54374
54570
  return "UnknownDataValidationRule" /* CommandResult.UnknownDataValidationRule */;
54375
54571
  }
@@ -54596,6 +54792,7 @@ class DataValidationPlugin extends CorePlugin {
54596
54792
  class FigurePlugin extends CorePlugin {
54597
54793
  static getters = ["getFigures", "getFigure", "getFigureSheetId"];
54598
54794
  figures = {};
54795
+ insertionOrders = []; // TODO use a list in master
54599
54796
  // ---------------------------------------------------------------------------
54600
54797
  // Command Handling
54601
54798
  // ---------------------------------------------------------------------------
@@ -54698,11 +54895,14 @@ class FigurePlugin extends CorePlugin {
54698
54895
  }
54699
54896
  addFigure(figure, sheetId) {
54700
54897
  this.history.update("figures", sheetId, figure.id, figure);
54898
+ this.history.update("insertionOrders", this.insertionOrders.length, figure.id);
54701
54899
  }
54702
54900
  deleteSheet(sheetId) {
54901
+ this.history.update("insertionOrders", this.insertionOrders.filter((id) => !this.figures[sheetId]?.[id]));
54703
54902
  this.history.update("figures", sheetId, undefined);
54704
54903
  }
54705
54904
  removeFigure(id, sheetId) {
54905
+ this.history.update("insertionOrders", this.insertionOrders.filter((figureId) => figureId !== id));
54706
54906
  this.history.update("figures", sheetId, id, undefined);
54707
54907
  }
54708
54908
  checkFigureExists(sheetId, figureId) {
@@ -54721,7 +54921,14 @@ class FigurePlugin extends CorePlugin {
54721
54921
  // Getters
54722
54922
  // ---------------------------------------------------------------------------
54723
54923
  getFigures(sheetId) {
54724
- return Object.values(this.figures[sheetId] || {}).filter(isDefined);
54924
+ const figures = [];
54925
+ for (const figureId of this.insertionOrders) {
54926
+ const figure = this.figures[sheetId]?.[figureId];
54927
+ if (figure) {
54928
+ figures.push(figure);
54929
+ }
54930
+ }
54931
+ return figures;
54725
54932
  }
54726
54933
  getFigure(sheetId, figureId) {
54727
54934
  return this.figures[sheetId]?.[figureId];
@@ -54734,11 +54941,9 @@ class FigurePlugin extends CorePlugin {
54734
54941
  // ---------------------------------------------------------------------------
54735
54942
  import(data) {
54736
54943
  for (let sheet of data.sheets) {
54737
- const figures = {};
54738
- sheet.figures.forEach((figure) => {
54739
- figures[figure.id] = figure;
54740
- });
54741
- this.figures[sheet.id] = figures;
54944
+ for (const figure of sheet.figures) {
54945
+ this.addFigure(figure, sheet.id);
54946
+ }
54742
54947
  }
54743
54948
  }
54744
54949
  export(data) {
@@ -56164,6 +56369,9 @@ class SheetPlugin extends CorePlugin {
56164
56369
  case "CREATE_SHEET": {
56165
56370
  return this.checkValidations(cmd, this.checkSheetName, this.checkSheetPosition);
56166
56371
  }
56372
+ case "DUPLICATE_SHEET": {
56373
+ return this.sheets[cmd.sheetIdTo] ? "DuplicatedSheetId" /* CommandResult.DuplicatedSheetId */ : "Success" /* CommandResult.Success */;
56374
+ }
56167
56375
  case "MOVE_SHEET":
56168
56376
  try {
56169
56377
  const currentIndex = this.orderedSheetIds.findIndex((id) => id === cmd.sheetId);
@@ -56975,6 +57183,10 @@ class SheetPlugin extends CorePlugin {
56975
57183
  checkZonesAreInSheet(cmd) {
56976
57184
  if (!("sheetId" in cmd))
56977
57185
  return "Success" /* CommandResult.Success */;
57186
+ if ("ranges" in cmd &&
57187
+ cmd.ranges.some((rangeData) => rangeData._sheetId !== "" && !this.getters.tryGetSheet(rangeData._sheetId))) {
57188
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
57189
+ }
56978
57190
  return this.checkZonesExistInSheet(cmd.sheetId, this.getCommandZones(cmd));
56979
57191
  }
56980
57192
  }
@@ -56983,6 +57195,7 @@ let nextTableId = 1;
56983
57195
  class TablePlugin extends CorePlugin {
56984
57196
  static getters = ["getCoreTable", "getCoreTables", "getCoreTableMatchingTopLeft"];
56985
57197
  tables = {};
57198
+ insertionOrders = {};
56986
57199
  adaptRanges(applyChange, sheetId) {
56987
57200
  const sheetIds = sheetId ? [sheetId] : this.getters.getSheetIds();
56988
57201
  for (const sheetId of sheetIds) {
@@ -56994,6 +57207,9 @@ class TablePlugin extends CorePlugin {
56994
57207
  allowDispatch(cmd) {
56995
57208
  switch (cmd.type) {
56996
57209
  case "CREATE_TABLE":
57210
+ if (cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId) || rangeData._sheetId !== cmd.sheetId)) {
57211
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
57212
+ }
56997
57213
  const zones = cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone);
56998
57214
  if (!areZonesContinuous(zones)) {
56999
57215
  return "NonContinuousTargets" /* CommandResult.NonContinuousTargets */;
@@ -57024,11 +57240,13 @@ class TablePlugin extends CorePlugin {
57024
57240
  switch (cmd.type) {
57025
57241
  case "CREATE_SHEET":
57026
57242
  this.history.update("tables", cmd.sheetId, {});
57243
+ this.history.update("insertionOrders", cmd.sheetId, []);
57027
57244
  break;
57028
57245
  case "DELETE_SHEET": {
57029
57246
  const tables = { ...this.tables };
57030
57247
  delete tables[cmd.sheetId];
57031
57248
  this.history.update("tables", tables);
57249
+ this.history.update("insertionOrders", cmd.sheetId, undefined);
57032
57250
  break;
57033
57251
  }
57034
57252
  case "DUPLICATE_SHEET": {
@@ -57040,6 +57258,9 @@ class TablePlugin extends CorePlugin {
57040
57258
  : this.copyStaticTableForSheet(cmd.sheetIdTo, table);
57041
57259
  }
57042
57260
  this.history.update("tables", cmd.sheetIdTo, newTables);
57261
+ this.history.update("insertionOrders", cmd.sheetIdTo, [
57262
+ ...(this.insertionOrders[cmd.sheetId] ?? []),
57263
+ ]);
57043
57264
  break;
57044
57265
  }
57045
57266
  case "CREATE_TABLE": {
@@ -57053,6 +57274,10 @@ class TablePlugin extends CorePlugin {
57053
57274
  ? this.createDynamicTable(id, union, config)
57054
57275
  : this.createStaticTable(id, cmd.tableType, union, config);
57055
57276
  this.history.update("tables", cmd.sheetId, newTable.id, newTable);
57277
+ this.history.update("insertionOrders", cmd.sheetId, [
57278
+ ...(this.insertionOrders[cmd.sheetId] ?? []),
57279
+ newTable.id,
57280
+ ]);
57056
57281
  break;
57057
57282
  }
57058
57283
  case "REMOVE_TABLE": {
@@ -57063,6 +57288,7 @@ class TablePlugin extends CorePlugin {
57063
57288
  }
57064
57289
  }
57065
57290
  this.history.update("tables", cmd.sheetId, tables);
57291
+ this.history.update("insertionOrders", cmd.sheetId, this.insertionOrders[cmd.sheetId]?.filter((id) => id in tables));
57066
57292
  break;
57067
57293
  }
57068
57294
  case "UPDATE_TABLE": {
@@ -57098,7 +57324,14 @@ class TablePlugin extends CorePlugin {
57098
57324
  }
57099
57325
  }
57100
57326
  getCoreTables(sheetId) {
57101
- return this.tables[sheetId] ? Object.values(this.tables[sheetId]).filter(isDefined) : [];
57327
+ const tables = [];
57328
+ for (const tableId of this.insertionOrders[sheetId] || []) {
57329
+ const table = this.tables[sheetId][tableId];
57330
+ if (table) {
57331
+ tables.push(table);
57332
+ }
57333
+ }
57334
+ return tables;
57102
57335
  }
57103
57336
  getCoreTable({ sheetId, col, row }) {
57104
57337
  return this.getCoreTables(sheetId).find((table) => isInside(col, row, table.range.zone));
@@ -57381,6 +57614,7 @@ class TablePlugin extends CorePlugin {
57381
57614
  // ---------------------------------------------------------------------------
57382
57615
  import(data) {
57383
57616
  for (const sheet of data.sheets) {
57617
+ const tableIds = [];
57384
57618
  for (const tableData of sheet.tables || []) {
57385
57619
  const uuid = `${nextTableId++}`;
57386
57620
  const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;
@@ -57390,7 +57624,9 @@ class TablePlugin extends CorePlugin {
57390
57624
  ? this.createDynamicTable(uuid, range, tableConfig)
57391
57625
  : this.createStaticTable(uuid, tableType, range, tableConfig);
57392
57626
  this.history.update("tables", sheet.id, table.id, table);
57627
+ tableIds.push(table.id);
57393
57628
  }
57629
+ this.history.update("insertionOrders", sheet.id, tableIds);
57394
57630
  }
57395
57631
  }
57396
57632
  export(data) {
@@ -57430,7 +57666,10 @@ class HeaderGroupingPlugin extends CorePlugin {
57430
57666
  allowDispatch(cmd) {
57431
57667
  switch (cmd.type) {
57432
57668
  case "GROUP_HEADERS": {
57433
- const { start, end } = cmd;
57669
+ const { start, end, sheetId } = cmd;
57670
+ if (!this.getters.tryGetSheet(sheetId)) {
57671
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
57672
+ }
57434
57673
  if (!this.getters.doesHeadersExist(cmd.sheetId, cmd.dimension, [start, end])) {
57435
57674
  return "InvalidHeaderGroupStartEnd" /* CommandResult.InvalidHeaderGroupStartEnd */;
57436
57675
  }
@@ -57443,7 +57682,10 @@ class HeaderGroupingPlugin extends CorePlugin {
57443
57682
  break;
57444
57683
  }
57445
57684
  case "UNGROUP_HEADERS": {
57446
- const { start, end } = cmd;
57685
+ const { start, end, sheetId } = cmd;
57686
+ if (!this.getters.tryGetSheet(sheetId)) {
57687
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
57688
+ }
57447
57689
  if (!this.getters.doesHeadersExist(cmd.sheetId, cmd.dimension, [start, end])) {
57448
57690
  return "InvalidHeaderGroupStartEnd" /* CommandResult.InvalidHeaderGroupStartEnd */;
57449
57691
  }
@@ -57454,6 +57696,9 @@ class HeaderGroupingPlugin extends CorePlugin {
57454
57696
  }
57455
57697
  case "UNFOLD_HEADER_GROUP":
57456
57698
  case "FOLD_HEADER_GROUP":
57699
+ if (!this.getters.tryGetSheet(cmd.sheetId)) {
57700
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
57701
+ }
57457
57702
  const group = this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);
57458
57703
  if (!group) {
57459
57704
  return "UnknownHeaderGroup" /* CommandResult.UnknownHeaderGroup */;
@@ -57854,6 +58099,9 @@ class PivotCorePlugin extends CorePlugin {
57854
58099
  return this.checkDuplicatedMeasureIds(cmd.pivot);
57855
58100
  }
57856
58101
  case "UPDATE_PIVOT": {
58102
+ if (!(cmd.pivotId in this.pivots)) {
58103
+ return "PivotIdNotFound" /* CommandResult.PivotIdNotFound */;
58104
+ }
57857
58105
  if (deepEquals(cmd.pivot, this.pivots[cmd.pivotId]?.definition)) {
57858
58106
  return "NoChanges" /* CommandResult.NoChanges */;
57859
58107
  }
@@ -57870,6 +58118,8 @@ class PivotCorePlugin extends CorePlugin {
57870
58118
  return "EmptyName" /* CommandResult.EmptyName */;
57871
58119
  }
57872
58120
  break;
58121
+ case "REMOVE_PIVOT":
58122
+ case "DUPLICATE_PIVOT":
57873
58123
  case "INSERT_PIVOT": {
57874
58124
  if (!(cmd.pivotId in this.pivots)) {
57875
58125
  return "PivotIdNotFound" /* CommandResult.PivotIdNotFound */;
@@ -57919,7 +58169,7 @@ class PivotCorePlugin extends CorePlugin {
57919
58169
  break;
57920
58170
  }
57921
58171
  case "UPDATE_PIVOT": {
57922
- this.history.update("pivots", cmd.pivotId, "definition", cmd.pivot);
58172
+ this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
57923
58173
  this.compileCalculatedMeasures(cmd.pivot.measures);
57924
58174
  break;
57925
58175
  }
@@ -57990,7 +58240,7 @@ class PivotCorePlugin extends CorePlugin {
57990
58240
  // Private
57991
58241
  // -------------------------------------------------------------------------
57992
58242
  addPivot(pivotId, pivot, formulaId = this.nextFormulaId.toString()) {
57993
- this.history.update("pivots", pivotId, { definition: pivot, formulaId });
58243
+ this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
57994
58244
  this.compileCalculatedMeasures(pivot.measures);
57995
58245
  this.history.update("formulaIds", formulaId, pivotId);
57996
58246
  this.history.update("nextFormulaId", this.nextFormulaId + 1);
@@ -59576,6 +59826,10 @@ class Evaluator {
59576
59826
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
59577
59827
  this.compilationParams.evalContext.updateDependencies = this.updateDependencies.bind(this);
59578
59828
  this.compilationParams.evalContext.addDependencies = this.addDependencies.bind(this);
59829
+ this.compilationParams.evalContext.lookupCaches = {
59830
+ forwardSearch: new Map(),
59831
+ reverseSearch: new Map(),
59832
+ };
59579
59833
  }
59580
59834
  createEmptyPositionSet() {
59581
59835
  const sheetSizes = {};
@@ -60178,6 +60432,7 @@ class EvaluationPlugin extends UIPlugin {
60178
60432
  exportForExcel(data) {
60179
60433
  for (const sheet of data.sheets) {
60180
60434
  sheet.cellValues = {};
60435
+ sheet.formulaSpillRanges = {};
60181
60436
  }
60182
60437
  for (const position of this.evaluator.getEvaluatedPositions()) {
60183
60438
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60189,8 +60444,9 @@ class EvaluationPlugin extends UIPlugin {
60189
60444
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60190
60445
  const formulaCell = this.getCorrespondingFormulaCell(position);
60191
60446
  if (formulaCell) {
60447
+ const cell = this.getters.getCell(position);
60192
60448
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60193
- isFormula = isExported;
60449
+ isFormula = isExported && cell?.content === formulaCell.content;
60194
60450
  // If the cell contains a non-exported formula and that is evaluates to
60195
60451
  // nothing* ,we don't export it.
60196
60452
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60213,7 +60469,11 @@ class EvaluationPlugin extends UIPlugin {
60213
60469
  content = !isExported ? newContent : exportedCellData;
60214
60470
  }
60215
60471
  exportedSheetData.cells[xc] = content;
60216
- exportedSheetData.cellValues[xc] = value;
60472
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
60473
+ const spillZone = this.getSpreadZone(position);
60474
+ if (spillZone) {
60475
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
60476
+ }
60217
60477
  }
60218
60478
  }
60219
60479
  /**
@@ -62420,7 +62680,7 @@ class AutofillPlugin extends UIPlugin {
62420
62680
  getRule(cell, cells) {
62421
62681
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62422
62682
  const rule = rules.find((rule) => rule.condition(cell, cells));
62423
- return rule && rule.generateRule(cell, cells);
62683
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62424
62684
  }
62425
62685
  /**
62426
62686
  * Create the generator to be able to autofill the next cells.
@@ -62896,6 +63156,9 @@ function updateChartRangesTransformation(toTransform, executed) {
62896
63156
  };
62897
63157
  }
62898
63158
  function createSheetTransformation(toTransform, executed) {
63159
+ if (toTransform.sheetId === executed.sheetId) {
63160
+ toTransform = { ...toTransform, sheetId: `${toTransform.sheetId}~` };
63161
+ }
62899
63162
  if (toTransform.name === executed.name) {
62900
63163
  return {
62901
63164
  ...toTransform,
@@ -63539,15 +63802,6 @@ class Session extends EventBus {
63539
63802
  }
63540
63803
  this.sendPendingMessage();
63541
63804
  }
63542
- dropPendingRevision(revisionId) {
63543
- this.revisions.drop(revisionId);
63544
- const revisionIds = this.pendingMessages
63545
- .filter((message) => message.type === "REMOTE_REVISION")
63546
- .map((message) => message.nextRevisionId);
63547
- this.trigger("pending-revisions-dropped", { revisionIds });
63548
- this.waitingAck = false;
63549
- this.waitingUndoRedoAck = false;
63550
- }
63551
63805
  /**
63552
63806
  * Send the next pending message
63553
63807
  */
@@ -63556,15 +63810,14 @@ class Session extends EventBus {
63556
63810
  if (!message)
63557
63811
  return;
63558
63812
  if (message.type === "REMOTE_REVISION") {
63559
- const revision = this.revisions.get(message.nextRevisionId);
63813
+ let revision = this.revisions.get(message.nextRevisionId);
63560
63814
  if (revision.commands.length === 0) {
63561
63815
  /**
63562
- * The command is empty, we have to drop all the next local revisions
63816
+ * The command is empty, we have to rebase all the next local revisions
63563
63817
  * to avoid issues with undo/redo
63564
63818
  */
63565
- this.dropPendingRevision(revision.id);
63566
- this.pendingMessages = [];
63567
- return;
63819
+ this.revisions.rebase(revision.id);
63820
+ revision = this.revisions.get(message.nextRevisionId);
63568
63821
  }
63569
63822
  message = {
63570
63823
  ...message,
@@ -63600,18 +63853,16 @@ class Session extends EventBus {
63600
63853
  case "REVISION_UNDONE": {
63601
63854
  this.waitingAck = false;
63602
63855
  this.pendingMessages = this.pendingMessages.filter((msg) => msg.nextRevisionId !== message.nextRevisionId);
63603
- const pendingRemoteRevisions = this.pendingMessages.filter((message) => message.type === "REMOTE_REVISION");
63604
- const firstTransformedRevisionIndex = pendingRemoteRevisions.findIndex((message) => !deepEquals(message.commands, this.revisions.get(message.nextRevisionId).commands));
63605
- if (firstTransformedRevisionIndex !== -1) {
63856
+ const firstPendingRevisionId = this.pendingMessages.findIndex((message) => message.type === "REMOTE_REVISION");
63857
+ if (firstPendingRevisionId !== -1) {
63606
63858
  /**
63607
63859
  * Some revisions undergo transformations that may cause issues with
63608
63860
  * undo/redo if the transformation is destructive (we don't get back
63609
63861
  * the original command by transforming it with the inverse).
63610
- * To prevent these problems, we must discard all subsequent local
63862
+ * To prevent these problems, we must rebase all subsequent local
63611
63863
  * revisions.
63612
63864
  */
63613
- this.dropPendingRevision(this.pendingMessages[firstTransformedRevisionIndex].nextRevisionId);
63614
- this.pendingMessages = this.pendingMessages.slice(0, firstTransformedRevisionIndex);
63865
+ this.revisions.rebase(this.pendingMessages[firstPendingRevisionId].nextRevisionId);
63615
63866
  }
63616
63867
  this.serverRevisionId = message.nextRevisionId;
63617
63868
  this.processedRevisions.add(message.nextRevisionId);
@@ -64739,6 +64990,10 @@ class SheetUIPlugin extends UIPlugin {
64739
64990
  */
64740
64991
  checkZonesAreInSheet(cmd) {
64741
64992
  const sheetId = "sheetId" in cmd ? cmd.sheetId : this.getters.tryGetActiveSheetId();
64993
+ if ("ranges" in cmd &&
64994
+ cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId))) {
64995
+ return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
64996
+ }
64742
64997
  const zones = this.getters.getCommandZones(cmd);
64743
64998
  if (!sheetId && zones.length > 0) {
64744
64999
  return "NoActiveSheet" /* CommandResult.NoActiveSheet */;
@@ -65293,7 +65548,6 @@ class HistoryPlugin extends UIPlugin {
65293
65548
  super(config);
65294
65549
  this.session = config.session;
65295
65550
  this.session.on("new-local-state-update", this, this.onNewLocalStateUpdate);
65296
- this.session.on("pending-revisions-dropped", this, ({ revisionIds }) => this.drop(revisionIds));
65297
65551
  this.session.on("snapshot", this, () => {
65298
65552
  this.undoStack = [];
65299
65553
  this.redoStack = [];
@@ -65363,10 +65617,6 @@ class HistoryPlugin extends UIPlugin {
65363
65617
  const lastNonRedoRevision = this.getPossibleRevisionToRepeat();
65364
65618
  return canRepeatRevision(lastNonRedoRevision);
65365
65619
  }
65366
- drop(revisionIds) {
65367
- this.undoStack = this.undoStack.filter((id) => !revisionIds.includes(id));
65368
- this.redoStack = [];
65369
- }
65370
65620
  onNewLocalStateUpdate({ id }) {
65371
65621
  this.undoStack.push(id);
65372
65622
  this.redoStack = [];
@@ -68070,7 +68320,9 @@ class HeaderPositionsUIPlugin extends UIPlugin {
68070
68320
  case "UNGROUP_HEADERS":
68071
68321
  case "GROUP_HEADERS":
68072
68322
  case "CREATE_SHEET":
68073
- this.headerPositions[cmd.sheetId] = this.computeHeaderPositionsOfSheet(cmd.sheetId);
68323
+ if (this.getters.tryGetSheet(cmd.sheetId)) {
68324
+ this.headerPositions[cmd.sheetId] = this.computeHeaderPositionsOfSheet(cmd.sheetId);
68325
+ }
68074
68326
  break;
68075
68327
  case "DUPLICATE_SHEET":
68076
68328
  this.headerPositions[cmd.sheetIdTo] = deepCopy(this.headerPositions[cmd.sheetId]);
@@ -68078,12 +68330,14 @@ class HeaderPositionsUIPlugin extends UIPlugin {
68078
68330
  }
68079
68331
  }
68080
68332
  finalize() {
68081
- if (this.isDirty) {
68082
- for (const sheetId of this.getters.getSheetIds()) {
68333
+ for (const sheetId of this.getters.getSheetIds()) {
68334
+ // sheets can be created without this plugin being aware of it
68335
+ // in concurrent situations.
68336
+ if (this.isDirty || !this.headerPositions[sheetId]) {
68083
68337
  this.headerPositions[sheetId] = this.computeHeaderPositionsOfSheet(sheetId);
68084
68338
  }
68085
- this.isDirty = false;
68086
68339
  }
68340
+ this.isDirty = false;
68087
68341
  }
68088
68342
  /**
68089
68343
  * Returns the size, start and end coordinates of a column on an unfolded sheet
@@ -71504,9 +71758,16 @@ class SelectiveHistory {
71504
71758
  this.fastForward();
71505
71759
  this.insert(redoId, this.buildEmpty(redoId), insertAfter);
71506
71760
  }
71507
- drop(operationId) {
71761
+ rebase(operationId) {
71762
+ const operation = this.get(operationId);
71763
+ const execution = [...this.tree.execution(this.HEAD_BRANCH).startAfter(operationId)];
71508
71764
  this.revertBefore(operationId);
71765
+ const baseId = this.HEAD_OPERATION.id;
71509
71766
  this.tree.drop(operationId);
71767
+ this.insert(operationId, operation, baseId);
71768
+ for (const { operation } of execution) {
71769
+ this.insert(operation.id, operation.data, this.HEAD_OPERATION.id);
71770
+ }
71510
71771
  }
71511
71772
  /**
71512
71773
  * Revert the state as it was *before* the given operation was executed.
@@ -72949,7 +73210,7 @@ function numberRef(reference) {
72949
73210
  `;
72950
73211
  }
72951
73212
 
72952
- function addFormula(formula, value) {
73213
+ function addFormula(formula, value, formulaSpillRange) {
72953
73214
  if (!formula) {
72954
73215
  return { attrs: [], node: escapeXml `` };
72955
73216
  }
@@ -72957,10 +73218,17 @@ function addFormula(formula, value) {
72957
73218
  if (type === undefined) {
72958
73219
  return { attrs: [], node: escapeXml `` };
72959
73220
  }
72960
- const attrs = [["t", type]];
73221
+ const attrs = [
73222
+ ["cm", "1"],
73223
+ ["t", type],
73224
+ ];
72961
73225
  const XlsxFormula = adaptFormulaToExcel(formula);
72962
73226
  const exportedValue = adaptFormulaValueToExcel(value);
72963
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73227
+ // We treat all formulas as array formulas (a simple formula
73228
+ // is an array formula that spills on only one cell) to avoid
73229
+ // trying to detect spilling sub-formulas which is not a trivial task.
73230
+ let node;
73231
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
72964
73232
  return { attrs, node };
72965
73233
  }
72966
73234
  function addContent(content, sharedStrings, forceString = false) {
@@ -73950,7 +74218,7 @@ function addRows(construct, data, sheet) {
73950
74218
  let cellNode = escapeXml ``;
73951
74219
  // Either formula or static value inside the cell
73952
74220
  if (content?.startsWith("=") && value !== undefined) {
73953
- const res = addFormula(content, value);
74221
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
73954
74222
  if (!res) {
73955
74223
  continue;
73956
74224
  }
@@ -74236,6 +74504,30 @@ function createWorksheets(data, construct) {
74236
74504
  `;
74237
74505
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74238
74506
  }
74507
+ const sheetMetadataXml = escapeXml /*xml*/ `
74508
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
74509
+ <metadataTypes count="1">
74510
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
74511
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
74512
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
74513
+ </metadataTypes>
74514
+ <futureMetadata name="XLDAPR" count="1">
74515
+ <bk>
74516
+ <extLst>
74517
+ <ext uri="{${ARRAY_FORMULA_URI}}">
74518
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
74519
+ </ext>
74520
+ </extLst>
74521
+ </bk>
74522
+ </futureMetadata>
74523
+ <cellMetadata count="1">
74524
+ <bk>
74525
+ <rc t="1" v="0" />
74526
+ </bk>
74527
+ </cellMetadata>
74528
+ </metadata>
74529
+ `;
74530
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74239
74531
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74240
74532
  type: XLSX_RELATION_TYPE.sharedStrings,
74241
74533
  target: "sharedStrings.xml",
@@ -74244,6 +74536,10 @@ function createWorksheets(data, construct) {
74244
74536
  type: XLSX_RELATION_TYPE.styles,
74245
74537
  target: "styles.xml",
74246
74538
  });
74539
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74540
+ type: XLSX_RELATION_TYPE.metadata,
74541
+ target: "metadata.xml",
74542
+ });
74247
74543
  return files;
74248
74544
  }
74249
74545
  /**
@@ -74617,6 +74913,11 @@ class Model extends EventBus {
74617
74913
  dispatch: (command) => {
74618
74914
  const result = this.checkDispatchAllowed(command);
74619
74915
  if (!result.isSuccessful) {
74916
+ // core views plugins need to be invalidated
74917
+ this.dispatchToHandlers(this.coreHandlers, {
74918
+ type: "UNDO",
74919
+ commands: [command],
74920
+ });
74620
74921
  return;
74621
74922
  }
74622
74923
  this.isReplayingCommand = true;
@@ -75167,6 +75468,6 @@ exports.tokenColors = tokenColors;
75167
75468
  exports.tokenize = tokenize;
75168
75469
 
75169
75470
 
75170
- __info__.version = "18.1.8";
75171
- __info__.date = "2025-02-14T08:42:08.322Z";
75172
- __info__.hash = "02682f4";
75471
+ __info__.version = "18.1.10";
75472
+ __info__.date = "2025-03-07T10:34:41.861Z";
75473
+ __info__.hash = "31e4526";