@odoo/o-spreadsheet 17.0.55 → 17.0.57

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.55
6
- * @date 2025-03-26T12:44:29.690Z
7
- * @hash c3a4769
5
+ * @version 17.0.57
6
+ * @date 2025-04-14T17:13:56.677Z
7
+ * @hash 6ac8bb7
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -711,8 +711,7 @@
711
711
  *
712
712
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
713
713
  */
714
- const whiteSpaceSpecialCharacters = [
715
- " ",
714
+ const specialWhiteSpaceSpecialCharacters = [
716
715
  "\t",
717
716
  "\f",
718
717
  "\v",
@@ -727,7 +726,7 @@
727
726
  String.fromCharCode(parseInt("3000", 16)),
728
727
  String.fromCharCode(parseInt("feff", 16)),
729
728
  ];
730
- const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|"), "g");
729
+ const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
731
730
  const newLineRegexp = /(\r\n|\r)/g;
732
731
  /**
733
732
  * Replace all different newlines characters by \n
@@ -2003,6 +2002,7 @@
2003
2002
  "CLEAR_FORMATTING",
2004
2003
  "SET_BORDER",
2005
2004
  "SET_ZONE_BORDERS",
2005
+ "SET_BORDERS_ON_TARGET",
2006
2006
  /** CHART */
2007
2007
  "CREATE_CHART",
2008
2008
  "UPDATE_CHART",
@@ -7532,8 +7532,12 @@
7532
7532
  str = replaceNewLines(str);
7533
7533
  const chars = new TokenizingChars(str);
7534
7534
  const result = [];
7535
+ const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
7536
+ ? tokenizeSpecialCharacterSpace
7537
+ : tokenizeSimpleSpace;
7535
7538
  while (!chars.isOver()) {
7536
- let token = tokenizeSpace(chars) ||
7539
+ let token = tokenizeNewLine(chars) ||
7540
+ tokenizeSpace(chars) ||
7537
7541
  tokenizeArgsSeparator(chars, locale) ||
7538
7542
  tokenizeParenthesis(chars) ||
7539
7543
  tokenizeOperator(chars) ||
@@ -7667,17 +7671,19 @@
7667
7671
  }
7668
7672
  return null;
7669
7673
  }
