@odoo/o-spreadsheet 18.1.0-alpha.4 → 18.1.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.1.0-alpha.4
6
- * @date 2024-11-13T15:06:47.769Z
7
- * @hash e1ad985
5
+ * @version 18.1.0-alpha.5
6
+ * @date 2024-11-22T14:22:50.899Z
7
+ * @hash e13bd67
8
8
  */
9
9
 
10
10
  'use strict';
@@ -130,7 +130,6 @@ const SELECTION_BORDER_COLOR = "#3266ca";
130
130
  const HEADER_BORDER_COLOR = "#C0C0C0";
131
131
  const CELL_BORDER_COLOR = "#E2E3E3";
132
132
  const BACKGROUND_CHART_COLOR = "#FFFFFF";
133
- const BORDER_CHART_COLOR = "#FFFFFF";
134
133
  const DISABLED_TEXT_COLOR = "#CACACA";
135
134
  const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;
136
135
  const LINK_COLOR = "#017E84";
@@ -3236,7 +3235,6 @@ var ClipboardMIMEType;
3236
3235
  (function (ClipboardMIMEType) {
3237
3236
  ClipboardMIMEType["PlainText"] = "text/plain";
3238
3237
  ClipboardMIMEType["Html"] = "text/html";
3239
- ClipboardMIMEType["OSpreadsheet"] = "web application/o-spreadsheet";
3240
3238
  })(ClipboardMIMEType || (ClipboardMIMEType = {}));
3241
3239
 
3242
3240
  function isSheetDependent(cmd) {
@@ -4346,7 +4344,9 @@ function linearSearch(data, target, mode, numberOfValues, getValueInData, revers
4346
4344
  return reverseSearch ? numberOfValues - i - 1 : i;
4347
4345
  }
4348
4346
  }
4349
- return reverseSearch ? numberOfValues - closestMatchIndex - 1 : closestMatchIndex;
4347
+ return reverseSearch && closestMatchIndex !== -1
4348
+ ? numberOfValues - closestMatchIndex - 1
4349
+ : closestMatchIndex;
4350
4350
  }
4351
4351
  /**
4352
4352
  * Normalize a value.
@@ -4400,47 +4400,83 @@ function isDataNonEmpty(data) {
4400
4400
  return true;
4401
4401
  }
4402
4402
 
4403
- function toCriterionDateNumber(dateValue) {
4404
- const today = DateTime.now();
4405
- switch (dateValue) {
4406
- case "today":
4407
- return jsDateToNumber(today);
4408
- case "yesterday":
4409
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));
4410
- case "tomorrow":
4411
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));
4412
- case "lastWeek":
4413
- return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));
4414
- case "lastMonth":
4415
- return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));
4416
- case "lastYear":
4417
- return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));
4418
- }
4403
+ /**
4404
+ * Add the `https` prefix to the url if it's missing
4405
+ */
4406
+ function withHttps(url) {
4407
+ return !/^https?:\/\//i.test(url) ? `https://${url}` : url;
4419
4408
  }
4420
- /** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates */
4421
- function getDateNumberCriterionValues(criterion, locale) {
4422
- if ("dateValue" in criterion && criterion.dateValue !== "exactDate") {
4423
- return [toCriterionDateNumber(criterion.dateValue)];
4424
- }
4425
- return criterion.values.map((value) => valueToDateNumber(value, locale));
4409
+ const urlRegistry = new Registry();
4410
+ function createWebLink(url, label) {
4411
+ url = withHttps(url);
4412
+ return {
4413
+ url,
4414
+ label: label || url,
4415
+ isExternal: true,
4416
+ isUrlEditable: true,
4417
+ };
4426
4418
  }
4427
- /** Convert the criterion values to numbers. Return undefined values if they cannot be converted to numbers. */
4428
- function getCriterionValuesAsNumber(criterion, locale) {
4429
- return criterion.values.map((value) => tryToNumber(value, locale));
4419
+ urlRegistry.add("sheet_URL", {
4420
+ match: (url) => isSheetUrl(url),
4421
+ createLink: (url, label) => {
4422
+ return {
4423
+ label,
4424
+ url,
4425
+ isExternal: false,
4426
+ isUrlEditable: false,
4427
+ };
4428
+ },
4429
+ urlRepresentation(url, getters) {
4430
+ const sheetId = parseSheetUrl(url);
4431
+ return getters.tryGetSheetName(sheetId) || _t("Invalid sheet");
4432
+ },
4433
+ open(url, env) {
4434
+ const sheetId = parseSheetUrl(url);
4435
+ const result = env.model.dispatch("ACTIVATE_SHEET", {
4436
+ sheetIdFrom: env.model.getters.getActiveSheetId(),
4437
+ sheetIdTo: sheetId,
4438
+ });
4439
+ if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
4440
+ env.notifyUser({
4441
+ type: "warning",
4442
+ sticky: false,
4443
+ text: _t("Cannot open the link because the linked sheet is hidden."),
4444
+ });
4445
+ }
4446
+ },
4447
+ sequence: 0,
4448
+ });
4449
+ const WebUrlSpec = {
4450
+ createLink: createWebLink,
4451
+ match: (url) => isWebLink(url),
4452
+ open: (url) => window.open(url, "_blank"),
4453
+ urlRepresentation: (url) => url,
4454
+ sequence: 0,
4455
+ };
4456
+ function findMatchingSpec(url) {
4457
+ return (urlRegistry
4458
+ .getAll()
4459
+ .sort((a, b) => a.sequence - b.sequence)
4460
+ .find((urlType) => urlType.match(url)) || WebUrlSpec);
4430
4461
  }
4431
-
4432
- const MAX_DELAY = 140;
4433
- const MIN_DELAY = 20;
4434
- const ACCELERATION = 0.035;
4435
- /**
4436
- * Decreasing exponential function used to determine the "speed" of edge-scrolling
4437
- * as the timeout delay.
4438
- *
4439
- * Returns a timeout delay in milliseconds.
4440
- */
4441
- function scrollDelay(value) {
4442
- // decreasing exponential from MAX_DELAY to MIN_DELAY
4443
- return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));
4462
+ function urlRepresentation(link, getters) {
4463
+ return findMatchingSpec(link.url).urlRepresentation(link.url, getters);
4464
+ }
4465
+ function openLink(link, env) {
4466
+ findMatchingSpec(link.url).open(link.url, env);
4467
+ }
4468
+ function detectLink(value) {
4469
+ if (typeof value !== "string") {
4470
+ return undefined;
4471
+ }
4472
+ if (isMarkdownLink(value)) {
4473
+ const { label, url } = parseMarkdownLink(value);
4474
+ return findMatchingSpec(url).createLink(url, label);
4475
+ }
4476
+ else if (isWebLink(value)) {
4477
+ return createWebLink(value);
4478
+ }
4479
+ return undefined;
4444
4480
  }
4445
4481
 
4446
4482
  function tokenizeFormat(str) {
@@ -5502,6 +5538,193 @@ function isTextFormat(format) {
5502
5538
  }
5503
5539
  }
5504
5540
 
