@odoo/o-spreadsheet 16.4.0-alpha.6 → 16.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -236,6 +236,7 @@
236
236
  const HEADER_FONT_SIZE = 11;
237
237
  const DEFAULT_FONT = "'Roboto', arial";
238
238
  const DEFAULT_VERTICAL_ALIGN = "bottom";
239
+ const DEFAULT_WRAPPING_MODE = "overflow";
239
240
  // Borders
240
241
  const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" };
241
242
  const DEFAULT_FILTER_BORDER_DESC = { style: "thin", color: FILTERS_COLOR };
@@ -393,82 +394,6 @@
393
394
  function clip(val, min, max) {
394
395
  return val < min ? min : val > max ? max : val;
395
396
  }
396
- function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
397
- return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN;
398
- }
399
- /**
400
- * Get the default height of the cell given its style.
401
- */
402
- function getDefaultCellHeight(cell) {
403
- if (!cell || !cell.content) {
404
- return DEFAULT_CELL_HEIGHT;
405
- }
406
- const fontSize = computeTextFontSizeInPixels(cell.style);
407
- // the number of lines should be computed from the formula result, but it's not evaluated at this point
408
- const numberOfLines = cell.isFormula ? 1 : cell.content.split(NEWLINE).length;
409
- return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;
410
- }
411
- function computeTextWidth(context, text, style) {
412
- const font = computeTextFont(style);
413
- if (!textWidthCache[font]) {
414
- textWidthCache[font] = {};
415
- }
416
- if (textWidthCache[font][text] === undefined) {
417
- context.save();
418
- context.font = font;
419
- const textWidth = context.measureText(text).width;
420
- context.restore();
421
- textWidthCache[font][text] = textWidth;
422
- }
423
- return textWidthCache[font][text];
424
- }
425
- const textWidthCache = {};
426
- function fontSizeInPixels(fontSize) {
427
- return Math.round((fontSize * 96) / 72);
428
- }
429
- function computeTextFont(style) {
430
- const italic = style.italic ? "italic " : "";
431
- const weight = style.bold ? "bold" : DEFAULT_FONT_WEIGHT;
432
- const size = computeTextFontSizeInPixels(style);
433
- return `${italic}${weight} ${size}px ${DEFAULT_FONT}`;
434
- }
435
- function computeTextFontSizeInPixels(style) {
436
- const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE;
437
- return fontSizeInPixels(sizeInPt);
438
- }
439
- /**
440
- * Return the font size that makes the width of a text match the given line width.
441
- * Minimum font size is 1.
442
- *
443
- * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.
444
- */
445
- function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) {
446
- let minFontSize = 1;
447
- if (getTextWidth(minFontSize) > lineWidth)
448
- return minFontSize;
449
- if (getTextWidth(maxFontSize) < lineWidth)
450
- return maxFontSize;
451
- // Dichotomic search
452
- let fontSize = (minFontSize + maxFontSize) / 2;
453
- let currentTextWidth = getTextWidth(fontSize);
454
- // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise
455
- let iterations = 0;
456
- while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {
457
- if (currentTextWidth >= lineWidth) {
458
- maxFontSize = (minFontSize + maxFontSize) / 2;
459
- }
460
- else {
461
- minFontSize = (minFontSize + maxFontSize) / 2;
462
- }
463
- fontSize = (minFontSize + maxFontSize) / 2;
464
- currentTextWidth = getTextWidth(fontSize);
465
- iterations++;
466
- }
467
- return fontSize;
468
- }
469
- function computeIconWidth(style) {
470
- return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;
471
- }
472
397
  /**
473
398
  * Create a range from start (included) to end (excluded).
474
399
  * range(10, 13) => [10, 11, 12]
@@ -723,10 +648,6 @@
723
648
  Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]);
724
649
  return cleanObject;
725
650
  }
726
- /** Transform a string to lower case. If the string is undefined, return an empty string */
727
- function toLowerCase(str) {
728
- return str ? str.toLowerCase() : "";
729
- }
730
651
  function transpose2dArray(matrix, callback = (val) => val) {
731
652
  if (!matrix.length)
732
653
  return [];
@@ -815,6 +736,14 @@
815
736
  return cache.get(args[0]);
816
737
  },
817
738
  }[funcName];
739
+ }
740
+ function removeIndexesFromArray(array, indexes) {
741
+ return array.filter((_, index) => !indexes.includes(index));
742
+ }
743
+ function insertItemsAtIndex(array, items, index) {
744
+ const newArray = [...array];
745
+ newArray.splice(index, 0, ...items);
746
+ return newArray;
818
747
  }
819
748
 
820
749
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
@@ -958,13 +887,13 @@
958
887
  let g = rgba.g.toString(16);
959
888
  let b = rgba.b.toString(16);
960
889
  let a = Math.round(rgba.a * 255).toString(16);
961
- if (r.length == 1)
890
+ if (r.length === 1)
962
891
  r = "0" + r;
963
- if (g.length == 1)
892
+ if (g.length === 1)
964
893
  g = "0" + g;
965
- if (b.length == 1)
894
+ if (b.length === 1)
966
895
  b = "0" + b;
967
- if (a.length == 1)
896
+ if (a.length === 1)
968
897
  a = "0" + a;
969
898
  if (a === "ff")
970
899
  a = "";
@@ -1067,13 +996,13 @@
1067
996
  let l = 0;
1068
997
  // Calculate hue
1069
998
  // No difference
1070
- if (delta == 0)
999
+ if (delta === 0)
1071
1000
  h = 0;
1072
1001
  // Red is max
1073
- else if (cMax == r)
1002
+ else if (cMax === r)
1074
1003
  h = ((g - b) / delta) % 6;
1075
1004
  // Green is max
1076
- else if (cMax == g)
1005
+ else if (cMax === g)
1077
1006
  h = (b - r) / delta + 2;
1078
1007
  // Blue is max
1079
1008
  else
@@ -1084,7 +1013,7 @@
1084
1013
  h += 360;
1085
1014
  l = (cMax + cMin) / 2;
1086
1015
  // Calculate saturation
1087
- s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
1016
+ s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
1088
1017
  // Multiply l and s by 100
1089
1018
  s = +(s * 100).toFixed(1);
1090
1019
  l = +(l * 100).toFixed(1);
@@ -1521,7 +1450,7 @@
1521
1450
  }
1522
1451
  function isLeapYear(year) {
1523
1452
  const _year = Math.trunc(year);
1524
- return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 == 0;
1453
+ return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 === 0;
1525
1454
  }
1526
1455
  function getYearFrac(startDate, endDate, _dayCountConvention) {
1527
1456
  if (startDate === endDate) {
@@ -2182,6 +2111,39 @@
2182
2111
  }
2183
2112
  return undefined;
2184
2113
  }
2114
+ function createCurrencyFormat(currency) {
2115
+ const decimalPlaces = currency.decimalPlaces ?? 2;
2116
+ const position = currency.position ?? "before";
2117
+ const code = currency.code ?? "";
2118
+ const symbol = currency.symbol ?? "";
2119
+ const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
2120
+ const numberFormat = "#,##0" + decimalRepresentation;
2121
+ let textExpression = `${code} ${symbol}`.trim();
2122
+ if (position === "after" && code) {
2123
+ textExpression = " " + textExpression;
2124
+ }
2125
+ return insertTextInFormat(textExpression, position, numberFormat);
2126
+ }
2127
+ function insertTextInFormat(text, position, format) {
2128
+ const textExpression = `[$${text}]`;
2129
+ return position === "before" ? textExpression + format : format + textExpression;
2130
+ }
2131
+ function roundFormat(format) {
2132
+ const internalFormat = parseFormat(format);
2133
+ const roundedFormat = internalFormat.map((formatPart) => {
2134
+ if (formatPart.type === "NUMBER") {
2135
+ return {
2136
+ type: formatPart.type,
2137
+ format: {
2138
+ ...formatPart.format,
2139
+ decimalPart: undefined,
2140
+ },
2141
+ };
2142
+ }
2143
+ return formatPart;
2144
+ });
2145
+ return convertInternalFormatToFormat(roundedFormat);
2146
+ }
2185
2147
  function createLargeNumberFormat(format, magnitude, postFix, locale) {
2186
2148
  const internalFormat = parseFormat(format || "#,##0");
2187
2149
  const largeNumberFormat = internalFormat
@@ -3275,6 +3237,169 @@
3275
3237
  return rows;
3276
3238
  }
3277
3239
 
3240
+ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
3241
+ return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN;
3242
+ }
3243
+ /**
3244
+ * Get the default height of the cell given its style.
3245
+ */
3246
+ function getDefaultCellHeight(ctx, cell, colSize) {
3247
+ if (!cell || !cell.content)
3248
+ return DEFAULT_CELL_HEIGHT;
3249
+ const maxWidth = cell.style?.wrapping ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
3250
+ const numberOfLines = cell.isFormula
3251
+ ? 1
3252
+ : splitTextToWidth(ctx, cell.content, cell.style, maxWidth).length;
3253
+ const fontSize = computeTextFontSizeInPixels(cell.style);
3254
+ return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;
3255
+ }
3256
+ const textWidthCache = {};
3257
+ function computeTextWidth(context, text, style) {
3258
+ const font = computeTextFont(style);
3259
+ if (!textWidthCache[font]) {
3260
+ textWidthCache[font] = {};
3261
+ }
3262
+ if (textWidthCache[font][text] === undefined) {
3263
+ context.save();
3264
+ context.font = font;
3265
+ const textWidth = context.measureText(text).width;
3266
+ context.restore();
3267
+ textWidthCache[font][text] = textWidth;
3268
+ }
3269
+ return textWidthCache[font][text];
3270
+ }
3271
+ function fontSizeInPixels(fontSize) {
3272
+ return Math.round((fontSize * 96) / 72);
3273
+ }
3274
+ function computeTextFont(style) {
3275
+ const italic = style.italic ? "italic " : "";
3276
+ const weight = style.bold ? "bold" : DEFAULT_FONT_WEIGHT;
3277
+ const size = computeTextFontSizeInPixels(style);
3278
+ return `${italic}${weight} ${size}px ${DEFAULT_FONT}`;
3279
+ }
3280
+ function computeTextFontSizeInPixels(style) {
3281
+ const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE;
3282
+ return fontSizeInPixels(sizeInPt);
3283
+ }
3284
+ function splitWordToSpecificWidth(ctx, word, width, style) {
3285
+ const wordWidth = computeTextWidth(ctx, word, style);
3286
+ if (wordWidth <= width) {
3287
+ return [word];
3288
+ }
3289
+ const splitWord = [];
3290
+ let wordPart = "";
3291
+ for (let l of word) {
3292
+ const wordPartWidth = computeTextWidth(ctx, wordPart + l, style);
3293
+ if (wordPartWidth > width) {
3294
+ splitWord.push(wordPart);
3295
+ wordPart = l;
3296
+ }
3297
+ else {
3298
+ wordPart += l;
3299
+ }
3300
+ }
3301
+ splitWord.push(wordPart);
3302
+ return splitWord;
3303
+ }
3304
+ /**
3305
+ * Return the given text, split in multiple lines if needed. The text will be split in multiple
3306
+ * line if it contains NEWLINE characters, or if it's longer than the given width.
3307
+ */
3308
+ function splitTextToWidth(ctx, text, style, width) {
3309
+ if (!style)
3310
+ style = {};
3311
+ const brokenText = [];
3312
+ // Checking if text contains NEWLINE before split makes it very slightly slower if text contains it,
3313
+ // but 5-10x faster if it doesn't
3314
+ const lines = text.includes(NEWLINE) ? text.split(NEWLINE) : [text];
3315
+ for (const line of lines) {
3316
+ const words = line.includes(" ") ? line.split(" ") : [line];
3317
+ if (!width) {
3318
+ brokenText.push(line);
3319
+ continue;
3320
+ }
3321
+ let textLine = "";
3322
+ let availableWidth = width;
3323
+ for (let word of words) {
3324
+ const splitWord = splitWordToSpecificWidth(ctx, word, width, style);
3325
+ const lastPart = splitWord.pop();
3326
+ const lastPartWidth = computeTextWidth(ctx, lastPart, style);
3327
+ // At this step: "splitWord" is an array composed of parts of word whose
3328
+ // length is at most equal to "width".
3329
+ // Last part contains the end of the word.
3330
+ // Note that: When word length is less than width, then lastPart is equal
3331
+ // to word and splitWord is empty
3332
+ if (splitWord.length) {
3333
+ if (textLine !== "") {
3334
+ brokenText.push(textLine);
3335
+ textLine = "";
3336
+ availableWidth = width;
3337
+ }
3338
+ splitWord.forEach((wordPart) => {
3339
+ brokenText.push(wordPart);
3340
+ });
3341
+ textLine = lastPart;
3342
+ availableWidth = width - lastPartWidth;
3343
+ }
3344
+ else {
3345
+ // here "lastPart" is equal to "word" and the "word" size is smaller than "width"
3346
+ const _word = textLine === "" ? lastPart : " " + lastPart;
3347
+ const wordWidth = computeTextWidth(ctx, _word, style);
3348
+ if (wordWidth <= availableWidth) {
3349
+ textLine += _word;
3350
+ availableWidth -= wordWidth;
3351
+ }
3352
+ else {
3353
+ brokenText.push(textLine);
3354
+ textLine = lastPart;
3355
+ availableWidth = width - lastPartWidth;
3356
+ }
3357
+ }
3358
+ }
3359
+ if (textLine !== "") {
3360
+ brokenText.push(textLine);
3361
+ }
3362
+ }
3363
+ return brokenText;
3364
+ }
3365
+ /**
3366
+ * Return the font size that makes the width of a text match the given line width.
3367
+ * Minimum font size is 1.
3368
+ *
3369
+ * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.
3370
+ */
3371
+ function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) {
3372
+ let minFontSize = 1;
3373
+ if (getTextWidth(minFontSize) > lineWidth)
3374
+ return minFontSize;
3375
+ if (getTextWidth(maxFontSize) < lineWidth)
3376
+ return maxFontSize;
3377
+ // Dichotomic search
3378
+ let fontSize = (minFontSize + maxFontSize) / 2;
3379
+ let currentTextWidth = getTextWidth(fontSize);
3380
+ // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise
3381
+ let iterations = 0;
3382
+ while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {
3383
+ if (currentTextWidth >= lineWidth) {
3384
+ maxFontSize = (minFontSize + maxFontSize) / 2;
3385
+ }
3386
+ else {
3387
+ minFontSize = (minFontSize + maxFontSize) / 2;
3388
+ }
3389
+ fontSize = (minFontSize + maxFontSize) / 2;
3390
+ currentTextWidth = getTextWidth(fontSize);
3391
+ iterations++;
3392
+ }
3393
+ return fontSize;
3394
+ }
3395
+ function computeIconWidth(style) {
3396
+ return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;
3397
+ }
3398
+ /** Transform a string to lower case. If the string is undefined, return an empty string */
3399
+ function toLowerCase(str) {
3400
+ return str ? str.toLowerCase() : "";
3401
+ }
3402
+
3278
3403
  /*
3279
3404
  * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
3280
3405
  * */