7670
- function tokenizeSpace(chars) {
7671
- let length = 0;
7672
- while (chars.current === NEWLINE) {
7673
- length++;
7674
- chars.shift();
7674
+ function tokenizeSpecialCharacterSpace(chars) {
7675
+ let spaces = "";
7676
+ while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
7677
+ spaces += chars.shift();
7675
7678
  }
7676
- if (length) {
7677
- return { type: "SPACE", value: NEWLINE.repeat(length) };
7679
+ if (spaces) {
7680
+ return { type: "SPACE", value: spaces };
7678
7681
  }
7682
+ return null;
7683
+ }
7684
+ function tokenizeSimpleSpace(chars) {
7679
7685
  let spaces = "";
7680
- while (chars.current && chars.current.match(whiteSpaceRegexp)) {
7686
+ while (chars.current === " ") {
7681
7687
  spaces += chars.shift();
7682
7688
  }
7683
7689
  if (spaces) {
@@ -7685,6 +7691,17 @@
7685
7691
  }
7686
7692
  return null;
7687
7693
  }
7694
+ function tokenizeNewLine(chars) {
7695
+ let length = 0;
7696
+ while (chars.current === NEWLINE) {
7697
+ length++;
7698
+ chars.shift();
7699
+ }
7700
+ if (length) {
7701
+ return { type: "SPACE", value: NEWLINE.repeat(length) };
7702
+ }
7703
+ return null;
7704
+ }
7688
7705
  function tokenizeInvalidRange(chars) {
7689
7706
  if (chars.currentStartsWith(INCORRECT_RANGE_STRING)) {
7690
7707
  chars.advanceBy(INCORRECT_RANGE_STRING.length);
@@ -34668,6 +34685,15 @@
34668
34685
  case "SET_BORDER":
34669
34686
  this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);
34670
34687
  break;
34688
+ case "SET_BORDERS_ON_TARGET":
34689
+ for (const zone of cmd.target) {
34690
+ for (let row = zone.top; row <= zone.bottom; row++) {
34691
+ for (let col = zone.left; col <= zone.right; col++) {
34692
+ this.setBorder(cmd.sheetId, col, row, cmd.border);
34693
+ }
34694
+ }
34695
+ }
34696
+ break;
34671
34697
  case "SET_ZONE_BORDERS":
34672
34698
  if (cmd.border) {
34673
34699
  const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
@@ -36674,6 +36700,353 @@
36674
36700
  }
36675
36701
  }
36676
36702
 
36703
+ /**
36704
+ * ####################################################
36705
+ * # INTRODUCTION
36706
+ * ####################################################
36707
+ *
36708
+ * This file contain the function recomputeZones.
36709
+ * This function try to recompute in a performant way
36710
+ * an ensemble of zones possibly overlapping to avoid
36711
+ * overlapping and to reduce the number of zones.
36712
+ *
36713
+ * It also allows to remove some zones from the ensemble.
36714
+ *
36715
+ * In the following example, 2 zones are overlapping.
36716
+ * Applying recomputeZones will return zones without
36717
+ * overlapping:
36718
+ *
36719
+ * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
36720
+ *
36721
+ * A B C D E A B C D E
36722
+ * 1 ___ 1 ___
36723
+ * 2 ___|_ | 2 ___| | |
36724
+ * 3 | |_|_| ---> 3 | | |_|
36725
+ * 4 |_____| 4 |___|_|
36726
+ * 6 6
36727
+ * 7 7
36728
+ *
36729
+ *
36730
+ * In the following example, 2 zones are contiguous.
36731
+ * Applying recomputeZones will return only one zone:
36732
+ *
36733
+ * ["B2:B3", "C2:D3"] ["B2:D3"]
36734
+ *
36735
+ * A B C D E A B C D E
36736
+ * 1 _ ___ 1 _____
36737
+ * 2 | | | ---> 2 | |
36738
+ * 3 |_|___| 3 |_____|
36739
+ * 4 4
36740
+ *
36741
+ *
36742
+ * In the following example, we want to remove a zone
36743
+ * from the ensemble. Applying recomputeZones will
36744
+ * return the ensemble without the zone to remove:
36745
+ *
36746
+ * remove ["C3:D3"] ["B2:B4", "C2:D2",
36747
+ * "C4:D4", "E2:E4"]
36748
+ *
36749
+ * A B C D E F A B C D E F
36750
+ * 1 _______ 1 _______
36751
+ * 2 | | ---> 2 | |___| |
36752
+ * 3 | xxx | 3 | |___| |
36753
+ * 4 |_______| 4 |_|___|_|
36754
+ * 5 5
36755
+ *
36756
+ *
36757
+ * The exercise seems simple when we have only 2 zones.
36758
+ * But with n zones and in a performant way, we want to
36759
+ * avoid comparing each zone with all the others.
36760
+ *
36761
+ *
36762
+ * ####################################################
36763
+ * # Methodological approach
36764
+ * ####################################################
36765
+ *
36766
+ * The methodological approach to avoid comparing each
36767
+ * zone with all the others is to use a data structure
36768
+ * that allow to quickly find which zones are
36769
+ * overlapping with any other given zone.
36770
+ *
36771
+ * Here the idea is to profile the zones at the columns level.
36772
+ *
36773
+ * To do that, we propose to use a data structure
36774
+ * composed of 2 parts:
36775
+ * - profilesStartingPosition: a sorted number array
36776
+ * indicating on which columns a new profile begins.
36777
+ * - profiles: a map where the key is a column
36778
+ * position (from profilesStartingPosition) and the
36779
+ * value is a sorted number array representing a
36780
+ * profile.
36781
+ *
36782
+ *
36783
+ * See the following example: here profileStartingPosition
36784
+ * corresponds to [A,C,E,G,K]
36785
+ * A B C D E F G H I J K so with number [0,2,4,6,10]
36786
+ * 1 ' ' ' '
36787
+ * 2 ' ' '_______' here profile correspond
36788
+ * 3 '___' |_______| for A to []
36789
+ * 4 | | for C to [3, 5]
36790
+ * 5 |___| for E to []
36791
+ * 6 for G to [2, 3]
36792
+ * 7 for K to []
36793
+ *
36794
+ *
36795
+ * Now we can easily find which zones are overlapping
36796
+ * with a given zone. Suppose we want to add a new zone
36797
+ * D5:H6 to the ensemble:
36798
+ *
36799
+ * With a binary search of left and right
36800
+ * A B C D E F G H I J K on profilesStartingPosition, we can
36801
+ * 1 ' ' ' ' find the indexes of the profiles on which
36802
+ * 2 ' ' '_______' to apply a modification.
36803
+ * 3 '___' |_______|
36804
+ * 4 | _|_______ Here we will:
36805
+ * 5 |_|_| | - add a new profile in D --> become [3, 6]
36806
+ * 6 |_________| - modify the profile in E --> become [4, 6]
36807
+ * 7 - modify the profile in G --> become [2, 3, 4, 6]
36808
+ * - add a new profile in I --> become [8, 10]
36809
+ *
36810
+ * See below the result:
36811
+ *
36812
+ * Note the particularity of the profile
36813
+ * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
36814
+ * 1 ' ' ' ' ' '
36815
+ * 2 ' ' ' '___'___' To know how to modify the profile (add a
36816
+ * 3 '_'_' |___|___| zone or remove it) we do a binary
36817
+ * 4 | | |___ ___ search of the top and bottom value on the
36818
+ * 5 |_| | | | profile array. Depending on the result index
36819
+ * 6 |_|___|___| parity (odd or even), because zone boundaries
36820
+ * 7 go by pairs, we know if we are in a zone or
36821
+ * not and how operate.
36822
+ */
36823
+ /**
36824
+ * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
36825
+ * This compute is particularly useful because after this function:
36826
+ * - you will find coordinate of a cell only once among all the zones
36827
+ * - the number of zones will be reduced to the minimum
36828
+ */
36829
+ function futureRecomputeZones(zones, zonesToRemove = []) {
36830
+ const profilesStartingPosition = [0];
36831
+ const profiles = new Map([[0, []]]);
36832
+ modifyProfiles(profilesStartingPosition, profiles, zones, false);
36833
+ modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
36834
+ return constructZonesFromProfiles(profilesStartingPosition, profiles);
36835
+ }
36836
+ function modifyProfiles(// export for testing only
36837
+ profilesStartingPosition, profiles, zones, toRemove = false) {
36838
+ for (const zone of zones) {
36839
+ const leftValue = zone.left;
36840
+ const rightValue = zone.right === undefined ? undefined : zone.right + 1;
36841
+ const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
36842
+ const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
36843
+ for (let i = leftIndex; i <= rightIndex; i++) {
36844
+ const profile = profiles.get(profilesStartingPosition[i]);
36845
+ modifyProfile(profile, zone, toRemove);
36846
+ }
36847
+ // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
36848
+ removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
36849
+ }
36850
+ }
36851
+ function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
36852
+ if (value === undefined) {
36853
+ // this is only the case when the value correspond to a bottom value that could be undefined
36854
+ return profilesStartingPosition.length - 1;
36855
+ }
36856
+ const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
36857
+ if (value != profilesStartingPosition[predecessorIndex]) {
36858
+ // mean that the value is not ending/starting at the same position as the previous/next profile
36859
+ // --> it's a new profile
36860
+ // --> we need to add it
36861
+ profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
36862
+ // suppose the we want to add the for the left value
36863
+ // following profile following zone: 'C', the predecessor index
36864
+ // for B: [1, 3] "C3:D4" correspond to 'B'.
36865
+ // The next line code will
36866
+ // A B C D A B C D copy the profile of 'B'
36867
+ // 1 '___' 1 '___' to 'C'. In the rest of the
36868
+ // 2 | | ---> 2 | _|_ process the 'modifyProfile'
36869
+ // 3 |___| 3 |_|_| | function will adapt the waiting
36870
+ // 4 4 |___| 'C' profile [1, 3] to the
36871
+ // correct 'C' profile [1, 4]
36872
+ profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
36873
+ return searchLeft ? predecessorIndex + 1 : predecessorIndex;
36874
+ }
36875
+ return searchLeft ? predecessorIndex : predecessorIndex - 1;
36876
+ }
36877
+ /**
36878
+ * Suppose the following Suppose we want to add We want to have the
36879
+ * profile: the following zone: following profile:
36880
+ *
36881
+ * A B C D E F A B C D E F A B C D E F
36882
+ * 1 '___' 1 ' ' 1 '___'
36883
+ * 2 |___| 2 '___' 2 | |
36884
+ * 3 ' ' 3 | | 3 | |
36885
+ * 4 '___' --> 4 | | --> 4 | |
36886
+ * 6 | | 6 |___| 6 | |
36887
+ * 7 |___| 7 7 |___|
36888
+ * 8 8 8
36889
+ *
36890
+ * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
36891
+ * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
36892
+ * ____ ____ correspond to 6
36893
+ * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
36894
+ * ____ is to know what must be deleted
36895
+ * Note that the 'filled [3, 7] and what must be added to the
36896
+ * zone' are always between existing profile.
36897
+ * an even index and its
36898
+ * next index
36899
+ *
36900
+ */
36901
+ function modifyProfile(profile, zone, toRemove = false) {
36902
+ const topValue = zone.top;
36903
+ const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
36904
+ const newPoints = [];
36905
+ // Case we want to add a zone to the profile:
36906
+ // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
36907
+ // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
36908
+ // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
36909
+ // so we can keep the index of the top position
36910
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
36911
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
36912
+ if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
36913
+ newPoints.push(topValue);
36914
+ }
36915
+ if (bottomValue === undefined) {
36916
+ // The following two code lines will not impact the final result,
36917
+ // but they will impact the intermediate profile.
36918
+ // We keep them for performance reason
36919
+ profile.splice(topPredIndex + 1);
36920
+ profile.push(...newPoints);
36921
+ return;
36922
+ }
36923
+ // Case we want to add a zone to the profile:
36924
+ // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
36925
+ // so we can keep the index of the bottom position.
36926
+ // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
36927
+ // so we don't need to add it to the profile. we can keep in reference the index of the successor
36928
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
36929
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
36930
+ if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
36931
+ newPoints.push(bottomValue);
36932
+ }
36933
+ // add the top and bottom value to the profile and
36934
+ // remove all information between the top and bottom index
36935
+ profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
36936
+ }
36937
+ function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
36938
+ const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
36939
+ const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
36940
+ for (let i = end; i > start; i--) {
36941
+ if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
36942
+ profiles.delete(profilesStartingPosition[i]);
36943
+ profilesStartingPosition.splice(i, 1);
36944
+ }
36945
+ }
36946
+ }
36947
+ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
36948
+ const mergedZone = [];
36949
+ let pendingZones = [];
36950
+ for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
36951
+ const left = profilesStartingPosition[colIndex];
36952
+ const profile = profiles.get(left);
36953
+ if (!profile || profile.length === 0) {
36954
+ mergedZone.push(...pendingZones);
36955
+ pendingZones = [];
36956
+ continue;
36957
+ }
36958
+ let right = profilesStartingPosition[colIndex + 1];
36959
+ if (right !== undefined) {
36960
+ right--;
36961
+ }
36962
+ const nextPendingZones = [];
36963
+ for (let i = 0; i < profile.length; i += 2) {
36964
+ const top = profile[i];
36965
+ let bottom = profile[i + 1];
36966
+ if (bottom !== undefined) {
36967
+ bottom--;
36968
+ }
36969
+ const profileZone = {
36970
+ top,
36971
+ left,
36972
+ bottom,
36973
+ right,
36974
+ hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
36975
+ };
36976
+ let findCorrespondingZone = false;
36977
+ for (let j = pendingZones.length - 1; j >= 0; j--) {
36978
+ const pendingZone = pendingZones[j];
36979
+ if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
36980
+ pendingZone.right = profileZone.right;
36981
+ pendingZones.splice(j, 1);
36982
+ nextPendingZones.push(pendingZone);
36983
+ findCorrespondingZone = true;
36984
+ break;
36985
+ }
36986
+ }
36987
+ if (!findCorrespondingZone) {
36988
+ nextPendingZones.push(profileZone);
36989
+ }
36990
+ }
36991
+ mergedZone.push(...pendingZones);
36992
+ pendingZones = nextPendingZones;
36993
+ }
36994
+ mergedZone.push(...pendingZones);
36995
+ return mergedZone;
36996
+ }
36997
+ function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
36998
+ let end = arr.length - 1;
36999
+ let result = -1;
37000
+ while (start <= end) {
37001
+ const mid = Math.floor((start + end) / 2);
37002
+ if (arr[mid] === val && matchEqual) {
37003
+ return mid;
37004
+ }
37005
+ else if (arr[mid] < val) {
37006
+ result = mid;
37007
+ start = mid + 1;
37008
+ }
37009
+ else {
37010
+ end = mid - 1;
37011
+ }
37012
+ }
37013
+ return result;
37014
+ }
37015
+ function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
37016
+ let end = arr.length - 1;
37017
+ let result = arr.length;
37018
+ while (start <= end) {
37019
+ const mid = Math.floor((start + end) / 2);
37020
+ if (arr[mid] === val && matchEqual) {
37021
+ return mid;
37022
+ }
37023
+ else if (arr[mid] > val) {
37024
+ result = mid;
37025
+ end = mid - 1;
37026
+ }
37027
+ else {
37028
+ start = mid + 1;
37029
+ }
37030
+ }
37031
+ return result;
37032
+ }
37033
+ /**
37034
+ * Compares two arrays.
37035
+ * For performance reasons, this function is to be preferred
37036
+ * to 'deepEquals' in the case we know that the inputs are arrays.
37037
+ */
37038
+ function deepEqualsArray(arr1, arr2) {
37039
+ if (arr1.length !== arr2.length) {
37040
+ return false;
37041
+ }
37042
+ for (let i = 0; i < arr1.length; i++) {
37043
+ if (!deepEquals(arr1[i], arr2[i])) {
37044
+ return false;
37045
+ }
37046
+ }
37047
+ return true;
37048
+ }
37049
+
36677
37050
  // -----------------------------------------------------------------------------