5541
+ function evaluateLiteral(literalCell, localeFormat) {
5542
+ const value = isTextFormat(localeFormat.format) ? literalCell.content : literalCell.parsedValue;
5543
+ const functionResult = { value, format: localeFormat.format };
5544
+ return createEvaluatedCell(functionResult, localeFormat.locale);
5545
+ }
5546
+ function parseLiteral(content, locale) {
5547
+ if (content.startsWith("=")) {
5548
+ throw new Error(`Cannot parse "${content}" because it's not a literal value. It's a formula`);
5549
+ }
5550
+ if (content === "") {
5551
+ return null;
5552
+ }
5553
+ if (isNumber(content, DEFAULT_LOCALE)) {
5554
+ return parseNumber(content, DEFAULT_LOCALE);
5555
+ }
5556
+ const internalDate = parseDateTime(content, locale);
5557
+ if (internalDate) {
5558
+ return internalDate.value;
5559
+ }
5560
+ if (isBoolean(content)) {
5561
+ return content.toUpperCase() === "TRUE";
5562
+ }
5563
+ return content;
5564
+ }
5565
+ function createEvaluatedCell(functionResult, locale = DEFAULT_LOCALE, cell) {
5566
+ const link = detectLink(functionResult.value);
5567
+ if (!link) {
5568
+ return _createEvaluatedCell(functionResult, locale, cell);
5569
+ }
5570
+ const value = parseLiteral(link.label, locale);
5571
+ const format = functionResult.format ||
5572
+ (typeof value === "number"
5573
+ ? detectDateFormat(link.label, locale) || detectNumberFormat(link.label)
5574
+ : undefined);
5575
+ const linkPayload = {
5576
+ value,
5577
+ format,
5578
+ };
5579
+ return {
5580
+ ..._createEvaluatedCell(linkPayload, locale, cell),
5581
+ link,
5582
+ };
5583
+ }
5584
+ function _createEvaluatedCell(functionResult, locale, cell) {
5585
+ let { value, format, message } = functionResult;
5586
+ format = cell?.format || format;
5587
+ const formattedValue = formatValue(value, { format, locale });
5588
+ if (isEvaluationError(value)) {
5589
+ return errorCell(value, message);
5590
+ }
5591
+ if (isTextFormat(format)) {
5592
+ // TO DO:
5593
+ // with the next line, the value of the cell is transformed depending on the format.
5594
+ // This shouldn't happen, by doing this, the formulas handling numbers are not able
5595
+ // to interpret the value as a number.
5596
+ return textCell(toString(value), format, formattedValue);
5597
+ }
5598
+ if (value === null) {
5599
+ return emptyCell(format);
5600
+ }
5601
+ if (typeof value === "number") {
5602
+ if (isDateTimeFormat(format || "")) {
5603
+ return dateTimeCell(value, format, formattedValue);
5604
+ }
5605
+ return numberCell(value, format, formattedValue);
5606
+ }
5607
+ if (typeof value === "boolean") {
5608
+ return booleanCell(value, format, formattedValue);
5609
+ }
5610
+ return textCell(value, format, formattedValue);
5611
+ }
5612
+ function textCell(value, format, formattedValue) {
5613
+ return {
5614
+ value,
5615
+ format,
5616
+ formattedValue,
5617
+ type: CellValueType.text,
5618
+ isAutoSummable: true,
5619
+ defaultAlign: "left",
5620
+ };
5621
+ }
5622
+ function numberCell(value, format, formattedValue) {
5623
+ return {
5624
+ value: value || 0, // necessary to avoid "-0" and NaN values,
5625
+ format,
5626
+ formattedValue,
5627
+ type: CellValueType.number,
5628
+ isAutoSummable: true,
5629
+ defaultAlign: "right",
5630
+ };
5631
+ }
5632
+ const emptyCell = memoize(function emptyCell(format) {
5633
+ return {
5634
+ value: null,
5635
+ format,
5636
+ formattedValue: "",
5637
+ type: CellValueType.empty,
5638
+ isAutoSummable: true,
5639
+ defaultAlign: "left",
5640
+ };
5641
+ });
5642
+ function dateTimeCell(value, format, formattedValue) {
5643
+ return {
5644
+ value,
5645
+ format,
5646
+ formattedValue,
5647
+ type: CellValueType.number,
5648
+ isAutoSummable: false,
5649
+ defaultAlign: "right",
5650
+ };
5651
+ }
5652
+ function booleanCell(value, format, formattedValue) {
5653
+ return {
5654
+ value,
5655
+ format,
5656
+ formattedValue,
5657
+ type: CellValueType.boolean,
5658
+ isAutoSummable: false,
5659
+ defaultAlign: "center",
5660
+ };
5661
+ }
5662
+ function errorCell(value, message) {
5663
+ return {
5664
+ value,
5665
+ formattedValue: value,
5666
+ message,
5667
+ type: CellValueType.error,
5668
+ isAutoSummable: false,
5669
+ defaultAlign: "center",
5670
+ };
5671
+ }
5672
+
5673
+ function toCriterionDateNumber(dateValue) {
5674
+ const today = DateTime.now();
5675
+ switch (dateValue) {
5676
+ case "today":
5677
+ return jsDateToNumber(today);
5678
+ case "yesterday":
5679
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));
5680
+ case "tomorrow":
5681
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));
5682
+ case "lastWeek":
5683
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));
5684
+ case "lastMonth":
5685
+ return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));
5686
+ case "lastYear":
5687
+ return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));
5688
+ }
5689
+ }
5690
+ /** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates */
5691
+ function getDateNumberCriterionValues(criterion, locale) {
5692
+ if ("dateValue" in criterion && criterion.dateValue !== "exactDate") {
5693
+ return [toCriterionDateNumber(criterion.dateValue)];
5694
+ }
5695
+ return criterion.values.map((value) => valueToDateNumber(value, locale));
5696
+ }
5697
+ /** Convert the criterion values to numbers. Return undefined values if they cannot be converted to numbers. */
5698
+ function getCriterionValuesAsNumber(criterion, locale) {
5699
+ return criterion.values.map((value) => tryToNumber(value, locale));
5700
+ }
5701
+ function getDateCriterionFormattedValues(values, locale) {
5702
+ return values.map((valueStr) => {
5703
+ if (valueStr.startsWith("=")) {
5704
+ return valueStr;
5705
+ }
5706
+ const value = parseLiteral(valueStr, locale);
5707
+ if (typeof value === "number") {
5708
+ return formatValue(value, { format: locale.dateFormat, locale });
5709
+ }
5710
+ return "";
5711
+ });
5712
+ }
5713
+
5714
+ const MAX_DELAY = 140;
5715
+ const MIN_DELAY = 20;
5716
+ const ACCELERATION = 0.035;
5717
+ /**
5718
+ * Decreasing exponential function used to determine the "speed" of edge-scrolling
5719
+ * as the timeout delay.
5720
+ *
5721
+ * Returns a timeout delay in milliseconds.
5722
+ */
5723
+ function scrollDelay(value) {
5724
+ // decreasing exponential from MAX_DELAY to MIN_DELAY
5725
+ return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));
5726
+ }
5727
+
5505
5728
  class RangeImpl {
5506
5729
  getSheetSize;
5507
5730
  _zone;
@@ -6149,6 +6372,22 @@ function getPasteZones(target, content) {
6149
6372
  const width = content[0].length, height = content.length;
6150
6373
  return target.map((t) => splitZoneForPaste(t, width, height)).flat();
6151
6374
  }
6375
+ function parseOSClipboardContent(content) {
6376
+ if (!content[ClipboardMIMEType.Html]) {
6377
+ return {
6378
+ text: content[ClipboardMIMEType.PlainText],
6379
+ };
6380
+ }
6381
+ const htmlDocument = new DOMParser().parseFromString(content[ClipboardMIMEType.Html], "text/html");
6382
+ const oSheetClipboardData = htmlDocument
6383
+ .querySelector("div")
6384
+ ?.getAttribute("data-osheet-clipboard");
6385
+ const spreadsheetContent = oSheetClipboardData && JSON.parse(oSheetClipboardData);
6386
+ return {
6387
+ text: content[ClipboardMIMEType.PlainText],
6388
+ data: spreadsheetContent,
6389
+ };
6390
+ }
6152
6391
 
6153
6392
  class ClipboardHandler {
6154
6393
  getters;
@@ -6170,7 +6409,7 @@ class ClipboardHandler {
6170
6409
  getPasteTarget(sheetId, target, content, options) {
6171
6410
  return { zones: [], sheetId };
6172
6411
  }
6173
- convertOSClipboardData(data) {
6412
+ convertTextToClipboardData(data) {
6174
6413
  return;
6175
6414
  }
6176
6415
  }
@@ -8013,7 +8252,7 @@ class CellClipboardHandler extends AbstractCellClipboardHandler {
8013
8252
  this.dispatch("CLEAR_CELL", target);
8014
8253
  }
8015
8254
  }
8016
- convertOSClipboardData(text) {
8255
+ convertTextToClipboardData(text) {
8017
8256
  const locale = this.getters.getLocale();
8018
8257
  const copiedData = {
8019
8258
  cells: [],
@@ -8121,6 +8360,7 @@ class ChartClipboardHandler extends AbstractFigureClipboardHandler {
8121
8360
 
8122
8361
  class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
8123
8362
  uuidGenerator = new UuidGenerator();
8363
+ queuedChanges = {};
8124
8364
  copy(data) {
8125
8365
  if (!data.zones.length) {
8126
8366
  return;
@@ -8142,6 +8382,7 @@ class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
8142
8382
  return { cfRules };
8143
8383
  }
8144
8384
  paste(target, clippedContent, options) {
8385
+ this.queuedChanges = {};
8145
8386
  if (options.pasteOption === "asValue") {
8146
8387
  return;
8147
8388
  }
@@ -8153,6 +8394,7 @@ class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
8153
8394
  else {
8154
8395
  this.pasteFromCut(sheetId, zones, clippedContent);
8155
8396
  }
8397
+ this.executeQueuedChanges();
8156
8398
  }
8157
8399
  pasteFromCut(sheetId, target, content) {
8158
8400
  const selection = target[0];
@@ -8192,34 +8434,56 @@ class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
8192
8434
  * Add or remove cells to a given conditional formatting rule.
8193
8435
  */
8194
8436
  adaptCFRules(sheetId, cf, toAdd, toRemove) {
8195
- const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
8196
- if (!newRangesXc) {
8197
- return;
8437
+ if (!this.queuedChanges[sheetId]) {
8438
+ this.queuedChanges[sheetId] = [];
8198
8439
  }
8199
- if (newRangesXc.length === 0) {
8200
- this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
8201
- return;
8440
+ const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.cf.id === cf.id);
8441
+ if (!queuedChange) {
8442
+ this.queuedChanges[sheetId].push({ toAdd, toRemove, cf });
8443
+ }
8444
+ else {
8445
+ queuedChange.toAdd.push(...toAdd);
8446
+ queuedChange.toRemove.push(...toRemove);
8447
+ }
8448
+ }
8449
+ executeQueuedChanges() {
8450
+ for (const sheetId in this.queuedChanges) {
8451
+ for (const { toAdd, toRemove, cf } of this.queuedChanges[sheetId]) {
8452
+ const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
8453
+ if (!newRangesXc) {
8454
+ continue;
8455
+ }
8456
+ if (newRangesXc.length === 0) {
8457
+ this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
8458
+ continue;
8459
+ }
8460
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
8461
+ cf: {
8462
+ id: cf.id,
8463
+ rule: cf.rule,
8464
+ stopIfTrue: cf.stopIfTrue,
8465
+ },
8466
+ ranges: newRangesXc,
8467
+ sheetId,
8468
+ });
8469
+ }
8202
8470
  }
8203
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
8204
- cf: {
8205
- id: cf.id,
8206
- rule: cf.rule,
8207
- stopIfTrue: cf.stopIfTrue,
8208
- },
8209
- ranges: newRangesXc,
8210
- sheetId,
8211
- });
8212
8471
  }
8213
8472
  getCFToCopyTo(targetSheetId, originCF) {
8214
- const cfInTarget = this.getters
8473
+ let targetCF = this.getters
8215
8474
  .getConditionalFormats(targetSheetId)
8216
8475
  .find((cf) => cf.stopIfTrue === originCF.stopIfTrue && deepEquals(cf.rule, originCF.rule));
8217
- return cfInTarget ? cfInTarget : { ...originCF, id: this.uuidGenerator.uuidv4(), ranges: [] };
8476
+ const queuedCfs = this.queuedChanges[targetSheetId];
8477
+ if (!targetCF && queuedCfs) {
8478
+ targetCF = queuedCfs.find((queued) => queued.cf.stopIfTrue === originCF.stopIfTrue && deepEquals(queued.cf.rule, originCF.rule))?.cf;
8479
+ }
8480
+ return targetCF || { ...originCF, id: this.uuidGenerator.uuidv4(), ranges: [] };
8218
8481
  }
8219
8482
  }
8220
8483
 
8221
8484
  class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8222
8485
  uuidGenerator = new UuidGenerator();
8486
+ queuedChanges = {};
8223
8487
  copy(data) {
8224
8488
  const { rowsIndexes, columnsIndexes } = data;
8225
8489
  const sheetId = data.sheetId;
@@ -8236,6 +8500,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8236
8500
  return { dvRules };
8237
8501
  }
8238
8502
  paste(target, clippedContent, options) {
8503
+ this.queuedChanges = {};
8239
8504
  if (options.pasteOption) {
8240
8505
  return;
8241
8506
  }
@@ -8247,6 +8512,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8247
8512
  else {
8248
8513
  this.pasteFromCut(sheetId, zones, clippedContent);
8249
8514
  }
8515
+ this.executeQueuedChanges();
8250
8516
  }
8251
8517
  pasteFromCut(sheetId, target, content) {
8252
8518
  const selection = target[0];
@@ -8293,29 +8559,55 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8293
8559
  }
8294
8560
  }
8295
8561
  getDataValidationRuleToCopyTo(targetSheetId, originRule, newId = true) {
8296
- const ruleInTargetSheet = this.getters
8562
+ let targetRule = this.getters
8297
8563
  .getDataValidationRules(targetSheetId)
8298
8564
  .find((rule) => deepEquals(originRule.criterion, rule.criterion) &&
8299
8565
  originRule.isBlocking === rule.isBlocking);
8300
- return ruleInTargetSheet
8301
- ? ruleInTargetSheet
8302
- : { ...originRule, id: newId ? this.uuidGenerator.uuidv4() : originRule.id, ranges: [] };
8566
+ const queuedRules = this.queuedChanges[targetSheetId];
8567
+ if (!targetRule && queuedRules) {
8568
+ targetRule = queuedRules.find((queued) => deepEquals(originRule.criterion, queued.rule.criterion) &&
8569
+ originRule.isBlocking === queued.rule.isBlocking)?.rule;
8570
+ }
8571
+ return (targetRule || {
8572
+ ...originRule,
8573
+ id: newId ? this.uuidGenerator.uuidv4() : originRule.id,
8574
+ ranges: [],
8575
+ });
8303
8576
  }
8304
8577
  /**
8305
8578
  * Add or remove XCs to a given data validation rule.
8306
8579
  */
8307
8580
  adaptDataValidationRule(sheetId, rule, toAdd, toRemove) {
8308
- const dvZones = rule.ranges.map((range) => range.zone);
8309
- const newDvZones = recomputeZones([...dvZones, ...toAdd], toRemove);
8310
- if (newDvZones.length === 0) {
8311
- this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: rule.id });
8312
- return;
8581
+ if (!this.queuedChanges[sheetId]) {
8582
+ this.queuedChanges[sheetId] = [];
8583
+ }
8584
+ const queuedChange = this.queuedChanges[sheetId].find((queued) => queued.rule.id === rule.id);
8585
+ if (!queuedChange) {
8586
+ this.queuedChanges[sheetId].push({ toAdd, toRemove, rule });
8587
+ }
8588
+ else {
8589
+ queuedChange.toAdd.push(...toAdd);
8590
+ queuedChange.toRemove.push(...toRemove);
8591
+ }
8592
+ }
8593
+ executeQueuedChanges() {
8594
+ for (const sheetId in this.queuedChanges) {
8595
+ for (const { toAdd, toRemove, rule: dv } of this.queuedChanges[sheetId]) {
8596
+ // Remove the zones first in case the same position is in toAdd and toRemove
8597
+ const dvZones = dv.ranges.map((range) => range.zone);
8598
+ const withRemovedZones = recomputeZones(dvZones, toRemove);
8599
+ const newDvZones = recomputeZones([...withRemovedZones, ...toAdd], []);
8600
+ if (newDvZones.length === 0) {
8601
+ this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: dv.id });
8602
+ continue;
8603
+ }
8604
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
8605
+ rule: dv,
8606
+ ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
8607
+ sheetId,
8608
+ });
8609
+ }
8313
8610
  }
8314
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
8315
- rule,
8316
- ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
8317
- sheetId,
8318
- });
8319
8611
  }
8320
8612
  }
8321
8613
 
@@ -10286,6 +10578,10 @@ function makeArg(str, description) {
10286
10578
  description,
10287
10579
  type: types,
10288
10580
  };
10581
+ const acceptErrors = types.includes("ANY") || types.includes("RANGE");
10582
+ if (acceptErrors) {
10583
+ result.acceptErrors = true;
10584
+ }
10289
10585
  if (isOptional) {
10290
10586
  result.optional = true;
10291
10587
  }
