@odoo/o-spreadsheet 16.4.0-alpha.7 → 16.5.0-alpha.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.
@@ -239,6 +239,7 @@ const DEFAULT_FONT_SIZE = 10;
239
239
  const HEADER_FONT_SIZE = 11;
240
240
  const DEFAULT_FONT = "'Roboto', arial";
241
241
  const DEFAULT_VERTICAL_ALIGN = "bottom";
242
+ const DEFAULT_WRAPPING_MODE = "overflow";
242
243
  // Borders
243
244
  const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" };
244
245
  const DEFAULT_FILTER_BORDER_DESC = { style: "thin", color: FILTERS_COLOR };
@@ -396,82 +397,6 @@ function getCanonicalSheetName(sheetName) {
396
397
  function clip(val, min, max) {
397
398
  return val < min ? min : val > max ? max : val;
398
399
  }
399
- function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
400
- return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN;
401
- }
402
- /**
403
- * Get the default height of the cell given its style.
404
- */
405
- function getDefaultCellHeight(cell) {
406
- if (!cell || !cell.content) {
407
- return DEFAULT_CELL_HEIGHT;
408
- }
409
- const fontSize = computeTextFontSizeInPixels(cell.style);
410
- // the number of lines should be computed from the formula result, but it's not evaluated at this point
411
- const numberOfLines = cell.isFormula ? 1 : cell.content.split(NEWLINE).length;
412
- return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;
413
- }
414
- function computeTextWidth(context, text, style) {
415
- const font = computeTextFont(style);
416
- if (!textWidthCache[font]) {
417
- textWidthCache[font] = {};
418
- }
419
- if (textWidthCache[font][text] === undefined) {
420
- context.save();
421
- context.font = font;
422
- const textWidth = context.measureText(text).width;
423
- context.restore();
424
- textWidthCache[font][text] = textWidth;
425
- }
426
- return textWidthCache[font][text];
427
- }
428
- const textWidthCache = {};
429
- function fontSizeInPixels(fontSize) {
430
- return Math.round((fontSize * 96) / 72);
431
- }
432
- function computeTextFont(style) {
433
- const italic = style.italic ? "italic " : "";
434
- const weight = style.bold ? "bold" : DEFAULT_FONT_WEIGHT;
435
- const size = computeTextFontSizeInPixels(style);
436
- return `${italic}${weight} ${size}px ${DEFAULT_FONT}`;
437
- }
438
- function computeTextFontSizeInPixels(style) {
439
- const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE;
440
- return fontSizeInPixels(sizeInPt);
441
- }
442
- /**
443
- * Return the font size that makes the width of a text match the given line width.
444
- * Minimum font size is 1.
445
- *
446
- * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.
447
- */
448
- function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) {
449
- let minFontSize = 1;
450
- if (getTextWidth(minFontSize) > lineWidth)
451
- return minFontSize;
452
- if (getTextWidth(maxFontSize) < lineWidth)
453
- return maxFontSize;
454
- // Dichotomic search
455
- let fontSize = (minFontSize + maxFontSize) / 2;
456
- let currentTextWidth = getTextWidth(fontSize);
457
- // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise
458
- let iterations = 0;
459
- while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {
460
- if (currentTextWidth >= lineWidth) {
461
- maxFontSize = (minFontSize + maxFontSize) / 2;
462
- }
463
- else {
464
- minFontSize = (minFontSize + maxFontSize) / 2;
465
- }
466
- fontSize = (minFontSize + maxFontSize) / 2;
467
- currentTextWidth = getTextWidth(fontSize);
468
- iterations++;
469
- }
470
- return fontSize;
471
- }
472
- function computeIconWidth(style) {
473
- return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;
474
- }
475
400
  /**
476
401
  * Create a range from start (included) to end (excluded).
477
402
  * range(10, 13) => [10, 11, 12]
@@ -726,10 +651,6 @@ function removeFalsyAttributes(obj) {
726
651
  Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]);
727
652
  return cleanObject;
728
653
  }
729
- /** Transform a string to lower case. If the string is undefined, return an empty string */
730
- function toLowerCase(str) {
731
- return str ? str.toLowerCase() : "";
732
- }
733
654
  function transpose2dArray(matrix, callback = (val) => val) {
734
655
  if (!matrix.length)
735
656
  return [];
@@ -818,6 +739,14 @@ function memoize(func) {
818
739
  return cache.get(args[0]);
819
740
  },
820
741
  }[funcName];
742
+ }
743
+ function removeIndexesFromArray(array, indexes) {
744
+ return array.filter((_, index) => !indexes.includes(index));
745
+ }
746
+ function insertItemsAtIndex(array, items, index) {
747
+ const newArray = [...array];
748
+ newArray.splice(index, 0, ...items);
749
+ return newArray;
821
750
  }
822
751
 
823
752
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
@@ -961,13 +890,13 @@ function rgbaToHex(rgba) {
961
890
  let g = rgba.g.toString(16);
962
891
  let b = rgba.b.toString(16);
963
892
  let a = Math.round(rgba.a * 255).toString(16);
964
- if (r.length == 1)
893
+ if (r.length === 1)
965
894
  r = "0" + r;
966
- if (g.length == 1)
895
+ if (g.length === 1)
967
896
  g = "0" + g;
968
- if (b.length == 1)
897
+ if (b.length === 1)
969
898
  b = "0" + b;
970
- if (a.length == 1)
899
+ if (a.length === 1)
971
900
  a = "0" + a;
972
901
  if (a === "ff")
973
902
  a = "";
@@ -1070,13 +999,13 @@ function rgbaToHSLA(rgba) {
1070
999
  let l = 0;
1071
1000
  // Calculate hue
1072
1001
  // No difference
1073
- if (delta == 0)
1002
+ if (delta === 0)
1074
1003
  h = 0;
1075
1004
  // Red is max
1076
- else if (cMax == r)
1005
+ else if (cMax === r)
1077
1006
  h = ((g - b) / delta) % 6;
1078
1007
  // Green is max
1079
- else if (cMax == g)
1008
+ else if (cMax === g)
1080
1009
  h = (b - r) / delta + 2;
1081
1010
  // Blue is max
1082
1011
  else
@@ -1087,7 +1016,7 @@ function rgbaToHSLA(rgba) {
1087
1016
  h += 360;
1088
1017
  l = (cMax + cMin) / 2;
1089
1018
  // Calculate saturation
1090
- s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
1019
+ s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
1091
1020
  // Multiply l and s by 100
1092
1021
  s = +(s * 100).toFixed(1);
1093
1022
  l = +(l * 100).toFixed(1);
@@ -1524,7 +1453,7 @@ function addMonthsToDate(date, months, keepEndOfMonth) {
1524
1453
  }
1525
1454
  function isLeapYear(year) {
1526
1455
  const _year = Math.trunc(year);
1527
- return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 == 0;
1456
+ return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 === 0;
1528
1457
  }
1529
1458
  function getYearFrac(startDate, endDate, _dayCountConvention) {
1530
1459
  if (startDate === endDate) {
@@ -2185,6 +2114,39 @@ function detectNumberFormat(content) {
2185
2114
  }
2186
2115
  return undefined;
2187
2116
  }
2117
+ function createCurrencyFormat(currency) {
2118
+ const decimalPlaces = currency.decimalPlaces ?? 2;
2119
+ const position = currency.position ?? "before";
2120
+ const code = currency.code ?? "";
2121
+ const symbol = currency.symbol ?? "";
2122
+ const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
2123
+ const numberFormat = "#,##0" + decimalRepresentation;
2124
+ let textExpression = `${code} ${symbol}`.trim();
2125
+ if (position === "after" && code) {
2126
+ textExpression = " " + textExpression;
2127
+ }
2128
+ return insertTextInFormat(textExpression, position, numberFormat);
2129
+ }
2130
+ function insertTextInFormat(text, position, format) {
2131
+ const textExpression = `[$${text}]`;
2132
+ return position === "before" ? textExpression + format : format + textExpression;
2133
+ }
2134
+ function roundFormat(format) {
2135
+ const internalFormat = parseFormat(format);
2136
+ const roundedFormat = internalFormat.map((formatPart) => {
2137
+ if (formatPart.type === "NUMBER") {
2138
+ return {
2139
+ type: formatPart.type,
2140
+ format: {
2141
+ ...formatPart.format,
2142
+ decimalPart: undefined,
2143
+ },
2144
+ };
2145
+ }
2146
+ return formatPart;
2147
+ });
2148
+ return convertInternalFormatToFormat(roundedFormat);
2149
+ }
2188
2150
  function createLargeNumberFormat(format, magnitude, postFix, locale) {
2189
2151
  const internalFormat = parseFormat(format || "#,##0");
2190
2152
  const largeNumberFormat = internalFormat
@@ -3278,6 +3240,169 @@ function createDefaultRows(rowNumber) {
3278
3240
  return rows;
3279
3241
  }
3280
3242
 
3243
+ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
3244
+ return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN;
3245
+ }
3246
+ /**
3247
+ * Get the default height of the cell given its style.
3248
+ */
3249
+ function getDefaultCellHeight(ctx, cell, colSize) {
3250
+ if (!cell || !cell.content)
3251
+ return DEFAULT_CELL_HEIGHT;
3252
+ const maxWidth = cell.style?.wrapping ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
3253
+ const numberOfLines = cell.isFormula
3254
+ ? 1
3255
+ : splitTextToWidth(ctx, cell.content, cell.style, maxWidth).length;
3256
+ const fontSize = computeTextFontSizeInPixels(cell.style);
3257
+ return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;
3258
+ }
3259
+ const textWidthCache = {};
3260
+ function computeTextWidth(context, text, style) {
3261
+ const font = computeTextFont(style);
3262
+ if (!textWidthCache[font]) {
3263
+ textWidthCache[font] = {};
3264
+ }
3265
+ if (textWidthCache[font][text] === undefined) {
3266
+ context.save();
3267
+ context.font = font;
3268
+ const textWidth = context.measureText(text).width;
3269
+ context.restore();
3270
+ textWidthCache[font][text] = textWidth;
3271
+ }
3272
+ return textWidthCache[font][text];
3273
+ }
3274
+ function fontSizeInPixels(fontSize) {
3275
+ return Math.round((fontSize * 96) / 72);
3276
+ }
3277
+ function computeTextFont(style) {
3278
+ const italic = style.italic ? "italic " : "";
3279
+ const weight = style.bold ? "bold" : DEFAULT_FONT_WEIGHT;
3280
+ const size = computeTextFontSizeInPixels(style);
3281
+ return `${italic}${weight} ${size}px ${DEFAULT_FONT}`;
3282
+ }
3283
+ function computeTextFontSizeInPixels(style) {
3284
+ const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE;
3285
+ return fontSizeInPixels(sizeInPt);
3286
+ }
3287
+ function splitWordToSpecificWidth(ctx, word, width, style) {
3288
+ const wordWidth = computeTextWidth(ctx, word, style);
3289
+ if (wordWidth <= width) {
3290
+ return [word];
3291
+ }
3292
+ const splitWord = [];
3293
+ let wordPart = "";
3294
+ for (let l of word) {
3295
+ const wordPartWidth = computeTextWidth(ctx, wordPart + l, style);
3296
+ if (wordPartWidth > width) {
3297
+ splitWord.push(wordPart);
3298
+ wordPart = l;
3299
+ }
3300
+ else {
3301
+ wordPart += l;
3302
+ }
3303
+ }
3304
+ splitWord.push(wordPart);
3305
+ return splitWord;
3306
+ }
3307
+ /**
3308
+ * Return the given text, split in multiple lines if needed. The text will be split in multiple
3309
+ * line if it contains NEWLINE characters, or if it's longer than the given width.
3310
+ */
3311
+ function splitTextToWidth(ctx, text, style, width) {
3312
+ if (!style)
3313
+ style = {};
3314
+ const brokenText = [];
3315
+ // Checking if text contains NEWLINE before split makes it very slightly slower if text contains it,
3316
+ // but 5-10x faster if it doesn't
3317
+ const lines = text.includes(NEWLINE) ? text.split(NEWLINE) : [text];
3318
+ for (const line of lines) {
3319
+ const words = line.includes(" ") ? line.split(" ") : [line];
3320
+ if (!width) {
3321
+ brokenText.push(line);
3322
+ continue;
3323
+ }
3324
+ let textLine = "";
3325
+ let availableWidth = width;
3326
+ for (let word of words) {
3327
+ const splitWord = splitWordToSpecificWidth(ctx, word, width, style);
3328
+ const lastPart = splitWord.pop();
3329
+ const lastPartWidth = computeTextWidth(ctx, lastPart, style);
3330
+ // At this step: "splitWord" is an array composed of parts of word whose
3331
+ // length is at most equal to "width".
3332
+ // Last part contains the end of the word.
3333
+ // Note that: When word length is less than width, then lastPart is equal
3334
+ // to word and splitWord is empty
3335
+ if (splitWord.length) {
3336
+ if (textLine !== "") {
3337
+ brokenText.push(textLine);
3338
+ textLine = "";
3339
+ availableWidth = width;
3340
+ }
3341
+ splitWord.forEach((wordPart) => {
3342
+ brokenText.push(wordPart);
3343
+ });
3344
+ textLine = lastPart;
3345
+ availableWidth = width - lastPartWidth;
3346
+ }
3347
+ else {
3348
+ // here "lastPart" is equal to "word" and the "word" size is smaller than "width"
3349
+ const _word = textLine === "" ? lastPart : " " + lastPart;
3350
+ const wordWidth = computeTextWidth(ctx, _word, style);
3351
+ if (wordWidth <= availableWidth) {
3352
+ textLine += _word;
3353
+ availableWidth -= wordWidth;
3354
+ }
3355
+ else {
3356
+ brokenText.push(textLine);
3357
+ textLine = lastPart;
3358
+ availableWidth = width - lastPartWidth;
3359
+ }
3360
+ }
3361
+ }
3362
+ if (textLine !== "") {
3363
+ brokenText.push(textLine);
3364
+ }
3365
+ }
3366
+ return brokenText;
3367
+ }
3368
+ /**
3369
+ * Return the font size that makes the width of a text match the given line width.
3370
+ * Minimum font size is 1.
3371
+ *
3372
+ * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.
3373
+ */
3374
+ function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) {
3375
+ let minFontSize = 1;
3376
+ if (getTextWidth(minFontSize) > lineWidth)
3377
+ return minFontSize;
3378
+ if (getTextWidth(maxFontSize) < lineWidth)
3379
+ return maxFontSize;
3380
+ // Dichotomic search
3381
+ let fontSize = (minFontSize + maxFontSize) / 2;
3382
+ let currentTextWidth = getTextWidth(fontSize);
3383
+ // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise
3384
+ let iterations = 0;
3385
+ while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {
3386
+ if (currentTextWidth >= lineWidth) {
3387
+ maxFontSize = (minFontSize + maxFontSize) / 2;
3388
+ }
3389
+ else {
3390
+ minFontSize = (minFontSize + maxFontSize) / 2;
3391
+ }
3392
+ fontSize = (minFontSize + maxFontSize) / 2;
3393
+ currentTextWidth = getTextWidth(fontSize);
3394
+ iterations++;
3395
+ }
3396
+ return fontSize;
3397
+ }
3398
+ function computeIconWidth(style) {
3399
+ return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;
3400
+ }
3401
+ /** Transform a string to lower case. If the string is undefined, return an empty string */
3402
+ function toLowerCase(str) {
3403
+ return str ? str.toLowerCase() : "";
3404
+ }
3405
+
3281
3406
  /*
3282
3407
  * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
3283
3408
  * */
@@ -3300,7 +3425,7 @@ class UuidGenerator {
3300
3425
  else {
3301
3426
  // mainly for jest and other browsers that do not have the crypto functionality
3302
3427
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
3303
- var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8;
3428
+ var r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
3304
3429
  return v.toString(16);
3305
3430
  });
3306
3431
  }
@@ -4128,6 +4253,7 @@ exports.CommandResult = void 0;
4128
4253
  CommandResult[CommandResult["NoSplitSeparatorInSelection"] = 92] = "NoSplitSeparatorInSelection";
4129
4254
  CommandResult[CommandResult["NoActiveSheet"] = 93] = "NoActiveSheet";
4130
4255
  CommandResult[CommandResult["InvalidLocale"] = 94] = "InvalidLocale";
4256
+ CommandResult[CommandResult["AlreadyInPaintingFormatMode"] = 95] = "AlreadyInPaintingFormatMode";
4131
4257
  })(exports.CommandResult || (exports.CommandResult = {}));