36678
37051
  // Constants
36679
37052
  // -----------------------------------------------------------------------------
@@ -36843,10 +37216,12 @@
36843
37216
  const replaceIndex = rules.findIndex((c) => c.id === cf.id);
36844
37217
  let currentRanges = [];
36845
37218
  if (replaceIndex > -1) {
36846
- currentRanges = rules[replaceIndex].ranges;
37219
+ currentRanges = rules[replaceIndex].ranges.map(toUnboundedZone);
36847
37220
  }
36848
37221
  currentRanges = currentRanges.concat(toAdd);
36849
- return recomputeZones(currentRanges, toRemove);
37222
+ // Remove the zones first in case the same position is in toAdd and toRemove
37223
+ const withRemovedZones = futureRecomputeZones(currentRanges, toRemove);
37224
+ return futureRecomputeZones([...toAdd, ...withRemovedZones], []);
36850
37225
  }
36851
37226
  // ---------------------------------------------------------------------------
36852
37227
  // Private
@@ -40581,353 +40956,6 @@
40581
40956
  }
40582
40957
  }
40583
40958
 
40584
- /**
40585
- * ####################################################
40586
- * # INTRODUCTION
40587
- * ####################################################
40588
- *
40589
- * This file contain the function recomputeZones.
40590
- * This function try to recompute in a performant way
40591
- * an ensemble of zones possibly overlapping to avoid
40592
- * overlapping and to reduce the number of zones.
40593
- *
40594
- * It also allows to remove some zones from the ensemble.
40595
- *
40596
- * In the following example, 2 zones are overlapping.
40597
- * Applying recomputeZones will return zones without
40598
- * overlapping:
40599
- *
40600
- * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
40601
- *
40602
- * A B C D E A B C D E
40603
- * 1 ___ 1 ___
40604
- * 2 ___|_ | 2 ___| | |
40605
- * 3 | |_|_| ---> 3 | | |_|
40606
- * 4 |_____| 4 |___|_|
40607
- * 6 6
40608
- * 7 7
40609
- *
40610
- *
40611
- * In the following example, 2 zones are contiguous.
40612
- * Applying recomputeZones will return only one zone:
40613
- *
40614
- * ["B2:B3", "C2:D3"] ["B2:D3"]
40615
- *
40616
- * A B C D E A B C D E
40617
- * 1 _ ___ 1 _____
40618
- * 2 | | | ---> 2 | |
40619
- * 3 |_|___| 3 |_____|
40620
- * 4 4
40621
- *
40622
- *
40623
- * In the following example, we want to remove a zone
40624
- * from the ensemble. Applying recomputeZones will
40625
- * return the ensemble without the zone to remove:
40626
- *
40627
- * remove ["C3:D3"] ["B2:B4", "C2:D2",
40628
- * "C4:D4", "E2:E4"]
40629
- *
40630
- * A B C D E F A B C D E F
40631
- * 1 _______ 1 _______
40632
- * 2 | | ---> 2 | |___| |
40633
- * 3 | xxx | 3 | |___| |
40634
- * 4 |_______| 4 |_|___|_|
40635
- * 5 5
40636
- *
40637
- *
40638
- * The exercise seems simple when we have only 2 zones.
40639
- * But with n zones and in a performant way, we want to
40640
- * avoid comparing each zone with all the others.
40641
- *
40642
- *
40643
- * ####################################################
40644
- * # Methodological approach
40645
- * ####################################################
40646
- *
40647
- * The methodological approach to avoid comparing each
40648
- * zone with all the others is to use a data structure
40649
- * that allow to quickly find which zones are
40650
- * overlapping with any other given zone.
40651
- *
40652
- * Here the idea is to profile the zones at the columns level.
40653
- *
40654
- * To do that, we propose to use a data structure
40655
- * composed of 2 parts:
40656
- * - profilesStartingPosition: a sorted number array
40657
- * indicating on which columns a new profile begins.
40658
- * - profiles: a map where the key is a column
40659
- * position (from profilesStartingPosition) and the
40660
- * value is a sorted number array representing a
40661
- * profile.
40662
- *
40663
- *
40664
- * See the following example: here profileStartingPosition
40665
- * corresponds to [A,C,E,G,K]
40666
- * A B C D E F G H I J K so with number [0,2,4,6,10]
40667
- * 1 ' ' ' '
40668
- * 2 ' ' '_______' here profile correspond
40669
- * 3 '___' |_______| for A to []
40670
- * 4 | | for C to [3, 5]
40671
- * 5 |___| for E to []
40672
- * 6 for G to [2, 3]
40673
- * 7 for K to []
40674
- *
40675
- *
40676
- * Now we can easily find which zones are overlapping
40677
- * with a given zone. Suppose we want to add a new zone
40678
- * D5:H6 to the ensemble:
40679
- *
40680
- * With a binary search of left and right
40681
- * A B C D E F G H I J K on profilesStartingPosition, we can
40682
- * 1 ' ' ' ' find the indexes of the profiles on which
40683
- * 2 ' ' '_______' to apply a modification.
40684
- * 3 '___' |_______|
40685
- * 4 | _|_______ Here we will:
40686
- * 5 |_|_| | - add a new profile in D --> become [3, 6]
40687
- * 6 |_________| - modify the profile in E --> become [4, 6]
40688
- * 7 - modify the profile in G --> become [2, 3, 4, 6]
40689
- * - add a new profile in I --> become [8, 10]
40690
- *
40691
- * See below the result:
40692
- *
40693
- * Note the particularity of the profile
40694
- * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
40695
- * 1 ' ' ' ' ' '
40696
- * 2 ' ' ' '___'___' To know how to modify the profile (add a
40697
- * 3 '_'_' |___|___| zone or remove it) we do a binary
40698
- * 4 | | |___ ___ search of the top and bottom value on the
40699
- * 5 |_| | | | profile array. Depending on the result index
40700
- * 6 |_|___|___| parity (odd or even), because zone boundaries
40701
- * 7 go by pairs, we know if we are in a zone or
40702
- * not and how operate.
40703
- */
40704
- /**
40705
- * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
40706
- * This compute is particularly useful because after this function:
40707
- * - you will find coordinate of a cell only once among all the zones
40708
- * - the number of zones will be reduced to the minimum
40709
- */
40710
- function futureRecomputeZones(zones, zonesToRemove = []) {
40711
- const profilesStartingPosition = [0];
40712
- const profiles = new Map([[0, []]]);
40713
- modifyProfiles(profilesStartingPosition, profiles, zones, false);
40714
- modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
40715
- return constructZonesFromProfiles(profilesStartingPosition, profiles);
40716
- }
40717
- function modifyProfiles(// export for testing only
40718
- profilesStartingPosition, profiles, zones, toRemove = false) {
40719
- for (const zone of zones) {
40720
- const leftValue = zone.left;
40721
- const rightValue = zone.right === undefined ? undefined : zone.right + 1;
40722
- const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
40723
- const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
40724
- for (let i = leftIndex; i <= rightIndex; i++) {
40725
- const profile = profiles.get(profilesStartingPosition[i]);
40726
- modifyProfile(profile, zone, toRemove);
40727
- }
40728
- // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
40729
- removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
40730
- }
40731
- }
40732
- function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
40733
- if (value === undefined) {
40734
- // this is only the case when the value correspond to a bottom value that could be undefined
40735
- return profilesStartingPosition.length - 1;
40736
- }
40737
- const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
40738
- if (value != profilesStartingPosition[predecessorIndex]) {
40739
- // mean that the value is not ending/starting at the same position as the previous/next profile
40740
- // --> it's a new profile
40741
- // --> we need to add it
40742
- profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
40743
- // suppose the we want to add the for the left value
40744
- // following profile following zone: 'C', the predecessor index
40745
- // for B: [1, 3] "C3:D4" correspond to 'B'.
40746
- // The next line code will
40747
- // A B C D A B C D copy the profile of 'B'
40748
- // 1 '___' 1 '___' to 'C'. In the rest of the
40749
- // 2 | | ---> 2 | _|_ process the 'modifyProfile'
40750
- // 3 |___| 3 |_|_| | function will adapt the waiting
40751
- // 4 4 |___| 'C' profile [1, 3] to the
40752
- // correct 'C' profile [1, 4]
40753
- profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
40754
- return searchLeft ? predecessorIndex + 1 : predecessorIndex;
40755
- }
40756
- return searchLeft ? predecessorIndex : predecessorIndex - 1;
40757
- }
40758
- /**
40759
- * Suppose the following Suppose we want to add We want to have the
40760
- * profile: the following zone: following profile:
40761
- *
40762
- * A B C D E F A B C D E F A B C D E F
40763
- * 1 '___' 1 ' ' 1 '___'
40764
- * 2 |___| 2 '___' 2 | |
40765
- * 3 ' ' 3 | | 3 | |
40766
- * 4 '___' --> 4 | | --> 4 | |
40767
- * 6 | | 6 |___| 6 | |
40768
- * 7 |___| 7 7 |___|
40769
- * 8 8 8
40770
- *
40771
- * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
40772
- * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
40773
- * ____ ____ correspond to 6
40774
- * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
40775
- * ____ is to know what must be deleted
40776
- * Note that the 'filled [3, 7] and what must be added to the
40777
- * zone' are always between existing profile.
40778
- * an even index and its
40779
- * next index
40780
- *
40781
- */
40782
- function modifyProfile(profile, zone, toRemove = false) {
40783
- const topValue = zone.top;
40784
- const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
40785
- const newPoints = [];
40786
- // Case we want to add a zone to the profile:
40787
- // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
40788
- // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
40789
- // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
40790
- // so we can keep the index of the top position
40791
- // Case we want to remove a zone from the profile: it's the opposite of the previous case
40792
- const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
40793
- if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
40794
- newPoints.push(topValue);
40795
- }
40796
- if (bottomValue === undefined) {
40797
- // The following two code lines will not impact the final result,
40798
- // but they will impact the intermediate profile.
40799
- // We keep them for performance reason
40800
- profile.splice(topPredIndex + 1);
40801
- profile.push(...newPoints);
40802
- return;
40803
- }
40804
- // Case we want to add a zone to the profile:
40805
- // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
40806
- // so we can keep the index of the bottom position.
40807
- // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
40808
- // so we don't need to add it to the profile. we can keep in reference the index of the successor
40809
- // Case we want to remove a zone from the profile: it's the opposite of the previous case
40810
- const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
40811
- if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
40812
- newPoints.push(bottomValue);
40813
- }
40814
- // add the top and bottom value to the profile and
40815
- // remove all information between the top and bottom index
40816
- profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
40817
- }
40818
- function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
40819
- const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
40820
- const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
40821
- for (let i = end; i > start; i--) {
40822
- if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
40823
- profiles.delete(profilesStartingPosition[i]);
40824
- profilesStartingPosition.splice(i, 1);
40825
- }
40826
- }
40827
- }
40828
- function constructZonesFromProfiles(profilesStartingPosition, profiles) {
40829
- const mergedZone = [];
40830
- let pendingZones = [];
40831
- for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
40832
- const left = profilesStartingPosition[colIndex];
40833
- const profile = profiles.get(left);
40834
- if (!profile || profile.length === 0) {
40835
- mergedZone.push(...pendingZones);
40836
- pendingZones = [];
40837
- continue;
40838
- }
40839
- let right = profilesStartingPosition[colIndex + 1];
40840
- if (right !== undefined) {
40841
- right--;
40842
- }
40843
- const nextPendingZones = [];
40844
- for (let i = 0; i < profile.length; i += 2) {
40845
- const top = profile[i];
40846
- let bottom = profile[i + 1];
40847
- if (bottom !== undefined) {
40848
- bottom--;
40849
- }
40850
- const profileZone = {
40851
- top,
40852
- left,
40853
- bottom,
40854
- right,
40855
- hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
40856
- };
40857
- let findCorrespondingZone = false;
40858
- for (let j = pendingZones.length - 1; j >= 0; j--) {
40859
- const pendingZone = pendingZones[j];
40860
- if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
40861
- pendingZone.right = profileZone.right;
40862
- pendingZones.splice(j, 1);
40863
- nextPendingZones.push(pendingZone);
40864
- findCorrespondingZone = true;
40865
- break;
40866
- }
40867
- }
40868
- if (!findCorrespondingZone) {
40869
- nextPendingZones.push(profileZone);
40870
- }
40871
- }
40872
- mergedZone.push(...pendingZones);
40873
- pendingZones = nextPendingZones;
40874
- }
40875
- mergedZone.push(...pendingZones);
40876
- return mergedZone;
40877
- }
40878
- function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
40879
- let end = arr.length - 1;
40880
- let result = -1;
40881
- while (start <= end) {
40882
- const mid = Math.floor((start + end) / 2);
40883
- if (arr[mid] === val && matchEqual) {
40884
- return mid;
40885
- }
40886
- else if (arr[mid] < val) {
40887
- result = mid;
40888
- start = mid + 1;
40889
- }
40890
- else {
40891
- end = mid - 1;
40892
- }
40893
- }
40894
- return result;
40895
- }
40896
- function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
40897
- let end = arr.length - 1;
40898
- let result = arr.length;
40899
- while (start <= end) {
40900
- const mid = Math.floor((start + end) / 2);
40901
- if (arr[mid] === val && matchEqual) {
40902
- return mid;
40903
- }
40904
- else if (arr[mid] > val) {
40905
- result = mid;
40906
- end = mid - 1;
40907
- }
40908
- else {
40909
- start = mid + 1;
40910
- }
40911
- }
40912
- return result;
40913
- }
40914
- /**
40915
- * Compares two arrays.
40916
- * For performance reasons, this function is to be preferred
40917
- * to 'deepEquals' in the case we know that the inputs are arrays.
40918
- */
40919
- function deepEqualsArray(arr1, arr2) {
40920
- if (arr1.length !== arr2.length) {
40921
- return false;
40922
- }
40923
- for (let i = 0; i < arr1.length; i++) {
40924
- if (!deepEquals(arr1[i], arr2[i])) {
40925
- return false;
40926
- }
40927
- }
40928
- return true;
40929
- }
40930
-
40931
40959
  function quickselect(arr, k, left, right, compare) {
40932
40960
  quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
40933
40961
  }