@@ -12348,8 +12644,8 @@ const AVERAGEIFS = {
12348
12644
  const COUNT = {
12349
12645
  description: _t("The number of numeric values in dataset."),
12350
12646
  args: [
12351
- arg("value1 (number, range<number>)", _t("The first value or range to consider when counting.")),
12352
- arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when counting.")),
12647
+ arg("value1 (number, any, range<number>)", _t("The first value or range to consider when counting.")),
12648
+ arg("value2 (number, any, range<number>, repeating)", _t("Additional values or ranges to consider when counting.")),
12353
12649
  ],
12354
12650
  compute: function (...values) {
12355
12651
  return countNumbers(values, this.locale);
@@ -12738,6 +13034,7 @@ const PEARSON = {
12738
13034
  },
12739
13035
  isExported: true,
12740
13036
  };
13037
+ // CORREL
12741
13038
  // In GSheet, CORREL is just an alias to PEARSON
12742
13039
  const CORREL = PEARSON;
12743
13040
  // -----------------------------------------------------------------------------
@@ -13348,7 +13645,7 @@ function getMatchingCells(database, field, criteria, locale) {
13348
13645
  }
13349
13646
  const databaseArgs = [
13350
13647
  arg("database (range)", _t("The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.")),
13351
- arg("field (any)", _t("Indicates which column in database contains the values to be extracted and operated on.")),
13648
+ arg("field (number, string)", _t("Indicates which column in database contains the values to be extracted and operated on.")),
13352
13649
  arg("criteria (range)", _t("An array or range containing zero or more criteria to filter the database values by before operating.")),
13353
13650
  ];
13354
13651
  // -----------------------------------------------------------------------------
@@ -17730,7 +18027,7 @@ const IFS = {
17730
18027
  args: [
17731
18028
  arg("condition1 (boolean)", _t("The first condition to be evaluated. This can be a boolean, a number, an array, or a reference to any of those.")),
17732
18029
  arg("value1 (any)", _t("The returned value if condition1 is TRUE.")),
17733
- arg("condition2 (boolean, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")),
18030
+ arg("condition2 (boolean, any, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")),
17734
18031
  arg("value2 (any, repeating)", _t("Additional values to be returned if their corresponding conditions are TRUE.")),
17735
18032
  ],
17736
18033
  compute: function (...values) {
@@ -17969,7 +18266,7 @@ const COLUMNS = {
17969
18266
  const HLOOKUP = {
17970
18267
  description: _t("Horizontal lookup"),
17971
18268
  args: [
17972
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18269
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
17973
18270
  arg("range (range)", _t("The range to consider for the search. The first row in the range is searched for the key specified in search_key.")),
17974
18271
  arg("index (number)", _t("The row index of the value to be returned, where the first row in range is numbered 1.")),
17975
18272
  arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the row to be searched (the first row of the specified range) is sorted, in which case the closest match for search_key will be returned.")),
@@ -17977,9 +18274,6 @@ const HLOOKUP = {
17977
18274
  compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
17978
18275
  const _index = Math.trunc(toNumber(index?.value, this.locale));
17979
18276
  assert(() => 1 <= _index && _index <= range[0].length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
17980
- if (searchKey && isEvaluationError(searchKey.value)) {
17981
- return searchKey;
17982
- }
17983
18277
  const getValueFromRange = (range, index) => range[index][0].value;
17984
18278
  const _isSorted = toBoolean(isSorted.value);
17985
18279
  const colIndex = _isSorted
@@ -18076,7 +18370,7 @@ const INDIRECT = {
18076
18370
  const LOOKUP = {
18077
18371
  description: _t("Look up a value."),
18078
18372
  args: [
18079
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18373
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18080
18374
  arg("search_array (range)", _t("One method of using this function is to provide a single sorted row or column search_array to look through for the search_key with a second argument result_range. The other way is to combine these two arguments into one search_array where the first row or column is searched and a value is returned from the last row or column in the array. If search_key is not found, a non-exact match may be returned.")),
18081
18375
  arg("result_range (range, optional)", _t("The range from which to return a result. The value returned corresponds to the location where search_key is found in search_range. This range must be only a single row or column and should not be used if using the search_result_array method.")),
18082
18376
  ],
@@ -18116,7 +18410,7 @@ const DEFAULT_SEARCH_TYPE = 1;
18116
18410
  const MATCH = {
18117
18411
  description: _t("Position of item in range that matches value."),
18118
18412
  args: [
18119
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18413
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18120
18414
  arg("range (any, range)", _t("The one-dimensional array to be searched.")),
18121
18415
  arg(`search_type (number, default=${DEFAULT_SEARCH_TYPE})`, _t("The search method. 1 (default) finds the largest value less than or equal to search_key when range is sorted in ascending order. 0 finds the exact value when range is unsorted. -1 finds the smallest value greater than or equal to search_key when range is sorted in descending order.")),
18122
18416
  ],
@@ -18191,7 +18485,7 @@ const ROWS = {
18191
18485
  const VLOOKUP = {
18192
18486
  description: _t("Vertical lookup."),
18193
18487
  args: [
18194
- arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18488
+ arg("search_key (string, number, boolean)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
18195
18489
  arg("range (any, range)", _t("The range to consider for the search. The first column in the range is searched for the key specified in search_key.")),
18196
18490
  arg("index (number)", _t("The column index of the value to be returned, where the first column in range is numbered 1.")),
18197
18491
  arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the column to be searched (the first column of the specified range) is sorted, in which case the closest match for search_key will be returned.")),
@@ -18199,9 +18493,6 @@ const VLOOKUP = {
18199
18493
  compute: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
18200
18494
  const _index = Math.trunc(toNumber(index?.value, this.locale));
18201
18495
  assert(() => 1 <= _index && _index <= range.length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
18202
- if (searchKey && isEvaluationError(searchKey.value)) {
18203
- return searchKey;
18204
- }
18205
18496
  const getValueFromRange = (range, index) => range[0][index].value;
18206
18497
  const _isSorted = toBoolean(isSorted.value);
18207
18498
  const rowIndex = _isSorted
@@ -18227,7 +18518,7 @@ const MATCH_MODE = {
18227
18518
  const XLOOKUP = {
18228
18519
  description: _t("Search a range for a match and return the corresponding item from a second range."),
18229
18520
  args: [
18230
- arg("search_key (any)", _t("The value to search for.")),
18521
+ arg("search_key (string,number,boolean)", _t("The value to search for.")),
18231
18522
  arg("lookup_range (any, range)", _t("The range to consider for the search. Should be a single column or a single row.")),
18232
18523
  arg("return_range (any, range)", _t("The range containing the return value. Should have the same dimensions as lookup_range.")),
18233
18524
  arg("if_not_found (any, optional)", _t("If a valid match is not found, return this value.")),
@@ -18252,9 +18543,6 @@ const XLOOKUP = {
18252
18543
  assert(() => lookupDirection === "col"
18253
18544
  ? returnRange[0].length === lookupRange[0].length
18254
18545
  : returnRange.length === lookupRange.length, _t("return_range should have the same dimensions as lookup_range."));
18255
- if (searchKey && isEvaluationError(searchKey.value)) {
18256
- return [[searchKey]];
18257
- }
18258
18546
  const getElement = lookupDirection === "col"
18259
18547
  ? (range, index) => range[0][index].value
18260
18548
  : (range, index) => range[index][0].value;
@@ -18279,13 +18567,14 @@ const XLOOKUP = {
18279
18567
  //--------------------------------------------------------------------------
18280
18568
  // Pivot functions
18281
18569
  //--------------------------------------------------------------------------
18570
+ // PIVOT.VALUE
18282
18571
  const PIVOT_VALUE = {
18283
18572
  description: _t("Get the value from a pivot."),
18284
18573
  args: [
18285
- arg("pivot_id (string)", _t("ID of the pivot.")),
18574
+ arg("pivot_id (number,string)", _t("ID of the pivot.")),
18286
18575
  arg("measure_name (string)", _t("Name of the measure.")),
18287
18576
  arg("domain_field_name (string,optional,repeating)", _t("Field name.")),
18288
- arg("domain_value (string,optional,repeating)", _t("Value.")),
18577
+ arg("domain_value (number,string,boolean,optional,repeating)", _t("Value.")),
18289
18578
  ],
18290
18579
  compute: function (formulaId, measureName, ...domainArgs) {
18291
18580
  const _pivotFormulaId = toString(formulaId);
@@ -18312,12 +18601,13 @@ const PIVOT_VALUE = {
18312
18601
  return pivot.getPivotCellValueAndFormat(_measure, domain);
18313
18602
  },
18314
18603
  };
18604
+ // PIVOT.HEADER
18315
18605
  const PIVOT_HEADER = {
18316
18606
  description: _t("Get the header of a pivot."),
18317
18607
  args: [
18318
- arg("pivot_id (string)", _t("ID of the pivot.")),
18608
+ arg("pivot_id (number,string)", _t("ID of the pivot.")),
18319
18609
  arg("domain_field_name (string,optional,repeating)", _t("Field name.")),
18320
- arg("domain_value (string,optional,repeating)", _t("Value.")),
18610
+ arg("domain_value (number,string,value,optional,repeating)", _t("Value.")),
18321
18611
  ],
18322
18612
  compute: function (pivotId, ...domainArgs) {
18323
18613
  const _pivotFormulaId = toString(pivotId);
@@ -18564,8 +18854,8 @@ const getNeutral = { number: 0, string: "", boolean: false };
18564
18854
  const EQ = {
18565
18855
  description: _t("Equal."),
18566
18856
  args: [
18567
- arg("value1 (any)", _t("The first value.")),
18568
- arg("value2 (any)", _t("The value to test against value1 for equality.")),
18857
+ arg("value1 (string, number, boolean)", _t("The first value.")),
18858
+ arg("value2 (string, number, boolean)", _t("The value to test against value1 for equality.")),
18569
18859
  ],
18570
18860
  compute: function (value1, value2) {
18571
18861
  if (isEvaluationError(value1?.value)) {
@@ -18616,8 +18906,8 @@ function applyRelationalOperator(value1, value2, cb) {
18616
18906
  const GT = {
18617
18907
  description: _t("Strictly greater than."),
18618
18908
  args: [
18619
- arg("value1 (any)", _t("The value to test as being greater than value2.")),
18620
- arg("value2 (any)", _t("The second value.")),
18909
+ arg("value1 (number, string, boolean)", _t("The value to test as being greater than value2.")),
18910
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18621
18911
  ],
18622
18912
  compute: function (value1, value2) {
18623
18913
  return applyRelationalOperator(value1, value2, (v1, v2) => {
@@ -18631,8 +18921,8 @@ const GT = {
18631
18921
  const GTE = {
18632
18922
  description: _t("Greater than or equal to."),
18633
18923
  args: [
18634
- arg("value1 (any)", _t("The value to test as being greater than or equal to value2.")),
18635
- arg("value2 (any)", _t("The second value.")),
18924
+ arg("value1 (number, string, boolean)", _t("The value to test as being greater than or equal to value2.")),
18925
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18636
18926
  ],
18637
18927
  compute: function (value1, value2) {
18638
18928
  return applyRelationalOperator(value1, value2, (v1, v2) => {
@@ -18646,8 +18936,8 @@ const GTE = {
18646
18936
  const LT = {
18647
18937
  description: _t("Less than."),
18648
18938
  args: [
18649
- arg("value1 (any)", _t("The value to test as being less than value2.")),
18650
- arg("value2 (any)", _t("The second value.")),
18939
+ arg("value1 (number, string, boolean)", _t("The value to test as being less than value2.")),
18940
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18651
18941
  ],
18652
18942
  compute: function (value1, value2) {
18653
18943
  const result = GTE.compute.bind(this)(value1, value2);
@@ -18663,8 +18953,8 @@ const LT = {
18663
18953
  const LTE = {
18664
18954
  description: _t("Less than or equal to."),
18665
18955
  args: [
18666
- arg("value1 (any)", _t("The value to test as being less than or equal to value2.")),
18667
- arg("value2 (any)", _t("The second value.")),
18956
+ arg("value1 (number, string, boolean)", _t("The value to test as being less than or equal to value2.")),
18957
+ arg("value2 (number, string, boolean)", _t("The second value.")),
18668
18958
  ],
18669
18959
  compute: function (value1, value2) {
18670
18960
  const result = GT.compute.bind(this)(value1, value2);
@@ -18712,8 +19002,8 @@ const MULTIPLY = {
18712
19002
  const NE = {
18713
19003
  description: _t("Not equal."),
18714
19004
  args: [
18715
- arg("value1 (any)", _t("The first value.")),
18716
- arg("value2 (any)", _t("The value to test against value1 for inequality.")),
19005
+ arg("value1 (string, number, boolean)", _t("The first value.")),
19006
+ arg("value2 (string, number, boolean)", _t("The value to test against value1 for inequality.")),
18717
19007
  ],
18718
19008
  compute: function (value1, value2) {
18719
19009
  const result = EQ.compute.bind(this)(value1, value2);
@@ -19631,6 +19921,16 @@ function createComputeFunction(descr, functionName) {
19631
19921
  });
19632
19922
  }
19633
19923
  function errorHandlingCompute(...args) {
19924
+ for (let i = 0; i < args.length; i++) {
19925
+ const arg = args[i];
19926
+ const argDefinition = descr.args[descr.getArgToFocus(i + 1) - 1];
19927
+ // Early exit if the argument is an error and the function does not accept errors
19928
+ // We only check scalar arguments, not matrix arguments for performance reasons.
19929
+ // Casting helpers are responsible for handling errors in matrix arguments.
19930
+ if (!argDefinition.acceptErrors && !isMatrix(arg) && isEvaluationError(arg?.value)) {
19931
+ return arg;
19932
+ }
19933
+ }
19634
19934
  try {
19635
19935
  return computeFunctionToObject.apply(this, args);
19636
19936
  }
@@ -19639,6 +19939,9 @@ function createComputeFunction(descr, functionName) {
19639
19939
  }
19640
19940
  }
19641
19941
  function computeFunctionToObject(...args) {
19942
+ if (this.debug) {
19943
+ debugger;
19944
+ }
19642
19945
  const result = descr.compute.apply(this, args);
19643
19946
  if (!isMatrix(result)) {
19644
19947
  if (typeof result === "object" && result !== null && "value" in result) {
@@ -20748,6 +21051,7 @@ function compileTokensOrThrow(tokens) {
20748
21051
  }
20749
21052
  if (ast.debug) {
20750
21053
  code.append("debugger;");
21054
+ code.append(`ctx["debug"] = true;`);
20751
21055
  }
20752
21056
  switch (ast.type) {
20753
21057
  case "BOOLEAN":
@@ -21346,217 +21650,6 @@ autoCompleteProviders.add("sheet_names", {
21346
21650
  },
21347
21651
  });
21348
21652
 
21349
- /**
21350
- * Add the `https` prefix to the url if it's missing
21351
- */
21352
- function withHttps(url) {
21353
- return !/^https?:\/\//i.test(url) ? `https://${url}` : url;
21354
- }
21355
- const urlRegistry = new Registry();
21356
- function createWebLink(url, label) {
21357
- url = withHttps(url);
21358
- return {
21359
- url,
21360
- label: label || url,
21361
- isExternal: true,
21362
- isUrlEditable: true,
21363
- };
21364
- }
21365
- urlRegistry.add("sheet_URL", {
21366
- match: (url) => isSheetUrl(url),
21367
- createLink: (url, label) => {
21368
- return {
21369
- label,
21370
- url,
21371
- isExternal: false,
21372
- isUrlEditable: false,
21373
- };
21374
- },
21375
- urlRepresentation(url, getters) {
21376
- const sheetId = parseSheetUrl(url);
21377
- return getters.tryGetSheetName(sheetId) || _t("Invalid sheet");
21378
- },
21379
- open(url, env) {
21380
- const sheetId = parseSheetUrl(url);
21381
- const result = env.model.dispatch("ACTIVATE_SHEET", {
21382
- sheetIdFrom: env.model.getters.getActiveSheetId(),
21383
- sheetIdTo: sheetId,
21384
- });
21385
- if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
21386
- env.notifyUser({
21387
- type: "warning",
21388
- sticky: false,
21389
- text: _t("Cannot open the link because the linked sheet is hidden."),
21390
- });
21391
- }
21392
- },
21393
- sequence: 0,
21394
- });
21395
- const WebUrlSpec = {
21396
- createLink: createWebLink,
21397
- match: (url) => isWebLink(url),
21398
- open: (url) => window.open(url, "_blank"),
21399
- urlRepresentation: (url) => url,
21400
- sequence: 0,
21401
- };
21402
- function findMatchingSpec(url) {
21403
- return (urlRegistry
21404
- .getAll()
21405
- .sort((a, b) => a.sequence - b.sequence)
21406
- .find((urlType) => urlType.match(url)) || WebUrlSpec);
21407
- }
21408
- function urlRepresentation(link, getters) {
21409
- return findMatchingSpec(link.url).urlRepresentation(link.url, getters);
21410
- }
21411
- function openLink(link, env) {
21412
- findMatchingSpec(link.url).open(link.url, env);
21413
- }
21414
- function detectLink(value) {
21415
- if (typeof value !== "string") {
21416
- return undefined;
21417
- }
21418
- if (isMarkdownLink(value)) {
21419
- const { label, url } = parseMarkdownLink(value);
21420
- return findMatchingSpec(url).createLink(url, label);
21421
- }
21422
- else if (isWebLink(value)) {
21423
- return createWebLink(value);
21424
- }
21425
- return undefined;
21426
- }
21427
-
21428
- function evaluateLiteral(literalCell, localeFormat) {
21429
- const value = isTextFormat(localeFormat.format) ? literalCell.content : literalCell.parsedValue;
21430
- const functionResult = { value, format: localeFormat.format };
21431
- return createEvaluatedCell(functionResult, localeFormat.locale);
21432
- }
21433
- function parseLiteral(content, locale) {
21434
- if (content.startsWith("=")) {
21435
- throw new Error(`Cannot parse "${content}" because it's not a literal value. It's a formula`);
21436
- }
21437
- if (content === "") {
21438
- return null;
21439
- }
21440
- if (isNumber(content, DEFAULT_LOCALE)) {
21441
- return parseNumber(content, DEFAULT_LOCALE);
21442
- }
21443
- const internalDate = parseDateTime(content, locale);
21444
- if (internalDate) {
21445
- return internalDate.value;
21446
- }
21447
- if (isBoolean(content)) {
21448
- return content.toUpperCase() === "TRUE";
21449
- }
21450
- return content;
21451
- }
21452
- function createEvaluatedCell(functionResult, locale = DEFAULT_LOCALE, cell) {
21453
- const link = detectLink(functionResult.value);
21454
- if (!link) {
21455
- return _createEvaluatedCell(functionResult, locale, cell);
21456
- }
21457
- const value = parseLiteral(link.label, locale);
21458
- const format = functionResult.format ||
21459
- (typeof value === "number"
21460
- ? detectDateFormat(link.label, locale) || detectNumberFormat(link.label)
21461
- : undefined);
21462
- const linkPayload = {
21463
- value,
21464
- format,
21465
- };
21466
- return {
21467
- ..._createEvaluatedCell(linkPayload, locale, cell),
21468
- link,
21469
- };
21470
- }
21471
- function _createEvaluatedCell(functionResult, locale, cell) {
21472
- let { value, format, message } = functionResult;
21473
- format = cell?.format || format;
21474
- const formattedValue = formatValue(value, { format, locale });
21475
- if (isEvaluationError(value)) {
21476
- return errorCell(value, message);
21477
- }
21478
- if (isTextFormat(format)) {
21479
- // TO DO:
21480
- // with the next line, the value of the cell is transformed depending on the format.
21481
- // This shouldn't happen, by doing this, the formulas handling numbers are not able
21482
- // to interpret the value as a number.
21483
- return textCell(toString(value), format, formattedValue);
21484
- }
21485
- if (value === null) {
21486
- return emptyCell(format);
21487
- }
21488
- if (typeof value === "number") {
21489
- if (isDateTimeFormat(format || "")) {
21490
- return dateTimeCell(value, format, formattedValue);
21491
- }
21492
- return numberCell(value, format, formattedValue);
21493
- }
21494
- if (typeof value === "boolean") {
21495
- return booleanCell(value, format, formattedValue);
21496
- }
21497
- return textCell(value, format, formattedValue);
21498
- }
21499
- function textCell(value, format, formattedValue) {
21500
- return {
21501
- value,
21502
- format,
21503
- formattedValue,
21504
- type: CellValueType.text,
21505
- isAutoSummable: true,
21506
- defaultAlign: "left",
21507
- };
21508
- }
21509
- function numberCell(value, format, formattedValue) {
21510
- return {
21511
- value: value || 0, // necessary to avoid "-0" and NaN values,
21512
- format,
21513
- formattedValue,
21514
- type: CellValueType.number,
21515
- isAutoSummable: true,
21516
- defaultAlign: "right",
21517
- };
21518
- }
21519
- const emptyCell = memoize(function emptyCell(format) {
21520
- return {
21521
- value: null,
21522
- format,
21523
- formattedValue: "",
21524
- type: CellValueType.empty,
21525
- isAutoSummable: true,
21526
- defaultAlign: "left",
21527
- };
21528
- });
21529
- function dateTimeCell(value, format, formattedValue) {
21530
- return {
21531
- value,
21532
- format,
21533
- formattedValue,
21534
- type: CellValueType.number,
21535
- isAutoSummable: false,
21536
- defaultAlign: "right",
21537
- };
21538
- }
21539
- function booleanCell(value, format, formattedValue) {
21540
- return {
21541
- value,
21542
- format,
21543
- formattedValue,
21544
- type: CellValueType.boolean,
21545
- isAutoSummable: false,
21546
- defaultAlign: "center",
21547
- };
21548
- }
21549
- function errorCell(value, message) {
21550
- return {
21551
- value,
21552
- formattedValue: value,
21553
- message,
21554
- type: CellValueType.error,
21555
- isAutoSummable: false,
21556
- defaultAlign: "center",
21557
- };
21558
- }
21559
-
21560
21653
  /**
21561
21654
  * An AutofillModifierImplementation is used to describe how to handle a
21562
21655
  * AutofillModifier.
@@ -22921,6 +23014,10 @@ var WarningTypes;
22921
23014
  WarningTypes["CfIconSetEmptyIconNotSupported"] = "IconSets with empty icons";
22922
23015
  WarningTypes["BadlyFormattedHyperlink"] = "Badly formatted hyperlink";
22923
23016
  WarningTypes["NumFmtIdNotSupported"] = "Number format";
23017
+ WarningTypes["TimeDataValidationNotSupported"] = "Time data validation rules";
23018
+ WarningTypes["TextLengthDataValidationNotSupported"] = "Text length data validation rules";
23019
+ WarningTypes["WholeNumberDataValidationNotSupported"] = "Whole number data validation rules";
23020
+ WarningTypes["NotEqualDateDataValidationNotSupported"] = "Not equal date data validation rules";
22924
23021
  })(WarningTypes || (WarningTypes = {}));
22925
23022
  class XLSXImportWarningManager {
22926
23023
  _parsingWarnings = new Set();
@@ -23299,6 +23396,25 @@ const IMAGE_EXTENSION_TO_MIMETYPE_MAPPING = {
23299
23396
  webp: "image/webp",
23300
23397
  jpg: "image/jpeg",
23301
23398
  };
23399
+ const XLSX_DV_DECIMAL_OPERATOR_MAPPING = {
23400
+ between: "isBetween",
23401
+ notBetween: "isNotBetween",
23402
+ equal: "isEqual",
23403
+ notEqual: "isNotEqual",
23404
+ greaterThan: "isGreaterThan",
23405
+ greaterThanOrEqual: "isGreaterOrEqualTo",
23406
+ lessThan: "isLessThan",
23407
+ lessThanOrEqual: "isLessOrEqualTo",
23408
+ };
23409
+ const XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING = {
23410
+ between: "dateIsBetween",
23411
+ notBetween: "dateIsNotBetween",
23412
+ equal: "dateIs",
23413
+ greaterThan: "dateIsAfter",
23414
+ greaterThanOrEqual: "dateIsOnOrAfter",
23415
+ lessThan: "dateIsBefore",
23416
+ lessThanOrEqual: "dateIsOnOrBefore",
23417
+ };
23302
23418
 
23303
23419
  /**
23304
23420
  * Most of the functions could stay private, but are exported for testing purposes
@@ -24080,6 +24196,20 @@ function getRowPosition(rowIndex, sheetData) {
24080
24196
  }
24081
24197
  return position / HEIGHT_FACTOR;
24082
24198
  }
24199
+ /**
24200
+ * Convert the o-spreadsheet data validation decimal
24201
+ * criterion type to the corresponding excel operator.
24202
+ */
24203
+ function convertDecimalCriterionTypeToExcelOperator(operator) {
24204
+ return Object.keys(XLSX_DV_DECIMAL_OPERATOR_MAPPING).find((key) => XLSX_DV_DECIMAL_OPERATOR_MAPPING[key] === operator);
24205
+ }
24206
+ /**
24207
+ * Convert the o-spreadsheet data validation date
24208
+ * criterion type to the corresponding excel operator.
24209
+ */
24210
+ function convertDateCriterionTypeToExcelOperator(operator) {
24211
+ return Object.keys(XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING).find((key) => XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[key] === operator);
24212
+ }
24083
24213
 
24084
24214
  function convertFigures(sheetData) {
24085
24215
  let id = 1;
@@ -24189,6 +24319,112 @@ function getPositionFromAnchor(anchor, sheetData) {
24189
24319
  };
24190
24320
  }
24191
24321
 
24322
+ function convertDataValidationRules(xlsxDataValidations, warningManager) {
24323
+ const dvRules = [];
24324
+ let dvId = 1;
24325
+ for (const dv of xlsxDataValidations) {
24326
+ if (!dv) {
24327
+ continue;
24328
+ }
24329
+ switch (dv.type) {
24330
+ case "time":
24331
+ warningManager.generateNotSupportedWarning(WarningTypes.TimeDataValidationNotSupported);
24332
+ break;
24333
+ case "textLength":
24334
+ warningManager.generateNotSupportedWarning(WarningTypes.TextLengthDataValidationNotSupported);
24335
+ break;
24336
+ case "whole":
24337
+ warningManager.generateNotSupportedWarning(WarningTypes.WholeNumberDataValidationNotSupported);
24338
+ break;
24339
+ case "decimal":
24340
+ const decimalRule = convertDecimalRule(dvId++, dv);
24341
+ dvRules.push(decimalRule);
24342
+ break;
24343
+ case "list":
24344
+ const listRule = convertListrule(dvId++, dv);
24345
+ dvRules.push(listRule);
24346
+ break;
24347
+ case "date":
24348
+ if (dv.operator === "notEqual") {
24349
+ warningManager.generateNotSupportedWarning(WarningTypes.NotEqualDateDataValidationNotSupported);
24350
+ break;
24351
+ }
24352
+ const dateRule = convertDateRule(dvId++, dv);
24353
+ dvRules.push(dateRule);
24354
+ break;
24355
+ case "custom":
24356
+ const customRule = convertCustomRule(dvId++, dv);
24357
+ dvRules.push(customRule);
24358
+ break;
24359
+ }
24360
+ }
24361
+ return dvRules;
24362
+ }
24363
+ function convertDecimalRule(id, dv) {
24364
+ const values = [dv.formula1.toString()];
24365
+ if (dv.formula2) {
24366
+ values.push(dv.formula2.toString());
24367
+ }
24368
+ return {
24369
+ id: id.toString(),
24370
+ ranges: dv.sqref,
24371
+ isBlocking: dv.errorStyle !== "warning",
24372
+ criterion: {
24373
+ type: XLSX_DV_DECIMAL_OPERATOR_MAPPING[dv.operator],
24374
+ values,
24375
+ },
24376
+ };
24377
+ }
24378
+ function convertListrule(id, dv) {
24379
+ const formula1 = dv.formula1.toString();
24380
+ const isRangeRule = rangeReference.test(formula1);
24381
+ return {
24382
+ id: id.toString(),
24383
+ ranges: dv.sqref,
24384
+ isBlocking: dv.errorStyle !== "warning",
24385
+ criterion: {
24386
+ type: isRangeRule ? "isValueInRange" : "isValueInList",
24387
+ values: isRangeRule ? [formula1] : formula1.replaceAll('"', "").split(","),
24388
+ displayStyle: "arrow",
24389
+ },
24390
+ };
24391
+ }
24392
+ function convertDateRule(id, dv) {
24393
+ let criterion;
24394
+ const values = [dv.formula1.toString()];
24395
+ if (dv.formula2) {
24396
+ values.push(dv.formula2.toString());
24397
+ criterion = {
24398
+ type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
24399
+ values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
24400
+ };
24401
+ }
24402
+ else {
24403
+ criterion = {
24404
+ type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
24405
+ values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
24406
+ dateValue: "exactDate",
24407
+ };
24408
+ }
24409
+ return {
24410
+ id: id.toString(),
24411
+ ranges: dv.sqref,
24412
+ isBlocking: dv.errorStyle !== "warning",
24413
+ criterion: criterion,
24414
+ };
24415
+ }
24416
+ function convertCustomRule(id, dv) {
24417
+ return {
24418
+ id: id.toString(),
24419
+ ranges: dv.sqref,
24420
+ isBlocking: dv.errorStyle !== "warning",
24421
+ criterion: {
24422
+ type: "customFormula",
24423
+ values: [`=${dv.formula1.toString()}`],
24424
+ },
24425
+ };
24426
+ }
24427
+
24192
24428
  /**
24193
24429
  * Match external reference (ex. '[1]Sheet 3'!$B$4)
24194
24430
  *
@@ -24309,6 +24545,7 @@ function convertSheets(data, warningManager) {
24309
24545
  cols: convertCols(sheet, sheetDims[0], colHeaderGroups),
24310
24546
  rows: convertRows(sheet, sheetDims[1], rowHeaderGroups),
24311
24547
  conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),
24548
+ dataValidationRules: convertDataValidationRules(sheet.dataValidations, warningManager),
24312
24549
  figures: convertFigures(sheet),
24313
24550
  isVisible: sheet.isVisible,
24314
24551
  panes: sheetOptions
@@ -25588,6 +25825,41 @@ class XlsxCfExtractor extends XlsxBaseExtractor {
25588
25825
  }
25589
25826
  }
25590
25827
 
25828
+ class XlsxDataValidationExtractor extends XlsxBaseExtractor {
25829
+ theme;
25830
+ constructor(sheetFile, xlsxStructure, warningManager, theme) {
25831
+ super(sheetFile, xlsxStructure, warningManager);
25832
+ this.theme = theme;
25833
+ }
25834
+ extractDataValidations() {
25835
+ const dataValidations = this.mapOnElements({ parent: this.rootFile.file.xml, query: "worksheet > dataValidations > dataValidation" }, (dvElement) => {
25836
+ return {
25837
+ type: this.extractAttr(dvElement, "type", { required: true }).asString(),
25838
+ operator: this.extractAttr(dvElement, "operator", {
25839
+ default: "between",
25840
+ })?.asString(),
25841
+ sqref: this.extractAttr(dvElement, "sqref", { required: true }).asString().split(" "),
25842
+ errorStyle: this.extractAttr(dvElement, "errorStyle")?.asString(),
25843
+ formula1: this.extractDataValidationFormula(dvElement, 1)[0],
25844
+ formula2: this.extractDataValidationFormula(dvElement, 2)[0],
25845
+ showErrorMessage: this.extractAttr(dvElement, "showErrorMessage")?.asBool(),
25846
+ errorTitle: this.extractAttr(dvElement, "errorTitle")?.asString(),
25847
+ error: this.extractAttr(dvElement, "error")?.asString(),
25848
+ showInputMessage: this.extractAttr(dvElement, "showInputMessage")?.asBool(),
25849
+ promptTitle: this.extractAttr(dvElement, "promptTitle")?.asString(),
25850
+ prompt: this.extractAttr(dvElement, "prompt")?.asString(),
25851
+ allowBlank: this.extractAttr(dvElement, "allowBlank")?.asBool(),
25852
+ };
25853
+ });
25854
+ return dataValidations;
25855
+ }
25856
+ extractDataValidationFormula(dvElement, index) {
25857
+ return this.mapOnElements({ parent: dvElement, query: `formula${index}` }, (cfFormulaElements) => {
25858
+ return this.extractTextContent(cfFormulaElements, { required: true });
25859
+ });
25860
+ }
25861
+ }
25862
+
25591
25863
  class XlsxChartExtractor extends XlsxBaseExtractor {
25592
25864
  extractChart() {
25593
25865
  return this.mapOnElements({ parent: this.rootFile.file.xml, query: "c:chartSpace" }, (rootChartElement) => {
@@ -25955,6 +26227,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
25955
26227
  sharedFormulas: this.extractSharedFormulas(sheetElement),
25956
26228
  merges: this.extractMerges(sheetElement),
25957
26229
  cfs: this.extractConditionalFormats(),
26230
+ dataValidations: this.extractDataValidations(),
25958
26231
  figures: this.extractFigures(sheetElement),
25959
26232
  hyperlinks: this.extractHyperLinks(sheetElement),
25960
26233
  tables: this.extractTables(sheetElement),
@@ -26026,6 +26299,9 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
26026
26299
  extractConditionalFormats() {
26027
26300
  return new XlsxCfExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractConditionalFormattings();
26028
26301
  }
26302
+ extractDataValidations() {
26303
+ return new XlsxDataValidationExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractDataValidations();
26304
+ }
26029
26305
  extractFigures(worksheet) {
26030
26306
  const figures = this.mapOnElements({ parent: worksheet, query: "drawing" }, (drawingElement) => {
26031
26307
  const drawingId = this.extractAttr(drawingElement, "r:id", { required: true })?.asString();
@@ -27333,6 +27609,7 @@ function createEmptySheet(sheetId, name) {
27333
27609
  rows: {},
27334
27610
  merges: [],
27335
27611
  conditionalFormats: [],
27612
+ dataValidationRules: [],
27336
27613
  figures: [],
27337
27614
  tables: [],
27338
27615
  isVisible: true,
@@ -27407,17 +27684,15 @@ function interactivePasteFromOS(env, target, clipboardContent, pasteOption) {
27407
27684
  });
27408
27685
  }
27409
27686
  catch (error) {
27410
- const parsedSpreadsheetContent = clipboardContent[ClipboardMIMEType.OSpreadsheet]
27411
- ? JSON.parse(clipboardContent[ClipboardMIMEType.OSpreadsheet])
27412
- : {};
27413
- if (parsedSpreadsheetContent.version && parsedSpreadsheetContent.version !== CURRENT_VERSION) {
27687
+ const parsedSpreadsheetContent = clipboardContent.data;
27688
+ if (parsedSpreadsheetContent?.version !== CURRENT_VERSION) {
27414
27689
  env.raiseError(_t("An unexpected error occurred while pasting content.\
27415
27690
  This is probably due to a spreadsheet version mismatch."));
27416
27691
  }
27417
27692
  result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", {
27418
27693
  target,
27419
27694
  clipboardContent: {
27420
- [ClipboardMIMEType.PlainText]: clipboardContent[ClipboardMIMEType.PlainText],
27695
+ text: clipboardContent.text,
27421
27696
  },
27422
27697
  pasteOption,
27423
27698
  });
@@ -27542,6 +27817,13 @@ const DVTerms = {
27542
27817
  numberValue: _t("The value must be a number"),
27543
27818
  dateValue: _t("The value must be a date"),
27544
27819
  validRange: _t("The value must be a valid range"),
27820
+ validFormula: _t("The formula must be valid"),
27821
+ },
27822
+ Errors: {
27823
+ ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid."),
27824
+ ["InvalidDataValidationCriterionValue" /* CommandResult.InvalidDataValidationCriterionValue */]: _t("One or more of the provided criteria values are invalid. Please review and correct them."),
27825
+ ["InvalidNumberOfCriterionValues" /* CommandResult.InvalidNumberOfCriterionValues */]: _t("One or more of the provided criteria values are missing."),
27826
+ Unexpected: _t("The rule is invalid for an unknown reason."),
27545
27827
  },
27546
27828
  };
27547
27829
  const TableTerms = {
@@ -27752,7 +28034,7 @@ function getBarChartData(definition, dataSets, labelRange, getters) {
27752
28034
  labels.length > dataSetsValues[0].data.length) {
27753
28035
  labels.shift();
27754
28036
  }
27755
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28037
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27756
28038
  if (definition.aggregated) {
27757
28039
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
27758
28040
  }
@@ -27807,7 +28089,7 @@ function getLineChartData(definition, dataSets, labelRange, getters) {
27807
28089
  labels.length > dataSetsValues[0].data.length) {
27808
28090
  labels.shift();
27809
28091
  }
27810
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28092
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27811
28093
  if (axisType === "time") {
27812
28094
  ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));
27813
28095
  }
@@ -27824,7 +28106,7 @@ function getLineChartData(definition, dataSets, labelRange, getters) {
27824
28106
  if (definition.cumulative) {
27825
28107
  let accumulator = 0;
27826
28108
  data = data.map((value) => {
27827
- if (!isNaN(value)) {
28109
+ if (!isNaN(parseFloat(value))) {
27828
28110
  accumulator += parseFloat(value);
27829
28111
  return accumulator;
27830
28112
  }
@@ -27857,11 +28139,11 @@ function getPieChartData(definition, dataSets, labelRange, getters) {
27857
28139
  labels.length > dataSetsValues[0].data.length) {
27858
28140
  labels.shift();
27859
28141
  }
27860
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28142
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27861
28143
  if (definition.aggregated) {
27862
28144
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
27863
28145
  }
27864
- ({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));
28146
+ ({ dataSetsValues, labels } = keepOnlyPositiveValues(labels, dataSetsValues));
27865
28147
  const dataSetFormat = getChartDatasetFormat(getters, dataSets, "left");
27866
28148
  return {
27867
28149
  dataSetsValues,
@@ -27879,7 +28161,7 @@ function getRadarChartData(definition, dataSets, labelRange, getters) {
27879
28161
  labels.length > dataSetsValues[0].data.length) {
27880
28162
  labels.shift();
27881
28163
  }
27882
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
28164
+ ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
27883
28165
  if (definition.aggregated) {
27884
28166
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
27885
28167
  }
@@ -28058,26 +28340,16 @@ function isLuxonTimeAdapterInstalled() {
28058
28340
  }
28059
28341
  return isInstalled;
28060
28342
  }
28061
- function filterNegativeValues(labels, datasets) {
28062
- const dataPointsIndexes = labels.reduce((indexes, label, i) => {
28063
- const shouldKeep = datasets.some((dataset) => {
28064
- const dataPoint = dataset.data[i];
28065
- return typeof dataPoint !== "number" || dataPoint >= 0;
28066
- });
28067
- if (shouldKeep) {
28068
- indexes.push(i);
28069
- }
28070
- return indexes;
28071
- }, []);
28072
- const filteredLabels = dataPointsIndexes.map((i) => labels[i] || "");
28073
- const filteredDatasets = datasets.map((dataset) => ({
28074
- ...dataset,
28075
- data: dataPointsIndexes.map((i) => {
28076
- const dataPoint = dataset.data[i];
28077
- return typeof dataPoint !== "number" || dataPoint >= 0 ? dataPoint : 0;
28078
- }),
28079
- }));
28080
- return { labels: filteredLabels, dataSetsValues: filteredDatasets };
28343
+ function keepOnlyPositiveValues(labels, datasets) {
28344
+ const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
28345
+ const filteredIndexes = range(0, numberOfDataPoints).filter((i) => datasets.some((ds) => typeof ds.data[i] === "number" && ds.data[i] > 0));
28346
+ return {
28347
+ labels: filteredIndexes.map((i) => labels[i] || ""),
28348
+ dataSetsValues: datasets.map((ds) => ({
28349
+ ...ds,
28350
+ data: filteredIndexes.map((i) => typeof ds.data[i] === "number" && ds.data[i] > 0 ? ds.data[i] : null),
28351
+ })),
28352
+ };
28081
28353
  }
28082
28354
  function fixEmptyLabelsForDateCharts(labels, dataSetsValues) {
28083
28355
  if (labels.length === 0 || labels.every((label) => !label)) {
@@ -28110,18 +28382,23 @@ function getData(getters, ds) {
28110
28382
  }
28111
28383
  return [];
28112
28384
  }
28113
- function filterEmptyDataPoints(labels, datasets) {
28385
+ /**
28386
+ * Filter the data points that:
28387
+ * - have neither a label nor a value
28388
+ * - have no label and a non-numeric value
28389
+ */
28390
+ function filterInvalidDataPoints(labels, datasets) {
28114
28391
  const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
28115
28392
  const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
28116
28393
  const label = labels[dataPointIndex];
28117
28394
  const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
28118
- return label || values.some((value) => value === 0 || Boolean(value));
28395
+ return label || values.some((value) => typeof value === "number");
28119
28396
  });
28120
28397
  return {
28121
28398
  labels: dataPointsIndexes.map((i) => labels[i] || ""),
28122
28399
  dataSetsValues: datasets.map((dataset) => ({
28123
28400
  ...dataset,
28124
- data: dataPointsIndexes.map((i) => dataset.data[i]),
28401
+ data: dataPointsIndexes.map((i) => typeof dataset.data[i] === "number" ? dataset.data[i] : null),
28125
28402
  })),
28126
28403
  };
28127
28404
  }
@@ -28264,8 +28541,8 @@ function getBarChartDatasets(definition, args) {
28264
28541
  const dataset = {
28265
28542
  label,
28266
28543
  data,
28267
- borderColor: BORDER_CHART_COLOR,
28268
- borderWidth: 1,
28544
+ borderColor: definition.background || BACKGROUND_CHART_COLOR,
28545
+ borderWidth: definition.stacked ? 1 : 0,
28269
28546
  backgroundColor,
28270
28547
  yAxisID: definition.horizontal ? "y" : definition.dataSets?.[index].yAxisId || "y",
28271
28548
  xAxisID: "x",
@@ -32937,13 +33214,11 @@ async function paste$1(env, pasteOption) {
32937
33214
  const osClipboard = await env.clipboard.read();
32938
33215
  switch (osClipboard.status) {
32939
33216
  case "ok":
32940
- const htmlDocument = new DOMParser().parseFromString(osClipboard.content[ClipboardMIMEType.Html] ?? "<div></div>", "text/html");
32941
- const osClipboardSpreadsheetContent = osClipboard.content[ClipboardMIMEType.OSpreadsheet] || "{}";
32942
- const clipboardId = JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
32943
- htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");
33217
+ const clipboardContent = parseOSClipboardContent(osClipboard.content);
33218
+ const clipboardId = clipboardContent.data?.clipboardId;
32944
33219
  const target = env.model.getters.getSelectedZones();
32945
33220
  if (env.model.getters.getClipboardId() !== clipboardId) {
32946
- interactivePasteFromOS(env, target, osClipboard.content, pasteOption);
33221
+ interactivePasteFromOS(env, target, clipboardContent, pasteOption);
32947
33222
  }
32948
33223
  else {
32949
33224
  interactivePaste(env, target, pasteOption);
@@ -36163,7 +36438,7 @@ css /* scss */ `
36163
36438
  flex-grow: 0;
36164
36439
  }
36165
36440
 
36166
- /** Make the character a bit bigger
36441
+ /* Make the character a bit bigger
36167
36442
  compared to its neighbor INPUT box */
36168
36443
  .o-remove-selection {
36169
36444
  font-size: calc(100% + 4px);
@@ -36768,7 +37043,7 @@ const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
36768
37043
  css /* scss */ `
36769
37044
  .o-color-picker {
36770
37045
  padding: ${PICKER_PADDING}px 0;
36771
- /** FIXME: this is useless, overiden by the popover container */
37046
+ /* FIXME: this is useless, overiden by the popover container */
36772
37047
  box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
36773
37048
  background-color: white;
36774
37049
  line-height: 1.2;
@@ -36911,7 +37186,7 @@ css /* scss */ `
36911
37186
  margin-right: 2px;
36912
37187
  }
36913
37188
  .o-wrong-color {
36914
- /** FIXME bootstrap class instead? */
37189
+ /* FIXME bootstrap class instead? */
36915
37190
  outline-color: red;
36916
37191
  border-color: red;
36917
37192
  &:focus {
@@ -38955,10 +39230,10 @@ css /* scss */ `
38955
39230
  }
38956
39231
 
38957
39232
  .fa-stack {
38958
- // reset stack size which is doubled by default
38959
- width: 1em;
38960
- height: 1em;
38961
- line-height: 1em;
39233
+ /* reset stack size which is doubled by default */
39234
+ width: ${CLOSE_ICON_RADIUS * 2}px;
39235
+ height: ${CLOSE_ICON_RADIUS * 2}px;
39236
+ line-height: ${CLOSE_ICON_RADIUS * 2}px;
38962
39237
  }
38963
39238
 
38964
39239
  .force-open-assistant {
@@ -38975,7 +39250,7 @@ css /* scss */ `
38975
39250
  margin: 1px 4px;
38976
39251
 
38977
39252
  .o-semi-bold {
38978
- /** FIXME: to remove in favor of Bootstrap
39253
+ /* FIXME: to remove in favor of Bootstrap
38979
39254
  * 'fw-semibold' when we upgrade to Bootstrap 5.2
38980
39255
  */
38981
39256
  font-weight: 600 !important;
@@ -40211,7 +40486,7 @@ css /* scss */ `
40211
40486
  .o-threshold-value {
40212
40487
  flex-grow: 1;
40213
40488
  flex-basis: 60%;
40214
- min-width: 0px; // input overflows in Firefox otherwise
40489
+ min-width: 0px; /* input overflows in Firefox otherwise */
40215
40490
  }
40216
40491
  .o-threshold-value input:disabled {
40217
40492
  background-color: #edebed;
@@ -41030,7 +41305,7 @@ dataValidationEvaluatorRegistry.add("dateIs", {
41030
41305
  name: _t("Date is"),
41031
41306
  getPreview: (criterion, getters) => {
41032
41307
  return criterion.dateValue === "exactDate"
41033
- ? _t("Date is %s", getDateCriterionFormattedValues(criterion, getters)[0])
41308
+ ? _t("Date is %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41034
41309
  : _t("Date is %s", DVTerms.DateIs[criterion.dateValue]);
41035
41310
  },
41036
41311
  });
@@ -41055,7 +41330,7 @@ dataValidationEvaluatorRegistry.add("dateIsBefore", {
41055
41330
  name: _t("Date is before"),
41056
41331
  getPreview: (criterion, getters) => {
41057
41332
  return criterion.dateValue === "exactDate"
41058
- ? _t("Date is before %s", getDateCriterionFormattedValues(criterion, getters)[0])
41333
+ ? _t("Date is before %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41059
41334
  : _t("Date is before %s", DVTerms.DateIsBefore[criterion.dateValue]);
41060
41335
  },
41061
41336
  });
@@ -41080,7 +41355,7 @@ dataValidationEvaluatorRegistry.add("dateIsOnOrBefore", {
41080
41355
  name: _t("Date is on or before"),
41081
41356
  getPreview: (criterion, getters) => {
41082
41357
  return criterion.dateValue === "exactDate"
41083
- ? _t("Date is on or before %s", getDateCriterionFormattedValues(criterion, getters)[0])
41358
+ ? _t("Date is on or before %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41084
41359
  : _t("Date is on or before %s", DVTerms.DateIsBefore[criterion.dateValue]);
41085
41360
  },
41086
41361
  });
@@ -41105,7 +41380,7 @@ dataValidationEvaluatorRegistry.add("dateIsAfter", {
41105
41380
  name: _t("Date is after"),
41106
41381
  getPreview: (criterion, getters) => {
41107
41382
  return criterion.dateValue === "exactDate"
41108
- ? _t("Date is after %s", getDateCriterionFormattedValues(criterion, getters)[0])
41383
+ ? _t("Date is after %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41109
41384
  : _t("Date is after %s", DVTerms.DateIsBefore[criterion.dateValue]);
41110
41385
  },
41111
41386
  });
@@ -41130,7 +41405,7 @@ dataValidationEvaluatorRegistry.add("dateIsOnOrAfter", {
41130
41405
  name: _t("Date is on or after"),
41131
41406
  getPreview: (criterion, getters) => {
41132
41407
  return criterion.dateValue === "exactDate"
41133
- ? _t("Date is on or after %s", getDateCriterionFormattedValues(criterion, getters)[0])
41408
+ ? _t("Date is on or after %s", getDateCriterionFormattedValues(criterion.values, getters.getLocale())[0])
41134
41409
  : _t("Date is on or after %s", DVTerms.DateIsBefore[criterion.dateValue]);
41135
41410
  },
41136
41411
  });
@@ -41156,7 +41431,7 @@ dataValidationEvaluatorRegistry.add("dateIsBetween", {
41156
41431
  numberOfValues: () => 2,
41157
41432
  name: _t("Date is between"),
41158
41433
  getPreview: (criterion, getters) => {
41159
- const values = getDateCriterionFormattedValues(criterion, getters);
41434
+ const values = getDateCriterionFormattedValues(criterion.values, getters.getLocale());
41160
41435
  return _t("Date is between %s and %s", values[0], values[1]);
41161
41436
  },
41162
41437
  });
@@ -41182,7 +41457,7 @@ dataValidationEvaluatorRegistry.add("dateIsNotBetween", {
41182
41457
  numberOfValues: () => 2,
41183
41458
  name: _t("Date is not between"),
41184
41459
  getPreview: (criterion, getters) => {
41185
- const values = getDateCriterionFormattedValues(criterion, getters);
41460
+ const values = getDateCriterionFormattedValues(criterion.values, getters.getLocale());
41186
41461
  return _t("Date is not between %s and %s", values[0], values[1]);
41187
41462
  },
41188
41463
  });
@@ -41465,19 +41740,6 @@ function checkValueIsNumber(value) {
41465
41740
  const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
41466
41741
  return valueAsNumber !== undefined;
41467
41742
  }
41468
- function getDateCriterionFormattedValues(criterion, getters) {
41469
- const locale = getters.getLocale();
41470
- return criterion.values.map((valueStr) => {
41471
- if (valueStr.startsWith("=")) {
41472
- return valueStr;
41473
- }
41474
- const value = parseLiteral(valueStr, locale);
41475
- if (typeof value === "number") {
41476
- return formatValue(value, { format: locale.dateFormat, locale });
41477
- }
41478
- return "";
41479
- });
41480
- }
41481
41743
 
41482
41744
  /** This component looks like a select input, but on click it opens a Menu with the items given as props instead of a dropdown */
41483
41745
  class SelectMenu extends owl.Component {
@@ -41559,6 +41821,7 @@ class DataValidationInput extends owl.Component {
41559
41821
  focused: false,
41560
41822
  onBlur: () => { },
41561
41823
  };
41824
+ static components = { StandaloneComposer: StandaloneComposer };
41562
41825
  inputRef = owl.useRef("input");
41563
41826
  setup() {
41564
41827
  owl.useEffect(() => {
@@ -41570,10 +41833,6 @@ class DataValidationInput extends owl.Component {
41570
41833
  state = owl.useState({
41571
41834
  shouldDisplayError: !!this.props.value, // Don't display error if user inputted nothing yet
41572
41835
  });
41573
- onValueChanged(ev) {
41574
- this.state.shouldDisplayError = true;
41575
- this.props.onValueChanged(ev.target.value);
41576
- }
41577
41836
  get placeholder() {
41578
41837
  const evaluator = dataValidationEvaluatorRegistry.get(this.props.criterionType);
41579
41838
  if (evaluator.allowedValues === "onlyFormulas") {
@@ -41584,6 +41843,27 @@ class DataValidationInput extends owl.Component {
41584
41843
  }
41585
41844
  return _t("Value or formula");
41586
41845
  }
41846
+ get allowedValues() {
41847
+ const evaluator = dataValidationEvaluatorRegistry.get(this.props.criterionType);
41848
+ return evaluator.allowedValues ?? "any";
41849
+ }
41850
+ onInputValueChanged(ev) {
41851
+ this.state.shouldDisplayError = true;
41852
+ this.props.onValueChanged(ev.target.value);
41853
+ }
41854
+ onChangeComposerValue(str) {
41855
+ this.state.shouldDisplayError = true;
41856
+ this.props.onValueChanged(str);
41857
+ }
41858
+ getDataValidationRuleInputComposerProps() {
41859
+ return {
41860
+ onConfirm: (str) => this.onChangeComposerValue(str),
41861
+ composerContent: this.props.value,
41862
+ placeholder: this.placeholder,
41863
+ class: "o-sidePanel-composer",
41864
+ defaultRangeSheetId: this.env.model.getters.getActiveSheetId(),
41865
+ };
41866
+ }
41587
41867
  get errorMessage() {
41588
41868
  if (!this.state.shouldDisplayError) {
41589
41869
  return undefined;
@@ -41919,13 +42199,13 @@ function getDataValidationCriterionMenuItems(callback) {
41919
42199
 
41920
42200
  class DataValidationEditor extends owl.Component {
41921
42201
  static template = "o-spreadsheet-DataValidationEditor";
41922
- static components = { SelectionInput, SelectMenu, Section };
42202
+ static components = { SelectionInput, SelectMenu, Section, ValidationMessages };
41923
42203
  static props = {
41924
42204
  rule: { type: Object, optional: true },
41925
42205
  onExit: Function,
41926
42206
  onCloseSidePanel: { type: Function, optional: true },
41927
42207
  };
41928
- state = owl.useState({ rule: this.defaultDataValidationRule });
42208
+ state = owl.useState({ rule: this.defaultDataValidationRule, errors: [] });
41929
42209
  setup() {
41930
42210
  if (this.props.rule) {
41931
42211
  const sheetId = this.env.model.getters.getActiveSheetId();
@@ -41950,15 +42230,15 @@ class DataValidationEditor extends owl.Component {
41950
42230
  this.state.rule.isBlocking = isBlocking === "true";
41951
42231
  }
41952
42232
  onSave() {
41953
- if (!this.canSave) {
41954
- return;
42233
+ if (this.state.rule) {
42234
+ const result = this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
42235
+ if (!result.isSuccessful) {
42236
+ this.state.errors = result.reasons;
42237
+ }
42238
+ else {
42239
+ this.props.onExit();
42240
+ }
41955
42241
  }
41956
- this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
41957
- this.props.onExit();
41958
- }
41959
- get canSave() {
41960
- return this.env.model.canDispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload)
41961
- .isSuccessful;
41962
42242
  }
41963
42243
  get dispatchPayload() {
41964
42244
  const rule = { ...this.state.rule, ranges: undefined };
@@ -41999,6 +42279,9 @@ class DataValidationEditor extends owl.Component {
41999
42279
  get criterionComponent() {
42000
42280
  return dataValidationPanelCriteriaRegistry.get(this.state.rule.criterion.type).component;
42001
42281
  }
42282
+ get errorMessages() {
42283
+ return this.state.errors.map((error) => DVTerms.Errors[error] || DVTerms.Errors.Unexpected);
42284
+ }
42002
42285
  }
42003
42286
 
42004
42287
  css /* scss */ `
@@ -42010,7 +42293,7 @@ css /* scss */ `
42010
42293
  border-bottom: 1px solid ${FIGURE_BORDER_COLOR};
42011
42294
 
42012
42295
  .o-dv-container {
42013
- min-width: 0; // otherwise flex won't shrink correctly
42296
+ min-width: 0; /* otherwise flex won't shrink correctly */
42014
42297
  }
42015
42298
 
42016
42299
  .o-dv-preview-description {
@@ -45028,6 +45311,9 @@ class PivotSidePanel extends owl.Component {
45028
45311
  PivotLayoutConfigurator,
45029
45312
  Section,
45030
45313
  };
45314
+ setup() {
45315
+ useHighlights(this);
45316
+ }
45031
45317
  get sidePanelEditor() {
45032
45318
  const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
45033
45319
  if (!pivot) {
@@ -45035,6 +45321,9 @@ class PivotSidePanel extends owl.Component {
45035
45321
  }
45036
45322
  return pivotSidePanelRegistry.get(pivot.type).editor;
45037
45323
  }
45324
+ get highlights() {
45325
+ return getPivotHighlights(this.env.model.getters, this.props.pivotId);
45326
+ }
45038
45327
  }
45039
45328
 
45040
45329
  css /* scss */ `
@@ -45704,7 +45993,7 @@ class TableStylePreview extends owl.Component {
45704
45993
 
45705
45994
  css /* scss */ `
45706
45995
  .o-table-style-popover {
45707
- /** 7 tables preview + padding by line */
45996
+ /* 7 tables preview + padding by line */
45708
45997
  width: calc((66px + 4px * 2) * 7 + 1.5rem * 2);
45709
45998
  background: #fff;
45710
45999
  font-size: 14px;
@@ -47256,7 +47545,7 @@ css /* scss */ `
47256
47545
  box-sizing: border-box !important;
47257
47546
  accent-color: #808080;
47258
47547
  margin: ${MARGIN}px;
47259
- /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
47548
+ /* required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
47260
47549
  position: absolute;
47261
47550
  }
47262
47551
  `;
@@ -47603,7 +47892,7 @@ css /*SCSS*/ `
47603
47892
  }
47604
47893
  }
47605
47894
  .o-figure-container {
47606
- -webkit-user-select: none; // safari
47895
+ -webkit-user-select: none; /* safari */
47607
47896
  user-select: none;
47608
47897
  }
47609
47898
  `;
@@ -50676,25 +50965,20 @@ class Grid extends owl.Component {
50676
50965
  if (!clipboardData) {
50677
50966
  return;
50678
50967
  }
50679
- const clipboardDataTextContent = clipboardData?.getData(ClipboardMIMEType.PlainText);
50680
- const clipboardDataHtmlContent = clipboardData?.getData(ClipboardMIMEType.Html);
50681
- const htmlDocument = new DOMParser().parseFromString(clipboardDataHtmlContent ?? "<div></div>", "text/html");
50682
- const osClipboardSpreadsheetContent = clipboardData.getData(ClipboardMIMEType.OSpreadsheet) || "{}";
50968
+ const osClipboard = {
50969
+ content: {
50970
+ [ClipboardMIMEType.PlainText]: clipboardData?.getData(ClipboardMIMEType.PlainText),
50971
+ [ClipboardMIMEType.Html]: clipboardData?.getData(ClipboardMIMEType.Html),
50972
+ },
50973
+ };
50683
50974
  const target = this.env.model.getters.getSelectedZones();
50684
50975
  const isCutOperation = this.env.model.getters.isCutOperation();
50685
- const clipboardId = JSON.parse(osClipboardSpreadsheetContent).clipboardId ??
50686
- htmlDocument.querySelector("div")?.getAttribute("data-clipboard-id");
50976
+ const clipboardContent = parseOSClipboardContent(osClipboard.content);
50977
+ const clipboardId = clipboardContent.data?.clipboardId;
50687
50978
  if (this.env.model.getters.getClipboardId() === clipboardId) {
50688
50979
  interactivePaste(this.env, target);
50689
50980
  }
50690
50981
  else {
50691
- const clipboardContent = {
50692
- [ClipboardMIMEType.PlainText]: clipboardDataTextContent,
50693
- [ClipboardMIMEType.Html]: clipboardDataHtmlContent,
50694
- };
50695
- if (osClipboardSpreadsheetContent !== "{}") {
50696
- clipboardContent[ClipboardMIMEType.OSpreadsheet] = osClipboardSpreadsheetContent;
50697
- }
50698
50982
  interactivePasteFromOS(this.env, target, clipboardContent);
50699
50983
  }
50700
50984
  if (isCutOperation) {
@@ -52514,8 +52798,9 @@ class ConditionalFormatPlugin extends CorePlugin {
52514
52798
  if (replaceIndex > -1) {
52515
52799
  currentRanges = rules[replaceIndex].ranges.map(toUnboundedZone);
52516
52800
  }
52517
- currentRanges = currentRanges.concat(toAdd);
52518
- return recomputeZones(currentRanges, toRemove).map((zone) => this.getters.getRangeDataFromZone(sheetId, zone));
52801
+ // Remove the zones first in case the same position is in toAdd and toRemove
52802
+ const withRemovedZones = recomputeZones(currentRanges, toRemove);
52803
+ return recomputeZones([...toAdd, ...withRemovedZones], []).map((zone) => this.getters.getRangeDataFromZone(sheetId, zone));
52519
52804
  }
52520
52805
  // ---------------------------------------------------------------------------
52521
52806
  // Private
@@ -52826,7 +53111,7 @@ class DataValidationPlugin extends CorePlugin {
52826
53111
  allowDispatch(cmd) {
52827
53112
  switch (cmd.type) {
52828
53113
  case "ADD_DATA_VALIDATION_RULE":
52829
- return this.checkValidations(cmd, this.chainValidations(this.checkEmptyRange, this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));
53114
+ return this.checkValidations(cmd, this.chainValidations(this.checkEmptyRange, this.checkValidRange, this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));
52830
53115
  case "REMOVE_DATA_VALIDATION_RULE":
52831
53116
  if (!this.rules[cmd.sheetId].find((rule) => rule.id === cmd.id)) {
52832
53117
  return "UnknownDataValidationRule" /* CommandResult.UnknownDataValidationRule */;
@@ -52985,6 +53270,20 @@ class DataValidationPlugin extends CorePlugin {
52985
53270
  }
52986
53271
  }
52987
53272
  }
53273
+ exportForExcel(data) {
53274
+ if (!data.sheets) {
53275
+ return;
53276
+ }
53277
+ for (const sheet of data.sheets) {
53278
+ sheet.dataValidationRules = [];
53279
+ for (const rule of this.rules[sheet.id]) {
53280
+ sheet.dataValidationRules.push({
53281
+ ...rule,
53282
+ ranges: rule.ranges.map((range) => this.getters.getRangeString(range, sheet.id, { useFixedReference: true })),
53283
+ });
53284
+ }
53285
+ }
53286
+ }
52988
53287
  checkCriterionTypeIsValid(cmd) {
52989
53288
  return dataValidationEvaluatorRegistry.contains(cmd.rule.criterion.type)
52990
53289
  ? "Success" /* CommandResult.Success */
@@ -53003,21 +53302,28 @@ class DataValidationPlugin extends CorePlugin {
53003
53302
  checkCriterionValuesAreValid(cmd) {
53004
53303
  const criterion = cmd.rule.criterion;
53005
53304
  const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);
53006
- if (criterion.values.some((value) => {
53007
- if (value.startsWith("=")) {
53008
- return evaluator.allowedValues === "onlyLiterals";
53009
- }
53010
- else if (evaluator.allowedValues === "onlyFormulas") {
53305
+ const isInvalid = (value) => {
53306
+ if (evaluator.allowedValues === "onlyFormulas" && !value.startsWith("=")) {
53011
53307
  return true;
53012
53308
  }
53013
- else {
53014
- return !evaluator.isCriterionValueValid(value);
53309
+ if (value.startsWith("=")) {
53310
+ return evaluator.allowedValues === "onlyLiterals" || compile(value).isBadExpression;
53015
53311
  }
53016
- })) {
53312
+ return !evaluator.isCriterionValueValid(value);
53313
+ };
53314
+ if (criterion.values.some(isInvalid)) {
53017
53315
  return "InvalidDataValidationCriterionValue" /* CommandResult.InvalidDataValidationCriterionValue */;
53018
53316
  }
53019
53317
  return "Success" /* CommandResult.Success */;
53020
53318
  }
53319
+ checkValidRange(cmd) {
53320
+ const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
53321
+ const stringRanges = ranges.map((range) => this.getters.getRangeString(range, cmd.sheetId));
53322
+ if (stringRanges.some((xc) => !this.getters.isRangeValid(xc))) {
53323
+ return "InvalidRange" /* CommandResult.InvalidRange */;
53324
+ }
53325
+ return "Success" /* CommandResult.Success */;
53326
+ }
53021
53327
  }
53022
53328
 
53023
53329
  class FigurePlugin extends CorePlugin {
@@ -54752,6 +55058,7 @@ class SheetPlugin extends CorePlugin {
54752
55058
  formats: {},
54753
55059
  borders: {},
54754
55060
  conditionalFormats: [],
55061
+ dataValidationRules: [],
54755
55062
  figures: [],
54756
55063
  tables: [],
54757
55064
  areGridLinesVisible: sheet.areGridLinesVisible === undefined ? true : sheet.areGridLinesVisible,
@@ -58121,6 +58428,9 @@ class Evaluator {
58121
58428
  }
58122
58429
  for (let i = 0; i < positions.length; ++i) {
58123
58430
  const position = positions[i];
58431
+ if (this.nextPositionsToUpdate.has(position)) {
58432
+ continue;
58433
+ }
58124
58434
  const evaluatedCell = this.computeCell(position);
58125
58435
  if (evaluatedCell !== EMPTY_CELL) {
58126
58436
  this.evaluatedCells.set(position, evaluatedCell);
@@ -58128,6 +58438,9 @@ class Evaluator {
58128
58438
  }
58129
58439
  onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
58130
58440
  }
58441
+ if (currentIteration >= MAX_ITERATION) {
58442
+ console.warn("Maximum iteration reached while evaluating cells");
58443
+ }
58131
58444
  }
58132
58445
  computeCell(position) {
58133
58446
  const evaluation = this.evaluatedCells.get(position);
@@ -58162,7 +58475,6 @@ class Evaluator {
58162
58475
  }
58163
58476
  finally {
58164
58477
  this.cellsBeingComputed.delete(cellId);
58165
- this.nextPositionsToUpdate.delete(position);
58166
58478
  }
58167
58479
  }
58168
58480
  computeAndSave(position) {
@@ -58198,6 +58510,7 @@ class Evaluator {
58198
58510
  invalidatePositionsDependingOnSpread(sheetId, resultZone) {
58199
58511
  // the result matrix is split in 2 zones to exclude the array formula position
58200
58512
  const invalidatedPositions = this.formulaDependencies().getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
58513
+ invalidatedPositions.delete({ sheetId, col: resultZone.left, row: resultZone.top });
58201
58514
  this.nextPositionsToUpdate.addMany(invalidatedPositions);
58202
58515
  }
58203
58516
  assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
@@ -59321,25 +59634,11 @@ class EvaluationDataValidationPlugin extends UIPlugin {
59321
59634
  }
59322
59635
  switch (cmd.type) {
59323
59636
  case "ADD_DATA_VALIDATION_RULE":
59324
- const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
59325
- if (cmd.rule.criterion.type === "isBoolean") {
59326
- this.setContentToBooleanCells({ ...cmd.rule, ranges });
59327
- }
59328
- delete this.validationResults[cmd.sheetId];
59329
- break;
59330
59637
  case "REMOVE_DATA_VALIDATION_RULE":
59331
59638
  delete this.validationResults[cmd.sheetId];
59332
59639
  break;
59333
59640
  }
59334
59641
  }
59335
- setContentToBooleanCells(rule) {
59336
- for (const position of getCellPositionsInRanges(rule.ranges)) {
59337
- const evaluatedCell = this.getters.getEvaluatedCell(position);
59338
- if (evaluatedCell.type !== CellValueType.boolean) {
59339
- this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
59340
- }
59341
- }
59342
- }
59343
59642
  isDataValidationInvalid(cellPosition) {
59344
59643
  return !this.getValidationResultForCell(cellPosition).isValid;
59345
59644
  }
@@ -59355,9 +59654,10 @@ class EvaluationDataValidationPlugin extends UIPlugin {
59355
59654
  getDataValidationInvalidCriterionValueMessage(criterionType, value) {
59356
59655
  const evaluator = dataValidationEvaluatorRegistry.get(criterionType);
59357
59656
  if (value.startsWith("=")) {
59358
- return evaluator.allowedValues === "onlyLiterals"
59359
- ? _t("The value must not be a formula")
59360
- : undefined;
59657
+ if (evaluator.allowedValues === "onlyLiterals") {
59658
+ return _t("The value must not be a formula");
59659
+ }
59660
+ return this.isValidFormula(value) ? undefined : DVTerms.CriterionError.validFormula;
59361
59661
  }
59362
59662
  else if (evaluator.allowedValues === "onlyFormulas") {
59363
59663
  return _t("The value must be a formula");
@@ -59383,6 +59683,9 @@ class EvaluationDataValidationPlugin extends UIPlugin {
59383
59683
  const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
59384
59684
  return error ? { error, rule, isValid: false } : VALID_RESULT;
59385
59685
  }
59686
+ isValidFormula(value) {
59687
+ return !compile(value).isBadExpression;
59688
+ }
59386
59689
  getValidationResultForCell(cellPosition) {
59387
59690
  const { col, row, sheetId } = cellPosition;
59388
59691
  if (!this.validationResults[sheetId]) {
@@ -61818,7 +62121,7 @@ class Session extends EventBus {
61818
62121
  * Notify the server that the user client left the collaborative session
61819
62122
  */
61820
62123
  async leave(data) {
61821
- if (Object.keys(this.clients).length === 1 && this.processedRevisions.size) {
62124
+ if (data && Object.keys(this.clients).length === 1 && this.processedRevisions.size) {
61822
62125
  await this.snapshot(data());
61823
62126
  }
61824
62127
  delete this.clients[this.clientId];
@@ -63370,6 +63673,42 @@ class CellComputedStylePlugin extends UIPlugin {
63370
63673
  }
63371
63674
  }
63372
63675
 
63676
+ class DataValidationInsertionPlugin extends UIPlugin {
63677
+ handle(cmd) {
63678
+ switch (cmd.type) {
63679
+ case "ADD_DATA_VALIDATION_RULE":
63680
+ if (cmd.rule.criterion.type === "isBoolean") {
63681
+ const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
63682
+ for (const position of getCellPositionsInRanges(ranges)) {
63683
+ const cell = this.getters.getCell(position);
63684
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
63685
+ if (!cell?.content) {
63686
+ this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
63687
+ // In this case, a cell has been updated in the core plugin but
63688
+ // not yet evaluated. This can occur after a paste operation.
63689
+ }
63690
+ else if (cell?.content && evaluatedCell.type === CellValueType.empty) {
63691
+ let value;
63692
+ if (cell.content.startsWith("=")) {
63693
+ const result = this.getters.evaluateFormula(position.sheetId, cell.content);
63694
+ value = (isMatrix(result) ? result[0][0] : result)?.toString();
63695
+ }
63696
+ else {
63697
+ value = cell.content;
63698
+ }
63699
+ if (!value || !isBoolean(value)) {
63700
+ this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
63701
+ }
63702
+ }
63703
+ else if (evaluatedCell.type !== CellValueType.boolean) {
63704
+ this.dispatch("UPDATE_CELL", { ...position, content: "FALSE" });
63705
+ }
63706
+ }
63707
+ }
63708
+ }
63709
+ }
63710
+ }
63711
+
63373
63712
  const genericRepeatsTransforms = [
63374
63713
  repeatSheetDependantCommand,
63375
63714
  repeatTargetDependantCommand,
@@ -64023,7 +64362,7 @@ class ClipboardPlugin extends UIPlugin {
64023
64362
  const zones = this.getters.getSelectedZones();
64024
64363
  return this.isCutAllowedOn(zones);
64025
64364
  case "PASTE_FROM_OS_CLIPBOARD": {
64026
- const copiedData = this.convertOSClipboardData(cmd.clipboardContent[ClipboardMIMEType.PlainText] ?? "");
64365
+ const copiedData = this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
64027
64366
  const pasteOption = cmd.pasteOption;
64028
64367
  return this.isPasteAllowed(cmd.target, copiedData, { pasteOption, isCutOperation: false });
64029
64368
  }
@@ -64076,12 +64415,9 @@ class ClipboardPlugin extends UIPlugin {
64076
64415
  break;
64077
64416
  case "PASTE_FROM_OS_CLIPBOARD": {
64078
64417
  this._isCutOperation = false;
64079
- if (cmd.clipboardContent[ClipboardMIMEType.OSpreadsheet]) {
64080
- this.copiedData = JSON.parse(cmd.clipboardContent[ClipboardMIMEType.OSpreadsheet]);
64081
- }
64082
- else {
64083
- this.copiedData = this.convertOSClipboardData(cmd.clipboardContent[ClipboardMIMEType.PlainText] ?? "");
64084
- }
64418
+ this.copiedData =
64419
+ cmd.clipboardContent.data ||
64420
+ this.convertTextToClipboardData(cmd.clipboardContent.text ?? "");
64085
64421
  const pasteOption = cmd.pasteOption;
64086
64422
  this.paste(cmd.target, this.copiedData, {
64087
64423
  pasteOption,
@@ -64212,11 +64548,11 @@ class ClipboardPlugin extends UIPlugin {
64212
64548
  }
64213
64549
  }
64214
64550
  }
64215
- convertOSClipboardData(clipboardData) {
64551
+ convertTextToClipboardData(clipboardData) {
64216
64552
  const handlers = this.selectClipboardHandlers({ figureId: true }).concat(this.selectClipboardHandlers({}));
64217
64553
  let copiedData = {};
64218
64554
  for (const { handlerName, handler } of handlers) {
64219
- const data = handler.convertOSClipboardData(clipboardData);
64555
+ const data = handler.convertTextToClipboardData(clipboardData);
64220
64556
  copiedData[handlerName] = data;
64221
64557
  const minimalKeys = ["sheetId", "cells", "zones", "figureId"];
64222
64558
  for (const key of minimalKeys) {
@@ -64385,21 +64721,20 @@ class ClipboardPlugin extends UIPlugin {
64385
64721
  return {
64386
64722
  [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),
64387
64723
  [ClipboardMIMEType.Html]: this.getHTMLContent(),
64388
- [ClipboardMIMEType.OSpreadsheet]: this.getSerializedGridData(),
64389
64724
  };
64390
64725
  }
64391
- getSerializedGridData() {
64726
+ getSheetData() {
64392
64727
  const data = {
64393
64728
  version: CURRENT_VERSION,
64394
64729
  clipboardId: this.clipboardId,
64395
64730
  };
64396
64731
  if (this.copiedData && "figureId" in this.copiedData) {
64397
- return JSON.stringify(data);
64732
+ return data;
64398
64733
  }
64399
- return JSON.stringify({
64734
+ return {
64400
64735
  ...data,
64401
64736
  ...this.copiedData,
64402
- });
64737
+ };
64403
64738
  }
64404
64739
  getPlainTextContent() {
64405
64740
  if (!this.copiedData?.cells) {
@@ -64416,31 +64751,36 @@ class ClipboardPlugin extends UIPlugin {
64416
64751
  .join("\n") || "\t");
64417
64752
  }
64418
64753
  getHTMLContent() {
64419
- if (!this.copiedData?.cells) {
64420
- return `<div data-clipboard-id="${this.clipboardId}">\t</div>`;
64754
+ let innerHTML = "";
64755
+ const cells = this.copiedData?.cells;
64756
+ if (!cells) {
64757
+ innerHTML = "\t";
64421
64758
  }
64422
- const cells = this.copiedData.cells;
64423
- if (cells.length === 1 && cells[0].length === 1) {
64424
- return `<div data-clipboard-id="${this.clipboardId}">${this.getters.getCellText(cells[0][0].position)}</div>`;
64759
+ else if (cells.length === 1 && cells[0].length === 1) {
64760
+ innerHTML = `${this.getters.getCellText(cells[0][0].position)}`;
64425
64761
  }
64426
- if (!cells[0][0]) {
64762
+ else if (!cells[0][0]) {
64427
64763
  return "";
64428
64764
  }
64429
- let htmlTable = `<div data-clipboard-id="${this.clipboardId}"><table border="1" style="border-collapse:collapse">`;
64430
- for (const row of cells) {
64431
- htmlTable += "<tr>";
64432
- for (const cell of row) {
64433
- if (!cell) {
64434
- continue;
64765
+ else {
64766
+ let htmlTable = `<table border="1" style="border-collapse:collapse">`;
64767
+ for (const row of cells) {
64768
+ htmlTable += "<tr>";
64769
+ for (const cell of row) {
64770
+ if (!cell) {
64771
+ continue;
64772
+ }
64773
+ const cssStyle = cssPropertiesToCss(cellStyleToCss(this.getters.getCellComputedStyle(cell.position)));
64774
+ const cellText = this.getters.getCellText(cell.position);
64775
+ htmlTable += `<td style="${cssStyle}">` + xmlEscape(cellText) + "</td>";
64435
64776
  }
64436
- const cssStyle = cssPropertiesToCss(cellStyleToCss(this.getters.getCellComputedStyle(cell.position)));
64437
- const cellText = this.getters.getCellText(cell.position);
64438
- htmlTable += `<td style="${cssStyle}">` + xmlEscape(cellText) + "</td>";
64777
+ htmlTable += "</tr>";
64439
64778
  }
64440
- htmlTable += "</tr>";
64779
+ htmlTable += "</table>";
64780
+ innerHTML = htmlTable;
64441
64781
  }
64442
- htmlTable += "</table></div>";
64443
- return htmlTable;
64782
+ const serializedData = JSON.stringify(this.getSheetData());
64783
+ return `<div data-osheet-clipboard='${xmlEscape(serializedData)}'>${innerHTML}</div>`;
64444
64784
  }
64445
64785
  isCutOperation() {
64446
64786
  return this._isCutOperation ?? false;
@@ -66420,7 +66760,8 @@ const featurePluginRegistry = new Registry()
66420
66760
  .add("history", HistoryPlugin)
66421
66761
  .add("data_cleanup", DataCleanupPlugin)
66422
66762
  .add("table_autofill", TableAutofillPlugin)
66423
- .add("table_ui_resize", TableResizeUI);
66763
+ .add("table_ui_resize", TableResizeUI)
66764
+ .add("datavalidation_insert", DataValidationInsertionPlugin);
66424
66765
  // Plugins which have a state, but which should not be shared in collaborative
66425
66766
  const statefulUIPluginRegistry = new Registry()
66426
66767
  .add("selection", GridSelectionPlugin)
@@ -67646,7 +67987,7 @@ css /* scss */ `
67646
67987
 
67647
67988
  .o-header-group-main-pane {
67648
67989
  &.o-group-rows {
67649
- margin-top: -2px; // Counteract o-header-group-frozen-pane-border offset
67990
+ margin-top: -2px; /* Counteract o-header-group-frozen-pane-border offset */
67650
67991
  }
67651
67992
  &.o-group-columns {
67652
67993
  margin-left: -2px;
@@ -68679,10 +69020,6 @@ class WebClipboardWrapper {
68679
69020
  [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),
68680
69021
  [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),
68681
69022
  };
68682
- const spreadsheetData = content[ClipboardMIMEType.OSpreadsheet];
68683
- if (spreadsheetData) {
68684
- clipboardItemData[ClipboardMIMEType.OSpreadsheet] = this.getBlob(content, ClipboardMIMEType.OSpreadsheet);
68685
- }
68686
69023
  return [new ClipboardItem(clipboardItemData)];
68687
69024
  }
68688
69025
  getBlob(clipboardContent, type) {
@@ -68724,7 +69061,7 @@ css /* scss */ `
68724
69061
  *:before,
68725
69062
  *:after {
68726
69063
  box-sizing: content-box;
68727
- /** rtl not supported ATM */
69064
+ /* rtl not supported ATM */
68728
69065
  direction: ltr;
68729
69066
  }
68730
69067
  .o-separator {
@@ -71629,6 +71966,124 @@ function getExcelThresholdType(type, position) {
71629
71966
  }
71630
71967
  }
71631
71968
 
71969
+ function addDataValidationRules(dataValidationRules) {
71970
+ const dvRulesCount = dataValidationRules.length;
71971
+ if (dvRulesCount === 0) {
71972
+ return [];
71973
+ }
71974
+ const dvNodes = [new XMLString(`<dataValidations count="${dvRulesCount}">`)];
71975
+ for (const dvRule of dataValidationRules) {
71976
+ switch (dvRule.criterion.type) {
71977
+ case "dateIs":
71978
+ case "dateIsBefore":
71979
+ case "dateIsOnOrBefore":
71980
+ case "dateIsAfter":
71981
+ case "dateIsOnOrAfter":
71982
+ case "dateIsBetween":
71983
+ case "dateIsNotBetween":
71984
+ dvNodes.push(addDateRule(dvRule));
71985
+ break;
71986
+ case "isEqual":
71987
+ case "isNotEqual":
71988
+ case "isGreaterThan":
71989
+ case "isGreaterOrEqualTo":
71990
+ case "isLessThan":
71991
+ case "isLessOrEqualTo":
71992
+ case "isBetween":
71993
+ case "isNotBetween":
71994
+ dvNodes.push(addDecimalRule(dvRule));
71995
+ break;
71996
+ case "isValueInRange":
71997
+ case "isValueInList":
71998
+ dvNodes.push(addListRule(dvRule));
71999
+ break;
72000
+ case "customFormula":
72001
+ dvNodes.push(addCustomFormulaRule(dvRule));
72002
+ break;
72003
+ default:
72004
+ console.warn(`Data validation ${dvRule.criterion.type} is not supported in xlsx.`);
72005
+ break;
72006
+ }
72007
+ }
72008
+ dvNodes.push(new XMLString("</dataValidations>"));
72009
+ return dvNodes;
72010
+ }
72011
+ function addDateRule(dvRule) {
72012
+ const rule = dvRule.criterion;
72013
+ const formula1 = adaptFormulaToExcel(rule.values[0]);
72014
+ const formula2 = rule.values[1] ? adaptFormulaToExcel(rule.values[1]) : undefined;
72015
+ const operator = convertDateCriterionTypeToExcelOperator(dvRule.criterion.type);
72016
+ const attributes = commonDataValidationAttributes(dvRule);
72017
+ attributes.push(["type", "date"], ["operator", operator]);
72018
+ if (formula2) {
72019
+ return escapeXml /*xml*/ `
72020
+ <dataValidation ${formatAttributes(attributes)}>
72021
+ <formula1>${toNumber(formula1, DEFAULT_LOCALE)}</formula1>
72022
+ <formula2>${toNumber(formula2, DEFAULT_LOCALE)}</formula2>
72023
+ </dataValidation>
72024
+ `;
72025
+ }
72026
+ return escapeXml /*xml*/ `
72027
+ <dataValidation ${formatAttributes(attributes)}>
72028
+ <formula1>${toNumber(formula1, DEFAULT_LOCALE)}</formula1>
72029
+ </dataValidation>
72030
+ `;
72031
+ }
72032
+ function addDecimalRule(dvRule) {
72033
+ const rule = dvRule.criterion;
72034
+ const formula1 = adaptFormulaToExcel(rule.values[0]);
72035
+ const formula2 = rule.values[1] ? adaptFormulaToExcel(rule.values[1]) : undefined;
72036
+ const operator = convertDecimalCriterionTypeToExcelOperator(dvRule.criterion.type);
72037
+ const attributes = commonDataValidationAttributes(dvRule);
72038
+ attributes.push(["type", "decimal"], ["operator", operator]);
72039
+ if (formula2) {
72040
+ return escapeXml /*xml*/ `
72041
+ <dataValidation ${formatAttributes(attributes)}>
72042
+ <formula1>${formula1}</formula1>
72043
+ <formula2>${formula2}</formula2>
72044
+ </dataValidation>
72045
+ `;
72046
+ }
72047
+ return escapeXml /*xml*/ `
72048
+ <dataValidation ${formatAttributes(attributes)}>
72049
+ <formula1>${formula1}</formula1>
72050
+ </dataValidation>
72051
+ `;
72052
+ }
72053
+ function addListRule(dvRule) {
72054
+ const rule = dvRule.criterion;
72055
+ const formula1 = dvRule.criterion.type === "isValueInRange"
72056
+ ? adaptFormulaToExcel(rule.values[0])
72057
+ : `"${rule.values.join(",")}"`;
72058
+ const attributes = commonDataValidationAttributes(dvRule);
72059
+ attributes.push(["type", "list"]);
72060
+ return escapeXml /*xml*/ `
72061
+ <dataValidation ${formatAttributes(attributes)}>
72062
+ <formula1>${formula1}</formula1>
72063
+ </dataValidation>
72064
+ `;
72065
+ }
72066
+ function addCustomFormulaRule(dvRule) {
72067
+ const rule = dvRule.criterion;
72068
+ const formula1 = adaptFormulaToExcel(rule.values[0]);
72069
+ const attributes = commonDataValidationAttributes(dvRule);
72070
+ attributes.push(["type", "custom"]);
72071
+ return escapeXml /*xml*/ `
72072
+ <dataValidation ${formatAttributes(attributes)}>
72073
+ <formula1>${formula1}</formula1>
72074
+ </dataValidation>
72075
+ `;
72076
+ }
72077
+ function commonDataValidationAttributes(dvRule) {
72078
+ return [
72079
+ ["allowBlank", "1"],
72080
+ ["showInputMessage", "1"],
72081
+ ["showErrorMessage", "1"],
72082
+ ["errorStyle", !dvRule.isBlocking ? "warning" : ""],
72083
+ ["sqref", dvRule.ranges.join(" ")],
72084
+ ];
72085
+ }
72086
+
71632
72087
  function createDrawing(drawingRelIds, sheet, figures) {
71633
72088
  const namespaces = [
71634
72089
  ["xmlns:xdr", NAMESPACE.drawing],
@@ -72413,6 +72868,7 @@ function createWorksheets(data, construct) {
72413
72868
  ${addRows(construct, data, sheet)}
72414
72869
  ${addMerges(sheet.merges)}
72415
72870
  ${joinXmlNodes(addConditionalFormatting(construct.dxfs, sheet.conditionalFormats))}
72871
+ ${joinXmlNodes(addDataValidationRules(sheet.dataValidationRules))}
72416
72872
  ${addHyperlinks(construct, data, sheetIndex)}
72417
72873
  ${drawingNode}
72418
72874
  ${tablesNode}
@@ -72736,7 +73192,8 @@ class Model extends EventBus {
72736
73192
  this.session.join(this.config.client);
72737
73193
  }
72738
73194
  async leaveSession() {
72739
- await this.session.leave(lazy(() => this.exportData()));
73195
+ const snapshot = this.getters.isReadonly() ? undefined : lazy(() => this.exportData());
73196
+ await this.session.leave(snapshot);
72740
73197
  }
72741
73198
  setupUiPlugin(Plugin) {
72742
73199
  const plugin = new Plugin(this.uiPluginConfig);
@@ -73338,6 +73795,6 @@ exports.tokenColors = tokenColors;
73338
73795
  exports.tokenize = tokenize;
73339
73796
 
73340
73797
 
73341
- __info__.version = "18.1.0-alpha.4";
73342
- __info__.date = "2024-11-13T15:06:47.769Z";
73343
- __info__.hash = "e1ad985";
73798
+ __info__.version = "18.1.0-alpha.5";
73799
+ __info__.date = "2024-11-22T14:22:50.899Z";
73800
+ __info__.hash = "e13bd67";