@@ -3297,7 +3422,7 @@
3297
3422
  else {
3298
3423
  // mainly for jest and other browsers that do not have the crypto functionality
3299
3424
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
3300
- var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8;
3425
+ var r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
3301
3426
  return v.toString(16);
3302
3427
  });
3303
3428
  }
@@ -4125,6 +4250,7 @@
4125
4250
  CommandResult[CommandResult["NoSplitSeparatorInSelection"] = 92] = "NoSplitSeparatorInSelection";
4126
4251
  CommandResult[CommandResult["NoActiveSheet"] = 93] = "NoActiveSheet";
4127
4252
  CommandResult[CommandResult["InvalidLocale"] = 94] = "InvalidLocale";
4253
+ CommandResult[CommandResult["AlreadyInPaintingFormatMode"] = 95] = "AlreadyInPaintingFormatMode";
4128
4254
  })(exports.CommandResult || (exports.CommandResult = {}));
4129
4255
 
4130
4256
  const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"];
@@ -5940,8 +6066,8 @@
5940
6066
  return this.props.menuItems.some((menuItem) => !!menuItem.icon || !!menuItem.isActive);
5941
6067
  }
5942
6068
  getIconName(menu) {
5943
- if (menu.icon) {
5944
- return menu.icon;
6069
+ if (menu.icon(this.env)) {
6070
+ return menu.icon(this.env);
5945
6071
  }
5946
6072
  if (menu.isActive?.(this.env)) {
5947
6073
  return "o-spreadsheet-Icon.CHECK";
@@ -6375,7 +6501,7 @@
6375
6501
  for (let pivot = 0; pivot < dim; pivot++) {
6376
6502
  let diagonalElement = C[pivot][pivot];
6377
6503
  // if we have a 0 on the diagonal we'll need to swap with a lower row
6378
- if (diagonalElement == 0) {
6504
+ if (diagonalElement === 0) {
6379
6505
  //look through every row below the i'th row
6380
6506
  for (let row = pivot + 1; row < dim; row++) {
6381
6507
  //if the ii'th row has a non-0 in the i'th col, swap it with that row
@@ -6402,7 +6528,7 @@
6402
6528
  // the other rows so that there will be 0's in this column in the
6403
6529
  // rows above and below this one
6404
6530
  for (let row = 0; row < dim; row++) {
6405
- if (row == pivot) {
6531
+ if (row === pivot) {
6406
6532
  continue;
6407
6533
  }
6408
6534
  // We want to change this element to 0
@@ -12467,7 +12593,7 @@
12467
12593
  return false;
12468
12594
  }
12469
12595
  catch (e) {
12470
- return e?.errorType == CellErrorType.NotAvailable;
12596
+ return e?.errorType === CellErrorType.NotAvailable;
12471
12597
  }
12472
12598
  },
12473
12599
  isExported: true,
@@ -12593,6 +12719,18 @@
12593
12719
  isExported: true,
12594
12720
  };
12595
12721
  // -----------------------------------------------------------------------------
12722
+ // FALSE
12723
+ // -----------------------------------------------------------------------------
12724
+ const FALSE = {
12725
+ description: _lt("Logical value `false`."),
12726
+ args: [],
12727
+ returns: ["BOOLEAN"],
12728
+ compute: function () {
12729
+ return false;
12730
+ },
12731
+ isExported: true,
12732
+ };
12733
+ // -----------------------------------------------------------------------------
12596
12734
  // IF
12597
12735
  // -----------------------------------------------------------------------------
12598
12736
  const IF = {
@@ -12728,6 +12866,18 @@
12728
12866
  isExported: true,
12729
12867
  };
12730
12868
  // -----------------------------------------------------------------------------
12869
+ // TRUE
12870
+ // -----------------------------------------------------------------------------
12871
+ const TRUE = {
12872
+ description: _lt("Logical value `true`."),
12873
+ args: [],
12874
+ returns: ["BOOLEAN"],
12875
+ compute: function () {
12876
+ return true;
12877
+ },
12878
+ isExported: true,
12879
+ };
12880
+ // -----------------------------------------------------------------------------
12731
12881
  // XOR
12732
12882
  // -----------------------------------------------------------------------------
12733
12883
  const XOR = {
@@ -12754,12 +12904,14 @@
12754
12904
  var logical = /*#__PURE__*/Object.freeze({
12755
12905
  __proto__: null,
12756
12906
  AND: AND,
12907
+ FALSE: FALSE,
12757
12908
  IF: IF,
12758
12909
  IFERROR: IFERROR,
12759
12910
  IFNA: IFNA,
12760
12911
  IFS: IFS,
12761
12912
  NOT: NOT,
12762
12913
  OR: OR,
12914
+ TRUE: TRUE,
12763
12915
  XOR: XOR
12764
12916
  });
12765
12917
 
@@ -13946,7 +14098,6 @@
13946
14098
  * is useful for the composer, which needs to be able to work with incomplete
13947
14099
  * formulas.
13948
14100
  */
13949
- const functions$3 = functionRegistry.content;
13950
14101
  const POSTFIX_UNARY_OPERATORS = ["%"];
13951
14102
  const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
13952
14103
  function tokenize(str, locale = DEFAULT_LOCALE) {
@@ -14077,17 +14228,11 @@
14077
14228
  }
14078
14229
  if (result.length) {
14079
14230
  const value = result;
14080
- const isFunction = value.toUpperCase() in functions$3;
14081
- if (isFunction) {
14082
- return { type: "FUNCTION", value };
14083
- }
14084
14231
  const isReference = rangeReference.test(value);
14085
14232
  if (isReference) {
14086
14233
  return { type: "REFERENCE", value };
14087
14234
  }
14088
- else {
14089
- return { type: "SYMBOL", value };
14090
- }
14235
+ return { type: "SYMBOL", value };
14091
14236
  }
14092
14237
  return null;
14093
14238
  }
@@ -14187,9 +14332,6 @@
14187
14332
  return { type: "NUMBER", value: parseNumber(current.value, DEFAULT_LOCALE) };
14188
14333
  case "STRING":
14189
14334
  return { type: "STRING", value: removeStringQuotes(current.value) };
14190
- case "FUNCTION":
14191
- const args = parseFunctionArgs(tokens);
14192
- return { type: "FUNCALL", value: current.value, args };
14193
14335
  case "INVALID_REFERENCE":
14194
14336
  throw new InvalidReferenceError();
14195
14337
  case "REFERENCE":
@@ -14206,13 +14348,15 @@
14206
14348
  value: current.value,
14207
14349
  };
14208
14350
  case "SYMBOL":
14209
- if (["TRUE", "FALSE"].includes(current.value.toUpperCase())) {
14210
- return { type: "BOOLEAN", value: current.value.toUpperCase() === "TRUE" };
14351
+ const value = current.value;
14352
+ const nextToken = tokens[0];
14353
+ if (nextToken?.type === "LEFT_PAREN" && functionRegex.test(current.value)) {
14354
+ const args = parseFunctionArgs(tokens);
14355
+ return { type: "FUNCALL", value: value, args };
14211
14356
  }
14212
- if (current.value) {
14213
- if (functionRegex.test(current.value) && tokens[0]?.type === "LEFT_PAREN") {
14214
- throw new UnknownFunctionError(current.value);
14215
- }
14357
+ const upperCaseValue = value.toUpperCase();
14358
+ if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
14359
+ return { type: "BOOLEAN", value: upperCaseValue === "TRUE" };
14216
14360
  }
14217
14361
  throw new BadExpressionError(_lt("Invalid formula"));
14218
14362
  case "LEFT_PAREN":
@@ -14314,36 +14458,60 @@
14314
14458
  * to nodes of a specific type.
14315
14459
  * Useful if you want to convert some part of a formula.
14316
14460
  *
14317
- * e.g.
14318
- * ```ts
14461
+ * @example
14319
14462
  * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel)
14320
14463
  *
14321
14464
  * function convertFormulaToExcel(ast: ASTFuncall) {
14322
14465
  * // ...
14323
14466
  * return modifiedAst
14324
14467
  * }
14325
- * ```
14326
14468
  */
14327
14469
  function convertAstNodes(ast, type, fn) {
14328
- if (type === ast.type) {
14329
- ast = fn(ast);
14470
+ return mapAst(ast, (ast) => {
14471
+ if (ast.type === type) {
14472
+ return fn(ast);
14473
+ }
14474
+ return ast;
14475
+ });
14476
+ }
14477
+ function iterateAstNodes(ast) {
14478
+ return Array.from(astIterator(ast));
14479
+ }
14480
+ function* astIterator(ast) {
14481
+ yield ast;
14482
+ switch (ast.type) {
14483
+ case "FUNCALL":
14484
+ for (const arg of ast.args) {
14485
+ yield* astIterator(arg);
14486
+ }
14487
+ break;
14488
+ case "UNARY_OPERATION":
14489
+ yield* astIterator(ast.operand);
14490
+ break;
14491
+ case "BIN_OPERATION":
14492
+ yield* astIterator(ast.left);
14493
+ yield* astIterator(ast.right);
14494
+ break;
14330
14495
  }
14496
+ }
14497
+ function mapAst(ast, fn) {
14498
+ ast = fn(ast);
14331
14499
  switch (ast.type) {
14332
14500
  case "FUNCALL":
14333
14501
  return {
14334
14502
  ...ast,
14335
- args: ast.args.map((child) => convertAstNodes(child, type, fn)),
14503
+ args: ast.args.map((child) => mapAst(child, fn)),
14336
14504
  };
14337
14505
  case "UNARY_OPERATION":
14338
14506
  return {
14339
14507
  ...ast,
14340
- operand: convertAstNodes(ast.operand, type, fn),
14508
+ operand: mapAst(ast.operand, fn),
14341
14509
  };
14342
14510
  case "BIN_OPERATION":
14343
14511
  return {
14344
14512
  ...ast,
14345
- right: convertAstNodes(ast.right, type, fn),
14346
- left: convertAstNodes(ast.left, type, fn),
14513
+ right: mapAst(ast.right, fn),
14514
+ left: mapAst(ast.left, fn),
14347
14515
  };
14348
14516
  default:
14349
14517
  return ast;
@@ -14613,6 +14781,9 @@
14613
14781
  const { args } = ast;
14614
14782
  const functionName = ast.value.toUpperCase();
14615
14783
  const functionDefinition = functions$2[functionName];
14784
+ if (!functionDefinition) {
14785
+ throw new UnknownFunctionError(ast.value);
14786
+ }
14616
14787
  assertEnoughArgs(ast);
14617
14788
  const compiledArgs = [];
14618
14789
  for (let i = 0; i < args.length; i++) {
@@ -14871,7 +15042,7 @@
14871
15042
  functionStarted = "";
14872
15043
  }
14873
15044
  switch (token.type) {
14874
- case "FUNCTION":
15045
+ case "SYMBOL":
14875
15046
  functionStarted = token.value;
14876
15047
  break;
14877
15048
  case "LEFT_PAREN":
@@ -14908,6 +15079,17 @@
14908
15079
  return mapParentFunction(mapParenthesis(enrichTokens(tokens)));
14909
15080
  }
14910
15081
 
15082
+ const functions$1 = functionRegistry.content;
15083
+ function isExportableToExcel(tokens) {
15084
+ try {
15085
+ const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === "FUNCALL" && !functions$1[ast.value.toUpperCase()]?.isExported);
15086
+ return nonExportableFunctions.length === 0;
15087
+ }
15088
+ catch (error) {
15089
+ return false;
15090
+ }
15091
+ }
15092
+
14911
15093
  function isValidLocale(locale) {
14912
15094
  if (!(locale &&
14913
15095
  typeof locale === "object" &&
@@ -15127,6 +15309,8 @@
15127
15309
  function createAction(item) {
15128
15310
  const name = item.name;
15129
15311
  const children = item.children;
15312
+ const description = item.description;
15313
+ const icon = item.icon;
15130
15314
  return {
15131
15315
  id: item.id || uuidGenerator$2.uuidv4(),
15132
15316
  name: typeof name === "function" ? name : () => name,
@@ -15144,8 +15328,8 @@
15144
15328
  : () => [],
15145
15329
  isReadonlyAllowed: item.isReadonlyAllowed || false,
15146
15330
  separator: item.separator || false,
15147
- icon: item.icon || "",
15148
- description: item.description || "",
15331
+ icon: typeof icon === "function" ? icon : () => icon || "",
15332
+ description: typeof description === "function" ? description : () => description || "",
15149
15333
  textColor: item.textColor,
15150
15334
  sequence: item.sequence || 0,
15151
15335
  };
@@ -16611,7 +16795,7 @@
16611
16795
  function convertDateFormatForMoment(format) {
16612
16796
  format = format.replace(/y/g, "Y");
16613
16797
  format = format.replace(/d/g, "D");
16614
- // "m" before "h" == month, "m" after "h" == minute
16798
+ // "m" before "h" === month, "m" after "h" === minute
16615
16799
  const indexH = format.indexOf("h");
16616
16800
  if (indexH >= 0) {
16617
16801
  format = format.slice(0, indexH).replace(/m/g, "M") + format.slice(indexH);
@@ -17465,6 +17649,7 @@
17465
17649
  env.model.dispatch("SELECT_FIGURE", { id: figureId });
17466
17650
  env.openSidePanel("ChartPanel");
17467
17651
  },
17652
+ icon: "o-spreadsheet-Icon.EDIT",
17468
17653
  },
17469
17654
  getCopyMenuItem(figureId, env),
17470
17655
  getCutMenuItem(figureId, env),
@@ -17480,8 +17665,14 @@
17480
17665
  id: "reset_size",
17481
17666
  name: _lt("Reset size"),
17482
17667
  sequence: 4,
17483
- execute: () => {
17484
- const size = env.model.getters.getImageSize(figureId);
17668
+ execute: async () => {
17669
+ const imagePath = env.model.getters.getImagePath(figureId);
17670
+ const size = env.model.getters.getImageSize(figureId) ??
17671
+ (await env.imageProvider?.getImageOriginalSize(imagePath));
17672
+ if (!env.model.getters.getImageSize(figureId)) {
17673
+ const image = env.model.getters.getImage(figureId);
17674
+ image.size = size;
17675
+ }
17485
17676
  const { height, width } = getMaxFigureSize(env.model.getters, size);
17486
17677
  env.model.dispatch("UPDATE_FIGURE", {
17487
17678
  sheetId: env.model.getters.getActiveSheetId(),
@@ -17490,6 +17681,7 @@
17490
17681
  width,
17491
17682
  });
17492
17683
  },
17684
+ icon: "o-spreadsheet-Icon.REFRESH",
17493
17685
  },
17494
17686
  getDeleteMenuItem(figureId, onFigureDeleted, env),
17495
17687
  ];
@@ -17506,6 +17698,7 @@
17506
17698
  env.model.dispatch("COPY");
17507
17699
  await env.clipboard.write(env.model.getters.getClipboardContent());
17508
17700
  },
17701
+ icon: "o-spreadsheet-Icon.COPY",
17509
17702
  };
17510
17703
  }
17511
17704
  function getCutMenuItem(figureId, env) {
@@ -17519,6 +17712,7 @@
17519
17712
  env.model.dispatch("CUT");
17520
17713
  await env.clipboard.write(env.model.getters.getClipboardContent());
17521
17714
  },
17715
+ icon: "o-spreadsheet-Icon.CUT",
17522
17716
  };
17523
17717
  }
17524
17718
  function getDeleteMenuItem(figureId, onFigureDeleted, env) {
@@ -17533,6 +17727,7 @@
17533
17727
  });
17534
17728
  onFigureDeleted();
17535
17729
  },
17730
+ icon: "o-spreadsheet-Icon.DELETE",
17536
17731
  };
17537
17732
  }
17538
17733
 
@@ -18600,7 +18795,7 @@
18600
18795
  };
18601
18796
  const categorieFunctionAll = {
18602
18797
  name: _lt("All"),
18603
- children: allFunctionListMenuBuilder(),
18798
+ children: [allFunctionListMenuBuilder],
18604
18799
  };
18605
18800
  function allFunctionListMenuBuilder() {
18606
18801
  const fnNames = functionRegistry.getKeys();
@@ -18932,7 +19127,10 @@
18932
19127
  };
18933
19128
  const formatNumberNumber = {
18934
19129
  name: _lt("Number"),
18935
- description: "1,000.12",
19130
+ description: (env) => formatValue(1000.12, {
19131
+ format: "#,##0.00",
19132
+ locale: env.model.getters.getLocale(),
19133
+ }),
18936
19134
  execute: (env) => setFormatter(env, "#,##0.00"),
18937
19135
  isActive: (env) => isFormatSelected(env, "#,##0.00"),
18938
19136
  };
@@ -18943,21 +19141,34 @@
18943
19141
  };
