@odoo/o-spreadsheet 17.0.19 → 17.0.20

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 17.0.19
6
- * @date 2024-04-18T16:41:42.291Z
7
- * @hash 9f3317a
5
+ * @version 17.0.20
6
+ * @date 2024-04-26T07:37:04.503Z
7
+ * @hash 065ac98
8
8
  */
9
9
 
10
10
  'use strict';
@@ -26668,17 +26668,13 @@ class Composer extends owl.Component {
26668
26668
  this.env.focusableElement.setFocusableElement(el);
26669
26669
  }
26670
26670
  this.contentHelper.updateEl(el);
26671
- this.processTokenAtCursor();
26672
26671
  });
26673
26672
  owl.useEffect(() => {
26674
26673
  this.processContent();
26675
26674
  });
26676
- owl.onPatched(() => {
26677
- // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
26678
- if (this.env.model.getters.getEditionMode() === "inactive") {
26679
- this.processTokenAtCursor();
26680
- }
26681
- });
26675
+ owl.useEffect(() => {
26676
+ this.processTokenAtCursor();
26677
+ }, () => [this.env.model.getters.getEditionMode() !== "inactive"]);
26682
26678
  }
26683
26679
  // ---------------------------------------------------------------------------
26684
26680
  // Handlers
@@ -27109,6 +27105,7 @@ class Composer extends owl.Component {
27109
27105
  const dataValidationAutocompleteValues = this.env.model.getters.getAutoCompleteDataValidationValues();
27110
27106
  if (!content.startsWith("=") && dataValidationAutocompleteValues.length) {
27111
27107
  this.showDataValidationAutocomplete(dataValidationAutocompleteValues);
27108
+ return;
27112
27109
  }
27113
27110
  if (content.startsWith("=")) {
27114
27111
  const token = this.env.model.getters.getTokenAtCursor();
@@ -34368,7 +34365,7 @@ class BordersPlugin extends CorePlugin {
34368
34365
  this.clearBorders(cmd.sheetId, cmd.target);
34369
34366
  break;
34370
34367
  case "REMOVE_COLUMNS_ROWS":
34371
- for (let el of cmd.elements) {
34368
+ for (let el of [...cmd.elements].sort((a, b) => b - a)) {
34372
34369
  if (cmd.dimension === "COL") {
34373
34370
  this.shiftBordersHorizontally(cmd.sheetId, el + 1, -1);
34374
34371
  }
@@ -40148,6 +40145,353 @@ class CompilationParametersBuilder {
40148
40145
  }
40149
40146
  }
40150
40147
 
40148
+ /**
40149
+ * ####################################################
40150
+ * # INTRODUCTION
40151
+ * ####################################################
40152
+ *
40153
+ * This file contain the function recomputeZones.
40154
+ * This function try to recompute in a performant way
40155
+ * an ensemble of zones possibly overlapping to avoid
40156
+ * overlapping and to reduce the number of zones.
40157
+ *
40158
+ * It also allows to remove some zones from the ensemble.
40159
+ *
40160
+ * In the following example, 2 zones are overlapping.
40161
+ * Applying recomputeZones will return zones without
40162
+ * overlapping:
40163
+ *
40164
+ * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
40165
+ *
40166
+ * A B C D E A B C D E
40167
+ * 1 ___ 1 ___
40168
+ * 2 ___|_ | 2 ___| | |
40169
+ * 3 | |_|_| ---> 3 | | |_|
40170
+ * 4 |_____| 4 |___|_|
40171
+ * 6 6
40172
+ * 7 7
40173
+ *
40174
+ *
40175
+ * In the following example, 2 zones are contiguous.
40176
+ * Applying recomputeZones will return only one zone:
40177
+ *
40178
+ * ["B2:B3", "C2:D3"] ["B2:D3"]
40179
+ *
40180
+ * A B C D E A B C D E
40181
+ * 1 _ ___ 1 _____
40182
+ * 2 | | | ---> 2 | |
40183
+ * 3 |_|___| 3 |_____|
40184
+ * 4 4
40185
+ *
40186
+ *
40187
+ * In the following example, we want to remove a zone
40188
+ * from the ensemble. Applying recomputeZones will
40189
+ * return the ensemble without the zone to remove:
40190
+ *
40191
+ * remove ["C3:D3"] ["B2:B4", "C2:D2",
40192
+ * "C4:D4", "E2:E4"]
40193
+ *
40194
+ * A B C D E F A B C D E F
40195
+ * 1 _______ 1 _______
40196
+ * 2 | | ---> 2 | |___| |
40197
+ * 3 | xxx | 3 | |___| |
40198
+ * 4 |_______| 4 |_|___|_|
40199
+ * 5 5
40200
+ *
40201
+ *
40202
+ * The exercise seems simple when we have only 2 zones.
40203
+ * But with n zones and in a performant way, we want to
40204
+ * avoid comparing each zone with all the others.
40205
+ *
40206
+ *
40207
+ * ####################################################
40208
+ * # Methodological approach
40209
+ * ####################################################
40210
+ *
40211
+ * The methodological approach to avoid comparing each
40212
+ * zone with all the others is to use a data structure
40213
+ * that allow to quickly find which zones are
40214
+ * overlapping with any other given zone.
40215
+ *
40216
+ * Here the idea is to profile the zones at the columns level.
40217
+ *
40218
+ * To do that, we propose to use a data structure
40219
+ * composed of 2 parts:
40220
+ * - profilesStartingPosition: a sorted number array
40221
+ * indicating on which columns a new profile begins.
40222
+ * - profiles: a map where the key is a column
40223
+ * position (from profilesStartingPosition) and the
40224
+ * value is a sorted number array representing a
40225
+ * profile.
40226
+ *
40227
+ *
40228
+ * See the following example: here profileStartingPosition
40229
+ * corresponds to [A,C,E,G,K]
40230
+ * A B C D E F G H I J K so with number [0,2,4,6,10]
40231
+ * 1 ' ' ' '
40232
+ * 2 ' ' '_______' here profile correspond
40233
+ * 3 '___' |_______| for A to []
40234
+ * 4 | | for C to [3, 5]
40235
+ * 5 |___| for E to []
40236
+ * 6 for G to [2, 3]
40237
+ * 7 for K to []
40238
+ *
40239
+ *
40240
+ * Now we can easily find which zones are overlapping
40241
+ * with a given zone. Suppose we want to add a new zone
40242
+ * D5:H6 to the ensemble:
40243
+ *
40244
+ * With a binary search of left and right
40245
+ * A B C D E F G H I J K on profilesStartingPosition, we can
40246
+ * 1 ' ' ' ' find the indexes of the profiles on which
40247
+ * 2 ' ' '_______' to apply a modification.
40248
+ * 3 '___' |_______|
40249
+ * 4 | _|_______ Here we will:
40250
+ * 5 |_|_| | - add a new profile in D --> become [3, 6]
40251
+ * 6 |_________| - modify the profile in E --> become [4, 6]
40252
+ * 7 - modify the profile in G --> become [2, 3, 4, 6]
40253
+ * - add a new profile in I --> become [8, 10]
40254
+ *
40255
+ * See below the result:
40256
+ *
40257
+ * Note the particularity of the profile
40258
+ * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
40259
+ * 1 ' ' ' ' ' '
40260
+ * 2 ' ' ' '___'___' To know how to modify the profile (add a
40261
+ * 3 '_'_' |___|___| zone or remove it) we do a binary
40262
+ * 4 | | |___ ___ search of the top and bottom value on the
40263
+ * 5 |_| | | | profile array. Depending on the result index
40264
+ * 6 |_|___|___| parity (odd or even), because zone boundaries
40265
+ * 7 go by pairs, we know if we are in a zone or
40266
+ * not and how operate.
40267
+ */
40268
+ /**
40269
+ * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
40270
+ * This compute is particularly useful because after this function:
40271
+ * - you will find coordinate of a cell only once among all the zones
40272
+ * - the number of zones will be reduced to the minimum
40273
+ */
40274
+ function futureRecomputeZones(zones, zonesToRemove = []) {
40275
+ const profilesStartingPosition = [0];
40276
+ const profiles = new Map([[0, []]]);
40277
+ modifyProfiles(profilesStartingPosition, profiles, zones, false);
40278
+ modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
40279
+ return constructZonesFromProfiles(profilesStartingPosition, profiles);
40280
+ }
40281
+ function modifyProfiles(// export for testing only
40282
+ profilesStartingPosition, profiles, zones, toRemove = false) {
40283
+ for (const zone of zones) {
40284
+ const leftValue = zone.left;
40285
+ const rightValue = zone.right === undefined ? undefined : zone.right + 1;
40286
+ const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
40287
+ const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
40288
+ for (let i = leftIndex; i <= rightIndex; i++) {
40289
+ const profile = profiles.get(profilesStartingPosition[i]);
40290
+ modifyProfile(profile, zone, toRemove);
40291
+ }
40292
+ // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
40293
+ removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
40294
+ }
40295
+ }
40296
+ function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
40297
+ if (value === undefined) {
40298
+ // this is only the case when the value correspond to a bottom value that could be undefined
40299
+ return profilesStartingPosition.length - 1;
40300
+ }
40301
+ const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
40302
+ if (value != profilesStartingPosition[predecessorIndex]) {
40303
+ // mean that the value is not ending/starting at the same position as the previous/next profile
40304
+ // --> it's a new profile
40305
+ // --> we need to add it
40306
+ profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
40307
+ // suppose the we want to add the for the left value
40308
+ // following profile following zone: 'C', the predecessor index
40309
+ // for B: [1, 3] "C3:D4" correspond to 'B'.
40310
+ // The next line code will
40311
+ // A B C D A B C D copy the profile of 'B'
40312
+ // 1 '___' 1 '___' to 'C'. In the rest of the
40313
+ // 2 | | ---> 2 | _|_ process the 'modifyProfile'
40314
+ // 3 |___| 3 |_|_| | function will adapt the waiting
40315
+ // 4 4 |___| 'C' profile [1, 3] to the
40316
+ // correct 'C' profile [1, 4]
40317
+ profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
40318
+ return searchLeft ? predecessorIndex + 1 : predecessorIndex;
40319
+ }
40320
+ return searchLeft ? predecessorIndex : predecessorIndex - 1;
40321
+ }
40322
+ /**
40323
+ * Suppose the following Suppose we want to add We want to have the
40324
+ * profile: the following zone: following profile:
40325
+ *
40326
+ * A B C D E F A B C D E F A B C D E F
40327
+ * 1 '___' 1 ' ' 1 '___'
40328
+ * 2 |___| 2 '___' 2 | |
40329
+ * 3 ' ' 3 | | 3 | |
40330
+ * 4 '___' --> 4 | | --> 4 | |
40331
+ * 6 | | 6 |___| 6 | |
40332
+ * 7 |___| 7 7 |___|
40333
+ * 8 8 8
40334
+ *
40335
+ * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
40336
+ * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
40337
+ * ____ ____ correspond to 6
40338
+ * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
40339
+ * ____ is to know what must be deleted
40340
+ * Note that the 'filled [3, 7] and what must be added to the
40341
+ * zone' are always between existing profile.
40342
+ * an even index and its
40343
+ * next index
40344
+ *
40345
+ */
40346
+ function modifyProfile(profile, zone, toRemove = false) {
40347
+ const topValue = zone.top;
40348
+ const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
40349
+ const newPoints = [];
40350
+ // Case we want to add a zone to the profile:
40351
+ // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
40352
+ // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
40353
+ // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
40354
+ // so we can keep the index of the top position
40355
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
40356
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
40357
+ if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
40358
+ newPoints.push(topValue);
40359
+ }
40360
+ if (bottomValue === undefined) {
40361
+ // The following two code lines will not impact the final result,
40362
+ // but they will impact the intermediate profile.
40363
+ // We keep them for performance reason
40364
+ profile.splice(topPredIndex + 1);
40365
+ profile.push(...newPoints);
40366
+ return;
40367
+ }
40368
+ // Case we want to add a zone to the profile:
40369
+ // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
40370
+ // so we can keep the index of the bottom position.
40371
+ // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
40372
+ // so we don't need to add it to the profile. we can keep in reference the index of the successor
40373
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
40374
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
40375
+ if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
40376
+ newPoints.push(bottomValue);
40377
+ }
40378
+ // add the top and bottom value to the profile and
40379
+ // remove all information between the top and bottom index
40380
+ profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
40381
+ }
40382
+ function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
40383
+ const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
40384
+ const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
40385
+ for (let i = end; i > start; i--) {
40386
+ if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
40387
+ profiles.delete(profilesStartingPosition[i]);
40388
+ profilesStartingPosition.splice(i, 1);
40389
+ }
40390
+ }
40391
+ }
40392
+ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
40393
+ const mergedZone = [];
40394
+ let pendingZones = [];
40395
+ for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
40396
+ const left = profilesStartingPosition[colIndex];
40397
+ const profile = profiles.get(left);
40398
+ if (!profile || profile.length === 0) {
40399
+ mergedZone.push(...pendingZones);
40400
+ pendingZones = [];
40401
+ continue;
40402
+ }
40403
+ let right = profilesStartingPosition[colIndex + 1];
40404
+ if (right !== undefined) {
40405
+ right--;
40406
+ }
40407
+ const nextPendingZones = [];
40408
+ for (let i = 0; i < profile.length; i += 2) {
40409
+ const top = profile[i];
40410
+ let bottom = profile[i + 1];
40411
+ if (bottom !== undefined) {
40412
+ bottom--;
40413
+ }
40414
+ const profileZone = {
40415
+ top,
40416
+ left,
40417
+ bottom,
40418
+ right,
40419
+ hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
40420
+ };
40421
+ let findCorrespondingZone = false;
40422
+ for (let j = pendingZones.length - 1; j >= 0; j--) {
40423
+ const pendingZone = pendingZones[j];
40424
+ if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
40425
+ pendingZone.right = profileZone.right;
40426
+ pendingZones.splice(j, 1);
40427
+ nextPendingZones.push(pendingZone);
40428
+ findCorrespondingZone = true;
40429
+ break;
40430
+ }
40431
+ }
40432
+ if (!findCorrespondingZone) {
40433
+ nextPendingZones.push(profileZone);
40434
+ }
40435
+ }
40436
+ mergedZone.push(...pendingZones);
40437
+ pendingZones = nextPendingZones;
40438
+ }
40439
+ mergedZone.push(...pendingZones);
40440
+ return mergedZone;
40441
+ }
40442
+ function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
40443
+ let end = arr.length - 1;
40444
+ let result = -1;
40445
+ while (start <= end) {
40446
+ const mid = Math.floor((start + end) / 2);
40447
+ if (arr[mid] === val && matchEqual) {
40448
+ return mid;
40449
+ }
40450
+ else if (arr[mid] < val) {
40451
+ result = mid;
40452
+ start = mid + 1;
40453
+ }
40454
+ else {
40455
+ end = mid - 1;
40456
+ }
40457
+ }
40458
+ return result;
40459
+ }
40460
+ function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
40461
+ let end = arr.length - 1;
40462
+ let result = arr.length;
40463
+ while (start <= end) {
40464
+ const mid = Math.floor((start + end) / 2);
40465
+ if (arr[mid] === val && matchEqual) {
40466
+ return mid;
40467
+ }
40468
+ else if (arr[mid] > val) {
40469
+ result = mid;
40470
+ end = mid - 1;
40471
+ }
40472
+ else {
40473
+ start = mid + 1;
40474
+ }
40475
+ }
40476
+ return result;
40477
+ }
40478
+ /**
40479
+ * Compares two arrays.
40480
+ * For performance reasons, this function is to be preferred
40481
+ * to 'deepEquals' in the case we know that the inputs are arrays.
40482
+ */
40483
+ function deepEqualsArray(arr1, arr2) {
40484
+ if (arr1.length !== arr2.length) {
40485
+ return false;
40486
+ }
40487
+ for (let i = 0; i < arr1.length; i++) {
40488
+ if (!deepEquals(arr1[i], arr2[i])) {
40489
+ return false;
40490
+ }
40491
+ }
40492
+ return true;
40493
+ }
40494
+
40151
40495
  function quickselect(arr, k, left, right, compare) {
40152
40496
  quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
40153
40497
  }