@@ -43524,25 +43552,6 @@
43524
43552
  case "AUTOFILL_AUTO":
43525
43553
  this.autofillAuto();
43526
43554
  break;
43527
- case "AUTOFILL_CELL":
43528
- this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
43529
- const sheetId = this.getters.getActiveSheetId();
43530
- this.dispatch("UPDATE_CELL", {
43531
- sheetId,
43532
- col: cmd.col,
43533
- row: cmd.row,
43534
- style: cmd.style || null,
43535
- content: cmd.content || "",
43536
- format: cmd.format || "",
43537
- });
43538
- this.dispatch("SET_BORDER", {
43539
- sheetId,
43540
- col: cmd.col,
43541
- row: cmd.row,
43542
- border: cmd.border,
43543
- });
43544
- this.autofillCF(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
43545
- this.autofillDV(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
43546
43555
  }
43547
43556
  }
43548
43557
  // ---------------------------------------------------------------------------
@@ -43566,6 +43575,7 @@
43566
43575
  }
43567
43576
  const source = this.getters.getSelectedZone();
43568
43577
  const target = this.autofillZone;
43578
+ const autofillCellsData = [];
43569
43579
  switch (this.direction) {
43570
43580
  case "down" /* DIRECTION.DOWN */:
43571
43581
  for (let col = source.left; col <= source.right; col++) {
@@ -43575,7 +43585,7 @@
43575
43585
  }
43576
43586
  const generator = this.createGenerator(xcs);
43577
43587
  for (let row = target.top; row <= target.bottom; row++) {
43578
- this.computeNewCell(generator, col, row, apply);
43588
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43579
43589
  }
43580
43590
  }
