@odoo/o-spreadsheet 19.1.0-alpha.12 → 19.1.0-alpha.14

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.
@@ -3,8 +3,8 @@
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
5
  * @version 19.1.0-alpha.3
6
- * @date 2025-11-12T14:16:26.552Z
7
- * @hash 6fefc9c
6
+ * @date 2025-12-02T05:34:07.213Z
7
+ * @hash 04cf666
8
8
  */
9
9
 
10
10
  class FunctionCodeBuilder {
@@ -634,6 +634,7 @@ const DEFAULT_STYLE = {
634
634
  fontSize: 10,
635
635
  fillColor: "",
636
636
  textColor: "",
637
+ rotation: 0,
637
638
  };
638
639
  const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign;
639
640
  // Fonts
@@ -679,7 +680,7 @@ const PIVOT_TABLE_CONFIG = {
679
680
  };
680
681
  const PIVOT_INDENT = 15;
681
682
  const PIVOT_COLLAPSE_ICON_SIZE = 12;
682
- const PIVOT_MAX_NUMBER_OF_CELLS = 1e5;
683
+ const PIVOT_MAX_NUMBER_OF_CELLS = 5e5;
683
684
 
684
685
  //------------------------------------------------------------------------------
685
686
  // Miscellaneous
@@ -3105,8 +3106,56 @@ function isMultipleElementMatrix(arg) {
3105
3106
  return isMatrix(arg) && !isSingleElementMatrix(arg);
3106
3107
  }
3107
3108
 
3109
+ function stackHorizontally(ranges, options) {
3110
+ const matrices = ranges.map(toMatrix);
3111
+ const nbRowsArr = matrices.map((m) => m?.[0]?.length ?? 0);
3112
+ const nbRows = Math.max(...nbRowsArr);
3113
+ if (options?.requireSameRowCount) {
3114
+ const firstLength = nbRowsArr[0];
3115
+ if (nbRowsArr.some((len) => len !== firstLength)) {
3116
+ return new EvaluationError(_t("All ranges in [[FUNCTION_NAME]] must have the same number of columns (got %s).", nbRowsArr.join(", ")));
3117
+ }
3118
+ }
3119
+ const result = [];
3120
+ for (const matrix of matrices) {
3121
+ for (let col = 0; col < matrix.length; col++) {
3122
+ // Fill with nulls if needed
3123
+ const array = Array(nbRows).fill({ value: null });
3124
+ for (let row = 0; row < matrix[col].length; row++) {
3125
+ array[row] = matrix[col][row];
3126
+ }
3127
+ result.push(array);
3128
+ }
3129
+ }
3130
+ return result;
3131
+ }
3132
+ function stackVertically(ranges, options) {
3133
+ const matrices = ranges.map(toMatrix);
3134
+ const nbColsArr = matrices.map((m) => m?.length ?? 0);
3135
+ const nbCols = Math.max(...nbColsArr);
3136
+ if (options?.requireSameColCount) {
3137
+ const firstLength = nbColsArr[0];
3138
+ if (nbColsArr.some((len) => len !== firstLength)) {
3139
+ return new EvaluationError(_t("All ranges in [[FUNCTION_NAME]] must have the same number of columns (got %s).", nbColsArr.join(", ")));
3140
+ }
3141
+ }
3142
+ const nbRows = matrices.reduce((acc, m) => acc + (m?.[0]?.length ?? 0), 0);
3143
+ const result = generateMatrix(nbCols, nbRows, () => ({
3144
+ value: null,
3145
+ }));
3146
+ let currentRow = 0;
3147
+ for (const matrix of matrices) {
3148
+ for (let col = 0; col < matrix.length; col++) {
3149
+ for (let row = 0; row < matrix[col].length; row++) {
3150
+ result[col][currentRow + row] = matrix[col][row];
3151
+ }
3152
+ }
3153
+ currentRow += matrix[0]?.length ?? 0;
3154
+ }
3155
+ return result;
3156
+ }
3108
3157
  // -----------------------------------------------------------------------------
3109
- // ARRAY_CONSTRAIN
3158
+ // ARRAY.CONSTRAIN
3110
3159
  // -----------------------------------------------------------------------------
3111
3160
  const ARRAY_CONSTRAIN = {
3112
3161
  description: _t("Returns a result array constrained to a specific width and height."),
@@ -3132,6 +3181,30 @@ const ARRAY_CONSTRAIN = {
3132
3181
  isExported: false,
3133
3182
  };
3134
3183
  // -----------------------------------------------------------------------------
3184
+ // ARRAY.LITERAL
3185
+ // -----------------------------------------------------------------------------
3186
+ const ARRAY_LITERAL = {
3187
+ description: _t("Appends ranges vertically and in sequence to return a larger array. All ranges must have the same number of columns."),
3188
+ args: [arg("range (any, range<any>, repeating)", _t("The range to be appended."))],
3189
+ compute: function (...ranges) {
3190
+ return stackVertically(ranges, { requireSameColCount: true });
3191
+ },
3192
+ isExported: false,
3193
+ hidden: true,
3194
+ };
3195
+ // -----------------------------------------------------------------------------
3196
+ // ARRAY.ROW
3197
+ // -----------------------------------------------------------------------------
3198
+ const ARRAY_ROW = {
3199
+ description: _t("Appends ranges horizontally and in sequence to return a larger array. All ranges must have the same number of rows."),
3200
+ args: [arg("range (any, range<any>, repeating)", _t("The range to be appended."))],
3201
+ compute: function (...ranges) {
3202
+ return stackHorizontally(ranges, { requireSameRowCount: true });
3203
+ },
3204
+ isExported: false,
3205
+ hidden: true,
3206
+ };
3207
+ // -----------------------------------------------------------------------------
3135
3208
  // CHOOSECOLS
3136
3209
  // -----------------------------------------------------------------------------
3137
3210
  const CHOOSECOLS = {
@@ -3277,20 +3350,7 @@ const HSTACK = {
3277
3350
  description: _t("Appends ranges horizontally and in sequence to return a larger array."),
3278
3351
  args: [arg("range (any, range<any>, repeating)", _t("The range to be appended."))],
3279
3352
  compute: function (...ranges) {
3280
- const nbRows = Math.max(...ranges.map((r) => r?.[0]?.length ?? 0));
3281
- const result = [];
3282
- for (const range of ranges) {
3283
- const _range = toMatrix(range);
3284
- for (let col = 0; col < _range.length; col++) {
3285
- //TODO: fill with #N/A for unavailable values instead of zeroes
3286
- const array = Array(nbRows).fill({ value: null });
3287
- for (let row = 0; row < _range[col].length; row++) {
3288
- array[row] = _range[col][row];
3289
- }
3290
- result.push(array);
3291
- }
3292
- }
3293
- return result;
3353
+ return stackHorizontally(ranges);
3294
3354
  },
3295
3355
  isExported: true,
3296
3356
  };
@@ -3549,22 +3609,7 @@ const VSTACK = {
3549
3609
  description: _t("Appends ranges vertically and in sequence to return a larger array."),
3550
3610
  args: [arg("range (any, range<any>, repeating)", _t("The range to be appended."))],
3551
3611
  compute: function (...ranges) {
3552
- const nbColumns = Math.max(...ranges.map((range) => toMatrix(range).length));
3553
- const nbRows = ranges.reduce((acc, range) => acc + toMatrix(range)[0].length, 0);
3554
- const result = Array(nbColumns)
3555
- .fill([])
3556
- .map(() => Array(nbRows).fill({ value: 0 })); // TODO fill with #N/A
3557
- let currentRow = 0;
3558
- for (const range of ranges) {
3559
- const _array = toMatrix(range);
3560
- for (let col = 0; col < _array.length; col++) {
3561
- for (let row = 0; row < _array[col].length; row++) {
3562
- result[col][currentRow + row] = _array[col][row];
3563
- }
3564
- }
3565
- currentRow += _array[0].length;
3566
- }
3567
- return result;
3612
+ return stackVertically(ranges);
3568
3613
  },
3569
3614
  isExported: true,
3570
3615
  };
@@ -3624,6 +3669,8 @@ const WRAPROWS = {
3624
3669
  var array = /*#__PURE__*/Object.freeze({
3625
3670
  __proto__: null,
3626
3671
  ARRAY_CONSTRAIN: ARRAY_CONSTRAIN,
3672
+ ARRAY_LITERAL: ARRAY_LITERAL,
3673
+ ARRAY_ROW: ARRAY_ROW,
3627
3674
  CHOOSECOLS: CHOOSECOLS,
3628
3675
  CHOOSEROWS: CHOOSEROWS,
3629
3676
  EXPAND: EXPAND,
@@ -3645,6 +3692,30 @@ var array = /*#__PURE__*/Object.freeze({
3645
3692
  WRAPROWS: WRAPROWS
3646
3693
  });
3647
3694
 
3695
+ const DEFAULT_LOCALES = [
3696
+ {
3697
+ name: "English (US)",
3698
+ code: "en_US",
3699
+ thousandsSeparator: ",",
3700
+ decimalSeparator: ".",
3701
+ weekStart: 7, // Sunday
3702
+ dateFormat: "m/d/yyyy",
3703
+ timeFormat: "hh:mm:ss a",
3704
+ formulaArgSeparator: ",",
3705
+ },
3706
+ {
3707
+ name: "French",
3708
+ code: "fr_FR",
3709
+ thousandsSeparator: " ",
3710
+ decimalSeparator: ",",
3711
+ weekStart: 1, // Monday
3712
+ dateFormat: "dd/mm/yyyy",
3713
+ timeFormat: "hh:mm:ss",
3714
+ formulaArgSeparator: ";",
3715
+ },
3716
+ ];
3717
+ const DEFAULT_LOCALE = DEFAULT_LOCALES[0];
3718
+
3648
3719
  function tokenizeFormat(str) {
3649
3720
  const chars = new TokenizingChars(str);
3650
3721
  const result = [];
@@ -3929,7 +4000,7 @@ function parseNumberFormatTokens(tokens) {
3929
4000
  }
3930
4001
  function parseDateFormatTokens(tokens) {
3931
4002
  const internalFormat = tokens && areValidDateFormatTokens(tokens) ? { type: "date", tokens } : undefined;
3932
- if (!internalFormat) {
4003
+ if (!internalFormat || !tokens?.some((token) => token.type === "DATE_PART")) {
3933
4004
  return undefined;
3934
4005
  }
3935
4006
  if (internalFormat.tokens.length &&
@@ -4166,6 +4237,9 @@ function repeatCharToFitWidth(formattedValue, formatWidth) {
4166
4237
  return prefix + repeatedChar.repeat(timesToRepeat) + padding + suffix;
4167
4238
  }
4168
4239
  function applyInternalNumberFormat(value, format, locale) {
4240
+ if (!format.integerPart.length && !format.decimalPart?.length) {
4241
+ return "";
4242
+ }
4169
4243
  if (value === Infinity) {
4170
4244
  return "∞" + (format.percentSymbols ? "%" : "");
4171
4245
  }
@@ -4772,7 +4846,7 @@ function getTokenNextReferenceType(xc) {
4772
4846
  function setXcToFixedReferenceType(xc, referenceType) {
4773
4847
  let sheetName;
4774
4848
  ({ sheetName, xc } = splitReference(xc));
4775
- sheetName = sheetName ? sheetName + "!" : "";
4849
+ sheetName = sheetName ? getCanonicalSymbolName(sheetName) + "!" : "";
4776
4850
  xc = xc.replace(/\$/g, "");
4777
4851
  const splitIndex = xc.indexOf(":");
4778
4852
  if (splitIndex >= 0) {
@@ -6052,6 +6126,47 @@ function splitIfAdjacent(zone, zoneToRemove) {
6052
6126
  return newZones;
6053
6127
  }
6054
6128
  }
6129
+ /**
6130
+ * Splits zone z2 by removing the overlapping zone z1 (fully inside z2).
6131
+ * Returns the remaining parts of z2 that don't overlap with z1.
6132
+ *
6133
+ * Diagram:
6134
+ * ┌──────────── z2 ─────────────┐
6135
+ * │ 1 │
6136
+ * │--------─────────────--------│
6137
+ * │ 2 | z1 | 3 │
6138
+ * │--------─────────────--------│
6139
+ * │ 4 │
6140
+ * └─────────────────────────────┘
6141
+ *
6142
+ * Input:
6143
+ * z1 = { top: 2, bottom: 3, left: 2, right: 3 }
6144
+ * z2 = { top: 1, bottom: 4, left: 1, right: 4 }
6145
+ *
6146
+ * Output:
6147
+ * [
6148
+ * { top: 4, bottom: 4, left: 1, right: 4 }, // bottom
6149
+ * { top: 2, bottom: 3, left: 4, right: 4 }, // right
6150
+ * { top: 2, bottom: 3, left: 1, right: 1 }, // left
6151
+ * { top: 1, bottom: 1, left: 1, right: 4 } // top
6152
+ * ]
6153
+ */
6154
+ function splitZone(z1, z2) {
6155
+ const zones = [];
6156
+ if (z1.bottom < z2.bottom) {
6157
+ zones.push({ ...z2, top: z1.bottom + 1 });
6158
+ }
6159
+ if (z1.right < z2.right) {
6160
+ zones.push({ ...z2, left: z1.right + 1, top: z1.top, bottom: z1.bottom });
6161
+ }
6162
+ if (z1.left > z2.left) {
6163
+ zones.push({ ...z2, right: z1.left - 1, top: z1.top, bottom: z1.bottom });
6164
+ }
6165
+ if (z1.top > z2.top) {
6166
+ zones.push({ ...z2, bottom: z1.top - 1 });
6167
+ }
6168
+ return zones;
6169
+ }
6055
6170
 
6056
6171
  function isSheetDependent(cmd) {
6057
6172
  return "sheetId" in cmd;
@@ -6082,7 +6197,6 @@ const invalidateEvaluationCommands = new Set([
6082
6197
  "REDO",
6083
6198
  "ADD_MERGE",
6084
6199
  "REMOVE_MERGE",
6085
- "DUPLICATE_SHEET",
6086
6200
  "UPDATE_LOCALE",
6087
6201
  "ADD_PIVOT",
6088
6202
  "UPDATE_PIVOT",
@@ -7649,7 +7763,7 @@ const SUBTOTAL = {
7649
7763
  if (!acceptHiddenCells && this.getters.isRowHiddenByUser(sheetId, row))
7650
7764
  continue;
7651
7765
  for (let col = left; col <= right; col++) {
7652
- const cell = this.getters.getCell({ sheetId, col, row });
7766
+ const cell = this.getters.getCorrespondingFormulaCell({ sheetId, col, row });
7653
7767
  if (!cell || !isSubtotalCell(cell)) {
7654
7768
  evaluatedCellToKeep.push(this.getters.getEvaluatedCell({ sheetId, col, row }));
7655
7769
  }
@@ -9544,30 +9658,6 @@ var database = /*#__PURE__*/Object.freeze({
9544
9658
  DVARP: DVARP
9545
9659
  });
9546
9660
 
9547
- const DEFAULT_LOCALES = [
9548
- {
9549
- name: "English (US)",
9550
- code: "en_US",
9551
- thousandsSeparator: ",",
9552
- decimalSeparator: ".",
9553
- weekStart: 7, // Sunday
9554
- dateFormat: "m/d/yyyy",
9555
- timeFormat: "hh:mm:ss a",
9556
- formulaArgSeparator: ",",
9557
- },
9558
- {
9559
- name: "French",
9560
- code: "fr_FR",
9561
- thousandsSeparator: " ",
9562
- decimalSeparator: ",",
9563
- weekStart: 1, // Monday
9564
- dateFormat: "dd/mm/yyyy",
9565
- timeFormat: "hh:mm:ss",
9566
- formulaArgSeparator: ";",
9567
- },
9568
- ];
9569
- const DEFAULT_LOCALE = DEFAULT_LOCALES[0];
9570
-
9571
9661
  /**
9572
9662
  * Tokenizer
9573
9663
  *
@@ -9596,7 +9686,9 @@ function tokenize(str, locale = DEFAULT_LOCALE) {
9596
9686
  while (!chars.isOver()) {
9597
9687
  let token = tokenizeNewLine(chars) ||
9598
9688
  tokenizeSpace(chars) ||
9689
+ tokenizeArrayRowSeparator(chars, locale) ||
9599
9690
  tokenizeArgsSeparator(chars, locale) ||
9691
+ tokenizeBraces(chars) ||
9600
9692
  tokenizeParenthesis(chars) ||
9601
9693
  tokenizeOperator(chars) ||
9602
9694
  tokenizeString(chars) ||
@@ -9629,6 +9721,17 @@ function tokenizeParenthesis(chars) {
9629
9721
  }
9630
9722
  return null;
9631
9723
  }
9724
+ const braces = {
9725
+ "{": { type: "LEFT_BRACE", value: "{" },
9726
+ "}": { type: "RIGHT_BRACE", value: "}" },
9727
+ };
9728
+ function tokenizeBraces(chars) {
9729
+ if (chars.current === "{" || chars.current === "}") {
9730
+ const value = chars.shift();
9731
+ return braces[value];
9732
+ }
9733
+ return null;
9734
+ }
9632
9735
  function tokenizeArgsSeparator(chars, locale) {
9633
9736
  if (chars.current === locale.formulaArgSeparator) {
9634
9737
  const value = chars.shift();
@@ -9637,6 +9740,20 @@ function tokenizeArgsSeparator(chars, locale) {
9637
9740
  }
9638
9741
  return null;
9639
9742
  }
9743
+ function tokenizeArrayRowSeparator(chars, locale) {
9744
+ // The array row separator is used in array literals to separate rows.
9745
+ // It is not explicitly defined in locales, but depends on the formulaArgSeparator.
9746
+ // Example: {1,2,3;4,5,6} — here, ';' separates rows and ',' separates columns.
9747
+ const rowSeparator = locale.formulaArgSeparator === ";" ? "\\" : ";";
9748
+ if (!rowSeparator) {
9749
+ return null;
9750
+ }
9751
+ if (chars.current === rowSeparator) {
9752
+ chars.shift();
9753
+ return { type: "ARRAY_ROW_SEPARATOR", value: rowSeparator };
9754
+ }
9755
+ return null;
9756
+ }
9640
9757
  function tokenizeOperator(chars) {
9641
9758
  for (const op of OPERATORS) {
9642
9759
  if (chars.currentStartsWith(op)) {
@@ -9857,6 +9974,9 @@ function _localizeFormula(formula, fromLocale, toLocale) {
9857
9974
  else if (token.type === "ARG_SEPARATOR") {
9858
9975
  localizedFormula += toLocale.formulaArgSeparator;
9859
9976
  }
9977
+ else if (token.type === "ARRAY_ROW_SEPARATOR") {
9978
+ localizedFormula += toLocale.formulaArgSeparator === ";" ? "\\" : ";";
9979
+ }
9860
9980
  else {
9861
9981
  localizedFormula += token.value;
9862
9982
  }
@@ -11020,7 +11140,6 @@ function sortMatrix(matrix, locale, ...criteria) {
11020
11140
  // -----------------------------------------------------------------------------
11021
11141
  const FILTER = {
11022
11142
  description: _t("Returns a filtered version of the source range, returning only rows or columns that meet the specified conditions."),
11023
- // TODO modify args description when vectorization on formulas is available
11024
11143
  args: [
11025
11144
  arg("range (any, range<any>)", _t("The data to be filtered.")),
11026
11145
  arg("condition (boolean, range<boolean>, repeating)", _t("Column or row containing true or false values corresponding to the range.")),
@@ -13972,18 +14091,17 @@ var logical = /*#__PURE__*/Object.freeze({
13972
14091
  ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
13973
14092
  ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
13974
14093
  },
13975
- GeoChart: {
13976
- ColorScales: {
13977
- blues: _t("Blues"),
13978
- cividis: _t("Cividis"),
13979
- greens: _t("Greens"),
13980
- greys: _t("Greys"),
13981
- oranges: _t("Oranges"),
13982
- purples: _t("Purples"),
13983
- rainbow: _t("Rainbow"),
13984
- reds: _t("Reds"),
13985
- viridis: _t("Viridis"),
13986
- },
14094
+ ColorScales: {
14095
+ blues: _t("Blues"),
14096
+ cividis: _t("Cividis"),
14097
+ custom: _t("Custom"),
14098
+ greens: _t("Greens"),
14099
+ greys: _t("Greys"),
14100
+ oranges: _t("Oranges"),
14101
+ purples: _t("Purples"),
14102
+ rainbow: _t("Rainbow"),
14103
+ reds: _t("Reds"),
14104
+ viridis: _t("Viridis"),
13987
14105
  },
13988
14106
  });
13989
14107
  ({
@@ -17221,6 +17339,8 @@ function parseOperand(tokens) {
17221
17339
  tokenStartIndex: current.tokenIndex,
17222
17340
  tokenEndIndex: rightParen.tokenIndex,
17223
17341
  };
17342
+ case "LEFT_BRACE":
17343
+ return parseArrayLiteral(tokens, current);
17224
17344
  case "OPERATOR":
17225
17345
  const operator = current.value;
17226
17346
  if (UNARY_OPERATORS_PREFIX.includes(operator)) {
@@ -17274,6 +17394,34 @@ function consumeOrThrow(tokens, type, message) {
17274
17394
  }
17275
17395
  return token;
17276
17396
  }
17397
+ function parseArrayLiteral(tokens, leftBrace) {
17398
+ const rows = [];
17399
+ let currentRow = [parseExpression(tokens)]; // there must be at least one element
17400
+ while (tokens.current?.type !== "RIGHT_BRACE") {
17401
+ const nextToken = tokens.shift();
17402
+ if (!nextToken) {
17403
+ throw new BadExpressionError(_t("Missing closing brace"));
17404
+ }
17405
+ else if (nextToken.type === "ARG_SEPARATOR") {
17406
+ currentRow.push(parseExpression(tokens));
17407
+ }
17408
+ else if (nextToken.type === "ARRAY_ROW_SEPARATOR") {
17409
+ rows.push(currentRow);
17410
+ currentRow = [parseExpression(tokens)];
17411
+ }
17412
+ else {
17413
+ throw new BadExpressionError(_t("Unexpected token: %s", nextToken.value));
17414
+ }
17415
+ }
17416
+ const rightBrace = consumeOrThrow(tokens, "RIGHT_BRACE", _t("Missing closing brace"));
17417
+ rows.push(currentRow);
17418
+ return {
17419
+ type: "ARRAY",
17420
+ value: rows,
17421
+ tokenStartIndex: leftBrace.tokenIndex,
17422
+ tokenEndIndex: rightBrace.tokenIndex,
17423
+ };
17424
+ }
17277
17425
  function parseExpression(tokens, parent_priority = 0) {
17278
17426
  if (tokens.length === 0) {
17279
17427
  throw new BadExpressionError();
@@ -17364,6 +17512,13 @@ function* astIterator(ast) {
17364
17512
  yield* astIterator(arg);
17365
17513
  }
17366
17514
  break;
17515
+ case "ARRAY":
17516
+ for (const row of ast.value) {
17517
+ for (const cell of row) {
17518
+ yield* astIterator(cell);
17519
+ }
17520
+ }
17521
+ break;
17367
17522
  case "UNARY_OPERATION":
17368
17523
  yield* astIterator(ast.operand);
17369
17524
  break;
@@ -17381,6 +17536,11 @@ function mapAst(ast, fn) {
17381
17536
  ...ast,
17382
17537
  args: ast.args.map((child) => mapAst(child, fn)),
17383
17538
  };
17539
+ case "ARRAY":
17540
+ return {
17541
+ ...ast,
17542
+ value: ast.value.map((row) => row.map((cell) => mapAst(cell, fn))),
17543
+ };
17384
17544
  case "UNARY_OPERATION":
17385
17545
  return {
17386
17546
  ...ast,
@@ -17539,6 +17699,23 @@ function compileTokensOrThrow(tokens) {
17539
17699
  code.append(...args);
17540
17700
  const fnName = ast.value.toUpperCase();
17541
17701
  return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`);
17702
+ case "ARRAY": {
17703
+ // a literal array is compiled into function calls
17704
+ const arrayFunctionCall = {
17705
+ type: "FUNCALL",
17706
+ value: "ARRAY.LITERAL",
17707
+ args: ast.value.map((row) => ({
17708
+ type: "FUNCALL",
17709
+ value: "ARRAY.ROW",
17710
+ args: row,
17711
+ tokenStartIndex: 0,
17712
+ tokenEndIndex: 0,
17713
+ })),
17714
+ tokenStartIndex: 0,
17715
+ tokenEndIndex: 0,
17716
+ };
17717
+ return compileAST(arrayFunctionCall);
17718
+ }
17542
17719
  case "UNARY_OPERATION": {
17543
17720
  const fnName = UNARY_OPERATOR_MAP[ast.value];
17544
17721
  const operand = compileAST(ast.operand, false, false).assignResultToVariable();
@@ -18818,6 +18995,17 @@ class AlternatingColorMap {
18818
18995
  return this.colors[id];
18819
18996
  }
18820
18997
  }
18998
+ const COLORSCHEMES = {
18999
+ greys: ["#ffffff", "#808080", "#000000"],
19000
+ blues: ["#f7fbff", "#6aaed6", "#08306b"],
19001
+ reds: ["#fff5f0", "#fb694a", "#67000d"],
19002
+ greens: ["#f7fcf5", "#73c476", "#00441b"],
19003
+ oranges: ["#fff5eb", "#fd8c3b", "#7f2704"],
19004
+ purples: ["#fcfbfd", "#9e9ac8", "#3f007d"],
19005
+ viridis: ["#440154", "#21918c", "#fde725"],
19006
+ cividis: ["#00224e", "#7d7c78", "#fee838"],
19007
+ rainbow: ["#B41DB4", "#FFFF00", "#00FFFF"],
19008
+ };
18821
19009
  /**
18822
19010
  * Returns a function that maps a value to a color using a color scale defined by the given
18823
19011
  * color/threshold values pairs.
@@ -25870,6 +26058,17 @@ class XlsxReader {
25870
26058
  }
25871
26059
  }
25872
26060
 
26061
+ function schemeToColorScale(scheme) {
26062
+ const colors = COLORSCHEMES[scheme];
26063
+ return colors === undefined
26064
+ ? undefined
26065
+ : {
26066
+ minColor: colors[0],
26067
+ midColor: colors.length === 3 ? colors[1] : undefined,
26068
+ maxColor: colors[colors.length - 1],
26069
+ };
26070
+ }
26071
+
25873
26072
  /**
25874
26073
  * parses a formula (as a string) into the same formula,
25875
26074
  * but with the references to other cells extracted
@@ -26513,6 +26712,29 @@ migrationStepRegistry
26513
26712
  }
26514
26713
  return data;
26515
26714
  },
26715
+ })
26716
+ .add("19.1.0", {
26717
+ migrate(data) {
26718
+ for (const sheet of data.sheets || []) {
26719
+ for (const figure of sheet.figures || []) {
26720
+ if (figure.tag === "chart" && figure.data.type === "geo") {
26721
+ if ("colorScale" in figure.data && typeof figure.data.colorScale === "string") {
26722
+ figure.data.colorScale = schemeToColorScale(figure.data.colorScale);
26723
+ }
26724
+ }
26725
+ if (figure.tag === "carousel") {
26726
+ for (const definition of Object.values(figure.data.chartDefinitions) || []) {
26727
+ if (definition.type === "geo") {
26728
+ if ("colorScale" in definition && typeof definition.colorScale === "string") {
26729
+ definition.colorScale = schemeToColorScale(definition.colorScale);
26730
+ }
26731
+ }
26732
+ }
26733
+ }
26734
+ }
26735
+ }
26736
+ return data;
26737
+ },
26516
26738
  });
26517
26739
  function fixOverlappingFilters(data) {
26518
26740
  for (const sheet of data.sheets || []) {
@@ -26769,18 +26991,22 @@ function dropCommands(initialMessages, commandType) {
26769
26991
  return messages;
26770
26992
  }
26771
26993
  function fixChartDefinitions(data, initialMessages) {
26994
+ /**
26995
+ * Revisions created after version 18.5.1 contain the full chart definition in the command
26996
+ * if the data was alreay updated to 18.5.1, then those older revision cannot (by definition) be reaplied
26997
+ * and should not be replayed.
26998
+ * FIXME: every command should be versionned when upgraded to allow finer tuning.
26999
+ */
27000
+ if (!data.version || compareVersions(String(data.version), "18.5.1") >= 0) {
27001
+ return initialMessages;
27002
+ }
26772
27003
  const messages = [];
26773
27004
  const map = {};
26774
27005
  for (const sheet of data.sheets || []) {
26775
27006
  sheet.figures?.forEach((figure) => {
26776
27007
  if (figure.tag === "chart") {
26777
27008
  // chart definition
26778
- if (data.version && compareVersions(String(data.version), "18.5.1") <= 0) {
26779
- map[figure.data.chartId] = figure.data;
26780
- }
26781
- else {
26782
- map[figure.id] = figure.data;
26783
- }
27009
+ map[figure.id] = figure.data;
26784
27010
  }
26785
27011
  });
26786
27012
  }
@@ -27313,22 +27539,34 @@ class BordersPlugin extends CorePlugin {
27313
27539
  addBorder(sheetId, zone, newBorder, force = false) {
27314
27540
  const borders = [];
27315
27541
  const plannedBorder = newBorder ? { zone, style: newBorder } : undefined;
27316
- const sideToClear = {
27317
- left: force || !!newBorder?.left,
27318
- right: force || !!newBorder?.right,
27319
- top: force || !!newBorder?.top,
27320
- bottom: force || !!newBorder?.bottom,
27542
+ // For each side, decide if we must clear the border on the *adjacent*
27543
+ // existing cell when we draw on the opposite side of the new zone.
27544
+ //
27545
+ // Example:
27546
+ // - newBorder.right is set → we draw border on the RIGHT side of `zone`
27547
+ // - the cell on the right may already have a LEFT border on that edge
27548
+ // In that case we clear that LEFT border, so only the new RIGHT border
27549
+ // remains on the shared edge.
27550
+ //
27551
+ // existingBorderSideToClear[side] = true means we should clear the border on that
27552
+ // side of the existing adjacent zone before adding the new border.
27553
+ const existingBorderSideToClear = {
27554
+ left: force || !!newBorder?.right,
27555
+ right: force || !!newBorder?.left,
27556
+ top: force || !!newBorder?.bottom,
27557
+ bottom: force || !!newBorder?.top,
27321
27558
  };
27322
27559
  let editingZone = [zone];
27323
27560
  for (const existingBorder of this.borders[sheetId] ?? []) {
27324
27561
  const inter = intersection(existingBorder.zone, zone);
27325
27562
  if (!inter) {
27326
- // Clear adjacent borders on which you write
27563
+ // Check if the existing border is adjacent to the new zone
27327
27564
  const adjacentEdge = adjacent(existingBorder.zone, zone);
27328
- if (adjacentEdge && sideToClear[adjacentEdge.position]) {
27565
+ if (adjacentEdge && existingBorderSideToClear[adjacentEdge.position]) {
27329
27566
  for (const newZone of splitIfAdjacent(existingBorder.zone, zone)) {
27330
27567
  const border = this.computeBorderFromZone(newZone, existingBorder);
27331
27568
  const adjacentEdge = adjacent(newZone, zone);
27569
+ // Clear the existing border on the side that touches the new zone
27332
27570
  switch (adjacentEdge?.position) {
27333
27571
  case "left":
27334
27572
  border.style.left = undefined;
@@ -29808,7 +30046,8 @@ class FigurePlugin extends CorePlugin {
29808
30046
  }
29809
30047
  break;
29810
30048
  case "DUPLICATE_SHEET": {
29811
- for (const figureId in this.figures[cmd.sheetId]) {
30049
+ for (const figure of this.getFigures(cmd.sheetId)) {
30050
+ const figureId = figure.id;
29812
30051
  const fig = this.figures[cmd.sheetId]?.[figureId];
29813
30052
  if (!fig) {
29814
30053
  continue;
@@ -32141,10 +32380,17 @@ class PivotCorePlugin extends CorePlugin {
32141
32380
  if (!pivot) {
32142
32381
  continue;
32143
32382
  }
32144
- for (const measure of pivot.definition.measures) {
32383
+ const def = deepCopy(pivot.definition);
32384
+ for (const measure of def.measures) {
32145
32385
  if (measure.computedBy?.formula === formulaString) {
32146
- const measureIndex = pivot.definition.measures.indexOf(measure);
32147
- this.history.update("pivots", pivotId, "definition", "measures", measureIndex, "computedBy", { formula: newFormulaString, sheetId });
32386
+ const measureIndex = def.measures.indexOf(measure);
32387
+ if (measureIndex !== -1) {
32388
+ def.measures[measureIndex].computedBy = {
32389
+ formula: newFormulaString,
32390
+ sheetId,
32391
+ };
32392
+ }
32393
+ this.dispatch("UPDATE_PIVOT", { pivotId, pivot: def });
32148
32394
  }
32149
32395
  }
32150
32396
  }
@@ -33176,6 +33422,9 @@ class SpreadsheetPivotCorePlugin extends CorePlugin {
33176
33422
  const { sheetId, zone } = definition.dataSet;
33177
33423
  const range = this.getters.getRangeFromZone(sheetId, zone);
33178
33424
  const adaptedRange = adaptPivotRange(range, applyChange);
33425
+ if (adaptedRange === range) {
33426
+ return;
33427
+ }
33179
33428
  const dataSet = adaptedRange && {
33180
33429
  sheetId: adaptedRange.sheetId,
33181
33430
  zone: adaptedRange.zone,
@@ -33215,6 +33464,13 @@ class StylePlugin extends CorePlugin {
33215
33464
  "getStyleColors",
33216
33465
  ];
33217
33466
  styles = {};
33467
+ allowDispatch(cmd) {
33468
+ switch (cmd.type) {
33469
+ case "SET_FORMATTING":
33470
+ return this.checkUselessSetFormatting(cmd);
33471
+ }
33472
+ return "Success" /* CommandResult.Success */;
33473
+ }
33218
33474
  handle(cmd) {
33219
33475
  switch (cmd.type) {
33220
33476
  case "ADD_MERGE":
@@ -33439,6 +33695,28 @@ class StylePlugin extends CorePlugin {
33439
33695
  exportForExcel(data) {
33440
33696
  this.export(data);
33441
33697
  }
33698
+ checkUselessSetFormatting(cmd) {
33699
+ const { sheetId, target } = cmd;
33700
+ const hasStyle = "style" in cmd;
33701
+ const hasFormat = "format" in cmd;
33702
+ if (!hasStyle && !hasFormat) {
33703
+ return "NoChanges" /* CommandResult.NoChanges */;
33704
+ }
33705
+ for (const zone of recomputeZones(target)) {
33706
+ for (let col = zone.left; col <= zone.right; col++) {
33707
+ for (let row = zone.top; row <= zone.bottom; row++) {
33708
+ const position = { sheetId, col, row };
33709
+ const cell = this.getters.getCell(position);
33710
+ const style = this.getCellStyle(position);
33711
+ if ((hasStyle && !deepEquals(style, cmd.style)) ||
33712
+ (hasFormat && cell?.format !== cmd.format)) {
33713
+ return "Success" /* CommandResult.Success */;
33714
+ }
33715
+ }
33716
+ }
33717
+ }
33718
+ return "NoChanges" /* CommandResult.NoChanges */;
33719
+ }
33442
33720
  }
33443
33721
 
33444
33722
  class TableStylePlugin extends CorePlugin {
@@ -36466,48 +36744,64 @@ function getDefaultCellHeight(ctx, cell, style, colSize) {
36466
36744
  }
36467
36745
  function getCellContentHeight(ctx, content, style, colSize) {
36468
36746
  const maxWidth = style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
36469
- const numberOfLines = splitTextToWidth(ctx, content, style, maxWidth).length;
36470
- const fontSize = computeTextFontSizeInPixels(style);
36471
- return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;
36747
+ const lines = splitTextToWidth(ctx, content, style, maxWidth);
36748
+ return computeMultilineTextSize(ctx, lines, style).height + 2 * PADDING_AUTORESIZE_VERTICAL;
36472
36749
  }
36473
36750
  function getDefaultContextFont(fontSize, bold = false, italic = false) {
36474
36751
  const italicStr = italic ? "italic" : "";
36475
36752
  const weight = bold ? "bold" : "";
36476
36753
  return `${italicStr} ${weight} ${fontSize}px ${DEFAULT_FONT}`;
36477
36754
  }
36478
- const textWidthCache = {};
36479
- function computeTextWidth(context, text, style, fontUnit = "pt") {
36755
+ function computeMultilineTextSize(context, textLines, style = {}, fontUnit = "pt") {
36756
+ if (!textLines.length)
36757
+ return { width: 0, height: 0 };
36480
36758
  const font = computeTextFont(style, fontUnit);
36481
- return computeCachedTextWidth(context, text, font);
36482
- }
36483
- function computeCachedTextWidth(context, text, font) {
36484
- if (!textWidthCache[font]) {
36485
- textWidthCache[font] = {};
36759
+ const sizes = textLines.map((line) => computeCachedTextDimension(context, line, font));
36760
+ const height = computeTextLinesHeight(sizes[0].height, textLines.length);
36761
+ const width = Math.max(...sizes.map((size) => size.width));
36762
+ if (!style.rotation) {
36763
+ return { height, width };
36486
36764
  }
36487
- if (textWidthCache[font][text] === undefined) {
36488
- const oldFont = context.font;
36489
- context.font = font;
36490
- textWidthCache[font][text] = context.measureText(text).width;
36491
- context.font = oldFont;
36765
+ const cos = Math.abs(Math.cos(style.rotation));
36766
+ const sin = Math.abs(Math.sin(style.rotation));
36767
+ return { width: width * cos + height * sin, height: sin * width + cos * height };
36768
+ }
36769
+ function computeTextWidth(context, text, style = {}, fontUnit = "pt") {
36770
+ const font = computeTextFont(style, fontUnit);
36771
+ return computeCachedTextWidth(context, text, font, style.rotation);
36772
+ }
36773
+ function computeCachedTextWidth(context, text, font, rotation) {
36774
+ const size = computeCachedTextDimension(context, text, font);
36775
+ if (!rotation) {
36776
+ return size.width;
36492
36777
  }
36493
- return textWidthCache[font][text];
36778
+ const cos = Math.abs(Math.cos(rotation));
36779
+ const sin = Math.abs(Math.sin(rotation));
36780
+ return size.width * cos + size.height * sin;
36494
36781
  }
36495
36782
  const textDimensionsCache = {};
36496
36783
  function computeTextDimension(context, text, style, fontUnit = "pt") {
36497
36784
  const font = computeTextFont(style, fontUnit);
36498
- context.save();
36499
- context.font = font;
36500
- const dimensions = computeCachedTextDimension(context, text);
36501
- context.restore();
36502
- return dimensions;
36503
- }
36504
- function computeCachedTextDimension(context, text) {
36505
- const font = context.font;
36785
+ const size = computeCachedTextDimension(context, text, font);
36786
+ if (!style.rotation) {
36787
+ return size;
36788
+ }
36789
+ const cos = Math.abs(Math.cos(style.rotation));
36790
+ const sin = Math.abs(Math.sin(style.rotation));
36791
+ return {
36792
+ width: size.width * cos + size.height * sin,
36793
+ height: size.height * cos + size.width * sin,
36794
+ };
36795
+ }
36796
+ function computeCachedTextDimension(context, text, font) {
36506
36797
  if (!textDimensionsCache[font]) {
36507
36798
  textDimensionsCache[font] = {};
36508
36799
  }
36509
36800
  if (textDimensionsCache[font][text] === undefined) {
36801
+ context.save();
36802
+ context.font = font;
36510
36803
  const measure = context.measureText(text);
36804
+ context.restore();
36511
36805
  const width = measure.width;
36512
36806
  const height = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
36513
36807
  textDimensionsCache[font][text] = { width, height };
@@ -38704,7 +38998,8 @@ class HeaderSizeUIPlugin extends CoreViewPlugin {
38704
38998
  }
38705
38999
  break;
38706
39000
  case "SET_FORMATTING":
38707
- if (cmd.style && ("fontSize" in cmd.style || "wrapping" in cmd.style)) {
39001
+ if (cmd.style &&
39002
+ ("fontSize" in cmd.style || "wrapping" in cmd.style || "rotation" in cmd.style)) {
38708
39003
  for (const zone of cmd.target) {
38709
39004
  // TODO FLDA use rangeSet
38710
39005
  this.updateRowSizeForZoneChange(cmd.sheetId, zone);
@@ -38861,6 +39156,10 @@ function astToFormula(ast) {
38861
39156
  ? `(${astToFormula(ast.operand)})`
38862
39157
  : astToFormula(ast.operand);
38863
39158
  return ast.value + rightOperand;
39159
+ case "ARRAY":
39160
+ return ("{" +
39161
+ ast.value.map((row) => row.map((cell) => astToFormula(cell)).join(",")).join(";") +
39162
+ "}");
38864
39163
  case "BIN_OPERATION":
38865
39164
  const leftOperation = leftOperandNeedsParenthesis(ast)
38866
39165
  ? `(${astToFormula(ast.left)})`
@@ -40425,7 +40724,6 @@ const dateGranularities = [
40425
40724
  pivotRegistry.add("SPREADSHEET", {
40426
40725
  ui: SpreadsheetPivot,
40427
40726
  definition: SpreadsheetPivotRuntimeDefinition,
40428
- externalData: false,
40429
40727
  dateGranularities: [...dateGranularities],
40430
40728
  datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
40431
40729
  isMeasureCandidate: (field) => field.type !== "boolean",
@@ -40467,9 +40765,7 @@ class PivotUIPlugin extends CoreViewPlugin {
40467
40765
  handle(cmd) {
40468
40766
  if (invalidateEvaluationCommands.has(cmd.type)) {
40469
40767
  for (const pivotId of this.getters.getPivotIds()) {
40470
- if (!pivotRegistry.get(this.getters.getPivotCoreDefinition(pivotId).type).externalData) {
40471
- this.setupPivot(pivotId, { recreate: true });
40472
- }
40768
+ this.setupPivot(pivotId, { recreate: true });
40473
40769
  }
40474
40770
  }
40475
40771
  switch (cmd.type) {
@@ -40683,7 +40979,7 @@ class PivotUIPlugin extends CoreViewPlugin {
40683
40979
  pivot.init({ reload: true });
40684
40980
  }
40685
40981
  setupPivot(pivotId, { recreate } = { recreate: false }) {
40686
- const definition = this.getters.getPivotCoreDefinition(pivotId);
40982
+ const definition = deepCopy(this.getters.getPivotCoreDefinition(pivotId));
40687
40983
  if (!(pivotId in this.pivots)) {
40688
40984
  const Pivot = withPivotPresentationLayer(pivotRegistry.get(definition.type).ui);
40689
40985
  this.pivots[pivotId] = new Pivot(this.custom, { definition, getters: this.getters });
@@ -43481,6 +43777,7 @@ class SheetUIPlugin extends UIPlugin {
43481
43777
  "getTextWidth",
43482
43778
  "getCellText",
43483
43779
  "getCellMultiLineText",
43780
+ "getMultilineTextSize",
43484
43781
  "getContiguousZone",
43485
43782
  "computeTextYCoordinate",
43486
43783
  ];
@@ -43531,7 +43828,7 @@ class SheetUIPlugin extends UIPlugin {
43531
43828
  const content = this.getters.getEvaluatedCell(position).formattedValue;
43532
43829
  if (content) {
43533
43830
  const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);
43534
- contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));
43831
+ contentWidth += computeMultilineTextSize(this.ctx, multiLineText, style).width;
43535
43832
  }
43536
43833
  for (const icon of this.getters.getCellIcons(position)) {
43537
43834
  contentWidth += icon.margin + icon.size;
@@ -43552,6 +43849,9 @@ class SheetUIPlugin extends UIPlugin {
43552
43849
  getTextWidth(text, style) {
43553
43850
  return computeTextWidth(this.ctx, text, style);
43554
43851
  }
43852
+ getMultilineTextSize(text, style) {
43853
+ return computeMultilineTextSize(this.ctx, text, style);
43854
+ }
43555
43855
  getCellText(position, args) {
43556
43856
  const cell = this.getters.getCell(position);
43557
43857
  const locale = this.getters.getLocale();
@@ -43750,6 +44050,15 @@ class CarouselUIPlugin extends UIPlugin {
43750
44050
  return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
43751
44051
  }
43752
44052
  return "Success" /* CommandResult.Success */;
44053
+ case "DUPLICATE_CAROUSEL_CHART":
44054
+ if (!this.getters.doesCarouselExist(cmd.carouselId) ||
44055
+ !this.getters
44056
+ .getCarousel(cmd.carouselId)
44057
+ .items.some((item) => item.type === "chart" && item.chartId === cmd.chartId) ||
44058
+ this.getters.getChart(cmd.duplicatedChartId)) {
44059
+ return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
44060
+ }
44061
+ return "Success" /* CommandResult.Success */;
43753
44062
  case "ADD_NEW_CHART_TO_CAROUSEL":
43754
44063
  if (!this.getters.doesCarouselExist(cmd.figureId)) {
43755
44064
  return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
@@ -43774,6 +44083,9 @@ class CarouselUIPlugin extends UIPlugin {
43774
44083
  case "ADD_FIGURE_CHART_TO_CAROUSEL":
43775
44084
  this.addFigureChartToCarousel(cmd.carouselFigureId, cmd.chartFigureId, cmd.sheetId);
43776
44085
  break;
44086
+ case "DUPLICATE_CAROUSEL_CHART":
44087
+ this.duplicateCarouselChart(cmd);
44088
+ break;
43777
44089
  case "UPDATE_CAROUSEL_ACTIVE_ITEM":
43778
44090
  this.carouselStates[cmd.figureId] = this.getCarouselItemId(cmd.item);
43779
44091
  break;
@@ -43912,6 +44224,29 @@ class CarouselUIPlugin extends UIPlugin {
43912
44224
  });
43913
44225
  this.dispatch("DELETE_FIGURE", { sheetId, figureId: chartFigureId });
43914
44226
  }
44227
+ duplicateCarouselChart({ carouselId, chartId, sheetId, duplicatedChartId, }) {
44228
+ const chart = this.getters.getChart(chartId);
44229
+ if (!chart) {
44230
+ return;
44231
+ }
44232
+ const carousel = this.getters.getCarousel(carouselId);
44233
+ const duplicatedItemIndex = carousel.items.findIndex((item) => item.type === "chart" && item.chartId === chartId);
44234
+ if (duplicatedItemIndex === -1) {
44235
+ return;
44236
+ }
44237
+ this.dispatch("CREATE_CHART", {
44238
+ chartId: duplicatedChartId,
44239
+ figureId: carouselId,
44240
+ sheetId,
44241
+ definition: chart.getDefinition(),
44242
+ });
44243
+ const carouselItems = insertItemsAtIndex(carousel.items, [{ type: "chart", chartId: duplicatedChartId }], duplicatedItemIndex + 1);
44244
+ this.dispatch("UPDATE_CAROUSEL", {
44245
+ sheetId,
44246
+ figureId: carouselId,
44247
+ definition: { ...carousel, items: carouselItems },
44248
+ });
44249
+ }
43915
44250
  getCarouselItemId(item) {
43916
44251
  return item.type === "chart" ? item.chartId : "carouselDataView";
43917
44252
  }
@@ -45127,25 +45462,43 @@ class GridSelectionPlugin extends UIPlugin {
45127
45462
  return "Success" /* CommandResult.Success */;
45128
45463
  }
45129
45464
  handleEvent(event) {
45130
- const anchor = event.anchor;
45131
- let zones = [];
45465
+ let anchor = event.anchor;
45466
+ let zones = [...this.gridSelection.zones];
45132
45467
  this.isUnbounded = event.options?.unbounded || false;
45133
45468
  switch (event.mode) {
45134
45469
  case "overrideSelection":
45135
45470
  zones = [anchor.zone];
45136
45471
  break;
45137
45472
  case "updateAnchor":
45138
- zones = [...this.gridSelection.zones];
45139
45473
  const index = zones.findIndex((z) => isEqual(z, event.previousAnchor.zone));
45140
45474
  if (index >= 0) {
45141
45475
  zones[index] = anchor.zone;
45142
45476
  }
45143
45477
  break;
45144
45478
  case "newAnchor":
45145
- zones = [...this.gridSelection.zones, anchor.zone];
45479
+ zones.push(anchor.zone);
45480
+ break;
45481
+ case "commitSelection":
45482
+ const zoneToSplit = zones.find((zone) => isZoneInside(anchor.zone, zone) && !isEqual(anchor.zone, zone));
45483
+ const removeFullAnchor = zones.filter((zone) => isEqual(anchor.zone, zone)).length > 1;
45484
+ if (removeFullAnchor && zones.length > 2) {
45485
+ zones = zones.filter((zone) => !isEqual(zone, anchor.zone));
45486
+ }
45487
+ else if (zoneToSplit) {
45488
+ const splittedZones = splitZone(anchor.zone, zoneToSplit);
45489
+ zones = zones
45490
+ .filter((z) => !isEqual(z, anchor.zone) && !isEqual(z, zoneToSplit))
45491
+ .concat(splittedZones);
45492
+ }
45493
+ zones = uniqueZones(zones);
45494
+ const lastZone = zones[zones.length - 1];
45495
+ anchor = {
45496
+ cell: { col: lastZone.left, row: lastZone.top },
45497
+ zone: lastZone,
45498
+ };
45146
45499
  break;
45147
45500
  }
45148
- this.setSelectionMixin(event.anchor, zones);
45501
+ this.setSelectionMixin(anchor, zones);
45149
45502
  /** Any change to the selection has to be reflected in the selection processor. */
45150
45503
  this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));
45151
45504
  const { col, row } = this.gridSelection.anchor.cell;
@@ -45425,7 +45778,7 @@ class GridSelectionPlugin extends UIPlugin {
45425
45778
  setSelectionMixin(anchor, zones) {
45426
45779
  const { anchor: clippedAnchor, zones: clippedZones } = this.clipSelection(this.getters.getActiveSheetId(), { anchor, zones });
45427
45780
  this.gridSelection.anchor = clippedAnchor;
45428
- this.gridSelection.zones = uniqueZones(clippedZones);
45781
+ this.gridSelection.zones = clippedZones;
45429
45782
  }
45430
45783
  /**
45431
45784
  * Change the anchor of the selection active cell to an absolute col and row index.
@@ -46135,6 +46488,7 @@ class SheetViewPlugin extends UIPlugin {
46135
46488
  "getGridOffset",
46136
46489
  "getViewportZoomLevel",
46137
46490
  "getScrollBarWidth",
46491
+ "getMaximumSheetOffset",
46138
46492
  ];
46139
46493
  viewports = {};
46140
46494
  /**
@@ -47240,6 +47594,9 @@ class EventStream {
47240
47594
  observe(owner, callbacks) {
47241
47595
  this.observers.set(owner, { owner, callbacks });
47242
47596
  }
47597
+ unobserve(owner) {
47598
+ this.observers.delete(owner);
47599
+ }
47243
47600
  /**
47244
47601
  * Capture the stream for yourself
47245
47602
  */
@@ -47332,6 +47689,9 @@ class SelectionStreamProcessorImpl {
47332
47689
  observe(owner, callbacks) {
47333
47690
  this.stream.observe(owner, callbacks);
47334
47691
  }
47692
+ unobserve(owner) {
47693
+ this.stream.unobserve(owner);
47694
+ }
47335
47695
  release(owner) {
47336
47696
  if (this.stream.isListening(owner)) {
47337
47697
  this.stream.release(owner);
@@ -47390,6 +47750,9 @@ class SelectionStreamProcessorImpl {
47390
47750
  bottom: Math.max(anchorRow, row),
47391
47751
  };
47392
47752
  const expandedZone = this.getters.expandZone(sheetId, zone);
47753
+ if (isEqual(this.anchor.zone, expandedZone)) {
47754
+ return new DispatchResult("NoChanges" /* CommandResult.NoChanges */);
47755
+ }
47393
47756
  const anchor = { zone: expandedZone, cell: { col: anchorCol, row: anchorRow } };
47394
47757
  return this.processEvent({
47395
47758
  mode: "updateAnchor",
@@ -47410,6 +47773,22 @@ class SelectionStreamProcessorImpl {
47410
47773
  mode: "newAnchor",
47411
47774
  });
47412
47775
  }
47776
+ /**
47777
+ * Triggered on mouse up to finalize the selection.
47778
+ * - If the current anchor zone already exists in the selection, it will be removed.
47779
+ * - If the anchor zone overlaps with existing zones, it will be split into
47780
+ * multiple non-overlapping parts.
47781
+ */
47782
+ commitSelection() {
47783
+ return this.processEvent({
47784
+ options: {
47785
+ scrollIntoView: false,
47786
+ unbounded: true,
47787
+ },
47788
+ anchor: this.anchor,
47789
+ mode: "commitSelection",
47790
+ });
47791
+ }
47413
47792
  /**
47414
47793
  * Increase or decrease the size of the current anchor zone.
47415
47794
  * The anchor cell remains where it is. It's the opposite side
@@ -50816,5 +51195,5 @@ export { BadExpressionError, BasePlugin, CellErrorType, CircularDependencyError,
50816
51195
 
50817
51196
 
50818
51197
  __info__.version = "19.1.0-alpha.3";
50819
- __info__.date = "2025-11-12T14:16:26.552Z";
50820
- __info__.hash = "6fefc9c";
51198
+ __info__.date = "2025-12-02T05:34:07.213Z";
51199
+ __info__.hash = "04cf666";