18944
19142
  const formatNumberPercent = {
18945
19143
  name: _lt("Percent"),
18946
- description: "10.12%",
19144
+ description: (env) => formatValue(0.1012, {
19145
+ format: "0.00%",
19146
+ locale: env.model.getters.getLocale(),
19147
+ }),
18947
19148
  execute: FORMAT_PERCENT_ACTION,
18948
19149
  isActive: (env) => isFormatSelected(env, "0.00%"),
18949
19150
  };
18950
19151
  const formatNumberCurrency = {
18951
19152
  name: _lt("Currency"),
18952
- description: "$1,000.12",
18953
- execute: (env) => setFormatter(env, "[$$]#,##0.00"),
18954
- isActive: (env) => isFormatSelected(env, "[$$]#,##0.00"),
19153
+ description: (env) => formatValue(1000.12, {
19154
+ format: env.model.config.defaultCurrencyFormat,
19155
+ locale: env.model.getters.getLocale(),
19156
+ }),
19157
+ execute: (env) => setFormatter(env, env.model.config.defaultCurrencyFormat),
19158
+ isActive: (env) => isFormatSelected(env, env.model.config.defaultCurrencyFormat),
18955
19159
  };
18956
19160
  const formatNumberCurrencyRounded = {
18957
19161
  name: _lt("Currency rounded"),
18958
- description: "$1,000",
18959
- execute: (env) => setFormatter(env, "[$$]#,##0"),
18960
- isActive: (env) => isFormatSelected(env, "[$$]#,##0"),
19162
+ description: (env) => formatValue(1000, {
19163
+ format: roundFormat(env.model.config.defaultCurrencyFormat),
19164
+ locale: env.model.getters.getLocale(),
19165
+ }),
19166
+ execute: (env) => setFormatter(env, roundFormat(env.model.config.defaultCurrencyFormat)),
19167
+ isActive: (env) => isFormatSelected(env, roundFormat(env.model.config.defaultCurrencyFormat)),
19168
+ isVisible: (env) => {
19169
+ const currencyFormat = env.model.config.defaultCurrencyFormat;
19170
+ return currencyFormat !== roundFormat(currencyFormat);
19171
+ },
18961
19172
  };
18962
19173
  const formatCustomCurrency = {
18963
19174
  name: _lt("Custom currency"),
@@ -18966,19 +19177,37 @@
18966
19177
  };
18967
19178
  const formatNumberDate = {
18968
19179
  name: _lt("Date"),
18969
- description: "9/26/2008",
19180
+ description: (env) => {
19181
+ const locale = env.model.getters.getLocale();
19182
+ return formatValue(parseLiteral("9/26/2023", DEFAULT_LOCALE), {
19183
+ format: locale.dateFormat,
19184
+ locale,
19185
+ });
19186
+ },
18970
19187
  execute: (env) => setFormatter(env, env.model.getters.getLocale().dateFormat),
18971
19188
  isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().dateFormat),
18972
19189
  };
18973
19190
  const formatNumberTime = {
18974
19191
  name: _lt("Time"),
18975
- description: "10:43:00 PM",
19192
+ description: (env) => {
19193
+ const locale = env.model.getters.getLocale();
19194
+ return formatValue(parseLiteral("9/26/2023 10:43:00 PM", DEFAULT_LOCALE), {
19195
+ format: locale.timeFormat,
19196
+ locale,
19197
+ });
19198
+ },
18976
19199
  execute: (env) => setFormatter(env, env.model.getters.getLocale().timeFormat),
18977
19200
  isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().timeFormat),
18978
19201
  };
18979
19202
  const formatNumberDateTime = {
18980
19203
  name: _lt("Date time"),
18981
- description: "9/26/2008 22:43:00",
19204
+ description: (env) => {
19205
+ const locale = env.model.getters.getLocale();
19206
+ return formatValue(parseLiteral("9/26/2023 22:43:00", DEFAULT_LOCALE), {
19207
+ format: locale.dateFormat + " " + locale.timeFormat,
19208
+ locale,
19209
+ });
19210
+ },
18982
19211
  execute: (env) => {
18983
19212
  const locale = env.model.getters.getLocale();
18984
19213
  setFormatter(env, locale.dateFormat + " " + locale.timeFormat);
@@ -19050,7 +19279,7 @@
19050
19279
  };
19051
19280
  const formatAlignmentHorizontal = {
19052
19281
  name: _lt("Horizontal align"),
19053
- icon: "o-spreadsheet-Icon.ALIGN_LEFT",
19282
+ icon: (env) => getHorizontalAlignmentIcon(env),
19054
19283
  };
19055
19284
  const formatAlignmentLeft = {
19056
19285
  name: _lt("Left"),
@@ -19075,46 +19304,50 @@
19075
19304
  };
19076
19305
  const formatAlignmentVertical = {
19077
19306
  name: _lt("Vertical align"),
19078
- icon: "o-spreadsheet-Icon.ALIGN_MIDDLE",
19307
+ icon: (env) => getVerticalAlignmentIcon(env),
19079
19308
  };
19080
19309
  const formatAlignmentTop = {
19081
19310
  name: _lt("Top"),
19082
19311
  execute: (env) => setStyle(env, { verticalAlign: "top" }),
19083
- isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "top",
19312
+ isActive: (env) => getVerticalAlign(env) === "top",
19084
19313
  icon: "o-spreadsheet-Icon.ALIGN_TOP",
19085
19314
  };
19086
19315
  const formatAlignmentMiddle = {
19087
19316
  name: _lt("Middle"),
19088
19317
  execute: (env) => setStyle(env, { verticalAlign: "middle" }),
19089
- isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "middle",
19318
+ isActive: (env) => getVerticalAlign(env) === "middle",
19090
19319
  icon: "o-spreadsheet-Icon.ALIGN_MIDDLE",
19091
19320
  };
19092
19321
  const formatAlignmentBottom = {
19093
19322
  name: _lt("Bottom"),
19094
19323
  execute: (env) => setStyle(env, { verticalAlign: "bottom" }),
19095
- isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "bottom",
19324
+ isActive: (env) => getVerticalAlign(env) === "bottom",
19096
19325
  icon: "o-spreadsheet-Icon.ALIGN_BOTTOM",
19097
19326
  };
19098
- const formatWrapping = {
19327
+ const formatWrappingIcon = {
19099
19328
  name: _lt("Wrapping"),
19100
19329
  icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW",
19101
19330
  };
19331
+ const formatWrapping = {
19332
+ name: _lt("Wrapping"),
19333
+ icon: (env) => getWrapModeIcon(env),
19334
+ };
19102
19335
  const formatWrappingOverflow = {
19103
19336
  name: _lt("Overflow"),
19104
19337
  execute: (env) => setStyle(env, { wrapping: "overflow" }),
19105
- isActive: (env) => (env.model.getters.getCurrentStyle().wrapping || "overflow") === "overflow",
19338
+ isActive: (env) => getWrappingMode(env) === "overflow",
19106
19339
  icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW",
19107
19340
  };
19108
19341
  const formatWrappingWrap = {
19109
19342
  name: _lt("Wrap"),
19110
19343
  execute: (env) => setStyle(env, { wrapping: "wrap" }),
19111
- isActive: (env) => env.model.getters.getCurrentStyle().wrapping === "wrap",
19344
+ isActive: (env) => getWrappingMode(env) === "wrap",
19112
19345
  icon: "o-spreadsheet-Icon.WRAPPING_WRAP",
19113
19346
  };
19114
19347
  const formatWrappingClip = {
19115
19348
  name: _lt("Clip"),
19116
19349
  execute: (env) => setStyle(env, { wrapping: "clip" }),
19117
- isActive: (env) => env.model.getters.getCurrentStyle().wrapping === "clip",
19350
+ isActive: (env) => getWrappingMode(env) === "clip",
19118
19351
  icon: "o-spreadsheet-Icon.WRAPPING_CLIP",
19119
19352
  };
19120
19353
  const textColor = {
@@ -19130,14 +19363,6 @@
19130
19363
  execute: OPEN_CF_SIDEPANEL_ACTION,
19131
19364
  icon: "o-spreadsheet-Icon.CONDITIONAL_FORMAT",
19132
19365
  };
19133
- const paintFormat = {
19134
- name: _lt("Paint Format"),
19135
- execute: (env) => env.model.dispatch("ACTIVATE_PAINT_FORMAT", {
19136
- target: env.model.getters.getSelectedZones(),
19137
- }),
19138
- icon: "o-spreadsheet-Icon.PAINT_FORMAT",
19139
- isActive: (env) => env.model.getters.isPaintingFormat(),
19140
- };
19141
19366
  const clearFormat = {
19142
19367
  name: _lt("Clear formatting"),
19143
19368
  description: "Ctrl+<",
@@ -19177,6 +19402,53 @@
19177
19402
  }
19178
19403
  const cell = env.model.getters.getActiveCell();
19179
19404
  return cell.defaultAlign;
19405
+ }
19406
+ function getVerticalAlign(env) {
19407
+ const style = env.model.getters.getCurrentStyle();
19408
+ if (style.verticalAlign) {
19409
+ return style.verticalAlign;
19410
+ }
19411
+ return DEFAULT_VERTICAL_ALIGN;
19412
+ }
19413
+ function getWrappingMode(env) {
19414
+ const style = env.model.getters.getCurrentStyle();
19415
+ if (style.wrapping) {
19416
+ return style.wrapping;
19417
+ }
19418
+ return DEFAULT_WRAPPING_MODE;
19419
+ }
19420
+ function getHorizontalAlignmentIcon(env) {
19421
+ const horizontalAlign = getHorizontalAlign(env);
19422
+ switch (horizontalAlign) {
19423
+ case "right":
19424
+ return "o-spreadsheet-Icon.ALIGN_RIGHT";
19425
+ case "center":
19426
+ return "o-spreadsheet-Icon.ALIGN_CENTER";
19427
+ default:
19428
+ return "o-spreadsheet-Icon.ALIGN_LEFT";
19429
+ }
19430
+ }
19431
+ function getVerticalAlignmentIcon(env) {
19432
+ const verticalAlign = getVerticalAlign(env);
19433
+ switch (verticalAlign) {
19434
+ case "top":
19435
+ return "o-spreadsheet-Icon.ALIGN_TOP";
19436
+ case "middle":
19437
+ return "o-spreadsheet-Icon.ALIGN_MIDDLE";
19438
+ default:
19439
+ return "o-spreadsheet-Icon.ALIGN_BOTTOM";
19440
+ }
19441
+ }
19442
+ function getWrapModeIcon(env) {
19443
+ const wrapMode = getWrappingMode(env);
19444
+ switch (wrapMode) {
19445
+ case "wrap":
19446
+ return "o-spreadsheet-Icon.WRAPPING_WRAP";
19447
+ case "clip":
19448
+ return "o-spreadsheet-Icon.WRAPPING_CLIP";
19449
+ default:
19450
+ return "o-spreadsheet-Icon.WRAPPING_OVERFLOW";
19451
+ }
19180
19452
  }
19181
19453
 
19182
19454
  var ACTION_FORMAT = /*#__PURE__*/Object.freeze({
@@ -19208,6 +19480,7 @@
19208
19480
  formatAlignmentTop: formatAlignmentTop,
19209
19481
  formatAlignmentMiddle: formatAlignmentMiddle,
19210
19482
  formatAlignmentBottom: formatAlignmentBottom,
19483
+ formatWrappingIcon: formatWrappingIcon,
19211
19484
  formatWrapping: formatWrapping,
19212
19485
  formatWrappingOverflow: formatWrappingOverflow,
19213
19486
  formatWrappingWrap: formatWrappingWrap,
@@ -19215,7 +19488,6 @@
19215
19488
  textColor: textColor,
19216
19489
  fillColor: fillColor,
19217
19490
  formatCF: formatCF,
19218
- paintFormat: paintFormat,
19219
19491
  clearFormat: clearFormat
19220
19492
  });
19221
19493
 
@@ -19978,7 +20250,7 @@
19978
20250
  separator: true,
19979
20251
  })