43581
43591
  break;
@@ -43587,7 +43597,7 @@
43587
43597
  }
43588
43598
  const generator = this.createGenerator(xcs);
43589
43599
  for (let row = target.bottom; row >= target.top; row--) {
43590
- this.computeNewCell(generator, col, row, apply);
43600
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43591
43601
  }
43592
43602
  }
43593
43603
  break;
@@ -43599,7 +43609,7 @@
43599
43609
  }
43600
43610
  const generator = this.createGenerator(xcs);
43601
43611
  for (let col = target.right; col >= target.left; col--) {
43602
- this.computeNewCell(generator, col, row, apply);
43612
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43603
43613
  }
43604
43614
  }
43605
43615
  break;
@@ -43611,12 +43621,26 @@
43611
43621
  }
43612
43622
  const generator = this.createGenerator(xcs);
43613
43623
  for (let col = target.left; col <= target.right; col++) {
43614
- this.computeNewCell(generator, col, row, apply);
43624
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43615
43625
  }
43616
43626
  }
43617
43627
  break;
43618
43628
  }
43619
43629
  if (apply) {
43630
+ const bordersZones = {};
43631
+ const cfNewRanges = {};
43632
+ const dvNewZones = {};
43633
+ const sheetId = this.getters.getActiveSheetId();
43634
+ for (const data of autofillCellsData) {
43635
+ this.collectBordersData(data, bordersZones);
43636
+ this.autofillMerge(sheetId, data);
43637
+ this.autofillCell(sheetId, data);
43638
+ this.collectConditionalFormatsData(sheetId, data, cfNewRanges);
43639
+ this.collectDataValidationsData(sheetId, data, dvNewZones);
43640
+ }
43641
+ this.autofillBorders(sheetId, bordersZones);
43642
+ this.autofillConditionalFormats(sheetId, cfNewRanges);
43643
+ this.autofillDataValidations(sheetId, dvNewZones);
43620
43644
  this.autofillZone = undefined;
43621
43645
  this.selection.resizeAnchorZone(this.direction, this.steps);
43622
43646
  this.lastCellSelected = {};
@@ -43625,6 +43649,95 @@
43625
43649
  this.tooltip = undefined;
43626
43650
  }