@@ -40911,7 +41255,7 @@ class FormulaDependencyGraph {
40911
41255
  }
40912
41256
  }
40913
41257
  /**
40914
- * Return the cell and all cells that depend on it,
41258
+ * Return all the cells that depend on the provided ranges,
40915
41259
  * in the correct order they should be evaluated.
40916
41260
  * This is called a topological ordering (excluding cycles)
40917
41261
  */
@@ -40922,11 +41266,20 @@ class FormulaDependencyGraph {
40922
41266
  const range = queue.pop();
40923
41267
  visited.addMany(this.encoder.encodeBoundingBox(range));
40924
41268
  const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
41269
+ const nextInQueue = {};
40925
41270
  for (const positionId of impactedPositionIds) {
40926
41271
  if (!visited.has(positionId)) {
40927
- queue.push(this.encoder.decodeToBoundingBox(positionId));
41272
+ const { sheetId, zone } = this.encoder.decodeToBoundingBox(positionId);
41273
+ if (!nextInQueue[sheetId]) {
41274
+ nextInQueue[sheetId] = [];
41275
+ }
41276
+ nextInQueue[sheetId].push(zone);
40928
41277
  }
40929
41278
  }
41279
+ for (const sheetId in nextInQueue) {
41280
+ const zones = futureRecomputeZones(nextInQueue[sheetId]);
41281
+ queue.push(...zones.map((zone) => ({ sheetId, zone })));
41282
+ }
40930
41283
  }