19980
20252
  .addChild("format_wrapping", ["format"], {
19981
- ...formatWrapping,
20253
+ ...formatWrappingIcon,
19982
20254
  sequence: 80,
19983
20255
  separator: true,
19984
20256
  })
@@ -22268,17 +22540,45 @@
22268
22540
  }
22269
22541
  get formatProposals() {
22270
22542
  const currency = this.availableCurrencies[this.state.selectedCurrencyIndex];
22271
- const proposalBases = this.initProposalBases(currency.decimalPlaces);
22272
- const firstPosition = currency.position;
22273
- const secondPosition = currency.position === "before" ? "after" : "before";
22543
+ const position = currency.position;
22544
+ const opposite = currency.position === "before" ? "after" : "before";
22274
22545
  const symbol = this.state.currencySymbol.trim() ? this.state.currencySymbol : "";
22275
22546
  const code = this.state.currencyCode.trim() ? this.state.currencyCode : "";
22276
- return code || symbol
22277
- ? [
22278
- ...this.createFormatProposals(proposalBases, symbol, code, firstPosition),
22279
- ...this.createFormatProposals(proposalBases, symbol, code, secondPosition),
22280
- ]
22281
- : [];
22547
+ const decimalPlaces = currency.decimalPlaces;
22548
+ if (!symbol && !code) {
22549
+ return [];
22550
+ }
22551
+ const simple = symbol ? createCurrencyFormat({ symbol, position, decimalPlaces }) : "";
22552
+ const rounded = simple ? roundFormat(simple) : "";
22553
+ const simpleWithCode = createCurrencyFormat({ symbol, position, decimalPlaces, code });
22554
+ const roundedWithCode = roundFormat(simpleWithCode);
22555
+ const simpleOpposite = symbol
22556
+ ? createCurrencyFormat({ symbol, position: opposite, decimalPlaces })
22557
+ : "";
22558
+ const roundedOpposite = simpleOpposite ? roundFormat(simpleOpposite) : "";
22559
+ const simpleOppositeWithCode = createCurrencyFormat({
22560
+ symbol,
22561
+ position: opposite,
22562
+ decimalPlaces,
22563
+ code,
22564
+ });
22565
+ const roundedOppositeWithCode = roundFormat(simpleOppositeWithCode);
22566
+ const formats = new Set([
22567
+ rounded,
22568
+ simple,
22569
+ roundedWithCode,
22570
+ simpleWithCode,
22571
+ roundedOpposite,
22572
+ simpleOpposite,
22573
+ roundedOppositeWithCode,
22574
+ simpleOppositeWithCode,
22575
+ ]);
22576
+ return [...formats]
22577
+ .filter((format) => format !== "")
22578
+ .map((format) => ({
22579
+ format,
22580
+ example: formatValue(1000.0, { format, locale: this.env.model.getters.getLocale() }),
22581
+ }));
22282
22582
  }
22283
22583
  get isSameFormat() {
22284
22584
  const selectedFormat = this.formatProposals[this.state.selectedFormatIndex];
@@ -22335,41 +22635,6 @@
22335
22635
  initAvailableCurrencies() {
22336
22636
  this.state.selectedCurrencyIndex = 0;
22337
22637
  }
22338
- initProposalBases(decimalPlaces) {
22339
- const result = [{ format: "#,##0", example: "1,000" }];
22340
- const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
22341
- if (decimalRepresentation) {
22342
- result.push({
22343
- format: "#,##0" + decimalRepresentation,
22344
- example: "1,000" + decimalRepresentation,
22345
- });
22346
- }
22347
- return result;
22348
- }
22349
- createFormatProposals(proposalBases, symbol, code, position) {
22350
- let formatProposals = [];
22351
- // 1 - add proposal with symbol and without code
22352
- if (symbol) {
22353
- for (let base of proposalBases) {
22354
- formatProposals.push(this.createFormatProposal(position, base.example, base.format, symbol));
22355
- }
22356
- }
22357
- // 2 - if code exist --> add more proposal with symbol and with code
22358
- if (code) {
22359
- for (let base of proposalBases) {
22360
- const expression = (position === "after" ? " " : "") + code + " " + symbol;
22361
- formatProposals.push(this.createFormatProposal(position, base.example, base.format, expression));
22362
- }
22363
- }
22364
- return formatProposals;
22365
- }
22366
- createFormatProposal(position, baseExample, formatBase, expression) {
22367
- const formatExpression = "[$" + expression + "]";
22368
- return {
22369
- example: position === "before" ? expression + baseExample : baseExample + expression,
22370
- format: position === "before" ? formatExpression + formatBase : formatBase + formatExpression,
22371
- };
22372
- }
22373
22638
  getCommonFormat() {
22374
22639
  const selectedZones = this.env.model.getters.getSelectedZones();
22375
22640
  const sheetId = this.env.model.getters.getActiveSheetId();
@@ -23172,7 +23437,7 @@
23172
23437
  selection.removeAllRanges();
23173
23438
  selection.addRange(range);
23174
23439
  }
23175
- if (start == end && start === 0) {
23440
+ if (start === end && start === 0) {
23176
23441
  range.setStart(this.el, 0);
23177
23442
  range.setEnd(this.el, 0);
23178
23443
  }
@@ -23425,7 +23690,7 @@
23425
23690
  }, 0);
23426
23691
  }
23427
23692
  }
23428
- if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent == "") {
23693
+ if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
23429
23694
  usedCharacters++;
23430
23695
  }
23431
23696
  return usedCharacters;
@@ -23534,7 +23799,7 @@
23534
23799
  argToFocus: Number,
23535
23800
  };
23536
23801
 
23537
- const functions$1 = functionRegistry.content;
23802
+ const functions = functionRegistry.content;
23538
23803
  const ASSISTANT_WIDTH = 300;
23539
23804
  const selectionIndicatorClass = "selector-flag";
23540
23805
  const selectionIndicatorColor = "#a9a9a9";
@@ -23568,10 +23833,6 @@
23568
23833
  padding-right: 3px;
23569
23834
  outline: none;
23570
23835
 
23571
- &.unfocusable {
23572
- pointer-events: none;
23573
- }
23574
-
23575
23836
  p {
23576
23837
  margin-bottom: 0px;
23577
23838
 
@@ -23819,7 +24080,10 @@
23819
24080
  }
23820
24081
  }