43627
43651
  }
43652
+ collectBordersData(data, bordersPositions) {
43653
+ const key = JSON.stringify(data.border);
43654
+ if (!(key in bordersPositions)) {
43655
+ bordersPositions[key] = [];
43656
+ }
43657
+ bordersPositions[key].push(positionToZone({ col: data.col, row: data.row }));
43658
+ }
43659
+ collectConditionalFormatsData(sheetId, data, cfNewRanges) {
43660
+ const { originCol, originRow, col, row } = data;
43661
+ const cfsAtOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
43662
+ const xc = toXC(col, row);
43663
+ for (const cf of cfsAtOrigin) {
43664
+ if (!(cf.id in cfNewRanges)) {
43665
+ cfNewRanges[cf.id] = [];
43666
+ }
43667
+ cfNewRanges[cf.id].push(xc);
43668
+ }
43669
+ }
43670
+ collectDataValidationsData(sheetId, data, dvNewZones) {
43671
+ const { originCol, originRow, col, row } = data;
43672
+ const cellPosition = { sheetId, col: originCol, row: originRow };
43673
+ const dvsAtOrigin = this.getters.getValidationRuleForCell(cellPosition);
43674
+ if (!dvsAtOrigin) {
43675
+ return;
43676
+ }
43677
+ if (!(dvsAtOrigin.id in dvNewZones)) {
43678
+ dvNewZones[dvsAtOrigin.id] = [];
43679
+ }
43680
+ dvNewZones[dvsAtOrigin.id].push(positionToZone({ col, row }));
43681
+ }
43682
+ autofillCell(sheetId, data) {
43683
+ this.dispatch("UPDATE_CELL", {
43684
+ sheetId,
43685
+ col: data.col,
43686
+ row: data.row,
43687
+ content: data.content || "",
43688
+ style: data.style || null,
43689
+ format: data.format || "",
43690
+ });
43691
+ // Still usefull in odoo ATM to autofill field sync
43692
+ this.dispatch("AUTOFILL_CELL", data);
43693
+ }
43694
+ autofillBorders(sheetId, bordersPositions) {
43695
+ for (const stringifiedBorder in bordersPositions) {
43696
+ const border = stringifiedBorder === "undefined" ? undefined : JSON.parse(stringifiedBorder);
43697
+ this.dispatch("SET_BORDERS_ON_TARGET", {
43698
+ sheetId,
43699
+ border,
43700
+ target: futureRecomputeZones(bordersPositions[stringifiedBorder]),
43701
+ });
43702
+ }
43703
+ }
43704
+ autofillConditionalFormats(sheetId, cfNewRanges) {
43705
+ for (const cfId in cfNewRanges) {
43706
+ const changes = cfNewRanges[cfId];
43707
+ const cf = this.getters.getConditionalFormats(sheetId).find((cf) => cf.id === cfId);
43708
+ if (!cf) {
43709
+ continue;
43710
+ }
43711
+ const newCfZones = this.getters.getAdaptedCfRanges(sheetId, cf, changes.map(toZone), []);
43712
+ if (newCfZones) {
43713
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
43714
+ cf: {
43715
+ id: cf.id,
43716
+ rule: cf.rule,
43717
+ stopIfTrue: cf.stopIfTrue,
43718
+ },
43719
+ ranges: newCfZones.map((xc) => this.getters.getRangeDataFromZone(sheetId, xc)),
43720
+ sheetId,
43721
+ });
43722
+ }
43723
+ }
43724
+ }
43725
+ autofillDataValidations(sheetId, dvNewZones) {
43726
+ for (const dvId in dvNewZones) {
43727
+ const changes = dvNewZones[dvId];
43728
+ const dvOrigin = this.getters.getDataValidationRule(sheetId, dvId);
43729
+ if (!dvOrigin) {
43730
+ continue;
43731
+ }
43732
+ const dvRangesXcs = dvOrigin.ranges.map((range) => range.zone);
43733
+ const newDvRanges = futureRecomputeZones(dvRangesXcs.concat(changes), []);
43734
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
43735
+ rule: dvOrigin,
43736
+ ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
43737
+ sheetId,
43738
+ });
43739
+ }
43740
+ }
43628
43741
  /**
43629
43742
  * Select a cell which becomes the last cell of the autofillZone
43630
43743
  */
@@ -43691,22 +43804,20 @@
43691
43804
  /**
43692
43805
  * Generate the next cell
43693
43806
  */