40931
41284
  visited.deleteMany(ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
40932
41285
  return visited;
@@ -41082,7 +41435,7 @@ class Evaluator {
41082
41435
  }
41083
41436
  if (!content) {
41084
41437
  // The previous content could have blocked some array formulas
41085
- impactedPositionIds.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(positionId));
41438
+ impactedPositionIds.addMany(this.getArrayFormulasBlockedBy(positionId));
41086
41439
  }
41087
41440
  }
41088
41441
  return impactedPositionIds;
@@ -41127,13 +41480,22 @@ class Evaluator {
41127
41480
  }
41128
41481
  return positionIds;
41129
41482
  }
41130
- getArrayFormulasBlockedByOrSpreadingOn(positionId) {
41483
+ /**
41484
+ * Return the position of formulas blocked by the given position
41485
+ * as well as all their dependencies.
41486
+ */
41487
+ getArrayFormulasBlockedBy(positionId) {
41131
41488
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
41132
41489
  return [];
41133
41490
  }
41134
41491
  const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
41135
41492
  const cells = new JetSet(arrayFormulas);
41136
- cells.addMany(this.getCellsDependingOn(arrayFormulas));
41493
+ const arrayFormulaPositionId = this.getArrayFormulaSpreadingOnId(positionId);
41494
+ if (arrayFormulaPositionId) {
41495
+ // ignore the formula spreading on the position. Keep only the blocked ones
41496
+ cells.delete(arrayFormulaPositionId);
41497
+ }
41498
+ cells.addMany(this.getCellsDependingOn(cells));
41137
41499
  return cells;
41138
41500
  }
41139
41501
  nextPositionsToUpdate = new JetSet();
@@ -41298,7 +41660,7 @@ class Evaluator {
41298
41660
  }
41299
41661
  this.evaluatedCells.delete(child);
41300
41662
  this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([child]));
41301
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(child));
41663
+ this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(child));
41302
41664
  }
41303
41665
  this.spreadingRelations.removeNode(positionId);
41304
41666
  }
@@ -57030,6 +57392,6 @@ exports.setTranslationMethod = setTranslationMethod;
57030
57392
  exports.tokenize = tokenize;
57031
57393
 
57032
57394
 
57033
- __info__.version = "17.0.19";
57034
- __info__.date = "2024-04-18T16:41:42.291Z";
57035
- __info__.hash = "9f3317a";
57395
+ __info__.version = "17.0.20";
57396
+ __info__.date = "2024-04-26T07:37:04.503Z";
57397
+ __info__.hash = "065ac98";