23821
24082
  showAutocomplete(searchTerm) {
23822
- if (!this.env.model.getters.getCurrentContent().startsWith("=")) {
24083
+ const searchTermUpperCase = searchTerm.toUpperCase();
24084
+ if (!this.env.model.getters.getCurrentContent().startsWith("=") ||
24085
+ searchTermUpperCase === "TRUE" ||
24086
+ searchTermUpperCase === "FALSE") {
23823
24087
  return;
23824
24088
  }
23825
24089
  this.autoCompleteState.showProvider = true;
@@ -23937,7 +24201,6 @@
23937
24201
  switch (token.type) {
23938
24202
  case "OPERATOR":
23939
24203
  case "NUMBER":
23940
- case "FUNCTION":
23941
24204
  case "ARG_SEPARATOR":
23942
24205
  case "STRING":
23943
24206
  result.push({ value: token.value, color: tokenColors[token.type] || "#000" });
@@ -23947,10 +24210,14 @@
23947
24210
  result.push({ value: token.value, color: this.rangeColor(xc, sheetName) || "#000" });
23948
24211
  break;
23949
24212
  case "SYMBOL":
23950
- let value = token.value;
23951
- if (["TRUE", "FALSE"].includes(value.toUpperCase())) {
24213
+ const value = token.value;
24214
+ const upperCaseValue = value.toUpperCase();
24215
+ if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
23952
24216
  result.push({ value: token.value, color: tokenColors.NUMBER });
23953
24217
  }
24218
+ else if (upperCaseValue in functionRegistry.content) {
24219
+ result.push({ value: token.value, color: tokenColors.FUNCTION });
24220
+ }
23954
24221
  else {
23955
24222
  result.push({ value: token.value, color: "#000" });
23956
24223
  }
@@ -24046,25 +24313,28 @@
24046
24313
  this.autoCompleteState.showProvider = false;
24047
24314
  this.functionDescriptionState.showDescription = false;
24048
24315
  if (content.startsWith("=")) {
24049
- const tokenAtCursor = this.env.model.getters.getTokenAtCursor();
24050
- if (tokenAtCursor) {
24051
- const { xc } = splitReference(tokenAtCursor.value);
24052
- if (tokenAtCursor.type === "FUNCTION" ||
24053
- (tokenAtCursor.type === "SYMBOL" && !rangeReference.test(xc))) {
24054
- // initialize Autocomplete Dropdown
24055
- this.showAutocomplete(tokenAtCursor.value);
24056
- }
24057
- else if (tokenAtCursor.functionContext && tokenAtCursor.type !== "UNKNOWN") {
24058
- // initialize Formula Assistant
24059
- const tokenContext = tokenAtCursor.functionContext;
24060
- const parentFunction = tokenContext.parent.toUpperCase();
24061
- const description = functions$1[parentFunction];
24062
- const argPosition = tokenContext.argPosition;
24063
- this.functionDescriptionState.functionName = parentFunction;
24064
- this.functionDescriptionState.functionDescription = description;
24065
- this.functionDescriptionState.argToFocus = description.getArgToFocus(argPosition + 1) - 1;
24066
- this.functionDescriptionState.showDescription = true;
24067
- }
24316
+ const token = this.env.model.getters.getTokenAtCursor();
24317
+ if (!token) {
24318
+ return;
24319
+ }
24320
+ if (token.type === "SYMBOL") {
24321
+ // initialize Autocomplete Dropdown
24322
+ this.showAutocomplete(token.value);
24323
+ return;
24324
+ }
24325
+ const tokenContext = token.functionContext;
24326
+ const parentFunction = tokenContext?.parent.toUpperCase();
24327
+ if (tokenContext &&
24328
+ parentFunction &&
24329
+ parentFunction in functions &&
24330
+ token.type !== "UNKNOWN") {
24331
+ // initialize Formula Assistant
24332
+ const description = functions[parentFunction];
24333
+ const argPosition = tokenContext.argPosition;
24334
+ this.functionDescriptionState.functionName = parentFunction;
24335
+ this.functionDescriptionState.functionDescription = description;
24336
+ this.functionDescriptionState.argToFocus = description.getArgToFocus(argPosition + 1) - 1;
24337
+ this.functionDescriptionState.showDescription = true;
24068
24338
  }
24069
24339
  }
24070
24340
  }
@@ -24967,6 +25237,14 @@
24967
25237
  };
24968
25238
  }
24969
25239
 
25240
+ const CURSOR_SVG = /*xml*/ `
25241
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="16"><path d="M6.5.4c1.3-.8 2.9-.1 3.8 1.4l2.9 5.1c.2.4.9 1.6-.4 2.3l-1.6.9 1.8 3.1c.2.4.1 1-.2 1.2l-1.6 1c-.3.1-.9 0-1.1-.4l-1.8-3.1-1.6 1c-.6.4-1.7 0-2.2-.8L0 4.3"/><path fill="#fff" d="M9.1 2a1.4 1.1 60 0 0-1.7-.6L5.5 2.5l.9 1.6-1 .6-.9-1.6-.6.4 1.8 3.1-1.3.7-1.8-3.1-1 .6 3.8 6.6 6.8-3.98M3.9 8.8 10.82 5l.795 1.4-6.81 3.96"/></svg>
25242
+ `;
25243
+ css /* scss */ `
25244
+ .o-paint-format-cursor {
25245
+ cursor: url("data:image/svg+xml,${encodeURIComponent(CURSOR_SVG)}"), auto;
25246
+ }
25247
+ `;
24970
25248
  function useCellHovered(env, gridRef, callback) {
24971
25249
  let hoveredPosition = {
24972
25250
  col: undefined,
@@ -25090,6 +25368,12 @@
25090
25368
  }
25091
25369
  return this.gridOverlay.el;
25092
25370
  }
25371
+ get style() {
25372
+ return this.props.gridOverlayDimensions;
25373
+ }
25374
+ get isPaintingFormat() {
25375
+ return this.env.model.getters.isPaintingFormat();
25376
+ }
25093
25377
  onMouseDown(ev) {
25094
25378
  if (ev.button > 0) {
25095
25379
  // not main button, probably a context menu
@@ -26267,6 +26551,9 @@
26267
26551
  else if (this.menuState.isOpen) {
26268
26552
  this.closeMenu();
26269
26553
  }
26554
+ else if (this.env.model.getters.isPaintingFormat()) {
26555
+ this.env.model.dispatch("CANCEL_PAINT_FORMAT");
26556
+ }
26270
26557
  else {
26271
26558
  this.env.model.dispatch("CLEAN_CLIPBOARD_HIGHLIGHT");
26272
26559
  }
@@ -26764,7 +27051,12 @@
26764
27051
  };
26765
27052
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
26766
27053
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
26767
- const WIDTH_FACTOR = 0.1317; // 100px => 13.17 u
27054
+ /**
27055
+ * Excel says its default column width is 8.43 characters (64px)
27056
+ * which makes WIDTH_FACTOR = 0.1317, but it doesn't work well
27057
+ * 0.143 is a value from dev's experiments.
27058
+ */
27059
+ const WIDTH_FACTOR = 0.143;
26768
27060
  /** unit : maximum number of characters a column can hold at the standard font size. What. */
26769
27061
  const EXCEL_DEFAULT_COL_WIDTH = 8.43;
26770
27062
  /** unit : points */
@@ -27281,7 +27573,7 @@
27281
27573
  64: "000000",
27282
27574
  65: "FFFFFF", // system background
27283
27575
  };
27284
- const IMAGE_MIMETYPE_EXTENSION_MAPPING = {
27576
+ const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
27285
27577
  "image/avif": "avif",
27286
27578
  "image/bmp": "bmp",
27287
27579
  "image/gif": "gif",
@@ -27290,6 +27582,17 @@
27290
27582
  "image/png": "png",
27291
27583
  "image/tiff": "tiff",
27292
27584
  "image/webp": "webp",
27585
+ };
27586
+ const IMAGE_EXTENSION_TO_MIMETYPE_MAPPING = {
27587
+ avif: "image/avif",
27588
+ bmp: "image/bmp",
27589
+ gif: "image/gif",
27590
+ ico: "image/vnd.microsoft.icon",
27591
+ jpeg: "image/jpeg",
27592
+ png: "image/png",
27593
+ tiff: "image/tiff",
27594
+ webp: "image/webp",
27595
+ jpg: "image/jpeg",
27293
27596
  };
27294
27597
 
27295
27598
  /**
@@ -27453,8 +27756,8 @@
27453
27756
  throw new Error("Multiple escaped blocks in format");
27454
27757
  }
27455
27758
  convertedFormat = convertedFormat.replace(/"(.*)"/g, "[$$$1]"); // replace '"..."' by '[$...]'
27456
- convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ == ignore width of next char for align purposes. Not supported ATM
27457
- convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * == repeat next character enough to fill the line. Not supported ATM
27759
+ convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ === ignore width of next char for align purposes. Not supported ATM
27760
+ convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * === repeat next character enough to fill the line. Not supported ATM
27458
27761
  convertedFormat = convertedFormat.replace(/\\ /g, " "); // unescape spaces
27459
27762
  convertedFormat = convertedFormat.replace(/\\./g, (match) => match[1]); // unescape other characters
27460
27763
  if (isXlsxDateFormat(convertedFormat)) {
@@ -28093,20 +28396,35 @@
28093
28396
  convertEMUToDotValue(figure.anchors[0].rowOffset);
28094
28397
  const y2 = getRowPosition(figure.anchors[1].row, sheetData) +
28095
28398
  convertEMUToDotValue(figure.anchors[1].rowOffset);
28096
- const width = x2 - x1;
28097
- const height = y2 - y1;
28098
- const chartData = convertChartData(figure.data);
28099
- if (!chartData)
28100
- return undefined;
28101
- return {
28102
- id: id,
28103
- x: x1,
28104
- y: y1,
28105
- width: width,
28106
- height: height,
28107
- tag: "chart",
28108
- data: convertChartData(figure.data),
28109
- };
28399
+ const figureData = { id, x: x1, y: y1 };
28400
+ if (isChartData(figure.data)) {
28401
+ return {
28402
+ ...figureData,
28403
+ width: x2 - x1,
28404
+ height: y2 - y1,
28405
+ tag: "chart",
28406
+ data: convertChartData(figure.data),
28407
+ };
28408
+ }
28409
+ else if (isImageData(figure.data)) {
28410
+ return {
28411
+ ...figureData,
28412
+ width: convertEMUToDotValue(figure.data.size.cx),
28413
+ height: convertEMUToDotValue(figure.data.size.cy),
28414
+ tag: "image",
28415
+ data: {
28416
+ path: figure.data.imageSrc,
28417
+ mimetype: figure.data.mimetype,
28418
+ },
28419
+ };
28420
+ }
28421
+ return undefined;
28422
+ }
28423
+ function isChartData(data) {
28424
+ return "dataSets" in data;
28425
+ }
28426
+ function isImageData(data) {
28427
+ return "imageSrc" in data;
28110
28428
  }
28111
28429
  function convertChartData(chartData) {
28112
28430
  const dataSetsHaveTitle = chartData.dataSets[0].label !== undefined;
@@ -28731,29 +29049,26 @@
28731
29049
  return new XMLString(concat(str));
28732
29050
  }
28733
29051
  /**
28734
- * Removes the namespace of all the xml tags in the string.
29052
+ * Removes the escaped namespace of all the xml tags in the string.
28735
29053
  *
28736
- * Eg. : "ns:test a" => "test a"
29054
+ * Eg. : "NAMESPACEnsNAMESPACEtest a" => "test a"
28737
29055
  */
28738
- function removeNamespaces(query) {
28739
- return query.replace(/[a-z0-9]+:(?=[a-z0-9]+)/gi, "");
29056
+ function removeTagEscapedNamespaces(tag) {
29057
+ return tag.replace(/NAMESPACE.*NAMESPACE(.*)/, "$1");
28740
29058
  }
28741
29059
  /**
28742
- * Escape the namespace's colons of all the xml tags in the string.
29060
+ * Encase the namespaces in the element's tags with NAMESPACE string
28743
29061
  *
28744
- * Eg. : "ns:test a" => "ns\\:test a"
28745
- */
28746
- function escapeNamespaces(query) {
28747
- return query.replace(/([a-z0-9]+):(?=[a-z0-9]+)/gi, "$1\\:");
28748
- }
28749
- /**
28750
- * Return true if the querySelector ignores the namespaces when searching for a tag in the DOM.
29062
+ * e.g. <x:foo> becomes <NAMESPACExNAMESPACEFoo>
28751
29063
  *
28752
- * Should return true if it's running on a browser, and false if it's running on jest (jsdom).
29064
+ * That's useful because namespaces aren't supported by the HTML specification, so it's arbitrary whether a HTML parser/querySelector
29065
+ * implementation will support namespaces in the tags or not.
28753
29066
  */
28754
- function areNamespaceIgnoredByQuerySelector() {
28755
- const doc = new DOMParser().parseFromString("<t:test xmlns:t='a'/>", "text/xml");
28756
- return doc.querySelector("test") !== null;
29067
+ function escapeTagNamespaces(str) {
29068
+ return str.replaceAll(/(<\/?)([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "$1" + "NAMESPACE" + "$2" + "NAMESPACE" + "$3");
29069
+ }
29070
+ function escapeQueryNameSpaces(query) {
29071
+ return query.replaceAll(/([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "NAMESPACE" + "$1" + "NAMESPACE" + "$2");
28757
29072
  }
28758
29073
 
28759
29074
  class AttributeValue {
@@ -28783,14 +29098,20 @@
28783
29098
  // The xml file we are currently parsing. We should have one Extractor class by XLSXImportFile, but
28784
29099
  // the XLSXImportFile contains both the main .xml file, and the .rels file
28785
29100
  currentFile = undefined;
28786
- // If the parser querySelector() implementation ignores tag namespaces or not
28787
- areNamespaceIgnored;
29101
+ /**
29102
+ * /!\ Important : There should be no namespaces in the tags of the XML files.
29103
+ *
29104
+ * This class use native querySelector and querySelectorAll, that's used for HTML (not XML). These aren't supposed to
29105
+ * handled namespaces, as they are not supported by the HTML specification. Some implementations (most browsers) do
29106
+ * actually support namespaces, but some don't (e.g. jsdom).
29107
+ *
29108
+ * The namespace should be escaped as with NAMESPACE string (eg. <t:foo> => <NAMESPACEtNAMESPACEfoo>).
29109
+ */
28788
29110
  constructor(rootFile, xlsxStructure, warningManager) {
28789
29111
  this.rootFile = rootFile;
28790
29112
  this.currentFile = rootFile.file.fileName;
28791
29113
  this.xlsxFileStructure = xlsxStructure;
28792
29114
  this.warningManager = warningManager;
28793
- this.areNamespaceIgnored = areNamespaceIgnoredByQuerySelector();
28794
29115
  this.relationships = {};
28795
29116
  if (rootFile.rels) {
28796
29117
  this.extractRelationships(rootFile.rels).map((rel) => {
@@ -28813,9 +29134,13 @@
28813
29134
  /**
28814
29135
  * Get the list of all the XLSX files in the XLSX file structure
28815
29136
  */
28816
- getListOfFiles() {
28817
- const files = Object.values(this.xlsxFileStructure).flat().filter(isDefined$1);
28818
- return files;
29137
+ getListOfXMLFiles() {
29138
+ const XMLFiles = Object.entries(this.xlsxFileStructure)
29139
+ .filter(([key]) => key !== "images")
29140
+ .map(([_, value]) => value)
29141
+ .flat()
29142
+ .filter(isDefined$1);
29143
+ return XMLFiles;
28819
29144
  }
28820
29145
  /**
28821
29146
  * Return an array containing the return value of the given function applied to all the XML elements
@@ -28987,43 +29312,38 @@
28987
29312
  return color;
28988
29313
  }
28989
29314
  /**
28990
- * Returns the xlsx file targeted by a relationship.
29315
+ * Returns the xml file targeted by a relationship.
28991
29316
  */
28992
29317
  getTargetXmlFile(relationship) {
28993
29318
  if (!relationship)
28994
29319
  throw new Error("Undefined target file");
28995
- let target = relationship.target;
28996
- target = target.replace("../", "");
28997
- target = target.replace("./", "");
29320
+ const target = this.processRelationshipTargetName(relationship.target);
28998
29321
  // Use "endsWith" because targets are relative paths, and we know the files by their absolute path.
28999
- const f = this.getListOfFiles().find((f) => f.file.fileName.endsWith(target));
29322
+ const f = this.getListOfXMLFiles().find((f) => f.file.fileName.endsWith(target));
29000
29323
  if (!f || !f.file)
29001
29324
  throw new Error("Cannot find target file");
29002
29325
  return f;
29003
29326
  }
29004
29327
  /**
29005
- * Wrapper of querySelector, but we'll remove the namespaces from the query if areNamespacesIgnored is true.
29006
- *
29007
- * Why we need to do this :
29008
- * - For an XML "<t:test />"
29009
- * - on Jest(jsdom) : xml.querySelector("test") == null, xml.querySelector("t\\:test") == <t:test />
29010
- * - on Browser : xml.querySelector("test") == <t:test />, xml.querySelector("t\\:test") == null
29328
+ * Returns the image parameters targeted by a relationship.
29011
29329
  */
29330
+ getTargetImageFile(relationship) {
29331
+ if (!relationship)
29332
+ throw new Error("Undefined target file");
29333
+ const target = this.processRelationshipTargetName(relationship.target);
29334
+ // Use "endsWith" because targets are relative paths, and we know the files by their absolute path.
29335
+ const f = this.xlsxFileStructure.images.find((f) => f.fileName.endsWith(target));
29336
+ if (!f)
29337
+ throw new Error("Cannot find target file");
29338
+ return f;
29339
+ }
29012
29340
  querySelector(element, query) {
29013
- query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);
29014
- return element.querySelector(query);
29341
+ const escapedQuery = escapeQueryNameSpaces(query);
29342
+ return element.querySelector(escapedQuery);
29015
29343
  }
29016
- /**
29017
- * Wrapper of querySelectorAll, but we'll remove the namespaces from the query if areNamespacesIgnored is true.
29018
- *
29019
- * Why we need to do this :
29020
- * - For an XML "<t:test />"
29021
- * - on Jest(jsdom) : xml.querySelectorAll("test") == [], xml.querySelectorAll("t\\:test") == [<t:test />]
29022
- * - on Browser : xml.querySelectorAll("test") == [<t:test />], xml.querySelectorAll("t\\:test") == []
29023
- */
29024
29344
  querySelectorAll(element, query) {
29025
- query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);
29026
- return element.querySelectorAll(query);
29345
+ const escapedQuery = escapeQueryNameSpaces(query);
29346
+ return element.querySelectorAll(escapedQuery);
29027
29347
  }
29028
29348
  /**
29029
29349
  * Get a color from its id in the Theme's colorScheme.
@@ -29047,6 +29367,10 @@
29047
29367
  return clrScheme[colorId].value;
29048
29368
  }
29049
29369
  }
29370
+ /** Remove signs of relative path. */
29371
+ processRelationshipTargetName(targetName) {
29372
+ return targetName.replace(/\.+\//, "");
29373
+ }
29050
29374
  }
29051
29375
 
29052
29376
  /**
@@ -29259,7 +29583,7 @@
29259
29583
  throw new Error("Missing plot area in the chart definition.");
29260
29584
  }
29261
29585
  for (let child of plotAreaElement.children) {
29262
- const tag = removeNamespaces(child.tagName);
29586
+ const tag = removeTagEscapedNamespaces(child.tagName);
29263
29587
  if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
29264
29588
  return tag;
29265
29589
  }
@@ -29271,20 +29595,21 @@
29271
29595
  class XlsxFigureExtractor extends XlsxBaseExtractor {
29272
29596
  extractFigures() {
29273
29597
  return this.mapOnElements({ parent: this.rootFile.file.xml, query: "xdr:wsDr", children: true }, (figureElement) => {
29274
- const anchorType = removeNamespaces(figureElement.tagName);
29598
+ const anchorType = removeTagEscapedNamespaces(figureElement.tagName);
29275
29599
  if (anchorType !== "twoCellAnchor") {
29276
29600
  throw new Error("Only twoCellAnchor are supported for xlsx drawings.");
29277
29601
  }
29278
29602
  const chartElement = this.querySelector(figureElement, "c:chart");
29279
- if (!chartElement) {
29280
- throw new Error("Only chart figures are currently supported.");
29603
+ const imageElement = this.querySelector(figureElement, "a:blip");
29604
+ if (!chartElement && !imageElement) {
29605
+ throw new Error("Only chart and image figures are currently supported.");
29281
29606
  }
29282
29607
  return {
29283
29608
  anchors: [
29284
29609
  this.extractFigureAnchor("xdr:from", figureElement),
29285
29610
  this.extractFigureAnchor("xdr:to", figureElement),
29286
29611
  ],
29287
- data: this.extractChart(chartElement),
29612
+ data: chartElement ? this.extractChart(chartElement) : this.extractImage(figureElement),
29288
29613
  };
29289
29614
  });
29290
29615
  }
@@ -29309,6 +29634,24 @@
29309
29634
  }
29310
29635
  return chartDefinition;
29311
29636
  }
29637
+ extractImage(figureElement) {
29638
+ const imageElement = this.querySelector(figureElement, "a:blip");
29639
+ const imageId = this.extractAttr(imageElement, "r:embed", { required: true }).asString();
29640
+ const image = this.getTargetImageFile(this.relationships[imageId]);
29641
+ if (!image) {
29642
+ throw new Error("Unable to extract image");
29643
+ }
29644
+ const shapePropertyElement = this.querySelector(figureElement, "a:xfrm");
29645
+ const extension = image.fileName.split(".").at(-1);
29646
+ return {
29647
+ imageSrc: image.imageSrc,
29648
+ mimetype: extension ? IMAGE_EXTENSION_TO_MIMETYPE_MAPPING[extension] : undefined,
29649
+ size: {
29650
+ cx: this.extractChildAttr(shapePropertyElement, "a:ext", "cx", { required: true }).asNum(),
29651
+ cy: this.extractChildAttr(shapePropertyElement, "a:ext", "cy", { required: true }).asNum(),
29652
+ },
29653
+ };
29654
+ }
29312
29655
  }
29313
29656
 
29314
29657
  /**
@@ -29878,13 +30221,22 @@
29878
30221
  class XlsxReader {
29879
30222
  warningManager;
29880
30223
  xmls;
30224
+ images;
29881
30225
  constructor(files) {
29882
30226
  this.warningManager = new XLSXImportWarningManager();
29883
30227
  this.xmls = {};
30228
+ this.images = [];
29884
30229
  for (let key of Object.keys(files)) {
29885
30230
  // Random files can be in xlsx (like a bin file for printer settings)
29886
30231
  if (key.endsWith(".xml") || key.endsWith(".rels")) {
29887
- this.xmls[key] = parseXML(new XMLString(files[key]));
30232
+ const contentString = escapeTagNamespaces(files[key]);
30233
+ this.xmls[key] = parseXML(new XMLString(contentString));
30234
+ }
30235
+ else if (key.includes("media/image")) {
30236
+ this.images.push({
30237
+ fileName: key,
30238
+ imageSrc: files[key]["imageSrc"],
30239
+ });
29888
30240
  }
29889
30241
  }
29890
30242
  }
@@ -29938,6 +30290,7 @@
29938
30290
  tables: getXLSXFilesOfType(CONTENT_TYPES.table, this.xmls),
29939
30291
  pivots: getXLSXFilesOfType(CONTENT_TYPES.pivot, this.xmls),
29940
30292
  externalLinks: getXLSXFilesOfType(CONTENT_TYPES.externalLink, this.xmls),
30293
+ images: this.images,
29941
30294
  };
29942
30295
  if (!xlsxFileStructure.workbook.rels) {
29943
30296
  throw Error(_lt("Cannot find workbook relations file"));
@@ -30944,7 +31297,7 @@
30944
31297
  }
30945
31298
  /**
30946
31299
  * Set the borders of a cell.
30947
- * It overrides the current border if override == true.
31300
+ * It overrides the current border if override === true.
30948
31301
  */
30949
31302
  setBorder(sheetId, col, row, border, override = true) {
30950
31303
  if (override || !this.borders?.[sheetId]?.[col]?.[row]?.vertical) {
@@ -32214,7 +32567,9 @@
32214
32567
  const numHeader = this.getters.getNumberRows(sheetId);
32215
32568
  let gridHeight = 0;
32216
32569
  for (let i = 0; i < numHeader; i++) {
32217
- gridHeight += this.getters.getRowSize(sheetId, i);
32570
+ // TODO : since the row size is an UI value now, this doesn't work anymore. Using the default cell height is
32571
+ // a temporary solution at best, but is broken.
32572
+ gridHeight += this.getters.getUserRowSize(sheetId, i) || DEFAULT_CELL_HEIGHT;
32218
32573
  }
32219
32574
  const figures = this.getters.getFigures(sheetId);
32220
32575
  for (const figure of figures) {
@@ -32640,33 +32995,15 @@
32640
32995
  }
32641
32996
 
32642
32997
  class HeaderSizePlugin extends CorePlugin {
32643
- static getters = ["getRowSize", "getColSize"];
32998
+ static getters = ["getUserRowSize", "getColSize"];
32644
32999
  sizes = {};
32645
33000
  handle(cmd) {
32646
33001
  switch (cmd.type) {
32647
33002
  case "CREATE_SHEET": {
32648
- const computedSizes = this.computeSheetSizes(cmd.sheetId);
32649
- const sizes = {
32650
- COL: computedSizes.COL.map((size) => ({
32651
- manualSize: undefined,
32652
- computedSize: lazy(size),
32653
- })),
32654
- ROW: computedSizes.ROW.map((size) => ({
32655
- manualSize: undefined,
32656
- computedSize: lazy(size),
32657
- })),
32658
- };
32659
- this.history.update("sizes", cmd.sheetId, sizes);
33003
+ this.history.update("sizes", cmd.sheetId, { COL: [], ROW: [] });
32660
33004
  break;
32661
33005
  }
32662
33006
  case "DUPLICATE_SHEET":
32663
- // make sure the values are computed in case the original sheet is deleted
32664
- for (const row of this.sizes[cmd.sheetId].ROW) {
32665
- row.computedSize();
32666
- }
32667
- for (const col of this.sizes[cmd.sheetId].COL) {
32668
- col.computedSize();
32669
- }
32670
33007
  this.history.update("sizes", cmd.sheetIdTo, deepCopy(this.sizes[cmd.sheetId]));
32671
33008
  break;
32672
33009
  case "DELETE_SHEET":
@@ -32675,21 +33012,8 @@
32675
33012
  this.history.update("sizes", sizes);
32676
33013
  break;
32677
33014
  case "REMOVE_COLUMNS_ROWS": {
32678
- let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]];
32679
- for (let headerIndex of [...cmd.elements].sort((a, b) => b - a)) {
32680
- sizes.splice(headerIndex, 1);
32681
- }
32682
- const min = Math.min(...cmd.elements);
32683
- sizes = sizes.map((size, row) => {
32684
- if (cmd.dimension === "ROW" && row >= min) {
32685
- // invalidate sizes
32686
- return {
32687
- manualSize: size.manualSize,
32688
- computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),
32689
- };
32690
- }
32691
- return size;
32692
- });
33015
+ const arr = this.sizes[cmd.sheetId][cmd.dimension];
33016
+ const sizes = removeIndexesFromArray(arr, cmd.elements);
32693
33017
  this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes);
32694
33018
  break;
32695
33019
  }
@@ -32698,51 +33022,18 @@
32698
33022
  const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
32699
33023
  const baseSize = sizes[cmd.base];
32700
33024
  sizes.splice(addIndex, 0, ...Array(cmd.quantity).fill(baseSize));
32701
- sizes = sizes.map((size, row) => {
32702
- if (cmd.dimension === "ROW" && row > cmd.base + cmd.quantity) {
32703
- // invalidate sizes
32704
- return {
32705
- manualSize: size.manualSize,
32706
- computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),
32707
- };
32708
- }
32709
- return size;
32710
- });
32711
33025
  this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes);
32712
33026
  break;
32713
33027
  }
32714
33028
  case "RESIZE_COLUMNS_ROWS":
32715
- for (let el of cmd.elements) {
32716
- if (cmd.dimension === "ROW") {
32717
- const height = this.getRowTallestCellSize(cmd.sheetId, el);
32718
- const size = height;
32719
- this.history.update("sizes", cmd.sheetId, cmd.dimension, el, {
32720
- manualSize: cmd.size || undefined,
32721
- computedSize: lazy(size),
32722
- });
33029
+ if (cmd.dimension === "ROW") {
33030
+ for (const el of cmd.elements) {
33031
+ this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);
32723
33032
  }
32724
- else {
32725
- this.history.update("sizes", cmd.sheetId, cmd.dimension, el, {
32726
- manualSize: cmd.size || undefined,
32727
- computedSize: lazy(cmd.size || DEFAULT_CELL_WIDTH),
32728
- });
32729
- }
32730
- }
32731
- break;
32732
- case "UPDATE_CELL":
32733
- if (!this.sizes[cmd.sheetId]?.["ROW"]?.[cmd.row]?.manualSize) {
32734
- const { sheetId, row } = cmd;
32735
- this.history.update("sizes", sheetId, "ROW", row, "computedSize", lazy(() => this.getRowTallestCellSize(sheetId, row)));
32736
33033
  }
32737
- break;
32738
- case "ADD_MERGE":
32739
- case "REMOVE_MERGE":
32740
- for (let target of cmd.target) {
32741
- for (let row of range(target.top, target.bottom + 1)) {
32742
- const rowHeight = this.getRowTallestCellSize(cmd.sheetId, row);
32743
- if (rowHeight !== this.getRowSize(cmd.sheetId, row)) {
32744
- this.history.update("sizes", cmd.sheetId, "ROW", row, "computedSize", lazy(rowHeight));
32745
- }
33034
+ else {
33035
+ for (const el of cmd.elements) {
33036
+ this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);
32746
33037
  }
32747
33038
  }
32748
33039
  break;
@@ -32750,94 +33041,29 @@
32750
33041
  return;
32751
33042
  }
32752
33043
  getColSize(sheetId, index) {
32753
- return this.getHeaderSize(sheetId, "COL", index);
32754
- }
32755
- getRowSize(sheetId, index) {
32756
- return this.getHeaderSize(sheetId, "ROW", index);
33044
+ return Math.round(this.sizes[sheetId]?.["COL"][index] || DEFAULT_CELL_WIDTH);
32757
33045
  }
32758
- getHeaderSize(sheetId, dimension, index) {
32759
- return Math.round(this.sizes[sheetId]?.[dimension][index]?.manualSize ||
32760
- this.sizes[sheetId]?.[dimension][index]?.computedSize() ||
32761
- this.getDefaultHeaderSize(dimension));
32762
- }
32763
- computeSheetSizes(sheetId) {
32764
- const sizes = { COL: [], ROW: [] };
32765
- for (let col of range(0, this.getters.getNumberCols(sheetId))) {
32766
- sizes.COL.push(this.getHeaderSize(sheetId, "COL", col));
32767
- }
32768
- for (let row of range(0, this.getters.getNumberRows(sheetId))) {
32769
- let rowSize = this.sizes[sheetId]?.["ROW"]?.[row].manualSize;
32770
- if (!rowSize) {
32771
- const height = this.getRowTallestCellSize(sheetId, row);
32772
- rowSize = height;
32773
- }
32774
- sizes.ROW.push(rowSize);
32775
- }
32776
- return sizes;
32777
- }
32778
- getDefaultHeaderSize(dimension) {
32779
- return dimension === "COL" ? DEFAULT_CELL_WIDTH : DEFAULT_CELL_HEIGHT;
32780
- }
32781
- /**
32782
- * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row
32783
- * merge, or the height of the cell computed based on its font size.
32784
- */
32785
- getCellHeight(position) {
32786
- const merge = this.getters.getMerge(position);
32787
- if (merge && merge.bottom !== merge.top) {
32788
- return DEFAULT_CELL_HEIGHT;
32789
- }
32790
- const cell = this.getters.getCell(position);
32791
- return getDefaultCellHeight(cell);
32792
- }
32793
- /**
32794
- * Get the tallest cell of a row and its size.
32795
- *
32796
- * The tallest cell of the row correspond to the cell with the biggest font size,
32797
- * and that is not part of a multi-line merge.
32798
- */
32799
- getRowTallestCellSize(sheetId, row) {
32800
- const cellIds = this.getters.getRowCells(sheetId, row);
32801
- let maxHeight = 0;
32802
- for (let i = 0; i < cellIds.length; i++) {
32803
- const cell = this.getters.getCellById(cellIds[i]);
32804
- if (!cell)
32805
- continue;
32806
- const position = this.getters.getCellPosition(cell.id);
32807
- const cellHeight = this.getCellHeight(position);
32808
- if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {
32809
- maxHeight = cellHeight;
32810
- }
32811
- }
32812
- if (maxHeight <= DEFAULT_CELL_HEIGHT) {
32813
- return DEFAULT_CELL_HEIGHT;
32814
- }
32815
- return maxHeight;
33046
+ getUserRowSize(sheetId, index) {
33047
+ const rowSize = this.sizes[sheetId]?.["ROW"][index];
33048
+ return rowSize ? Math.round(rowSize) : undefined;
32816
33049
  }
32817
33050
  import(data) {
32818
33051
  for (let sheet of data.sheets) {
32819
- const manualSizes = { COL: [], ROW: [] };
33052
+ const sizes = {
33053
+ COL: Array(sheet.colNumber).fill(undefined),
33054
+ ROW: Array(sheet.rowNumber).fill(undefined),
33055
+ };
32820
33056
  for (let [rowIndex, row] of Object.entries(sheet.rows)) {
32821
33057
  if (row.size) {
32822
- manualSizes["ROW"][rowIndex] = row.size;
33058
+ sizes["ROW"][rowIndex] = row.size;
32823
33059
  }
32824
33060
  }
32825
33061
  for (let [colIndex, col] of Object.entries(sheet.cols)) {
32826
33062
  if (col.size) {
32827
- manualSizes["COL"][colIndex] = col.size;
32828
- }
32829
- }
32830
- const computedSizes = this.computeSheetSizes(sheet.id);
32831
- this.sizes[sheet.id] = {
32832
- COL: computedSizes.COL.map((size, i) => ({
32833
- manualSize: manualSizes.COL[i],
32834
- computedSize: lazy(size),
32835
- })),
32836
- ROW: computedSizes.ROW.map((size, i) => ({
32837
- manualSize: manualSizes.ROW[i],
32838
- computedSize: lazy(size),
32839
- })),
32840
- };
33063
+ sizes["COL"][colIndex] = col.size;
33064
+ }
33065
+ }
33066
+ this.sizes[sheet.id] = sizes;
32841
33067
  }
32842
33068
  return;
32843
33069
  }
@@ -32858,9 +33084,12 @@
32858
33084
  if (sheet.rows === undefined) {
32859
33085
  sheet.rows = {};
32860
33086
  }
32861
- for (let row of range(0, this.getters.getNumberRows(sheet.id))) {
32862
- if (exportDefaults || this.sizes[sheet.id]["ROW"][row]?.manualSize) {
32863
- sheet.rows[row] = { ...sheet.rows[row], size: this.getRowSize(sheet.id, row) };
33087
+ for (const row of range(0, this.getters.getNumberRows(sheet.id))) {
33088
+ if (exportDefaults || this.sizes[sheet.id]["ROW"][row]) {
33089
+ sheet.rows[row] = {
33090
+ ...sheet.rows[row],
33091
+ size: this.getUserRowSize(sheet.id, row) ?? DEFAULT_CELL_HEIGHT,
33092
+ };
32864
33093
  }
32865
33094
  }
32866
33095
  // Export col sizes
@@ -32868,7 +33097,7 @@
32868
33097
  sheet.cols = {};
32869
33098
  }
32870
33099
  for (let col of range(0, this.getters.getNumberCols(sheet.id))) {
32871
- if (exportDefaults || this.sizes[sheet.id]["COL"][col]?.manualSize) {
33100
+ if (exportDefaults || this.sizes[sheet.id]["COL"][col]) {
32872
33101
  sheet.cols[col] = { ...sheet.cols[col], size: this.getColSize(sheet.id, col) };
32873
33102
  }
32874
33103
  }
@@ -33088,7 +33317,7 @@
33088
33317
  handle(cmd) {
33089
33318
  switch (cmd.type) {
33090
33319
  case "CREATE_IMAGE":
33091
- this.addFigure(cmd.figureId, cmd.sheetId, cmd.position, cmd.size);
33320
+ this.addImage(cmd.figureId, cmd.sheetId, cmd.position, cmd.size);
33092
33321
  this.history.update("images", cmd.sheetId, cmd.figureId, cmd.definition);
33093
33322
  this.syncedImages.add(cmd.definition.path);
33094
33323
  break;
@@ -33152,7 +33381,7 @@
33152
33381
  // ---------------------------------------------------------------------------
33153
33382
  // Private
33154
33383
  // ---------------------------------------------------------------------------
33155
- addFigure(id, sheetId, position, size) {
33384
+ addImage(id, sheetId, position, size) {
33156
33385
  const figure = {
33157
33386
  id,
33158
33387
  x: position.x,
@@ -35659,7 +35888,6 @@
35659
35888
  }
35660
35889
  }
35661
35890
 
35662
- const functions = functionRegistry.content;
35663
35891
  //#region
35664
35892
  // ---------------------------------------------------------------------------
35665
35893
  // INTRODUCTION
@@ -35881,9 +36109,7 @@
35881
36109
  let isExported = true;
35882
36110
  const formulaCell = this.getCorrespondingFormulaCell(position);
35883
36111
  if (formulaCell) {
35884
- isExported = formulaCell.compiledFormula.tokens
35885
- .filter((tk) => tk.type === "FUNCTION")
35886
- .every((tk) => functions[tk.value.toUpperCase()].isExported);
36112
+ isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
35887
36113
  isFormula = isExported;
35888
36114
  if (!isExported) {
35889
36115
  newContent = value.toString();
@@ -36427,7 +36653,7 @@
36427
36653
  case "ContainsText":
36428
36654
  return cell.value.toString().indexOf(values[0].toString()) > -1;
36429
36655
  case "NotContains":
36430
- return !cell.value || cell.value.toString().indexOf(values[0].toString()) == -1;
36656
+ return !cell.value || cell.value.toString().indexOf(values[0].toString()) === -1;
36431
36657
  case "GreaterThan":
36432
36658
  return cell.value > values[0];
36433
36659
  case "GreaterThanOrEqual":
@@ -36454,6 +36680,156 @@
36454
36680
  };
36455
36681
  }
36456
36682
 
36683
+ class UIRowSizePlugin extends UIPlugin {
36684
+ static getters = ["getRowSize"];
36685
+ tallestCellInRow = {};
36686
+ ctx = document.createElement("canvas").getContext("2d");
36687
+ handle(cmd) {
36688
+ switch (cmd.type) {
36689
+ case "START":
36690
+ for (const sheetId of this.getters.getSheetIds()) {
36691
+ this.initializeSheet(sheetId);
36692
+ }
36693
+ break;
36694
+ case "CREATE_SHEET": {
36695
+ this.initializeSheet(cmd.sheetId);
36696
+ break;
36697
+ }
36698
+ case "DUPLICATE_SHEET": {
36699
+ const tallestCells = deepCopy(this.tallestCellInRow[cmd.sheetId]);
36700
+ this.history.update("tallestCellInRow", cmd.sheetIdTo, tallestCells);
36701
+ break;
36702
+ }
36703
+ case "DELETE_SHEET":
36704
+ const tallestCells = { ...this.tallestCellInRow };
36705
+ delete tallestCells[cmd.sheetId];
36706
+ this.history.update("tallestCellInRow", tallestCells);
36707
+ break;
36708
+ case "REMOVE_COLUMNS_ROWS": {
36709
+ if (cmd.dimension === "COL") {
36710
+ return;
36711
+ }
36712
+ const tallestCells = removeIndexesFromArray(this.tallestCellInRow[cmd.sheetId], cmd.elements);
36713
+ this.history.update("tallestCellInRow", cmd.sheetId, tallestCells);
36714
+ break;
36715
+ }
36716
+ case "ADD_COLUMNS_ROWS": {
36717
+ if (cmd.dimension === "COL") {
36718
+ return;
36719
+ }
36720
+ const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
36721
+ const newCells = Array(cmd.quantity).fill(undefined);
36722
+ const newTallestCells = insertItemsAtIndex(this.tallestCellInRow[cmd.sheetId], newCells, addIndex);
36723
+ this.history.update("tallestCellInRow", cmd.sheetId, newTallestCells);
36724
+ break;
36725
+ }
36726
+ case "RESIZE_COLUMNS_ROWS":
36727
+ {
36728
+ const sheetId = cmd.sheetId;
36729
+ if (cmd.dimension === "ROW") {
36730
+ for (const row of cmd.elements) {
36731
+ const tallestCell = this.getRowTallestCell(sheetId, row);
36732
+ this.history.update("tallestCellInRow", sheetId, row, tallestCell);
36733
+ }
36734
+ }
36735
+ else {
36736
+ // Recompute row heights on col size change, they might have changed because of wrapped text
36737
+ for (const row of range(0, this.getters.getNumberRows(sheetId))) {
36738
+ for (const col of cmd.elements) {
36739
+ this.updateRowSizeForCellChange(sheetId, row, col);
36740
+ }
36741
+ }
36742
+ }
36743
+ }
36744
+ break;
36745
+ case "UPDATE_CELL":
36746
+ this.updateRowSizeForCellChange(cmd.sheetId, cmd.row, cmd.col);
36747
+ break;
36748
+ case "ADD_MERGE":
36749
+ case "REMOVE_MERGE":
36750
+ for (const target of cmd.target) {
36751
+ for (const position of positions(target)) {
36752
+ this.updateRowSizeForCellChange(cmd.sheetId, position.row, position.col);
36753
+ }
36754
+ }
36755
+ }
36756
+ return;
36757
+ }
36758
+ getRowSize(sheetId, row) {
36759
+ return Math.round(this.getters.getUserRowSize(sheetId, row) ??
36760
+ this.tallestCellInRow[sheetId][row]?.size ??
36761
+ DEFAULT_CELL_HEIGHT);
36762
+ }
36763
+ updateRowSizeForCellChange(sheetId, row, col) {
36764
+ const tallestCellInRow = this.tallestCellInRow[sheetId]?.[row];
36765
+ if (tallestCellInRow?.cell.col === col) {
36766
+ const newTallestCell = this.getRowTallestCell(sheetId, row);
36767
+ this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
36768
+ }
36769
+ const updatedCellHeight = this.getCellHeight({ sheetId, col, row });
36770
+ if (updatedCellHeight <= DEFAULT_CELL_HEIGHT) {
36771
+ return;
36772
+ }
36773
+ if ((!tallestCellInRow && updatedCellHeight > DEFAULT_CELL_HEIGHT) ||
36774
+ (tallestCellInRow && updatedCellHeight > tallestCellInRow.size)) {
36775
+ const newTallestCell = { cell: { sheetId, col, row }, size: updatedCellHeight };
36776
+ this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
36777
+ }
36778
+ }
36779
+ initializeSheet(sheetId) {
36780
+ const tallestCells = [];
36781
+ for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
36782
+ const tallestCell = this.getRowTallestCell(sheetId, row);
36783
+ tallestCells.push(tallestCell);
36784
+ }
36785
+ this.history.update("tallestCellInRow", sheetId, tallestCells);
36786
+ }
36787
+ /**
36788
+ * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row
36789
+ * merge, or the height of the cell computed based on its style/content.
36790
+ */
36791
+ getCellHeight(position) {
36792
+ if (this.isInMultiRowMerge(position)) {
36793
+ return DEFAULT_CELL_HEIGHT;
36794
+ }
36795
+ const cell = this.getters.getCell(position);
36796
+ const colSize = this.getters.getColSize(position.sheetId, position.col);
36797
+ return getDefaultCellHeight(this.ctx, cell, colSize);
36798
+ }
36799
+ isInMultiRowMerge(position) {
36800
+ const merge = this.getters.getMerge(position);
36801
+ return !!merge && merge.bottom !== merge.top;
36802
+ }
36803
+ /**
36804
+ * Get the tallest cell of a row and its size.
36805
+ */
36806
+ getRowTallestCell(sheetId, row) {
36807
+ const userRowSize = this.getters.getUserRowSize(sheetId, row);
36808
+ if (userRowSize !== undefined) {
36809
+ return undefined;
36810
+ }
36811
+ const cellIds = this.getters.getRowCells(sheetId, row);
36812
+ let maxHeight = 0;
36813
+ let tallestCell = undefined;
36814
+ for (let i = 0; i < cellIds.length; i++) {
36815
+ const cell = this.getters.getCellById(cellIds[i]);
36816
+ if (!cell) {
36817
+ continue;
36818
+ }
36819
+ const position = this.getters.getCellPosition(cell.id);
36820
+ const cellHeight = this.getCellHeight(position);
36821
+ if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {
36822
+ maxHeight = cellHeight;
36823
+ tallestCell = { cell: position, size: cellHeight };
36824
+ }
36825
+ }
36826
+ if (tallestCell && tallestCell.size > DEFAULT_CELL_HEIGHT) {
36827
+ return tallestCell;
36828
+ }
36829
+ return undefined;
36830
+ }
36831
+ }
36832
+
36457
36833
  /**
36458
36834
  * This plugin manage the autofill.
36459
36835
  *
@@ -39887,10 +40263,8 @@
39887
40263
  }
39888
40264
  updateCellCommands.push(newCellValues);
39889
40265
  }
39890
- for (const cmd of updateCellCommands) {
39891
- this.dispatch("UPDATE_CELL", cmd);
39892
- }
39893
40266
  }
40267
+ updateCellCommands.forEach((cmdPayload) => this.dispatch("UPDATE_CELL", cmdPayload));
39894
40268
  }
39895
40269
  /**
39896
40270
  * Return the distances between main merge cells in the zone.
@@ -39991,24 +40365,28 @@
39991
40365
  // Getters
39992
40366
  // ---------------------------------------------------------------------------
39993
40367
  getCellWidth(position) {
39994
- const text = this.getCellText(position);
39995
40368
  const style = this.getters.getCellComputedStyle(position);
39996
- const multiLineText = text.split(NEWLINE);
39997
- let contentWidth = Math.max(...multiLineText.map((line) => this.getTextWidth(line, style)));
40369
+ let contentWidth = 0;
40370
+ const content = this.getters.getEvaluatedCell(position).formattedValue;
40371
+ if (content) {
40372
+ const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);
40373
+ contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));
40374
+ }
39998
40375
  const icon = this.getters.getConditionalIcon(position);
39999
40376
  if (icon) {
40000
- contentWidth += computeIconWidth(this.getters.getCellStyle(position));
40377
+ contentWidth += computeIconWidth(style);
40001
40378
  }
40002
40379
  const isFilterHeader = this.getters.isFilterHeader(position);
40003
40380
  if (isFilterHeader) {
40004
40381
  contentWidth += ICON_EDGE_LENGTH + FILTER_ICON_MARGIN;
40005
40382
  }
40006
- if (contentWidth > 0) {
40007
- contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;
40008
- if (this.getters.getCellStyle(position).wrapping === "wrap") {
40009
- const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);
40010
- return Math.min(colWidth, contentWidth);
40011
- }
40383
+ if (contentWidth === 0) {
40384
+ return 0;
40385
+ }
40386
+ contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;
40387
+ if (style.wrapping === "wrap") {
40388
+ const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);
40389
+ return Math.min(colWidth, contentWidth);
40012
40390
  }
40013
40391
  return contentWidth;
40014
40392
  }
@@ -40031,56 +40409,7 @@
40031
40409
  getCellMultiLineText(position, width) {
40032
40410
  const style = this.getters.getCellStyle(position);
40033
40411
  const text = this.getters.getCellText(position, this.getters.shouldShowFormulas());
40034
- const brokenText = [];
40035
- for (const line of text.split("\n")) {
40036
- const words = line.split(" ");
40037
- if (!width) {
40038
- brokenText.push(line);
40039
- continue;
40040
- }
40041
- let textLine = "";
40042
- let availableWidth = width;
40043
- for (let word of words) {
40044
- const splitWord = this.splitWordToSpecificWidth(this.ctx, word, width, style);
40045
- const lastPart = splitWord.pop();
40046
- const lastPartWidth = computeTextWidth(this.ctx, lastPart, style);
40047
- // At this step: "splitWord" is an array composed of parts of word whose
40048
- // length is at most equal to "width".
40049
- // Last part contains the end of the word.
40050
- // Note that: When word length is less than width, then lastPart is equal
40051
- // to word and splitWord is empty
40052
- if (splitWord.length) {
40053
- if (textLine !== "") {
40054
- brokenText.push(textLine);
40055
- textLine = "";
40056
- availableWidth = width;
40057
- }
40058
- splitWord.forEach((wordPart) => {
40059
- brokenText.push(wordPart);
40060
- });
40061
- textLine = lastPart;
40062
- availableWidth = width - lastPartWidth;
40063
- }
40064
- else {
40065
- // here "lastPart" is equal to "word" and the "word" size is smaller than "width"
40066
- const _word = textLine === "" ? lastPart : " " + lastPart;
40067
- const wordWidth = computeTextWidth(this.ctx, _word, style);
40068
- if (wordWidth <= availableWidth) {
40069
- textLine += _word;
40070
- availableWidth -= wordWidth;
40071
- }
40072
- else {
40073
- brokenText.push(textLine);
40074
- textLine = lastPart;
40075
- availableWidth = width - lastPartWidth;
40076
- }
40077
- }
40078
- }
40079
- if (textLine !== "") {
40080
- brokenText.push(textLine);
40081
- }
40082
- }
40083
- return brokenText;
40412
+ return splitTextToWidth(this.ctx, text, style, width);
40084
40413
  }
40085
40414
  /**
40086
40415
  * Returns the size, start and end coordinates of a column on an unfolded sheet
@@ -40139,26 +40468,6 @@
40139
40468
  const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
40140
40469
  return Math.max(0, ...sizes);
40141
40470
  }
40142
- splitWordToSpecificWidth(ctx, word, width, style) {
40143
- const wordWidth = computeTextWidth(ctx, word, style);
40144
- if (wordWidth <= width) {
40145
- return [word];
40146
- }
40147
- const splitWord = [];
40148
- let wordPart = "";
40149
- for (let l of word) {
40150
- const wordPartWidth = computeTextWidth(ctx, wordPart + l, style);
40151
- if (wordPartWidth > width) {
40152
- splitWord.push(wordPart);
40153
- wordPart = l;
40154
- }
40155
- else {
40156
- wordPart += l;
40157
- }
40158
- }
40159
- splitWord.push(wordPart);
40160
- return splitWord;
40161
- }
40162
40471
  /**
40163
40472
  * Check that any "sheetId" in the command matches an existing
40164
40473
  * sheet.
@@ -41150,7 +41459,7 @@
41150
41459
  .join("\n") || "\t");
41151
41460
  }
41152
41461
  getHTMLContent() {
41153
- if (this.cells.length == 1 && this.cells[0].length == 1) {
41462
+ if (this.cells.length === 1 && this.cells[0].length === 1) {
41154
41463
  return this.getters.getCellText(this.cells[0][0].position);
41155
41464
  }
41156
41465
  let htmlTable = '<table border="1" style="border-collapse:collapse">';
@@ -41475,7 +41784,7 @@
41475
41784
  status = "invisible";
41476
41785
  state;
41477
41786
  lastPasteState;
41478
- _isPaintingFormat = false;
41787
+ paintFormatStatus = "inactive";
41479
41788
  originSheetId;
41480
41789
  // ---------------------------------------------------------------------------
41481
41790
  // Command Handling
@@ -41490,7 +41799,7 @@
41490
41799
  if (!this.state) {
41491
41800
  return 24 /* CommandResult.EmptyClipboard */;
41492
41801
  }
41493
- const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "onlyFormat" : undefined);
41802
+ const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
41494
41803
  return this.state.isPasteAllowed(cmd.target, { pasteOption });
41495
41804
  case "PASTE_FROM_OS_CLIPBOARD": {
41496
41805
  const state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection);
@@ -41506,6 +41815,11 @@
41506
41815
  const state = this.getClipboardStateForCopyCells(cut, "CUT");
41507
41816
  return state.isPasteAllowed(paste);
41508
41817
  }
41818
+ case "ACTIVATE_PAINT_FORMAT": {
41819
+ if (this.paintFormatStatus !== "inactive") {
41820
+ return 95 /* CommandResult.AlreadyInPaintingFormatMode */;
41821
+ }
41822
+ }
41509
41823
  }
41510
41824
  return 0 /* CommandResult.Success */;
41511
41825
  }
@@ -41522,11 +41836,13 @@
41522
41836
  if (!this.state) {
41523
41837
  break;
41524
41838
  }
41525
- const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "onlyFormat" : undefined);
41526
- this._isPaintingFormat = false;
41839
+ const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
41527
41840
  this.state.paste(cmd.target, { pasteOption, shouldPasteCF: true, selectTarget: true });
41528
41841
  this.lastPasteState = this.state;
41529
- this.status = "invisible";
41842
+ if (this.paintFormatStatus === "oneOff") {
41843
+ this.paintFormatStatus = "inactive";
41844
+ this.status = "invisible";
41845
+ }
41530
41846
  break;
41531
41847
  case "CLEAN_CLIPBOARD_HIGHLIGHT":
41532
41848
  this.status = "invisible";
@@ -41594,8 +41910,13 @@
41594
41910
  case "ACTIVATE_PAINT_FORMAT": {
41595
41911
  const zones = this.getters.getSelectedZones();
41596
41912
  this.state = this.getClipboardStateForCopyCells(zones, "COPY");
41597
- this._isPaintingFormat = true;
41598
41913
  this.status = "visible";
41914
+ if (cmd.persistent) {
41915
+ this.paintFormatStatus = "persistent";
41916
+ }
41917
+ else {
41918
+ this.paintFormatStatus = "oneOff";
41919
+ }
41599
41920
  break;
41600
41921
  }
41601
41922
  case "DELETE_SHEET":
@@ -41607,6 +41928,11 @@
41607
41928
  this.status = "invisible";
41608
41929
  }
41609
41930
  break;
41931
+ case "CANCEL_PAINT_FORMAT": {
41932
+ this.paintFormatStatus = "inactive";
41933
+ this.status = "invisible";
41934
+ break;
41935
+ }
41610
41936
  default:
41611
41937
  if (isCoreCommand(cmd)) {
41612
41938
  this.status = "invisible";
@@ -41637,7 +41963,7 @@
41637
41963
  return this.state ? this.state.operation === "CUT" : false;
41638
41964
  }
41639
41965
  isPaintingFormat() {
41640
- return this._isPaintingFormat;
41966
+ return this.paintFormatStatus !== "inactive";
41641
41967
  }
41642
41968
  // ---------------------------------------------------------------------------
41643
41969
  // Private methods
@@ -44272,6 +44598,7 @@
44272
44598
  .add("evaluation", EvaluationPlugin)
44273
44599
  .add("evaluation_chart", EvaluationChartPlugin)
44274
44600
  .add("evaluation_cf", EvaluationConditionalFormatPlugin)
44601
+ .add("row_size", UIRowSizePlugin)
44275
44602
  .add("custom_colors", CustomColorsPlugin);
44276
44603
 
44277
44604
  const clickableCellRegistry = new Registry();
@@ -44289,7 +44616,7 @@
44289
44616
  async requestImage() {
44290
44617
  const file = await this.getImageFromUser();
44291
44618
  const path = await this.fileStore.upload(file);
44292
- const size = await this.getImageSize(path);
44619
+ const size = await this.getImageOriginalSize(path);
44293
44620
  return { path, size, mimetype: file.type };
44294
44621
  }
44295
44622
  getImageFromUser() {
@@ -44308,7 +44635,7 @@
44308
44635
  input.click();
44309
44636
  });
44310
44637
  }
44311
- getImageSize(path) {
44638
+ getImageOriginalSize(path) {
44312
44639
  return new Promise((resolve, reject) => {
44313
44640
  const image = new Image();
44314
44641
  image.src = path;
@@ -45527,11 +45854,11 @@
45527
45854
  }
45528
45855
  get title() {
45529
45856
  const name = this.actionButton.name(this.env);
45530
- const description = this.actionButton.description;
45857
+ const description = this.actionButton.description(this.env);
45531
45858
  return name + (description ? ` (${description})` : "");
45532
45859
  }
45533
45860
  get iconTitle() {
45534
- return this.actionButton.icon;
45861
+ return this.actionButton.icon(this.env);
45535
45862
  }
45536
45863
  onClick(ev) {
45537
45864
  if (this.isEnabled) {
@@ -45793,6 +46120,10 @@
45793
46120
  top: 20%;
45794
46121
  }
45795
46122
  }
46123
+
46124
+ .user-select-text {
46125
+ user-select: text;
46126
+ }
45796
46127
  `;
45797
46128
  class TopBarComposer extends owl.Component {
45798
46129
  static template = "o-spreadsheet-TopBarComposer";
@@ -45912,6 +46243,27 @@
45912
46243
  class: String,
45913
46244
  };
45914
46245
 
46246
+ class PaintFormatButton extends owl.Component {
46247
+ static template = "o-spreadsheet-PaintFormatButton";
46248
+ get isActive() {
46249
+ return this.env.model.getters.isPaintingFormat();
46250
+ }
46251
+ onDblClick() {
46252
+ this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: true });
46253
+ }
46254
+ togglePaintFormat() {
46255
+ if (this.isActive) {
46256
+ this.env.model.dispatch("CANCEL_PAINT_FORMAT");
46257
+ }
46258
+ else {
46259
+ this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: false });
46260
+ }
46261
+ }
46262
+ }
46263
+ PaintFormatButton.props = {
46264
+ class: { type: String, optional: true },
46265
+ };
46266
+
45915
46267
  // -----------------------------------------------------------------------------
45916
46268
  // TopBar
45917
46269
  // -----------------------------------------------------------------------------
@@ -46012,6 +46364,7 @@
46012
46364
  TopBarComposer,
46013
46365
  FontSizeEditor,
46014
46366
  ActionButton,
46367
+ PaintFormatButton,
46015
46368
  BorderEditorWidget,
46016
46369
  };
46017
46370
  state = owl.useState({
@@ -47864,7 +48217,7 @@
47864
48217
  : "nextCluster";
47865
48218
  while (true) {
47866
48219
  const nextCellPosition = this.getNextCellPosition(currentPosition, dim, dir);
47867
- // Break if nextPosition == currentPosition, which happens if there's no next valid position
48220
+ // Break if nextPosition === currentPosition, which happens if there's no next valid position
47868
48221
  if (currentPosition.col === nextCellPosition.col &&
47869
48222
  currentPosition.row === nextCellPosition.row) {
47870
48223
  break;
@@ -48831,6 +49184,8 @@
48831
49184
  ["name", `Image ${imageId}`],
48832
49185
  ["title", "Image"],
48833
49186
  ];
49187
+ const cx = convertDotValueToEMU(figure.width);
49188
+ const cy = convertDotValueToEMU(figure.height);
48834
49189
  return escapeXml /*xml*/ `
48835
49190
  <xdr:twoCellAnchor editAs="oneCell">
48836
49191
  <xdr:from>
@@ -48857,6 +49212,9 @@
48857
49212
  </a:stretch>
48858
49213
  </xdr:blipFill>
48859
49214
  <xdr:spPr>
49215
+ <a:xfrm>
49216
+ <a:ext cx="${cx}" cy="${cy}" />
49217
+ </a:xfrm>
48860
49218
  <a:prstGeom prst="rect">
48861
49219
  <a:avLst/>
48862
49220
  </a:prstGeom>
@@ -49376,7 +49734,7 @@
49376
49734
  const mimeType = image.data.mimetype;
49377
49735
  if (mimeType === undefined)
49378
49736
  continue;
49379
- const extension = IMAGE_MIMETYPE_EXTENSION_MAPPING[mimeType];
49737
+ const extension = IMAGE_MIMETYPE_TO_EXTENSION_MAPPING[mimeType];
49380
49738
  // only support exporting images with mimetypes specified in the mapping
49381
49739
  if (extension === undefined)
49382
49740
  continue;
@@ -49512,7 +49870,7 @@
49512
49870
  function createContentTypes(files) {
49513
49871
  const overrideNodes = [];
49514
49872
  // hard-code supported image mimetypes
49515
- const imageDefaultNodes = Object.entries(IMAGE_MIMETYPE_EXTENSION_MAPPING).map(([mimetype, extension]) => createDefaultXMLElement(extension, mimetype));
49873
+ const imageDefaultNodes = Object.entries(IMAGE_MIMETYPE_TO_EXTENSION_MAPPING).map(([mimetype, extension]) => createDefaultXMLElement(extension, mimetype));
49516
49874
  for (const file of files) {
49517
49875
  if ("contentType" in file && file.contentType) {
49518
49876
  overrideNodes.push(createOverride("/" + file.path, CONTENT_TYPES[file.contentType]));
@@ -49773,6 +50131,7 @@
49773
50131
  ...config,
49774
50132
  mode: config.mode || "normal",
49775
50133
  custom: config.custom || {},
50134
+ defaultCurrencyFormat: config.defaultCurrencyFormat || "[$$]#,##0.00",
49776
50135
  external: this.setupExternalConfig(config.external || {}),
49777
50136
  transportService,
49778
50137
  client,
@@ -50070,6 +50429,7 @@
50070
50429
  numberToLetters,
50071
50430
  UuidGenerator,
50072
50431
  formatValue,
50432
+ createCurrencyFormat,
50073
50433
  computeTextWidth,
50074
50434
  createEmptyWorkbookData,
50075
50435
  createEmptySheet,
@@ -50142,6 +50502,7 @@
50142
50502
  exports.invalidateCFEvaluationCommands = invalidateCFEvaluationCommands;
50143
50503
  exports.invalidateDependenciesCommands = invalidateDependenciesCommands;
50144
50504
  exports.invalidateEvaluationCommands = invalidateEvaluationCommands;
50505
+ exports.iterateAstNodes = iterateAstNodes;
50145
50506
  exports.links = links;
50146
50507
  exports.load = load;
50147
50508
  exports.parse = parse;
@@ -50154,9 +50515,9 @@
50154
50515
  Object.defineProperty(exports, '__esModule', { value: true });
50155
50516
 
50156
50517
 
50157
- __info__.version = '16.4.0-alpha.6';
50158
- __info__.date = '2023-06-28T11:40:12.377Z';
50159
- __info__.hash = '7d15d3c';
50518
+ __info__.version = '16.4.0';
50519
+ __info__.date = '2023-07-24T06:26:19.494Z';
50520
+ __info__.hash = '4eb614c';
50160
50521
 
50161
50522
 
50162
50523
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);