43694
- computeNewCell(generator, col, row, apply) {
43807
+ computeNewCell(generator, col, row) {
43695
43808
  const { cellData, tooltip, origin } = generator.next();
43696
43809
  const { content, style, border, format } = cellData;
43697
43810
  this.tooltip = tooltip;
43698
- if (apply) {
43699
- this.dispatch("AUTOFILL_CELL", {
43700
- originCol: origin.col,
43701
- originRow: origin.row,
43702
- col,
43703
- row,
43704
- content,
43705
- style,
43706
- border,
43707
- format,
43708
- });
43709
- }
43811
+ return {
43812
+ originCol: origin.col,
43813
+ originRow: origin.row,
43814
+ col,
43815
+ row,
43816
+ content,
43817
+ style,
43818
+ border,
43819
+ format,
43820
+ };
43710
43821
  }
43711
43822
  /**
43712
43823
  * Get the rule associated to the current cell
@@ -43774,8 +43885,8 @@
43774
43885
  ? position[first].value
43775
43886
  : position[second].value;
43776
43887
  }
43777
- autoFillMerge(originCol, originRow, col, row) {
43778
- const sheetId = this.getters.getActiveSheetId();
43888
+ autofillMerge(sheetId, data) {
43889
+ const { originCol, originRow, col, row } = data;
43779
43890
  const position = { sheetId, col, row };
43780
43891
  const originPosition = { sheetId, col: originCol, row: originRow };
43781
43892
  if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {
@@ -43802,35 +43913,6 @@
43802
43913
  });
43803
43914
  }
43804
43915
  }
43805
- autofillCF(originCol, originRow, col, row) {
43806
- const sheetId = this.getters.getActiveSheetId();
43807
- const cfOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
43808
- for (const cf of cfOrigin) {
43809
- const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, [toXC(col, row)], []);
43810
- if (newCfRanges) {
43811
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
43812
- cf: deepCopy(cf),
43813
- ranges: newCfRanges.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
43814
- sheetId,
43815
- });
43816
- }
43817
- }
43818
- }
43819
- autofillDV(originCol, originRow, col, row) {
43820
- const sheetId = this.getters.getActiveSheetId();
43821
- const cellPosition = { sheetId, col: originCol, row: originRow };
43822
- const dvOrigin = this.getters.getValidationRuleForCell(cellPosition);
43823
- if (!dvOrigin) {
43824
- return;
43825
- }
43826
- const dvRangesXcs = dvOrigin.ranges.map((range) => this.getters.getRangeString(range, sheetId));
43827
- const newDvRanges = recomputeZones(dvRangesXcs.concat(toXC(col, row)), []);
43828
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
43829
- rule: dvOrigin,
43830
- ranges: newDvRanges.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
43831
- sheetId,
43832
- });
43833
- }
43834
43916
  // ---------------------------------------------------------------------------
43835
43917
  // Grid rendering
43836
43918
  // ---------------------------------------------------------------------------
@@ -45217,6 +45299,9 @@
45217
45299
  copiedTables;
45218
45300
  zones;
45219
45301
  uuidGenerator = new UuidGenerator();
45302
+ queuedBordersToAdd = {};
45303
+ queuedCFChanges = {};
45304
+ queuedDVChanges = {};
45220
45305
  constructor(zones, operation, getters, dispatch, selection) {
45221
45306
  super(operation, getters, dispatch, selection);
45222
45307
  if (!zones.length) {
@@ -45326,6 +45411,8 @@
45326
45411
  * Paste the clipboard content in the given target
45327
45412
  */
45328
45413
  paste(target, options) {
45414
+ this.queuedCFChanges = {};
45415
+ this.queuedDVChanges = {};
45329
45416
  if (this.operation === "COPY") {
45330
45417
  this.pasteFromCopy(target, options);
45331
45418
  }
@@ -45338,6 +45425,7 @@
45338
45425
  if (options?.selectTarget) {
45339
45426
  this.selectPastedZone(width, height, isCutOperation, target);
45340
45427
  }
45428
+ this.executeQueuedChanges(this.getters.getActiveSheetId());
45341
45429
  }
45342
45430
  pasteFromCopy(target, options) {
45343
45431
  if (target.length === 1) {
@@ -45512,7 +45600,11 @@
45512
45600
  left: targetBorders?.left || originBorders?.left,
45513
45601
  right: targetBorders?.right || originBorders?.right,
45514
45602
  };
45515
- this.dispatch("SET_BORDER", { sheetId, col, row, border });
45603
+ const borderKey = JSON.stringify(border);
45604
+ if (!this.queuedBordersToAdd[borderKey]) {
45605
+ this.queuedBordersToAdd[borderKey] = [];
45606
+ }
45607
+ this.queuedBordersToAdd[borderKey].push(positionToZone(target));
45516
45608
  if (clipboardOption?.pasteOption === "onlyFormat") {
45517
45609
  this.dispatch("UPDATE_CELL", {
45518
45610
  ...target,
@@ -45658,7 +45750,8 @@
45658
45750
  }
45659
45751
  }
45660
45752
  pasteCf(origin, target) {
45661
- const xc = toXC(target.col, target.row);
45753
+ const zone = positionToZone(target);
45754
+ const originZone = positionToZone(origin);
45662
45755
  for (let rule of this.getters.getConditionalFormats(origin.sheetId)) {
45663
45756
  for (let range of rule.ranges) {
45664
45757
  if (isInside(origin.col, origin.row, this.getters.getRangeFromSheetXC(origin.sheetId, range).zone)) {
@@ -45666,15 +45759,15 @@
45666
45759
  const toRemoveRange = [];
45667
45760
  if (this.operation === "CUT") {
45668
45761
  //remove from current rule
45669
- toRemoveRange.push(toXC(origin.col, origin.row));
45762
+ toRemoveRange.push(originZone);
45670
45763
  }
45671
45764
  if (origin.sheetId === target.sheetId) {
45672
- this.adaptCFRules(origin.sheetId, cf, [xc], toRemoveRange);
45765
+ this.adaptCFRules(origin.sheetId, cf, [zone], toRemoveRange);
45673
45766
  }
45674
45767
  else {
45675
45768
  this.adaptCFRules(origin.sheetId, cf, [], toRemoveRange);
45676
45769
  const cfToCopyTo = this.getCFToCopyTo(target.sheetId, cf);
45677
- this.adaptCFRules(target.sheetId, cfToCopyTo, [xc], []);
45770
+ this.adaptCFRules(target.sheetId, cfToCopyTo, [zone], []);
45678
45771
  }
45679
45772
  }
45680
45773
  }
@@ -45684,79 +45777,130 @@
45684
45777
  * Add or remove cells to a given conditional formatting rule.
45685
45778
  */
45686
45779
  adaptCFRules(sheetId, cf, toAdd, toRemove) {
45687
- const newRangesXC = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
45688
- if (!newRangesXC) {
45689
- return;
45780
+ if (!this.queuedCFChanges[sheetId]) {
45781
+ this.queuedCFChanges[sheetId] = [];
45690
45782
  }
45691
- if (newRangesXC.length === 0) {
45692
- this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
45693
- return;
45783
+ const queuedChange = this.queuedCFChanges[sheetId].find((queued) => queued.cf.id === cf.id);
45784
+ if (!queuedChange) {
45785
+ this.queuedCFChanges[sheetId].push({ toAdd, toRemove, cf });
45786
+ }
45787
+ else {
45788
+ queuedChange.toAdd.push(...toAdd);
45789
+ queuedChange.toRemove.push(...toRemove);
45694
45790
  }
45695
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
45696
- cf: {
45697
- id: cf.id,
45698
- rule: cf.rule,
45699
- stopIfTrue: cf.stopIfTrue,
45700
- },
45701
- ranges: newRangesXC.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
45702
- sheetId,
45703
- });
45704
45791
  }
45705
45792
  getCFToCopyTo(targetSheetId, originCF) {
45706
- const cfInTarget = this.getters
45793
+ let targetCF = this.getters
45707
45794
  .getConditionalFormats(targetSheetId)
45708
45795
  .find((cf) => cf.stopIfTrue === originCF.stopIfTrue && deepEquals(cf.rule, originCF.rule));
45709
- return cfInTarget
45710
- ? cfInTarget
45711
- : { ...originCF, id: this.uuidGenerator.smallUuid(), ranges: [] };
45796
+ const queuedCfs = this.queuedCFChanges[targetSheetId];
45797
+ if (!targetCF && queuedCfs) {
45798
+ targetCF = queuedCfs.find((queued) => queued.cf.stopIfTrue === originCF.stopIfTrue && deepEquals(queued.cf.rule, originCF.rule))?.cf;
45799
+ }
45800
+ return targetCF ? targetCF : { ...originCF, id: this.uuidGenerator.smallUuid(), ranges: [] };
45712
45801
  }
45713
45802
  pasteDataValidation(origin, target) {
45714
45803
  const rule = this.getters.getValidationRuleForCell(origin);
45804
+ const originZone = positionToZone(origin);
45715
45805
  if (!rule) {
45716
45806
  return;
45717
45807
  }
45718
- const xc = toXC(target.col, target.row);
45808
+ const zone = positionToZone(target);
45719
45809
  for (const range of rule.ranges) {
45720
45810
  if (isInside(origin.col, origin.row, range.zone)) {
45721
45811
  const toRemoveRange = [];
45722
45812
  if (this.operation === "CUT") {
45723
- toRemoveRange.push(toXC(origin.col, origin.row));
45813
+ toRemoveRange.push(originZone);
45724
45814
  }
45725
45815
  if (origin.sheetId === target.sheetId) {
45726
- this.adaptDataValidationRule(origin.sheetId, rule, [xc], toRemoveRange);
45816
+ const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
45817
+ this.adaptDataValidationRule(origin.sheetId, copyToRule, [zone], toRemoveRange);
45727
45818
  }
45728
45819
  else {
45729
- this.adaptDataValidationRule(origin.sheetId, rule, [], toRemoveRange);
45730
- let copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
45731
- this.adaptDataValidationRule(target.sheetId, copyToRule, [xc], []);
45820
+ const originRule = this.getters.getValidationRuleForCell(origin);
45821
+ if (originRule) {
45822
+ this.adaptDataValidationRule(origin.sheetId, originRule, [], toRemoveRange);
45823
+ }
45824
+ const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
45825
+ this.adaptDataValidationRule(target.sheetId, copyToRule, [zone], []);
45732
45826
  }
45733
45827
  }
45734
45828
  }
45735
45829
  }
45736
45830
  getDataValidationRuleToCopyTo(targetSheetId, originRule) {
45737
- const ruleInTargetSheet = this.getters
45831
+ let targetRule = this.getters
45738
45832
  .getDataValidationRules(targetSheetId)
45739
45833
  .find((rule) => deepEquals(originRule.criterion, rule.criterion) &&
45740
45834
  originRule.isBlocking === rule.isBlocking);
45741
- return ruleInTargetSheet
45742
- ? ruleInTargetSheet
45743
- : { ...originRule, id: this.uuidGenerator.smallUuid(), ranges: [] };
45835
+ const queuedRules = this.queuedDVChanges[targetSheetId];
45836
+ if (!targetRule && queuedRules) {
45837
+ targetRule = queuedRules.find((queued) => deepEquals(originRule.criterion, queued.rule.criterion) &&
45838
+ originRule.isBlocking === queued.rule.isBlocking)?.rule;
45839
+ }
45840
+ return targetRule || { ...originRule, id: this.uuidGenerator.smallUuid(), ranges: [] };
45744
45841
  }
45745
45842
  /**
45746
45843
  * Add or remove XCs to a given data validation rule.
45747
45844
  */
45748
45845
  adaptDataValidationRule(sheetId, rule, toAdd, toRemove) {
45749
- const dvRangesXcs = rule.ranges.map((range) => this.getters.getRangeString(range, sheetId));
45750
- const newRangesXC = recomputeZones([...dvRangesXcs, ...toAdd], toRemove);
45751
- if (newRangesXC.length === 0) {
45752
- this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: rule.id });
45753
- return;
45846
+ if (!this.queuedDVChanges[sheetId]) {
45847
+ this.queuedDVChanges[sheetId] = [];
45754
45848
  }
45755
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
45756
- rule,
45757
- ranges: newRangesXC.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
45758
- sheetId,
45759
- });
45849
+ const queuedChange = this.queuedDVChanges[sheetId].find((queued) => queued.rule.id === rule.id);
45850
+ if (!queuedChange) {
45851
+ this.queuedDVChanges[sheetId].push({ toAdd, toRemove, rule: deepCopy(rule) });
45852
+ }
45853
+ else {
45854
+ queuedChange.toAdd.push(...toAdd);
45855
+ queuedChange.toRemove.push(...toRemove);
45856
+ }
45857
+ }
45858
+ executeQueuedChanges(pasteSheetTarget) {
45859
+ for (const sheetId in this.queuedCFChanges) {
45860
+ for (const { toAdd, toRemove, cf } of this.queuedCFChanges[sheetId]) {
45861
+ const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
45862
+ if (!newRangesXc) {
45863
+ continue;
45864
+ }
45865
+ if (newRangesXc.length === 0) {
45866
+ this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
45867
+ continue;
45868
+ }
45869
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
45870
+ cf: {
45871
+ id: cf.id,
45872
+ rule: cf.rule,
45873
+ stopIfTrue: cf.stopIfTrue,
45874
+ },
45875
+ ranges: newRangesXc.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
45876
+ sheetId,
45877
+ });
45878
+ }
45879
+ }
45880
+ for (const sheetId in this.queuedDVChanges) {
45881
+ for (const { toAdd, toRemove, rule: dv } of this.queuedDVChanges[sheetId]) {
45882
+ // Remove the zones first in case the same position is in toAdd and toRemove
45883
+ const dvZones = dv.ranges.map((range) => range.zone);
45884
+ const withRemovedZones = futureRecomputeZones(dvZones, toRemove);
45885
+ const newDvZones = futureRecomputeZones([...withRemovedZones, ...toAdd], []);
45886
+ if (newDvZones.length === 0) {
45887
+ this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: dv.id });
45888
+ continue;
45889
+ }
45890
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
45891
+ rule: { id: dv.id, criterion: dv.criterion, isBlocking: dv.isBlocking },
45892
+ ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
45893
+ sheetId,
45894
+ });
45895
+ }
45896
+ }
45897
+ for (const borderKey in this.queuedBordersToAdd) {
45898
+ const zones = this.queuedBordersToAdd[borderKey];
45899
+ const border = JSON.parse(borderKey);
45900
+ const target = futureRecomputeZones(zones, []);
45901
+ this.dispatch("SET_BORDERS_ON_TARGET", { sheetId: pasteSheetTarget, target, border });
45902
+ }
45903
+ this.queuedBordersToAdd = {};
45760
45904
  }
45761
45905
  }
45762
45906
 
@@ -58104,9 +58248,9 @@
58104
58248
  exports.tokenize = tokenize;
58105
58249
 
58106
58250
 
58107
- __info__.version = "17.0.55";
58108
- __info__.date = "2025-03-26T12:44:29.690Z";
58109
- __info__.hash = "c3a4769";
58251
+ __info__.version = "17.0.57";
58252
+ __info__.date = "2025-04-14T17:13:56.677Z";
58253
+ __info__.hash = "6ac8bb7";
58110
58254
 
58111
58255
 
58112
58256
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);