4132
4258
 
4133
4259
  const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"];
@@ -5943,8 +6069,8 @@ class Menu extends owl.Component {
5943
6069
  return this.props.menuItems.some((menuItem) => !!menuItem.icon || !!menuItem.isActive);
5944
6070
  }
5945
6071
  getIconName(menu) {
5946
- if (menu.icon) {
5947
- return menu.icon;
6072
+ if (menu.icon(this.env)) {
6073
+ return menu.icon(this.env);
5948
6074
  }
5949
6075
  if (menu.isActive?.(this.env)) {
5950
6076
  return "o-spreadsheet-Icon.CHECK";
@@ -6378,7 +6504,7 @@ function invertMatrix(M) {
6378
6504
  for (let pivot = 0; pivot < dim; pivot++) {
6379
6505
  let diagonalElement = C[pivot][pivot];
6380
6506
  // if we have a 0 on the diagonal we'll need to swap with a lower row
6381
- if (diagonalElement == 0) {
6507
+ if (diagonalElement === 0) {
6382
6508
  //look through every row below the i'th row
6383
6509
  for (let row = pivot + 1; row < dim; row++) {
6384
6510
  //if the ii'th row has a non-0 in the i'th col, swap it with that row
@@ -6405,7 +6531,7 @@ function invertMatrix(M) {
6405
6531
  // the other rows so that there will be 0's in this column in the
6406
6532
  // rows above and below this one
6407
6533
  for (let row = 0; row < dim; row++) {
6408
- if (row == pivot) {
6534
+ if (row === pivot) {
6409
6535
  continue;
6410
6536
  }
6411
6537
  // We want to change this element to 0
@@ -12470,7 +12596,7 @@ const ISNA = {
12470
12596
  return false;
12471
12597
  }
12472
12598
  catch (e) {
12473
- return e?.errorType == CellErrorType.NotAvailable;
12599
+ return e?.errorType === CellErrorType.NotAvailable;
12474
12600
  }
12475
12601
  },
12476
12602
  isExported: true,
@@ -15186,6 +15312,8 @@ const uuidGenerator$2 = new UuidGenerator();
15186
15312
  function createAction(item) {
15187
15313
  const name = item.name;
15188
15314
  const children = item.children;
15315
+ const description = item.description;
15316
+ const icon = item.icon;
15189
15317
  return {
15190
15318
  id: item.id || uuidGenerator$2.uuidv4(),
15191
15319
  name: typeof name === "function" ? name : () => name,
@@ -15203,8 +15331,8 @@ function createAction(item) {
15203
15331
  : () => [],
15204
15332
  isReadonlyAllowed: item.isReadonlyAllowed || false,
15205
15333
  separator: item.separator || false,
15206
- icon: item.icon || "",
15207
- description: item.description || "",
15334
+ icon: typeof icon === "function" ? icon : () => icon || "",
15335
+ description: typeof description === "function" ? description : () => description || "",
15208
15336
  textColor: item.textColor,
15209
15337
  sequence: item.sequence || 0,
15210
15338
  };
@@ -16670,7 +16798,7 @@ function getChartTimeOptions(labels, labelFormat, locale) {
16670
16798
  function convertDateFormatForMoment(format) {
16671
16799
  format = format.replace(/y/g, "Y");
16672
16800
  format = format.replace(/d/g, "D");
16673
- // "m" before "h" == month, "m" after "h" == minute
16801
+ // "m" before "h" === month, "m" after "h" === minute
16674
16802
  const indexH = format.indexOf("h");
16675
16803
  if (indexH >= 0) {
16676
16804
  format = format.slice(0, indexH).replace(/m/g, "M") + format.slice(indexH);
@@ -17524,6 +17652,7 @@ function getChartMenu(figureId, onFigureDeleted, env) {
17524
17652
  env.model.dispatch("SELECT_FIGURE", { id: figureId });
17525
17653
  env.openSidePanel("ChartPanel");
17526
17654
  },
17655
+ icon: "o-spreadsheet-Icon.EDIT",
17527
17656
  },
17528
17657
  getCopyMenuItem(figureId, env),
17529
17658
  getCutMenuItem(figureId, env),
@@ -17555,6 +17684,7 @@ function getImageMenuRegistry(figureId, onFigureDeleted, env) {
17555
17684
  width,
17556
17685
  });
17557
17686
  },
17687
+ icon: "o-spreadsheet-Icon.REFRESH",
17558
17688
  },
17559
17689
  getDeleteMenuItem(figureId, onFigureDeleted, env),
17560
17690
  ];
@@ -17571,6 +17701,7 @@ function getCopyMenuItem(figureId, env) {
17571
17701
  env.model.dispatch("COPY");
17572
17702
  await env.clipboard.write(env.model.getters.getClipboardContent());
17573
17703
  },
17704
+ icon: "o-spreadsheet-Icon.COPY",
17574
17705
  };
17575
17706
  }
17576
17707
  function getCutMenuItem(figureId, env) {
@@ -17584,6 +17715,7 @@ function getCutMenuItem(figureId, env) {
17584
17715
  env.model.dispatch("CUT");
17585
17716
  await env.clipboard.write(env.model.getters.getClipboardContent());
17586
17717
  },
17718
+ icon: "o-spreadsheet-Icon.CUT",
17587
17719
  };
17588
17720
  }
17589
17721
  function getDeleteMenuItem(figureId, onFigureDeleted, env) {
@@ -17598,6 +17730,7 @@ function getDeleteMenuItem(figureId, onFigureDeleted, env) {
17598
17730
  });
17599
17731
  onFigureDeleted();
17600
17732
  },
17733
+ icon: "o-spreadsheet-Icon.DELETE",
17601
17734
  };
17602
17735
  }
17603
17736
 
@@ -18665,7 +18798,7 @@ const insertFunctionMin = {
18665
18798
  };
18666
18799
  const categorieFunctionAll = {
18667
18800
  name: _lt("All"),
18668
- children: allFunctionListMenuBuilder(),
18801
+ children: [allFunctionListMenuBuilder],
18669
18802
  };
18670
18803
  function allFunctionListMenuBuilder() {
18671
18804
  const fnNames = functionRegistry.getKeys();
@@ -18997,7 +19130,10 @@ const formatNumberAutomatic = {
18997
19130
  };
18998
19131
  const formatNumberNumber = {
18999
19132
  name: _lt("Number"),
19000
- description: "1,000.12",
19133
+ description: (env) => formatValue(1000.12, {
19134
+ format: "#,##0.00",
19135
+ locale: env.model.getters.getLocale(),
19136
+ }),
19001
19137
  execute: (env) => setFormatter(env, "#,##0.00"),
19002
19138
  isActive: (env) => isFormatSelected(env, "#,##0.00"),
19003
19139
  };
@@ -19008,21 +19144,34 @@ const formatPercent = {
19008
19144
  };
19009
19145
  const formatNumberPercent = {
19010
19146
  name: _lt("Percent"),
19011
- description: "10.12%",
19147
+ description: (env) => formatValue(0.1012, {
19148
+ format: "0.00%",
19149
+ locale: env.model.getters.getLocale(),
19150
+ }),
19012
19151
  execute: FORMAT_PERCENT_ACTION,
19013
19152
  isActive: (env) => isFormatSelected(env, "0.00%"),
19014
19153
  };
19015
19154
  const formatNumberCurrency = {
19016
19155
  name: _lt("Currency"),
19017
- description: "$1,000.12",
19018
- execute: (env) => setFormatter(env, "[$$]#,##0.00"),
19019
- isActive: (env) => isFormatSelected(env, "[$$]#,##0.00"),
19156
+ description: (env) => formatValue(1000.12, {
19157
+ format: env.model.config.defaultCurrencyFormat,
19158
+ locale: env.model.getters.getLocale(),
19159
+ }),
19160
+ execute: (env) => setFormatter(env, env.model.config.defaultCurrencyFormat),
19161
+ isActive: (env) => isFormatSelected(env, env.model.config.defaultCurrencyFormat),
19020
19162
  };
19021
19163
  const formatNumberCurrencyRounded = {
19022
19164
  name: _lt("Currency rounded"),
19023
- description: "$1,000",
19024
- execute: (env) => setFormatter(env, "[$$]#,##0"),
19025
- isActive: (env) => isFormatSelected(env, "[$$]#,##0"),
19165
+ description: (env) => formatValue(1000, {
19166
+ format: roundFormat(env.model.config.defaultCurrencyFormat),
19167
+ locale: env.model.getters.getLocale(),
19168
+ }),
19169
+ execute: (env) => setFormatter(env, roundFormat(env.model.config.defaultCurrencyFormat)),
19170
+ isActive: (env) => isFormatSelected(env, roundFormat(env.model.config.defaultCurrencyFormat)),
19171
+ isVisible: (env) => {
19172
+ const currencyFormat = env.model.config.defaultCurrencyFormat;
19173
+ return currencyFormat !== roundFormat(currencyFormat);
19174
+ },
19026
19175
  };
19027
19176
  const formatCustomCurrency = {
19028
19177
  name: _lt("Custom currency"),
@@ -19031,19 +19180,37 @@ const formatCustomCurrency = {
19031
19180
  };
19032
19181
  const formatNumberDate = {
19033
19182
  name: _lt("Date"),
19034
- description: "9/26/2008",
19183
+ description: (env) => {
19184
+ const locale = env.model.getters.getLocale();
19185
+ return formatValue(parseLiteral("9/26/2023", DEFAULT_LOCALE), {
19186
+ format: locale.dateFormat,
19187
+ locale,
19188
+ });
19189
+ },
19035
19190
  execute: (env) => setFormatter(env, env.model.getters.getLocale().dateFormat),
19036
19191
  isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().dateFormat),
19037
19192
  };
19038
19193
  const formatNumberTime = {
19039
19194
  name: _lt("Time"),
19040
- description: "10:43:00 PM",
19195
+ description: (env) => {
19196
+ const locale = env.model.getters.getLocale();
19197
+ return formatValue(parseLiteral("9/26/2023 10:43:00 PM", DEFAULT_LOCALE), {
19198
+ format: locale.timeFormat,
19199
+ locale,
19200
+ });
19201
+ },
19041
19202
  execute: (env) => setFormatter(env, env.model.getters.getLocale().timeFormat),
19042
19203
  isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().timeFormat),
19043
19204
  };
19044
19205
  const formatNumberDateTime = {
19045
19206
  name: _lt("Date time"),
19046
- description: "9/26/2008 22:43:00",
19207
+ description: (env) => {
19208
+ const locale = env.model.getters.getLocale();
19209
+ return formatValue(parseLiteral("9/26/2023 22:43:00", DEFAULT_LOCALE), {
19210
+ format: locale.dateFormat + " " + locale.timeFormat,
19211
+ locale,
19212
+ });
19213
+ },
19047
19214
  execute: (env) => {
19048
19215
  const locale = env.model.getters.getLocale();
19049
19216
  setFormatter(env, locale.dateFormat + " " + locale.timeFormat);
@@ -19115,7 +19282,7 @@ const formatAlignment = {
19115
19282
  };
19116
19283
  const formatAlignmentHorizontal = {
19117
19284
  name: _lt("Horizontal align"),
19118
- icon: "o-spreadsheet-Icon.ALIGN_LEFT",
19285
+ icon: (env) => getHorizontalAlignmentIcon(env),
19119
19286
  };
19120
19287
  const formatAlignmentLeft = {
19121
19288
  name: _lt("Left"),
@@ -19140,46 +19307,50 @@ const formatAlignmentRight = {
19140
19307
  };
19141
19308
  const formatAlignmentVertical = {
19142
19309
  name: _lt("Vertical align"),
19143
- icon: "o-spreadsheet-Icon.ALIGN_MIDDLE",
19310
+ icon: (env) => getVerticalAlignmentIcon(env),
19144
19311
  };
19145
19312
  const formatAlignmentTop = {
19146
19313
  name: _lt("Top"),
19147
19314
  execute: (env) => setStyle(env, { verticalAlign: "top" }),
19148
- isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "top",
19315
+ isActive: (env) => getVerticalAlign(env) === "top",
19149
19316
  icon: "o-spreadsheet-Icon.ALIGN_TOP",
19150
19317
  };
19151
19318
  const formatAlignmentMiddle = {
19152
19319
  name: _lt("Middle"),
19153
19320
  execute: (env) => setStyle(env, { verticalAlign: "middle" }),
19154
- isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "middle",
19321
+ isActive: (env) => getVerticalAlign(env) === "middle",
19155
19322
  icon: "o-spreadsheet-Icon.ALIGN_MIDDLE",
19156
19323
  };
19157
19324
  const formatAlignmentBottom = {
19158
19325
  name: _lt("Bottom"),
19159
19326
  execute: (env) => setStyle(env, { verticalAlign: "bottom" }),
19160
- isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "bottom",
19327
+ isActive: (env) => getVerticalAlign(env) === "bottom",
19161
19328
  icon: "o-spreadsheet-Icon.ALIGN_BOTTOM",
19162
19329
  };
19163
- const formatWrapping = {
19330
+ const formatWrappingIcon = {
19164
19331
  name: _lt("Wrapping"),
19165
19332
  icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW",
19166
19333
  };
19334
+ const formatWrapping = {
19335
+ name: _lt("Wrapping"),
19336
+ icon: (env) => getWrapModeIcon(env),
19337
+ };
19167
19338
  const formatWrappingOverflow = {
19168
19339
  name: _lt("Overflow"),
19169
19340
  execute: (env) => setStyle(env, { wrapping: "overflow" }),
19170
- isActive: (env) => (env.model.getters.getCurrentStyle().wrapping || "overflow") === "overflow",
19341
+ isActive: (env) => getWrappingMode(env) === "overflow",
19171
19342
  icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW",
19172
19343
  };
19173
19344
  const formatWrappingWrap = {
19174
19345
  name: _lt("Wrap"),
19175
19346
  execute: (env) => setStyle(env, { wrapping: "wrap" }),
19176
- isActive: (env) => env.model.getters.getCurrentStyle().wrapping === "wrap",
19347
+ isActive: (env) => getWrappingMode(env) === "wrap",
19177
19348
  icon: "o-spreadsheet-Icon.WRAPPING_WRAP",
19178
19349
  };
19179
19350
  const formatWrappingClip = {
19180
19351
  name: _lt("Clip"),
19181
19352
  execute: (env) => setStyle(env, { wrapping: "clip" }),
19182
- isActive: (env) => env.model.getters.getCurrentStyle().wrapping === "clip",
19353
+ isActive: (env) => getWrappingMode(env) === "clip",
19183
19354
  icon: "o-spreadsheet-Icon.WRAPPING_CLIP",
19184
19355
  };
19185
19356
  const textColor = {
@@ -19195,14 +19366,6 @@ const formatCF = {
19195
19366
  execute: OPEN_CF_SIDEPANEL_ACTION,
19196
19367
  icon: "o-spreadsheet-Icon.CONDITIONAL_FORMAT",
19197
19368
  };
19198
- const paintFormat = {
19199
- name: _lt("Paint Format"),
19200
- execute: (env) => env.model.dispatch("ACTIVATE_PAINT_FORMAT", {
19201
- target: env.model.getters.getSelectedZones(),
19202
- }),
19203
- icon: "o-spreadsheet-Icon.PAINT_FORMAT",
19204
- isActive: (env) => env.model.getters.isPaintingFormat(),
19205
- };
19206
19369
  const clearFormat = {
19207
19370
  name: _lt("Clear formatting"),
19208
19371
  description: "Ctrl+<",
@@ -19242,6 +19405,53 @@ function getHorizontalAlign(env) {
19242
19405
  }
19243
19406
  const cell = env.model.getters.getActiveCell();
19244
19407
  return cell.defaultAlign;
19408
+ }
19409
+ function getVerticalAlign(env) {
19410
+ const style = env.model.getters.getCurrentStyle();
19411
+ if (style.verticalAlign) {
19412
+ return style.verticalAlign;
19413
+ }
19414
+ return DEFAULT_VERTICAL_ALIGN;
19415
+ }
19416
+ function getWrappingMode(env) {
19417
+ const style = env.model.getters.getCurrentStyle();
19418
+ if (style.wrapping) {
19419
+ return style.wrapping;
19420
+ }
19421
+ return DEFAULT_WRAPPING_MODE;
19422
+ }
19423
+ function getHorizontalAlignmentIcon(env) {
19424
+ const horizontalAlign = getHorizontalAlign(env);
19425
+ switch (horizontalAlign) {
19426
+ case "right":
19427
+ return "o-spreadsheet-Icon.ALIGN_RIGHT";
19428
+ case "center":
19429
+ return "o-spreadsheet-Icon.ALIGN_CENTER";
19430
+ default:
19431
+ return "o-spreadsheet-Icon.ALIGN_LEFT";
19432
+ }
19433
+ }
19434
+ function getVerticalAlignmentIcon(env) {
19435
+ const verticalAlign = getVerticalAlign(env);
19436
+ switch (verticalAlign) {
19437
+ case "top":
19438
+ return "o-spreadsheet-Icon.ALIGN_TOP";
19439
+ case "middle":
19440
+ return "o-spreadsheet-Icon.ALIGN_MIDDLE";
19441
+ default:
19442
+ return "o-spreadsheet-Icon.ALIGN_BOTTOM";
19443
+ }
19444
+ }
19445
+ function getWrapModeIcon(env) {
19446
+ const wrapMode = getWrappingMode(env);
19447
+ switch (wrapMode) {
19448
+ case "wrap":
19449
+ return "o-spreadsheet-Icon.WRAPPING_WRAP";
19450
+ case "clip":
19451
+ return "o-spreadsheet-Icon.WRAPPING_CLIP";
19452
+ default:
19453
+ return "o-spreadsheet-Icon.WRAPPING_OVERFLOW";
19454
+ }
19245
19455
  }
19246
19456
 
19247
19457
  var ACTION_FORMAT = /*#__PURE__*/Object.freeze({
@@ -19273,6 +19483,7 @@ var ACTION_FORMAT = /*#__PURE__*/Object.freeze({
19273
19483
  formatAlignmentTop: formatAlignmentTop,
19274
19484
  formatAlignmentMiddle: formatAlignmentMiddle,
19275
19485
  formatAlignmentBottom: formatAlignmentBottom,
19486
+ formatWrappingIcon: formatWrappingIcon,
19276
19487
  formatWrapping: formatWrapping,
19277
19488
  formatWrappingOverflow: formatWrappingOverflow,
19278
19489
  formatWrappingWrap: formatWrappingWrap,
@@ -19280,7 +19491,6 @@ var ACTION_FORMAT = /*#__PURE__*/Object.freeze({
19280
19491
  textColor: textColor,
19281
19492
  fillColor: fillColor,
19282
19493
  formatCF: formatCF,
19283
- paintFormat: paintFormat,
19284
19494
  clearFormat: clearFormat
19285
19495
  });
19286
19496
 
@@ -20043,7 +20253,7 @@ topbarMenuRegistry
20043
20253
  separator: true,
20044
20254
  })
20045
20255
  .addChild("format_wrapping", ["format"], {
20046
- ...formatWrapping,
20256
+ ...formatWrappingIcon,
20047
20257
  sequence: 80,
20048
20258
  separator: true,
20049
20259
  })
@@ -22333,17 +22543,45 @@ class CustomCurrencyPanel extends owl.Component {
22333
22543
  }
22334
22544
  get formatProposals() {
22335
22545
  const currency = this.availableCurrencies[this.state.selectedCurrencyIndex];
22336
- const proposalBases = this.initProposalBases(currency.decimalPlaces);
22337
- const firstPosition = currency.position;
22338
- const secondPosition = currency.position === "before" ? "after" : "before";
22546
+ const position = currency.position;
22547
+ const opposite = currency.position === "before" ? "after" : "before";
22339
22548
  const symbol = this.state.currencySymbol.trim() ? this.state.currencySymbol : "";
22340
22549
  const code = this.state.currencyCode.trim() ? this.state.currencyCode : "";
22341
- return code || symbol
22342
- ? [
22343
- ...this.createFormatProposals(proposalBases, symbol, code, firstPosition),
22344
- ...this.createFormatProposals(proposalBases, symbol, code, secondPosition),
22345
- ]
22346
- : [];
22550
+ const decimalPlaces = currency.decimalPlaces;
22551
+ if (!symbol && !code) {
22552
+ return [];
22553
+ }
22554
+ const simple = symbol ? createCurrencyFormat({ symbol, position, decimalPlaces }) : "";
22555
+ const rounded = simple ? roundFormat(simple) : "";
22556
+ const simpleWithCode = createCurrencyFormat({ symbol, position, decimalPlaces, code });
22557
+ const roundedWithCode = roundFormat(simpleWithCode);
22558
+ const simpleOpposite = symbol
22559
+ ? createCurrencyFormat({ symbol, position: opposite, decimalPlaces })
22560
+ : "";
22561
+ const roundedOpposite = simpleOpposite ? roundFormat(simpleOpposite) : "";
22562
+ const simpleOppositeWithCode = createCurrencyFormat({
22563
+ symbol,
22564
+ position: opposite,
22565
+ decimalPlaces,
22566
+ code,
22567
+ });
22568
+ const roundedOppositeWithCode = roundFormat(simpleOppositeWithCode);
22569
+ const formats = new Set([
22570
+ rounded,
22571
+ simple,
22572
+ roundedWithCode,
22573
+ simpleWithCode,
22574
+ roundedOpposite,
22575
+ simpleOpposite,
22576
+ roundedOppositeWithCode,
22577
+ simpleOppositeWithCode,
22578
+ ]);
22579
+ return [...formats]
22580
+ .filter((format) => format !== "")
22581
+ .map((format) => ({
22582
+ format,
22583
+ example: formatValue(1000.0, { format, locale: this.env.model.getters.getLocale() }),
22584
+ }));
22347
22585
  }
22348
22586
  get isSameFormat() {
22349
22587
  const selectedFormat = this.formatProposals[this.state.selectedFormatIndex];
@@ -22400,41 +22638,6 @@ class CustomCurrencyPanel extends owl.Component {
22400
22638
  initAvailableCurrencies() {
22401
22639
  this.state.selectedCurrencyIndex = 0;
22402
22640
  }
22403
- initProposalBases(decimalPlaces) {
22404
- const result = [{ format: "#,##0", example: "1,000" }];
22405
- const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
22406
- if (decimalRepresentation) {
22407
- result.push({
22408
- format: "#,##0" + decimalRepresentation,
22409
- example: "1,000" + decimalRepresentation,
22410
- });
22411
- }
22412
- return result;
22413
- }
22414
- createFormatProposals(proposalBases, symbol, code, position) {
22415
- let formatProposals = [];
22416
- // 1 - add proposal with symbol and without code
22417
- if (symbol) {
22418
- for (let base of proposalBases) {
22419
- formatProposals.push(this.createFormatProposal(position, base.example, base.format, symbol));
22420
- }
22421
- }
22422
- // 2 - if code exist --> add more proposal with symbol and with code
22423
- if (code) {
22424
- for (let base of proposalBases) {
22425
- const expression = (position === "after" ? " " : "") + code + " " + symbol;
22426
- formatProposals.push(this.createFormatProposal(position, base.example, base.format, expression));
22427
- }
22428
- }
22429
- return formatProposals;
22430
- }
22431
- createFormatProposal(position, baseExample, formatBase, expression) {
22432
- const formatExpression = "[$" + expression + "]";
22433
- return {
22434
- example: position === "before" ? expression + baseExample : baseExample + expression,
22435
- format: position === "before" ? formatExpression + formatBase : formatBase + formatExpression,
22436
- };
22437
- }
22438
22641
  getCommonFormat() {
22439
22642
  const selectedZones = this.env.model.getters.getSelectedZones();
22440
22643
  const sheetId = this.env.model.getters.getActiveSheetId();
@@ -23237,7 +23440,7 @@ class ContentEditableHelper {
23237
23440
  selection.removeAllRanges();
23238
23441
  selection.addRange(range);
23239
23442
  }
23240
- if (start == end && start === 0) {
23443
+ if (start === end && start === 0) {
23241
23444
  range.setStart(this.el, 0);
23242
23445
  range.setEnd(this.el, 0);
23243
23446
  }
@@ -23490,7 +23693,7 @@ class ContentEditableHelper {
23490
23693
  }, 0);
23491
23694
  }
23492
23695
  }
23493
- if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent == "") {
23696
+ if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
23494
23697
  usedCharacters++;
23495
23698
  }
23496
23699
  return usedCharacters;
@@ -25037,6 +25240,14 @@ function useInterval(callback, delay) {
25037
25240
  };
25038
25241
  }
25039
25242
 
25243
+ const CURSOR_SVG = /*xml*/ `
25244
+ <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>
25245
+ `;
25246
+ css /* scss */ `
25247
+ .o-paint-format-cursor {
25248
+ cursor: url("data:image/svg+xml,${encodeURIComponent(CURSOR_SVG)}"), auto;
25249
+ }
25250
+ `;
25040
25251
  function useCellHovered(env, gridRef, callback) {
25041
25252
  let hoveredPosition = {
25042
25253
  col: undefined,
@@ -25160,6 +25371,12 @@ class GridOverlay extends owl.Component {
25160
25371
  }
25161
25372
  return this.gridOverlay.el;
25162
25373
  }
25374
+ get style() {
25375
+ return this.props.gridOverlayDimensions;
25376
+ }
25377
+ get isPaintingFormat() {
25378
+ return this.env.model.getters.isPaintingFormat();
25379
+ }
25163
25380
  onMouseDown(ev) {
25164
25381
  if (ev.button > 0) {
25165
25382
  // not main button, probably a context menu
@@ -26337,6 +26554,9 @@ class Grid extends owl.Component {
26337
26554
  else if (this.menuState.isOpen) {
26338
26555
  this.closeMenu();
26339
26556
  }
26557
+ else if (this.env.model.getters.isPaintingFormat()) {
26558
+ this.env.model.dispatch("CANCEL_PAINT_FORMAT");
26559
+ }
26340
26560
  else {
26341
26561
  this.env.model.dispatch("CLEAN_CLIPBOARD_HIGHLIGHT");
26342
26562
  }
@@ -27539,8 +27759,8 @@ function convertXlsxFormat(numFmtId, formats, warningManager) {
27539
27759
  throw new Error("Multiple escaped blocks in format");
27540
27760
  }
27541
27761
  convertedFormat = convertedFormat.replace(/"(.*)"/g, "[$$$1]"); // replace '"..."' by '[$...]'
27542
- convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ == ignore width of next char for align purposes. Not supported ATM
27543
- convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * == repeat next character enough to fill the line. Not supported ATM
27762
+ convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ === ignore width of next char for align purposes. Not supported ATM
27763
+ convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * === repeat next character enough to fill the line. Not supported ATM
27544
27764
  convertedFormat = convertedFormat.replace(/\\ /g, " "); // unescape spaces
27545
27765
  convertedFormat = convertedFormat.replace(/\\./g, (match) => match[1]); // unescape other characters
27546
27766
  if (isXlsxDateFormat(convertedFormat)) {
@@ -28832,29 +29052,26 @@ function escapeXml(strings, ...expressions) {
28832
29052
  return new XMLString(concat(str));
28833
29053
  }
28834
29054
  /**
28835
- * Removes the namespace of all the xml tags in the string.
29055
+ * Removes the escaped namespace of all the xml tags in the string.
28836
29056
  *
28837
- * Eg. : "ns:test a" => "test a"
29057
+ * Eg. : "NAMESPACEnsNAMESPACEtest a" => "test a"
28838
29058
  */
28839
- function removeNamespaces(query) {
28840
- return query.replace(/[a-z0-9]+:(?=[a-z0-9]+)/gi, "");
29059
+ function removeTagEscapedNamespaces(tag) {
29060
+ return tag.replace(/NAMESPACE.*NAMESPACE(.*)/, "$1");
28841
29061
  }
28842
29062
  /**
28843
- * Escape the namespace's colons of all the xml tags in the string.
29063
+ * Encase the namespaces in the element's tags with NAMESPACE string
28844
29064
  *
28845
- * Eg. : "ns:test a" => "ns\\:test a"
28846
- */
28847
- function escapeNamespaces(query) {
28848
- return query.replace(/([a-z0-9]+):(?=[a-z0-9]+)/gi, "$1\\:");
28849
- }
28850
- /**
28851
- * Return true if the querySelector ignores the namespaces when searching for a tag in the DOM.
29065
+ * e.g. <x:foo> becomes <NAMESPACExNAMESPACEFoo>
28852
29066
  *
28853
- * Should return true if it's running on a browser, and false if it's running on jest (jsdom).
29067
+ * That's useful because namespaces aren't supported by the HTML specification, so it's arbitrary whether a HTML parser/querySelector
29068
+ * implementation will support namespaces in the tags or not.
28854
29069
  */
28855
- function areNamespaceIgnoredByQuerySelector() {
28856
- const doc = new DOMParser().parseFromString("<t:test xmlns:t='a'/>", "text/xml");
28857
- return doc.querySelector("test") !== null;
29070
+ function escapeTagNamespaces(str) {
29071
+ return str.replaceAll(/(<\/?)([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "$1" + "NAMESPACE" + "$2" + "NAMESPACE" + "$3");
29072
+ }
29073
+ function escapeQueryNameSpaces(query) {
29074
+ return query.replaceAll(/([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "NAMESPACE" + "$1" + "NAMESPACE" + "$2");
28858
29075
  }
28859
29076
 
28860
29077
  class AttributeValue {
@@ -28884,14 +29101,20 @@ class XlsxBaseExtractor {
28884
29101
  // The xml file we are currently parsing. We should have one Extractor class by XLSXImportFile, but
28885
29102
  // the XLSXImportFile contains both the main .xml file, and the .rels file
28886
29103
  currentFile = undefined;
28887
- // If the parser querySelector() implementation ignores tag namespaces or not
28888
- areNamespaceIgnored;
29104
+ /**
29105
+ * /!\ Important : There should be no namespaces in the tags of the XML files.
29106
+ *
29107
+ * This class use native querySelector and querySelectorAll, that's used for HTML (not XML). These aren't supposed to
29108
+ * handled namespaces, as they are not supported by the HTML specification. Some implementations (most browsers) do
29109
+ * actually support namespaces, but some don't (e.g. jsdom).
29110
+ *
29111
+ * The namespace should be escaped as with NAMESPACE string (eg. <t:foo> => <NAMESPACEtNAMESPACEfoo>).
29112
+ */
28889
29113
  constructor(rootFile, xlsxStructure, warningManager) {
28890
29114
  this.rootFile = rootFile;
28891
29115
  this.currentFile = rootFile.file.fileName;
28892
29116
  this.xlsxFileStructure = xlsxStructure;
28893
29117
  this.warningManager = warningManager;
28894
- this.areNamespaceIgnored = areNamespaceIgnoredByQuerySelector();
28895
29118
  this.relationships = {};
28896
29119
  if (rootFile.rels) {
28897
29120
  this.extractRelationships(rootFile.rels).map((rel) => {
@@ -29117,29 +29340,13 @@ class XlsxBaseExtractor {
29117
29340
  throw new Error("Cannot find target file");
29118
29341
  return f;
29119
29342
  }
29120
- /**
29121
- * Wrapper of querySelector, but we'll remove the namespaces from the query if areNamespacesIgnored is true.
29122
- *
29123
- * Why we need to do this :
29124
- * - For an XML "<t:test />"
29125
- * - on Jest(jsdom) : xml.querySelector("test") == null, xml.querySelector("t\\:test") == <t:test />
29126
- * - on Browser : xml.querySelector("test") == <t:test />, xml.querySelector("t\\:test") == null
29127
- */
29128
29343
  querySelector(element, query) {
29129
- query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);
29130
- return element.querySelector(query);
29344
+ const escapedQuery = escapeQueryNameSpaces(query);
29345
+ return element.querySelector(escapedQuery);
29131
29346
  }
29132
- /**
29133
- * Wrapper of querySelectorAll, but we'll remove the namespaces from the query if areNamespacesIgnored is true.
29134
- *
29135
- * Why we need to do this :
29136
- * - For an XML "<t:test />"
29137
- * - on Jest(jsdom) : xml.querySelectorAll("test") == [], xml.querySelectorAll("t\\:test") == [<t:test />]
29138
- * - on Browser : xml.querySelectorAll("test") == [<t:test />], xml.querySelectorAll("t\\:test") == []
29139
- */
29140
29347
  querySelectorAll(element, query) {
29141
- query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query);
29142
- return element.querySelectorAll(query);
29348
+ const escapedQuery = escapeQueryNameSpaces(query);
29349
+ return element.querySelectorAll(escapedQuery);
29143
29350
  }
29144
29351
  /**
29145
29352
  * Get a color from its id in the Theme's colorScheme.
@@ -29379,7 +29586,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
29379
29586
  throw new Error("Missing plot area in the chart definition.");
29380
29587
  }
29381
29588
  for (let child of plotAreaElement.children) {
29382
- const tag = removeNamespaces(child.tagName);
29589
+ const tag = removeTagEscapedNamespaces(child.tagName);
29383
29590
  if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
29384
29591
  return tag;
29385
29592
  }
@@ -29391,7 +29598,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
29391
29598
  class XlsxFigureExtractor extends XlsxBaseExtractor {
29392
29599
  extractFigures() {
29393
29600
  return this.mapOnElements({ parent: this.rootFile.file.xml, query: "xdr:wsDr", children: true }, (figureElement) => {
29394
- const anchorType = removeNamespaces(figureElement.tagName);
29601
+ const anchorType = removeTagEscapedNamespaces(figureElement.tagName);
29395
29602
  if (anchorType !== "twoCellAnchor") {
29396
29603
  throw new Error("Only twoCellAnchor are supported for xlsx drawings.");
29397
29604
  }
@@ -30025,7 +30232,8 @@ class XlsxReader {
30025
30232
  for (let key of Object.keys(files)) {
30026
30233
  // Random files can be in xlsx (like a bin file for printer settings)
30027
30234
  if (key.endsWith(".xml") || key.endsWith(".rels")) {
30028
- this.xmls[key] = parseXML(new XMLString(files[key]));
30235
+ const contentString = escapeTagNamespaces(files[key]);
30236
+ this.xmls[key] = parseXML(new XMLString(contentString));
30029
30237
  }
30030
30238
  else if (key.includes("media/image")) {
30031
30239
  this.images.push({
@@ -31092,7 +31300,7 @@ class BordersPlugin extends CorePlugin {
31092
31300
  }
31093
31301
  /**
31094
31302
  * Set the borders of a cell.
31095
- * It overrides the current border if override == true.
31303
+ * It overrides the current border if override === true.
31096
31304
  */
31097
31305
  setBorder(sheetId, col, row, border, override = true) {
31098
31306
  if (override || !this.borders?.[sheetId]?.[col]?.[row]?.vertical) {
@@ -32362,7 +32570,9 @@ class FigurePlugin extends CorePlugin {
32362
32570
  const numHeader = this.getters.getNumberRows(sheetId);
32363
32571
  let gridHeight = 0;
32364
32572
  for (let i = 0; i < numHeader; i++) {
32365
- gridHeight += this.getters.getRowSize(sheetId, i);
32573
+ // TODO : since the row size is an UI value now, this doesn't work anymore. Using the default cell height is
32574
+ // a temporary solution at best, but is broken.
32575
+ gridHeight += this.getters.getUserRowSize(sheetId, i) || DEFAULT_CELL_HEIGHT;
32366
32576
  }
32367
32577
  const figures = this.getters.getFigures(sheetId);
32368
32578
  for (const figure of figures) {
@@ -32788,33 +32998,15 @@ class FiltersPlugin extends CorePlugin {
32788
32998
  }
32789
32999
 
32790
33000
  class HeaderSizePlugin extends CorePlugin {
32791
- static getters = ["getRowSize", "getColSize"];
33001
+ static getters = ["getUserRowSize", "getColSize"];
32792
33002
  sizes = {};
32793
33003
  handle(cmd) {
32794
33004
  switch (cmd.type) {
32795
33005
  case "CREATE_SHEET": {
32796
- const computedSizes = this.computeSheetSizes(cmd.sheetId);
32797
- const sizes = {
32798
- COL: computedSizes.COL.map((size) => ({
32799
- manualSize: undefined,
32800
- computedSize: lazy(size),
32801
- })),
32802
- ROW: computedSizes.ROW.map((size) => ({
32803
- manualSize: undefined,
32804
- computedSize: lazy(size),
32805
- })),
32806
- };
32807
- this.history.update("sizes", cmd.sheetId, sizes);
33006
+ this.history.update("sizes", cmd.sheetId, { COL: [], ROW: [] });
32808
33007
  break;
32809
33008
  }
32810
33009
  case "DUPLICATE_SHEET":
32811
- // make sure the values are computed in case the original sheet is deleted
32812
- for (const row of this.sizes[cmd.sheetId].ROW) {
32813
- row.computedSize();
32814
- }
32815
- for (const col of this.sizes[cmd.sheetId].COL) {
32816
- col.computedSize();
32817
- }
32818
33010
  this.history.update("sizes", cmd.sheetIdTo, deepCopy(this.sizes[cmd.sheetId]));
32819
33011
  break;
32820
33012
  case "DELETE_SHEET":
@@ -32823,21 +33015,8 @@ class HeaderSizePlugin extends CorePlugin {
32823
33015
  this.history.update("sizes", sizes);
32824
33016
  break;
32825
33017
  case "REMOVE_COLUMNS_ROWS": {
32826
- let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]];
32827
- for (let headerIndex of [...cmd.elements].sort((a, b) => b - a)) {
32828
- sizes.splice(headerIndex, 1);
32829
- }
32830
- const min = Math.min(...cmd.elements);
32831
- sizes = sizes.map((size, row) => {
32832
- if (cmd.dimension === "ROW" && row >= min) {
32833
- // invalidate sizes
32834
- return {
32835
- manualSize: size.manualSize,
32836
- computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),
32837
- };
32838
- }
32839
- return size;
32840
- });
33018
+ const arr = this.sizes[cmd.sheetId][cmd.dimension];
33019
+ const sizes = removeIndexesFromArray(arr, cmd.elements);
32841
33020
  this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes);
32842
33021
  break;
32843
33022
  }
@@ -32846,51 +33025,18 @@ class HeaderSizePlugin extends CorePlugin {
32846
33025
  const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
32847
33026
  const baseSize = sizes[cmd.base];
32848
33027
  sizes.splice(addIndex, 0, ...Array(cmd.quantity).fill(baseSize));
32849
- sizes = sizes.map((size, row) => {
32850
- if (cmd.dimension === "ROW" && row > cmd.base + cmd.quantity) {
32851
- // invalidate sizes
32852
- return {
32853
- manualSize: size.manualSize,
32854
- computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)),
32855
- };
32856
- }
32857
- return size;
32858
- });
32859
33028
  this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes);
32860
33029
  break;
32861
33030
  }
32862
33031
  case "RESIZE_COLUMNS_ROWS":
32863
- for (let el of cmd.elements) {
32864
- if (cmd.dimension === "ROW") {
32865
- const height = this.getRowTallestCellSize(cmd.sheetId, el);
32866
- const size = height;
32867
- this.history.update("sizes", cmd.sheetId, cmd.dimension, el, {
32868
- manualSize: cmd.size || undefined,
32869
- computedSize: lazy(size),
32870
- });
32871
- }
32872
- else {
32873
- this.history.update("sizes", cmd.sheetId, cmd.dimension, el, {
32874
- manualSize: cmd.size || undefined,
32875
- computedSize: lazy(cmd.size || DEFAULT_CELL_WIDTH),
32876
- });
33032
+ if (cmd.dimension === "ROW") {
33033
+ for (const el of cmd.elements) {
33034
+ this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);
32877
33035
  }
32878
33036
  }
32879
- break;
32880
- case "UPDATE_CELL":
32881
- if (!this.sizes[cmd.sheetId]?.["ROW"]?.[cmd.row]?.manualSize) {
32882
- const { sheetId, row } = cmd;
32883
- this.history.update("sizes", sheetId, "ROW", row, "computedSize", lazy(() => this.getRowTallestCellSize(sheetId, row)));
32884
- }
32885
- break;
32886
- case "ADD_MERGE":
32887
- case "REMOVE_MERGE":
32888
- for (let target of cmd.target) {
32889
- for (let row of range(target.top, target.bottom + 1)) {
32890
- const rowHeight = this.getRowTallestCellSize(cmd.sheetId, row);
32891
- if (rowHeight !== this.getRowSize(cmd.sheetId, row)) {
32892
- this.history.update("sizes", cmd.sheetId, "ROW", row, "computedSize", lazy(rowHeight));
32893
- }
33037
+ else {
33038
+ for (const el of cmd.elements) {
33039
+ this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);
32894
33040
  }
32895
33041
  }
32896
33042
  break;
@@ -32898,94 +33044,29 @@ class HeaderSizePlugin extends CorePlugin {
32898
33044
  return;
32899
33045
  }
32900
33046
  getColSize(sheetId, index) {
32901
- return this.getHeaderSize(sheetId, "COL", index);
32902
- }
32903
- getRowSize(sheetId, index) {
32904
- return this.getHeaderSize(sheetId, "ROW", index);
33047
+ return Math.round(this.sizes[sheetId]?.["COL"][index] || DEFAULT_CELL_WIDTH);
32905
33048
  }
32906
- getHeaderSize(sheetId, dimension, index) {
32907
- return Math.round(this.sizes[sheetId]?.[dimension][index]?.manualSize ||
32908
- this.sizes[sheetId]?.[dimension][index]?.computedSize() ||
32909
- this.getDefaultHeaderSize(dimension));
32910
- }
32911
- computeSheetSizes(sheetId) {
32912
- const sizes = { COL: [], ROW: [] };
32913
- for (let col of range(0, this.getters.getNumberCols(sheetId))) {
32914
- sizes.COL.push(this.getHeaderSize(sheetId, "COL", col));
32915
- }
32916
- for (let row of range(0, this.getters.getNumberRows(sheetId))) {
32917
- let rowSize = this.sizes[sheetId]?.["ROW"]?.[row].manualSize;
32918
- if (!rowSize) {
32919
- const height = this.getRowTallestCellSize(sheetId, row);
32920
- rowSize = height;
32921
- }
32922
- sizes.ROW.push(rowSize);
32923
- }
32924
- return sizes;
32925
- }
32926
- getDefaultHeaderSize(dimension) {
32927
- return dimension === "COL" ? DEFAULT_CELL_WIDTH : DEFAULT_CELL_HEIGHT;
32928
- }
32929
- /**
32930
- * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row
32931
- * merge, or the height of the cell computed based on its font size.
32932
- */
32933
- getCellHeight(position) {
32934
- const merge = this.getters.getMerge(position);
32935
- if (merge && merge.bottom !== merge.top) {
32936
- return DEFAULT_CELL_HEIGHT;
32937
- }
32938
- const cell = this.getters.getCell(position);
32939
- return getDefaultCellHeight(cell);
32940
- }
32941
- /**
32942
- * Get the tallest cell of a row and its size.
32943
- *
32944
- * The tallest cell of the row correspond to the cell with the biggest font size,
32945
- * and that is not part of a multi-line merge.
32946
- */
32947
- getRowTallestCellSize(sheetId, row) {
32948
- const cellIds = this.getters.getRowCells(sheetId, row);
32949
- let maxHeight = 0;
32950
- for (let i = 0; i < cellIds.length; i++) {
32951
- const cell = this.getters.getCellById(cellIds[i]);
32952
- if (!cell)
32953
- continue;
32954
- const position = this.getters.getCellPosition(cell.id);
32955
- const cellHeight = this.getCellHeight(position);
32956
- if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {
32957
- maxHeight = cellHeight;
32958
- }
32959
- }
32960
- if (maxHeight <= DEFAULT_CELL_HEIGHT) {
32961
- return DEFAULT_CELL_HEIGHT;
32962
- }
32963
- return maxHeight;
33049
+ getUserRowSize(sheetId, index) {
33050
+ const rowSize = this.sizes[sheetId]?.["ROW"][index];
33051
+ return rowSize ? Math.round(rowSize) : undefined;
32964
33052
  }
32965
33053
  import(data) {
32966
33054
  for (let sheet of data.sheets) {
32967
- const manualSizes = { COL: [], ROW: [] };
33055
+ const sizes = {
33056
+ COL: Array(sheet.colNumber).fill(undefined),
33057
+ ROW: Array(sheet.rowNumber).fill(undefined),
33058
+ };
32968
33059
  for (let [rowIndex, row] of Object.entries(sheet.rows)) {
32969
33060
  if (row.size) {
32970
- manualSizes["ROW"][rowIndex] = row.size;
33061
+ sizes["ROW"][rowIndex] = row.size;
32971
33062
  }
32972
33063
  }
32973
33064
  for (let [colIndex, col] of Object.entries(sheet.cols)) {
32974
33065
  if (col.size) {
32975
- manualSizes["COL"][colIndex] = col.size;
32976
- }
32977
- }
32978
- const computedSizes = this.computeSheetSizes(sheet.id);
32979
- this.sizes[sheet.id] = {
32980
- COL: computedSizes.COL.map((size, i) => ({
32981
- manualSize: manualSizes.COL[i],
32982
- computedSize: lazy(size),
32983
- })),
32984
- ROW: computedSizes.ROW.map((size, i) => ({
32985
- manualSize: manualSizes.ROW[i],
32986
- computedSize: lazy(size),
32987
- })),
32988
- };
33066
+ sizes["COL"][colIndex] = col.size;
33067
+ }
33068
+ }
33069
+ this.sizes[sheet.id] = sizes;
32989
33070
  }
32990
33071
  return;
32991
33072
  }
@@ -33006,9 +33087,12 @@ class HeaderSizePlugin extends CorePlugin {
33006
33087
  if (sheet.rows === undefined) {
33007
33088
  sheet.rows = {};
33008
33089
  }
33009
- for (let row of range(0, this.getters.getNumberRows(sheet.id))) {
33010
- if (exportDefaults || this.sizes[sheet.id]["ROW"][row]?.manualSize) {
33011
- sheet.rows[row] = { ...sheet.rows[row], size: this.getRowSize(sheet.id, row) };
33090
+ for (const row of range(0, this.getters.getNumberRows(sheet.id))) {
33091
+ if (exportDefaults || this.sizes[sheet.id]["ROW"][row]) {
33092
+ sheet.rows[row] = {
33093
+ ...sheet.rows[row],
33094
+ size: this.getUserRowSize(sheet.id, row) ?? DEFAULT_CELL_HEIGHT,
33095
+ };
33012
33096
  }
33013
33097
  }
33014
33098
  // Export col sizes
@@ -33016,7 +33100,7 @@ class HeaderSizePlugin extends CorePlugin {
33016
33100
  sheet.cols = {};
33017
33101
  }
33018
33102
  for (let col of range(0, this.getters.getNumberCols(sheet.id))) {
33019
- if (exportDefaults || this.sizes[sheet.id]["COL"][col]?.manualSize) {
33103
+ if (exportDefaults || this.sizes[sheet.id]["COL"][col]) {
33020
33104
  sheet.cols[col] = { ...sheet.cols[col], size: this.getColSize(sheet.id, col) };
33021
33105
  }
33022
33106
  }
@@ -36572,7 +36656,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36572
36656
  case "ContainsText":
36573
36657
  return cell.value.toString().indexOf(values[0].toString()) > -1;
36574
36658
  case "NotContains":
36575
- return !cell.value || cell.value.toString().indexOf(values[0].toString()) == -1;
36659
+ return !cell.value || cell.value.toString().indexOf(values[0].toString()) === -1;
36576
36660
  case "GreaterThan":
36577
36661
  return cell.value > values[0];
36578
36662
  case "GreaterThanOrEqual":
@@ -36599,6 +36683,156 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36599
36683
  };
36600
36684
  }
36601
36685
 
36686
+ class UIRowSizePlugin extends UIPlugin {
36687
+ static getters = ["getRowSize"];
36688
+ tallestCellInRow = {};
36689
+ ctx = document.createElement("canvas").getContext("2d");
36690
+ handle(cmd) {
36691
+ switch (cmd.type) {
36692
+ case "START":
36693
+ for (const sheetId of this.getters.getSheetIds()) {
36694
+ this.initializeSheet(sheetId);
36695
+ }
36696
+ break;
36697
+ case "CREATE_SHEET": {
36698
+ this.initializeSheet(cmd.sheetId);
36699
+ break;
36700
+ }
36701
+ case "DUPLICATE_SHEET": {
36702
+ const tallestCells = deepCopy(this.tallestCellInRow[cmd.sheetId]);
36703
+ this.history.update("tallestCellInRow", cmd.sheetIdTo, tallestCells);
36704
+ break;
36705
+ }
36706
+ case "DELETE_SHEET":
36707
+ const tallestCells = { ...this.tallestCellInRow };
36708
+ delete tallestCells[cmd.sheetId];
36709
+ this.history.update("tallestCellInRow", tallestCells);
36710
+ break;
36711
+ case "REMOVE_COLUMNS_ROWS": {
36712
+ if (cmd.dimension === "COL") {
36713
+ return;
36714
+ }
36715
+ const tallestCells = removeIndexesFromArray(this.tallestCellInRow[cmd.sheetId], cmd.elements);
36716
+ this.history.update("tallestCellInRow", cmd.sheetId, tallestCells);
36717
+ break;
36718
+ }
36719
+ case "ADD_COLUMNS_ROWS": {
36720
+ if (cmd.dimension === "COL") {
36721
+ return;
36722
+ }
36723
+ const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
36724
+ const newCells = Array(cmd.quantity).fill(undefined);
36725
+ const newTallestCells = insertItemsAtIndex(this.tallestCellInRow[cmd.sheetId], newCells, addIndex);
36726
+ this.history.update("tallestCellInRow", cmd.sheetId, newTallestCells);
36727
+ break;
36728
+ }
36729
+ case "RESIZE_COLUMNS_ROWS":
36730
+ {
36731
+ const sheetId = cmd.sheetId;
36732
+ if (cmd.dimension === "ROW") {
36733
+ for (const row of cmd.elements) {
36734
+ const tallestCell = this.getRowTallestCell(sheetId, row);
36735
+ this.history.update("tallestCellInRow", sheetId, row, tallestCell);
36736
+ }
36737
+ }
36738
+ else {
36739
+ // Recompute row heights on col size change, they might have changed because of wrapped text
36740
+ for (const row of range(0, this.getters.getNumberRows(sheetId))) {
36741
+ for (const col of cmd.elements) {
36742
+ this.updateRowSizeForCellChange(sheetId, row, col);
36743
+ }
36744
+ }
36745
+ }
36746
+ }
36747
+ break;
36748
+ case "UPDATE_CELL":
36749
+ this.updateRowSizeForCellChange(cmd.sheetId, cmd.row, cmd.col);
36750
+ break;
36751
+ case "ADD_MERGE":
36752
+ case "REMOVE_MERGE":
36753
+ for (const target of cmd.target) {
36754
+ for (const position of positions(target)) {
36755
+ this.updateRowSizeForCellChange(cmd.sheetId, position.row, position.col);
36756
+ }
36757
+ }
36758
+ }
36759
+ return;
36760
+ }
36761
+ getRowSize(sheetId, row) {
36762
+ return Math.round(this.getters.getUserRowSize(sheetId, row) ??
36763
+ this.tallestCellInRow[sheetId][row]?.size ??
36764
+ DEFAULT_CELL_HEIGHT);
36765
+ }
36766
+ updateRowSizeForCellChange(sheetId, row, col) {
36767
+ const tallestCellInRow = this.tallestCellInRow[sheetId]?.[row];
36768
+ if (tallestCellInRow?.cell.col === col) {
36769
+ const newTallestCell = this.getRowTallestCell(sheetId, row);
36770
+ this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
36771
+ }
36772
+ const updatedCellHeight = this.getCellHeight({ sheetId, col, row });
36773
+ if (updatedCellHeight <= DEFAULT_CELL_HEIGHT) {
36774
+ return;
36775
+ }
36776
+ if ((!tallestCellInRow && updatedCellHeight > DEFAULT_CELL_HEIGHT) ||
36777
+ (tallestCellInRow && updatedCellHeight > tallestCellInRow.size)) {
36778
+ const newTallestCell = { cell: { sheetId, col, row }, size: updatedCellHeight };
36779
+ this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
36780
+ }
36781
+ }
36782
+ initializeSheet(sheetId) {
36783
+ const tallestCells = [];
36784
+ for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
36785
+ const tallestCell = this.getRowTallestCell(sheetId, row);
36786
+ tallestCells.push(tallestCell);
36787
+ }
36788
+ this.history.update("tallestCellInRow", sheetId, tallestCells);
36789
+ }
36790
+ /**
36791
+ * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row
36792
+ * merge, or the height of the cell computed based on its style/content.
36793
+ */
36794
+ getCellHeight(position) {
36795
+ if (this.isInMultiRowMerge(position)) {
36796
+ return DEFAULT_CELL_HEIGHT;
36797
+ }
36798
+ const cell = this.getters.getCell(position);
36799
+ const colSize = this.getters.getColSize(position.sheetId, position.col);
36800
+ return getDefaultCellHeight(this.ctx, cell, colSize);
36801
+ }
36802
+ isInMultiRowMerge(position) {
36803
+ const merge = this.getters.getMerge(position);
36804
+ return !!merge && merge.bottom !== merge.top;
36805
+ }
36806
+ /**
36807
+ * Get the tallest cell of a row and its size.
36808
+ */
36809
+ getRowTallestCell(sheetId, row) {
36810
+ const userRowSize = this.getters.getUserRowSize(sheetId, row);
36811
+ if (userRowSize !== undefined) {
36812
+ return undefined;
36813
+ }
36814
+ const cellIds = this.getters.getRowCells(sheetId, row);
36815
+ let maxHeight = 0;
36816
+ let tallestCell = undefined;
36817
+ for (let i = 0; i < cellIds.length; i++) {
36818
+ const cell = this.getters.getCellById(cellIds[i]);
36819
+ if (!cell) {
36820
+ continue;
36821
+ }
36822
+ const position = this.getters.getCellPosition(cell.id);
36823
+ const cellHeight = this.getCellHeight(position);
36824
+ if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {
36825
+ maxHeight = cellHeight;
36826
+ tallestCell = { cell: position, size: cellHeight };
36827
+ }
36828
+ }
36829
+ if (tallestCell && tallestCell.size > DEFAULT_CELL_HEIGHT) {
36830
+ return tallestCell;
36831
+ }
36832
+ return undefined;
36833
+ }
36834
+ }
36835
+
36602
36836
  /**
36603
36837
  * This plugin manage the autofill.
36604
36838
  *
@@ -40032,10 +40266,8 @@ class SortPlugin extends UIPlugin {
40032
40266
  }
40033
40267
  updateCellCommands.push(newCellValues);
40034
40268
  }
40035
- for (const cmd of updateCellCommands) {
40036
- this.dispatch("UPDATE_CELL", cmd);
40037
- }
40038
40269
  }
40270
+ updateCellCommands.forEach((cmdPayload) => this.dispatch("UPDATE_CELL", cmdPayload));
40039
40271
  }
40040
40272
  /**
40041
40273
  * Return the distances between main merge cells in the zone.
@@ -40136,24 +40368,28 @@ class SheetUIPlugin extends UIPlugin {
40136
40368
  // Getters
40137
40369
  // ---------------------------------------------------------------------------
40138
40370
  getCellWidth(position) {
40139
- const text = this.getCellText(position);
40140
40371
  const style = this.getters.getCellComputedStyle(position);
40141
- const multiLineText = text.split(NEWLINE);
40142
- let contentWidth = Math.max(...multiLineText.map((line) => this.getTextWidth(line, style)));
40372
+ let contentWidth = 0;
40373
+ const content = this.getters.getEvaluatedCell(position).formattedValue;
40374
+ if (content) {
40375
+ const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);
40376
+ contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));
40377
+ }
40143
40378
  const icon = this.getters.getConditionalIcon(position);
40144
40379
  if (icon) {
40145
- contentWidth += computeIconWidth(this.getters.getCellStyle(position));
40380
+ contentWidth += computeIconWidth(style);
40146
40381
  }
40147
40382
  const isFilterHeader = this.getters.isFilterHeader(position);
40148
40383
  if (isFilterHeader) {
40149
40384
  contentWidth += ICON_EDGE_LENGTH + FILTER_ICON_MARGIN;
40150
40385
  }
40151
- if (contentWidth > 0) {
40152
- contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;
40153
- if (this.getters.getCellStyle(position).wrapping === "wrap") {
40154
- const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);
40155
- return Math.min(colWidth, contentWidth);
40156
- }
40386
+ if (contentWidth === 0) {
40387
+ return 0;
40388
+ }
40389
+ contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;
40390
+ if (style.wrapping === "wrap") {
40391
+ const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);
40392
+ return Math.min(colWidth, contentWidth);
40157
40393
  }
40158
40394
  return contentWidth;
40159
40395
  }
@@ -40176,56 +40412,7 @@ class SheetUIPlugin extends UIPlugin {
40176
40412
  getCellMultiLineText(position, width) {
40177
40413
  const style = this.getters.getCellStyle(position);
40178
40414
  const text = this.getters.getCellText(position, this.getters.shouldShowFormulas());
40179
- const brokenText = [];
40180
- for (const line of text.split("\n")) {
40181
- const words = line.split(" ");
40182
- if (!width) {
40183
- brokenText.push(line);
40184
- continue;
40185
- }
40186
- let textLine = "";
40187
- let availableWidth = width;
40188
- for (let word of words) {
40189
- const splitWord = this.splitWordToSpecificWidth(this.ctx, word, width, style);
40190
- const lastPart = splitWord.pop();
40191
- const lastPartWidth = computeTextWidth(this.ctx, lastPart, style);
40192
- // At this step: "splitWord" is an array composed of parts of word whose
40193
- // length is at most equal to "width".
40194
- // Last part contains the end of the word.
40195
- // Note that: When word length is less than width, then lastPart is equal
40196
- // to word and splitWord is empty
40197
- if (splitWord.length) {
40198
- if (textLine !== "") {
40199
- brokenText.push(textLine);
40200
- textLine = "";
40201
- availableWidth = width;
40202
- }
40203
- splitWord.forEach((wordPart) => {
40204
- brokenText.push(wordPart);
40205
- });
40206
- textLine = lastPart;
40207
- availableWidth = width - lastPartWidth;
40208
- }
40209
- else {
40210
- // here "lastPart" is equal to "word" and the "word" size is smaller than "width"
40211
- const _word = textLine === "" ? lastPart : " " + lastPart;
40212
- const wordWidth = computeTextWidth(this.ctx, _word, style);
40213
- if (wordWidth <= availableWidth) {
40214
- textLine += _word;
40215
- availableWidth -= wordWidth;
40216
- }
40217
- else {
40218
- brokenText.push(textLine);
40219
- textLine = lastPart;
40220
- availableWidth = width - lastPartWidth;
40221
- }
40222
- }
40223
- }
40224
- if (textLine !== "") {
40225
- brokenText.push(textLine);
40226
- }
40227
- }
40228
- return brokenText;
40415
+ return splitTextToWidth(this.ctx, text, style, width);
40229
40416
  }
40230
40417
  /**
40231
40418
  * Returns the size, start and end coordinates of a column on an unfolded sheet
@@ -40284,26 +40471,6 @@ class SheetUIPlugin extends UIPlugin {
40284
40471
  const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
40285
40472
  return Math.max(0, ...sizes);
40286
40473
  }
40287
- splitWordToSpecificWidth(ctx, word, width, style) {
40288
- const wordWidth = computeTextWidth(ctx, word, style);
40289
- if (wordWidth <= width) {
40290
- return [word];
40291
- }
40292
- const splitWord = [];
40293
- let wordPart = "";
40294
- for (let l of word) {
40295
- const wordPartWidth = computeTextWidth(ctx, wordPart + l, style);
40296
- if (wordPartWidth > width) {
40297
- splitWord.push(wordPart);
40298
- wordPart = l;
40299
- }
40300
- else {
40301
- wordPart += l;
40302
- }
40303
- }
40304
- splitWord.push(wordPart);
40305
- return splitWord;
40306
- }
40307
40474
  /**
40308
40475
  * Check that any "sheetId" in the command matches an existing
40309
40476
  * sheet.
@@ -41295,7 +41462,7 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
41295
41462
  .join("\n") || "\t");
41296
41463
  }
41297
41464
  getHTMLContent() {
41298
- if (this.cells.length == 1 && this.cells[0].length == 1) {
41465
+ if (this.cells.length === 1 && this.cells[0].length === 1) {
41299
41466
  return this.getters.getCellText(this.cells[0][0].position);
41300
41467
  }
41301
41468
  let htmlTable = '<table border="1" style="border-collapse:collapse">';
@@ -41620,7 +41787,7 @@ class ClipboardPlugin extends UIPlugin {
41620
41787
  status = "invisible";
41621
41788
  state;
41622
41789
  lastPasteState;
41623
- _isPaintingFormat = false;
41790
+ paintFormatStatus = "inactive";
41624
41791
  originSheetId;
41625
41792
  // ---------------------------------------------------------------------------
41626
41793
  // Command Handling
@@ -41635,7 +41802,7 @@ class ClipboardPlugin extends UIPlugin {
41635
41802
  if (!this.state) {
41636
41803
  return 24 /* CommandResult.EmptyClipboard */;
41637
41804
  }
41638
- const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "onlyFormat" : undefined);
41805
+ const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
41639
41806
  return this.state.isPasteAllowed(cmd.target, { pasteOption });
41640
41807
  case "PASTE_FROM_OS_CLIPBOARD": {
41641
41808
  const state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection);
@@ -41651,6 +41818,11 @@ class ClipboardPlugin extends UIPlugin {
41651
41818
  const state = this.getClipboardStateForCopyCells(cut, "CUT");
41652
41819
  return state.isPasteAllowed(paste);
41653
41820
  }
41821
+ case "ACTIVATE_PAINT_FORMAT": {
41822
+ if (this.paintFormatStatus !== "inactive") {
41823
+ return 95 /* CommandResult.AlreadyInPaintingFormatMode */;
41824
+ }
41825
+ }
41654
41826
  }
41655
41827
  return 0 /* CommandResult.Success */;
41656
41828
  }
@@ -41667,11 +41839,13 @@ class ClipboardPlugin extends UIPlugin {
41667
41839
  if (!this.state) {
41668
41840
  break;
41669
41841
  }
41670
- const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "onlyFormat" : undefined);
41671
- this._isPaintingFormat = false;
41842
+ const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
41672
41843
  this.state.paste(cmd.target, { pasteOption, shouldPasteCF: true, selectTarget: true });
41673
41844
  this.lastPasteState = this.state;
41674
- this.status = "invisible";
41845
+ if (this.paintFormatStatus === "oneOff") {
41846
+ this.paintFormatStatus = "inactive";
41847
+ this.status = "invisible";
41848
+ }
41675
41849
  break;
41676
41850
  case "CLEAN_CLIPBOARD_HIGHLIGHT":
41677
41851
  this.status = "invisible";
@@ -41739,8 +41913,13 @@ class ClipboardPlugin extends UIPlugin {
41739
41913
  case "ACTIVATE_PAINT_FORMAT": {
41740
41914
  const zones = this.getters.getSelectedZones();
41741
41915
  this.state = this.getClipboardStateForCopyCells(zones, "COPY");
41742
- this._isPaintingFormat = true;
41743
41916
  this.status = "visible";
41917
+ if (cmd.persistent) {
41918
+ this.paintFormatStatus = "persistent";
41919
+ }
41920
+ else {
41921
+ this.paintFormatStatus = "oneOff";
41922
+ }
41744
41923
  break;
41745
41924
  }
41746
41925
  case "DELETE_SHEET":
@@ -41752,6 +41931,11 @@ class ClipboardPlugin extends UIPlugin {
41752
41931
  this.status = "invisible";
41753
41932
  }
41754
41933
  break;
41934
+ case "CANCEL_PAINT_FORMAT": {
41935
+ this.paintFormatStatus = "inactive";
41936
+ this.status = "invisible";
41937
+ break;
41938
+ }
41755
41939
  default:
41756
41940
  if (isCoreCommand(cmd)) {
41757
41941
  this.status = "invisible";
@@ -41782,7 +41966,7 @@ class ClipboardPlugin extends UIPlugin {
41782
41966
  return this.state ? this.state.operation === "CUT" : false;
41783
41967
  }
41784
41968
  isPaintingFormat() {
41785
- return this._isPaintingFormat;
41969
+ return this.paintFormatStatus !== "inactive";
41786
41970
  }
41787
41971
  // ---------------------------------------------------------------------------
41788
41972
  // Private methods
@@ -44417,6 +44601,7 @@ const coreViewsPluginRegistry = new Registry()
44417
44601
  .add("evaluation", EvaluationPlugin)
44418
44602
  .add("evaluation_chart", EvaluationChartPlugin)
44419
44603
  .add("evaluation_cf", EvaluationConditionalFormatPlugin)
44604
+ .add("row_size", UIRowSizePlugin)
44420
44605
  .add("custom_colors", CustomColorsPlugin);
44421
44606
 
44422
44607
  const clickableCellRegistry = new Registry();
@@ -45672,11 +45857,11 @@ class ActionButton extends owl.Component {
45672
45857
  }
45673
45858
  get title() {
45674
45859
  const name = this.actionButton.name(this.env);
45675
- const description = this.actionButton.description;
45860
+ const description = this.actionButton.description(this.env);
45676
45861
  return name + (description ? ` (${description})` : "");
45677
45862
  }
45678
45863
  get iconTitle() {
45679
- return this.actionButton.icon;
45864
+ return this.actionButton.icon(this.env);
45680
45865
  }
45681
45866
  onClick(ev) {
45682
45867
  if (this.isEnabled) {
@@ -46061,6 +46246,27 @@ FontSizeEditor.props = {
46061
46246
  class: String,
46062
46247
  };
46063
46248
 
46249
+ class PaintFormatButton extends owl.Component {
46250
+ static template = "o-spreadsheet-PaintFormatButton";
46251
+ get isActive() {
46252
+ return this.env.model.getters.isPaintingFormat();
46253
+ }
46254
+ onDblClick() {
46255
+ this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: true });
46256
+ }
46257
+ togglePaintFormat() {
46258
+ if (this.isActive) {
46259
+ this.env.model.dispatch("CANCEL_PAINT_FORMAT");
46260
+ }
46261
+ else {
46262
+ this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: false });
46263
+ }
46264
+ }
46265
+ }
46266
+ PaintFormatButton.props = {
46267
+ class: { type: String, optional: true },
46268
+ };
46269
+
46064
46270
  // -----------------------------------------------------------------------------
46065
46271
  // TopBar
46066
46272
  // -----------------------------------------------------------------------------
@@ -46161,6 +46367,7 @@ class TopBar extends owl.Component {
46161
46367
  TopBarComposer,
46162
46368
  FontSizeEditor,
46163
46369
  ActionButton,
46370
+ PaintFormatButton,
46164
46371
  BorderEditorWidget,
46165
46372
  };
46166
46373
  state = owl.useState({
@@ -48013,7 +48220,7 @@ class SelectionStreamProcessorImpl {
48013
48220
  : "nextCluster";
48014
48221
  while (true) {
48015
48222
  const nextCellPosition = this.getNextCellPosition(currentPosition, dim, dir);
48016
- // Break if nextPosition == currentPosition, which happens if there's no next valid position
48223
+ // Break if nextPosition === currentPosition, which happens if there's no next valid position
48017
48224
  if (currentPosition.col === nextCellPosition.col &&
48018
48225
  currentPosition.row === nextCellPosition.row) {
48019
48226
  break;
@@ -49927,6 +50134,7 @@ class Model extends EventBus {
49927
50134
  ...config,
49928
50135
  mode: config.mode || "normal",
49929
50136
  custom: config.custom || {},
50137
+ defaultCurrencyFormat: config.defaultCurrencyFormat || "[$$]#,##0.00",
49930
50138
  external: this.setupExternalConfig(config.external || {}),
49931
50139
  transportService,
49932
50140
  client,
@@ -50224,6 +50432,7 @@ const helpers = {
50224
50432
  numberToLetters,
50225
50433
  UuidGenerator,
50226
50434
  formatValue,
50435
+ createCurrencyFormat,
50227
50436
  computeTextWidth,
50228
50437
  createEmptyWorkbookData,
50229
50438
  createEmptySheet,
@@ -50307,6 +50516,6 @@ exports.setTranslationMethod = setTranslationMethod;
50307
50516
  exports.tokenize = tokenize;
50308
50517
 
50309
50518
 
50310
- __info__.version = '16.4.0-alpha.7';
50311
- __info__.date = '2023-07-04T13:46:24.130Z';
50312
- __info__.hash = 'b04cf78';
50519
+ __info__.version = '16.5.0-alpha.0';
50520
+ __info__.date = '2023-07-24T06:48:58.196Z';
50521
+ __info__.hash = '3ba2c3c';