@odoo/o-spreadsheet 17.0.56 → 17.0.58

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.56
6
- * @date 2025-04-04T08:36:58.258Z
7
- * @hash c484787
5
+ * @version 17.0.58
6
+ * @date 2025-04-18T16:20:26.099Z
7
+ * @hash 88d92de
8
8
  */
9
9
 
10
10
  import { Component, useRef, onMounted, useEffect, onWillPatch, useState, onWillUpdateProps, onPatched, useComponent, useExternalListener, onWillUnmount, onWillStart, xml, useChildSubEnv, useSubEnv, markRaw } from '@odoo/owl';
@@ -2001,6 +2001,7 @@ const coreTypes = new Set([
2001
2001
  "CLEAR_FORMATTING",
2002
2002
  "SET_BORDER",
2003
2003
  "SET_ZONE_BORDERS",
2004
+ "SET_BORDERS_ON_TARGET",
2004
2005
  /** CHART */
2005
2006
  "CREATE_CHART",
2006
2007
  "UPDATE_CHART",
@@ -4815,7 +4816,7 @@ function transformZone(zone, executed) {
4815
4816
  if (executed.type === "ADD_COLUMNS_ROWS") {
4816
4817
  return expandZoneOnInsertion(zone, executed.dimension === "COL" ? "left" : "top", executed.base, executed.position, executed.quantity);
4817
4818
  }
4818
- return { ...zone };
4819
+ return zone;
4819
4820
  }
4820
4821
  function transformRangeData(range, executed) {
4821
4822
  const deletedSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
@@ -7616,6 +7617,12 @@ function tokenizeString(chars) {
7616
7617
  }
7617
7618
  return null;
7618
7619
  }
7620
+ /**
7621
+ - \p{L} is for any letter (from any language)
7622
+ - \p{N} is for any number
7623
+ - the u flag at the end is for unicode, which enables the `\p{...}` syntax
7624
+ */
7625
+ const unicodeSymbolCharRegexp = /\p{L}|\p{N}|_|\.|!|\$/u;
7619
7626
  const SYMBOL_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.!$");
7620
7627
  /**
7621
7628
  * A "Symbol" is just basically any word-like element that can appear in a
@@ -7656,7 +7663,8 @@ function tokenizeSymbol(chars) {
7656
7663
  };
7657
7664
  }
7658
7665
  }
7659
- while (chars.current && SYMBOL_CHARS.has(chars.current)) {
7666
+ while (chars.current &&
7667
+ (SYMBOL_CHARS.has(chars.current) || chars.current.match(unicodeSymbolCharRegexp))) {
7660
7668
  result += chars.shift();
7661
7669
  }
7662
7670
  if (result.length) {
@@ -34683,6 +34691,15 @@ class BordersPlugin extends CorePlugin {
34683
34691
  case "SET_BORDER":
34684
34692
  this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);
34685
34693
  break;
34694
+ case "SET_BORDERS_ON_TARGET":
34695
+ for (const zone of cmd.target) {
34696
+ for (let row = zone.top; row <= zone.bottom; row++) {
34697
+ for (let col = zone.left; col <= zone.right; col++) {
34698
+ this.setBorder(cmd.sheetId, col, row, cmd.border);
34699
+ }
34700
+ }
34701
+ }
34702
+ break;
34686
34703
  case "SET_ZONE_BORDERS":
34687
34704
  if (cmd.border) {
34688
34705
  const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
@@ -36689,6 +36706,353 @@ class ChartPlugin extends CorePlugin {
36689
36706
  }
36690
36707
  }
36691
36708
 
36709
+ /**
36710
+ * ####################################################
36711
+ * # INTRODUCTION
36712
+ * ####################################################
36713
+ *
36714
+ * This file contain the function recomputeZones.
36715
+ * This function try to recompute in a performant way
36716
+ * an ensemble of zones possibly overlapping to avoid
36717
+ * overlapping and to reduce the number of zones.
36718
+ *
36719
+ * It also allows to remove some zones from the ensemble.
36720
+ *
36721
+ * In the following example, 2 zones are overlapping.
36722
+ * Applying recomputeZones will return zones without
36723
+ * overlapping:
36724
+ *
36725
+ * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
36726
+ *
36727
+ * A B C D E A B C D E
36728
+ * 1 ___ 1 ___
36729
+ * 2 ___|_ | 2 ___| | |
36730
+ * 3 | |_|_| ---> 3 | | |_|
36731
+ * 4 |_____| 4 |___|_|
36732
+ * 6 6
36733
+ * 7 7
36734
+ *
36735
+ *
36736
+ * In the following example, 2 zones are contiguous.
36737
+ * Applying recomputeZones will return only one zone:
36738
+ *
36739
+ * ["B2:B3", "C2:D3"] ["B2:D3"]
36740
+ *
36741
+ * A B C D E A B C D E
36742
+ * 1 _ ___ 1 _____
36743
+ * 2 | | | ---> 2 | |
36744
+ * 3 |_|___| 3 |_____|
36745
+ * 4 4
36746
+ *
36747
+ *
36748
+ * In the following example, we want to remove a zone
36749
+ * from the ensemble. Applying recomputeZones will
36750
+ * return the ensemble without the zone to remove:
36751
+ *
36752
+ * remove ["C3:D3"] ["B2:B4", "C2:D2",
36753
+ * "C4:D4", "E2:E4"]
36754
+ *
36755
+ * A B C D E F A B C D E F
36756
+ * 1 _______ 1 _______
36757
+ * 2 | | ---> 2 | |___| |
36758
+ * 3 | xxx | 3 | |___| |
36759
+ * 4 |_______| 4 |_|___|_|
36760
+ * 5 5
36761
+ *
36762
+ *
36763
+ * The exercise seems simple when we have only 2 zones.
36764
+ * But with n zones and in a performant way, we want to
36765
+ * avoid comparing each zone with all the others.
36766
+ *
36767
+ *
36768
+ * ####################################################
36769
+ * # Methodological approach
36770
+ * ####################################################
36771
+ *
36772
+ * The methodological approach to avoid comparing each
36773
+ * zone with all the others is to use a data structure
36774
+ * that allow to quickly find which zones are
36775
+ * overlapping with any other given zone.
36776
+ *
36777
+ * Here the idea is to profile the zones at the columns level.
36778
+ *
36779
+ * To do that, we propose to use a data structure
36780
+ * composed of 2 parts:
36781
+ * - profilesStartingPosition: a sorted number array
36782
+ * indicating on which columns a new profile begins.
36783
+ * - profiles: a map where the key is a column
36784
+ * position (from profilesStartingPosition) and the
36785
+ * value is a sorted number array representing a
36786
+ * profile.
36787
+ *
36788
+ *
36789
+ * See the following example: here profileStartingPosition
36790
+ * corresponds to [A,C,E,G,K]
36791
+ * A B C D E F G H I J K so with number [0,2,4,6,10]
36792
+ * 1 ' ' ' '
36793
+ * 2 ' ' '_______' here profile correspond
36794
+ * 3 '___' |_______| for A to []
36795
+ * 4 | | for C to [3, 5]
36796
+ * 5 |___| for E to []
36797
+ * 6 for G to [2, 3]
36798
+ * 7 for K to []
36799
+ *
36800
+ *
36801
+ * Now we can easily find which zones are overlapping
36802
+ * with a given zone. Suppose we want to add a new zone
36803
+ * D5:H6 to the ensemble:
36804
+ *
36805
+ * With a binary search of left and right
36806
+ * A B C D E F G H I J K on profilesStartingPosition, we can
36807
+ * 1 ' ' ' ' find the indexes of the profiles on which
36808
+ * 2 ' ' '_______' to apply a modification.
36809
+ * 3 '___' |_______|
36810
+ * 4 | _|_______ Here we will:
36811
+ * 5 |_|_| | - add a new profile in D --> become [3, 6]
36812
+ * 6 |_________| - modify the profile in E --> become [4, 6]
36813
+ * 7 - modify the profile in G --> become [2, 3, 4, 6]
36814
+ * - add a new profile in I --> become [8, 10]
36815
+ *
36816
+ * See below the result:
36817
+ *
36818
+ * Note the particularity of the profile
36819
+ * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
36820
+ * 1 ' ' ' ' ' '
36821
+ * 2 ' ' ' '___'___' To know how to modify the profile (add a
36822
+ * 3 '_'_' |___|___| zone or remove it) we do a binary
36823
+ * 4 | | |___ ___ search of the top and bottom value on the
36824
+ * 5 |_| | | | profile array. Depending on the result index
36825
+ * 6 |_|___|___| parity (odd or even), because zone boundaries
36826
+ * 7 go by pairs, we know if we are in a zone or
36827
+ * not and how operate.
36828
+ */
36829
+ /**
36830
+ * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
36831
+ * This compute is particularly useful because after this function:
36832
+ * - you will find coordinate of a cell only once among all the zones
36833
+ * - the number of zones will be reduced to the minimum
36834
+ */
36835
+ function futureRecomputeZones(zones, zonesToRemove = []) {
36836
+ const profilesStartingPosition = [0];
36837
+ const profiles = new Map([[0, []]]);
36838
+ modifyProfiles(profilesStartingPosition, profiles, zones, false);
36839
+ modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
36840
+ return constructZonesFromProfiles(profilesStartingPosition, profiles);
36841
+ }
36842
+ function modifyProfiles(// export for testing only
36843
+ profilesStartingPosition, profiles, zones, toRemove = false) {
36844
+ for (const zone of zones) {
36845
+ const leftValue = zone.left;
36846
+ const rightValue = zone.right === undefined ? undefined : zone.right + 1;
36847
+ const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
36848
+ const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
36849
+ for (let i = leftIndex; i <= rightIndex; i++) {
36850
+ const profile = profiles.get(profilesStartingPosition[i]);
36851
+ modifyProfile(profile, zone, toRemove);
36852
+ }
36853
+ // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
36854
+ removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
36855
+ }
36856
+ }
36857
+ function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
36858
+ if (value === undefined) {
36859
+ // this is only the case when the value correspond to a bottom value that could be undefined
36860
+ return profilesStartingPosition.length - 1;
36861
+ }
36862
+ const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
36863
+ if (value != profilesStartingPosition[predecessorIndex]) {
36864
+ // mean that the value is not ending/starting at the same position as the previous/next profile
36865
+ // --> it's a new profile
36866
+ // --> we need to add it
36867
+ profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
36868
+ // suppose the we want to add the for the left value
36869
+ // following profile following zone: 'C', the predecessor index
36870
+ // for B: [1, 3] "C3:D4" correspond to 'B'.
36871
+ // The next line code will
36872
+ // A B C D A B C D copy the profile of 'B'
36873
+ // 1 '___' 1 '___' to 'C'. In the rest of the
36874
+ // 2 | | ---> 2 | _|_ process the 'modifyProfile'
36875
+ // 3 |___| 3 |_|_| | function will adapt the waiting
36876
+ // 4 4 |___| 'C' profile [1, 3] to the
36877
+ // correct 'C' profile [1, 4]
36878
+ profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
36879
+ return searchLeft ? predecessorIndex + 1 : predecessorIndex;
36880
+ }
36881
+ return searchLeft ? predecessorIndex : predecessorIndex - 1;
36882
+ }
36883
+ /**
36884
+ * Suppose the following Suppose we want to add We want to have the
36885
+ * profile: the following zone: following profile:
36886
+ *
36887
+ * A B C D E F A B C D E F A B C D E F
36888
+ * 1 '___' 1 ' ' 1 '___'
36889
+ * 2 |___| 2 '___' 2 | |
36890
+ * 3 ' ' 3 | | 3 | |
36891
+ * 4 '___' --> 4 | | --> 4 | |
36892
+ * 6 | | 6 |___| 6 | |
36893
+ * 7 |___| 7 7 |___|
36894
+ * 8 8 8
36895
+ *
36896
+ * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
36897
+ * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
36898
+ * ____ ____ correspond to 6
36899
+ * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
36900
+ * ____ is to know what must be deleted
36901
+ * Note that the 'filled [3, 7] and what must be added to the
36902
+ * zone' are always between existing profile.
36903
+ * an even index and its
36904
+ * next index
36905
+ *
36906
+ */
36907
+ function modifyProfile(profile, zone, toRemove = false) {
36908
+ const topValue = zone.top;
36909
+ const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
36910
+ const newPoints = [];
36911
+ // Case we want to add a zone to the profile:
36912
+ // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
36913
+ // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
36914
+ // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
36915
+ // so we can keep the index of the top position
36916
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
36917
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
36918
+ if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
36919
+ newPoints.push(topValue);
36920
+ }
36921
+ if (bottomValue === undefined) {
36922
+ // The following two code lines will not impact the final result,
36923
+ // but they will impact the intermediate profile.
36924
+ // We keep them for performance reason
36925
+ profile.splice(topPredIndex + 1);
36926
+ profile.push(...newPoints);
36927
+ return;
36928
+ }
36929
+ // Case we want to add a zone to the profile:
36930
+ // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
36931
+ // so we can keep the index of the bottom position.
36932
+ // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
36933
+ // so we don't need to add it to the profile. we can keep in reference the index of the successor
36934
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
36935
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
36936
+ if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
36937
+ newPoints.push(bottomValue);
36938
+ }
36939
+ // add the top and bottom value to the profile and
36940
+ // remove all information between the top and bottom index
36941
+ profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
36942
+ }
36943
+ function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
36944
+ const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
36945
+ const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
36946
+ for (let i = end; i > start; i--) {
36947
+ if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
36948
+ profiles.delete(profilesStartingPosition[i]);
36949
+ profilesStartingPosition.splice(i, 1);
36950
+ }
36951
+ }
36952
+ }
36953
+ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
36954
+ const mergedZone = [];
36955
+ let pendingZones = [];
36956
+ for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
36957
+ const left = profilesStartingPosition[colIndex];
36958
+ const profile = profiles.get(left);
36959
+ if (!profile || profile.length === 0) {
36960
+ mergedZone.push(...pendingZones);
36961
+ pendingZones = [];
36962
+ continue;
36963
+ }
36964
+ let right = profilesStartingPosition[colIndex + 1];
36965
+ if (right !== undefined) {
36966
+ right--;
36967
+ }
36968
+ const nextPendingZones = [];
36969
+ for (let i = 0; i < profile.length; i += 2) {
36970
+ const top = profile[i];
36971
+ let bottom = profile[i + 1];
36972
+ if (bottom !== undefined) {
36973
+ bottom--;
36974
+ }
36975
+ const profileZone = {
36976
+ top,
36977
+ left,
36978
+ bottom,
36979
+ right,
36980
+ hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
36981
+ };
36982
+ let findCorrespondingZone = false;
36983
+ for (let j = pendingZones.length - 1; j >= 0; j--) {
36984
+ const pendingZone = pendingZones[j];
36985
+ if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
36986
+ pendingZone.right = profileZone.right;
36987
+ pendingZones.splice(j, 1);
36988
+ nextPendingZones.push(pendingZone);
36989
+ findCorrespondingZone = true;
36990
+ break;
36991
+ }
36992
+ }
36993
+ if (!findCorrespondingZone) {
36994
+ nextPendingZones.push(profileZone);
36995
+ }
36996
+ }
36997
+ mergedZone.push(...pendingZones);
36998
+ pendingZones = nextPendingZones;
36999
+ }
37000
+ mergedZone.push(...pendingZones);
37001
+ return mergedZone;
37002
+ }
37003
+ function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
37004
+ let end = arr.length - 1;
37005
+ let result = -1;
37006
+ while (start <= end) {
37007
+ const mid = Math.floor((start + end) / 2);
37008
+ if (arr[mid] === val && matchEqual) {
37009
+ return mid;
37010
+ }
37011
+ else if (arr[mid] < val) {
37012
+ result = mid;
37013
+ start = mid + 1;
37014
+ }
37015
+ else {
37016
+ end = mid - 1;
37017
+ }
37018
+ }
37019
+ return result;
37020
+ }
37021
+ function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
37022
+ let end = arr.length - 1;
37023
+ let result = arr.length;
37024
+ while (start <= end) {
37025
+ const mid = Math.floor((start + end) / 2);
37026
+ if (arr[mid] === val && matchEqual) {
37027
+ return mid;
37028
+ }
37029
+ else if (arr[mid] > val) {
37030
+ result = mid;
37031
+ end = mid - 1;
37032
+ }
37033
+ else {
37034
+ start = mid + 1;
37035
+ }
37036
+ }
37037
+ return result;
37038
+ }
37039
+ /**
37040
+ * Compares two arrays.
37041
+ * For performance reasons, this function is to be preferred
37042
+ * to 'deepEquals' in the case we know that the inputs are arrays.
37043
+ */
37044
+ function deepEqualsArray(arr1, arr2) {
37045
+ if (arr1.length !== arr2.length) {
37046
+ return false;
37047
+ }
37048
+ for (let i = 0; i < arr1.length; i++) {
37049
+ if (!deepEquals(arr1[i], arr2[i])) {
37050
+ return false;
37051
+ }
37052
+ }
37053
+ return true;
37054
+ }
37055
+
36692
37056
  // -----------------------------------------------------------------------------
36693
37057
  // Constants
36694
37058
  // -----------------------------------------------------------------------------
@@ -36858,10 +37222,12 @@ class ConditionalFormatPlugin extends CorePlugin {
36858
37222
  const replaceIndex = rules.findIndex((c) => c.id === cf.id);
36859
37223
  let currentRanges = [];
36860
37224
  if (replaceIndex > -1) {
36861
- currentRanges = rules[replaceIndex].ranges;
37225
+ currentRanges = rules[replaceIndex].ranges.map(toUnboundedZone);
36862
37226
  }
36863
37227
  currentRanges = currentRanges.concat(toAdd);
36864
- return recomputeZones(currentRanges, toRemove);
37228
+ // Remove the zones first in case the same position is in toAdd and toRemove
37229
+ const withRemovedZones = futureRecomputeZones(currentRanges, toRemove);
37230
+ return futureRecomputeZones([...toAdd, ...withRemovedZones], []);
36865
37231
  }
36866
37232
  // ---------------------------------------------------------------------------
36867
37233
  // Private
@@ -40596,353 +40962,6 @@ class CompilationParametersBuilder {
40596
40962
  }
40597
40963
  }
40598
40964
 
40599
- /**
40600
- * ####################################################
40601
- * # INTRODUCTION
40602
- * ####################################################
40603
- *
40604
- * This file contain the function recomputeZones.
40605
- * This function try to recompute in a performant way
40606
- * an ensemble of zones possibly overlapping to avoid
40607
- * overlapping and to reduce the number of zones.
40608
- *
40609
- * It also allows to remove some zones from the ensemble.
40610
- *
40611
- * In the following example, 2 zones are overlapping.
40612
- * Applying recomputeZones will return zones without
40613
- * overlapping:
40614
- *
40615
- * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
40616
- *
40617
- * A B C D E A B C D E
40618
- * 1 ___ 1 ___
40619
- * 2 ___|_ | 2 ___| | |
40620
- * 3 | |_|_| ---> 3 | | |_|
40621
- * 4 |_____| 4 |___|_|
40622
- * 6 6
40623
- * 7 7
40624
- *
40625
- *
40626
- * In the following example, 2 zones are contiguous.
40627
- * Applying recomputeZones will return only one zone:
40628
- *
40629
- * ["B2:B3", "C2:D3"] ["B2:D3"]
40630
- *
40631
- * A B C D E A B C D E
40632
- * 1 _ ___ 1 _____
40633
- * 2 | | | ---> 2 | |
40634
- * 3 |_|___| 3 |_____|
40635
- * 4 4
40636
- *
40637
- *
40638
- * In the following example, we want to remove a zone
40639
- * from the ensemble. Applying recomputeZones will
40640
- * return the ensemble without the zone to remove:
40641
- *
40642
- * remove ["C3:D3"] ["B2:B4", "C2:D2",
40643
- * "C4:D4", "E2:E4"]
40644
- *
40645
- * A B C D E F A B C D E F
40646
- * 1 _______ 1 _______
40647
- * 2 | | ---> 2 | |___| |
40648
- * 3 | xxx | 3 | |___| |
40649
- * 4 |_______| 4 |_|___|_|
40650
- * 5 5
40651
- *
40652
- *
40653
- * The exercise seems simple when we have only 2 zones.
40654
- * But with n zones and in a performant way, we want to
40655
- * avoid comparing each zone with all the others.
40656
- *
40657
- *
40658
- * ####################################################
40659
- * # Methodological approach
40660
- * ####################################################
40661
- *
40662
- * The methodological approach to avoid comparing each
40663
- * zone with all the others is to use a data structure
40664
- * that allow to quickly find which zones are
40665
- * overlapping with any other given zone.
40666
- *
40667
- * Here the idea is to profile the zones at the columns level.
40668
- *
40669
- * To do that, we propose to use a data structure
40670
- * composed of 2 parts:
40671
- * - profilesStartingPosition: a sorted number array
40672
- * indicating on which columns a new profile begins.
40673
- * - profiles: a map where the key is a column
40674
- * position (from profilesStartingPosition) and the
40675
- * value is a sorted number array representing a
40676
- * profile.
40677
- *
40678
- *
40679
- * See the following example: here profileStartingPosition
40680
- * corresponds to [A,C,E,G,K]
40681
- * A B C D E F G H I J K so with number [0,2,4,6,10]
40682
- * 1 ' ' ' '
40683
- * 2 ' ' '_______' here profile correspond
40684
- * 3 '___' |_______| for A to []
40685
- * 4 | | for C to [3, 5]
40686
- * 5 |___| for E to []
40687
- * 6 for G to [2, 3]
40688
- * 7 for K to []
40689
- *
40690
- *
40691
- * Now we can easily find which zones are overlapping
40692
- * with a given zone. Suppose we want to add a new zone
40693
- * D5:H6 to the ensemble:
40694
- *
40695
- * With a binary search of left and right
40696
- * A B C D E F G H I J K on profilesStartingPosition, we can
40697
- * 1 ' ' ' ' find the indexes of the profiles on which
40698
- * 2 ' ' '_______' to apply a modification.
40699
- * 3 '___' |_______|
40700
- * 4 | _|_______ Here we will:
40701
- * 5 |_|_| | - add a new profile in D --> become [3, 6]
40702
- * 6 |_________| - modify the profile in E --> become [4, 6]
40703
- * 7 - modify the profile in G --> become [2, 3, 4, 6]
40704
- * - add a new profile in I --> become [8, 10]
40705
- *
40706
- * See below the result:
40707
- *
40708
- * Note the particularity of the profile
40709
- * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
40710
- * 1 ' ' ' ' ' '
40711
- * 2 ' ' ' '___'___' To know how to modify the profile (add a
40712
- * 3 '_'_' |___|___| zone or remove it) we do a binary
40713
- * 4 | | |___ ___ search of the top and bottom value on the
40714
- * 5 |_| | | | profile array. Depending on the result index
40715
- * 6 |_|___|___| parity (odd or even), because zone boundaries
40716
- * 7 go by pairs, we know if we are in a zone or
40717
- * not and how operate.
40718
- */
40719
- /**
40720
- * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
40721
- * This compute is particularly useful because after this function:
40722
- * - you will find coordinate of a cell only once among all the zones
40723
- * - the number of zones will be reduced to the minimum
40724
- */
40725
- function futureRecomputeZones(zones, zonesToRemove = []) {
40726
- const profilesStartingPosition = [0];
40727
- const profiles = new Map([[0, []]]);
40728
- modifyProfiles(profilesStartingPosition, profiles, zones, false);
40729
- modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
40730
- return constructZonesFromProfiles(profilesStartingPosition, profiles);
40731
- }
40732
- function modifyProfiles(// export for testing only
40733
- profilesStartingPosition, profiles, zones, toRemove = false) {
40734
- for (const zone of zones) {
40735
- const leftValue = zone.left;
40736
- const rightValue = zone.right === undefined ? undefined : zone.right + 1;
40737
- const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
40738
- const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
40739
- for (let i = leftIndex; i <= rightIndex; i++) {
40740
- const profile = profiles.get(profilesStartingPosition[i]);
40741
- modifyProfile(profile, zone, toRemove);
40742
- }
40743
- // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
40744
- removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
40745
- }
40746
- }
40747
- function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
40748
- if (value === undefined) {
40749
- // this is only the case when the value correspond to a bottom value that could be undefined
40750
- return profilesStartingPosition.length - 1;
40751
- }
40752
- const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
40753
- if (value != profilesStartingPosition[predecessorIndex]) {
40754
- // mean that the value is not ending/starting at the same position as the previous/next profile
40755
- // --> it's a new profile
40756
- // --> we need to add it
40757
- profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
40758
- // suppose the we want to add the for the left value
40759
- // following profile following zone: 'C', the predecessor index
40760
- // for B: [1, 3] "C3:D4" correspond to 'B'.
40761
- // The next line code will
40762
- // A B C D A B C D copy the profile of 'B'
40763
- // 1 '___' 1 '___' to 'C'. In the rest of the
40764
- // 2 | | ---> 2 | _|_ process the 'modifyProfile'
40765
- // 3 |___| 3 |_|_| | function will adapt the waiting
40766
- // 4 4 |___| 'C' profile [1, 3] to the
40767
- // correct 'C' profile [1, 4]
40768
- profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
40769
- return searchLeft ? predecessorIndex + 1 : predecessorIndex;
40770
- }
40771
- return searchLeft ? predecessorIndex : predecessorIndex - 1;
40772
- }
40773
- /**
40774
- * Suppose the following Suppose we want to add We want to have the
40775
- * profile: the following zone: following profile:
40776
- *
40777
- * A B C D E F A B C D E F A B C D E F
40778
- * 1 '___' 1 ' ' 1 '___'
40779
- * 2 |___| 2 '___' 2 | |
40780
- * 3 ' ' 3 | | 3 | |
40781
- * 4 '___' --> 4 | | --> 4 | |
40782
- * 6 | | 6 |___| 6 | |
40783
- * 7 |___| 7 7 |___|
40784
- * 8 8 8
40785
- *
40786
- * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
40787
- * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
40788
- * ____ ____ correspond to 6
40789
- * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
40790
- * ____ is to know what must be deleted
40791
- * Note that the 'filled [3, 7] and what must be added to the
40792
- * zone' are always between existing profile.
40793
- * an even index and its
40794
- * next index
40795
- *
40796
- */
40797
- function modifyProfile(profile, zone, toRemove = false) {
40798
- const topValue = zone.top;
40799
- const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
40800
- const newPoints = [];
40801
- // Case we want to add a zone to the profile:
40802
- // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
40803
- // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
40804
- // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
40805
- // so we can keep the index of the top position
40806
- // Case we want to remove a zone from the profile: it's the opposite of the previous case
40807
- const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
40808
- if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
40809
- newPoints.push(topValue);
40810
- }
40811
- if (bottomValue === undefined) {
40812
- // The following two code lines will not impact the final result,
40813
- // but they will impact the intermediate profile.
40814
- // We keep them for performance reason
40815
- profile.splice(topPredIndex + 1);
40816
- profile.push(...newPoints);
40817
- return;
40818
- }
40819
- // Case we want to add a zone to the profile:
40820
- // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
40821
- // so we can keep the index of the bottom position.
40822
- // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
40823
- // so we don't need to add it to the profile. we can keep in reference the index of the successor
40824
- // Case we want to remove a zone from the profile: it's the opposite of the previous case
40825
- const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
40826
- if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
40827
- newPoints.push(bottomValue);
40828
- }
40829
- // add the top and bottom value to the profile and
40830
- // remove all information between the top and bottom index
40831
- profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
40832
- }
40833
- function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
40834
- const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
40835
- const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
40836
- for (let i = end; i > start; i--) {
40837
- if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
40838
- profiles.delete(profilesStartingPosition[i]);
40839
- profilesStartingPosition.splice(i, 1);
40840
- }
40841
- }
40842
- }
40843
- function constructZonesFromProfiles(profilesStartingPosition, profiles) {
40844
- const mergedZone = [];
40845
- let pendingZones = [];
40846
- for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
40847
- const left = profilesStartingPosition[colIndex];
40848
- const profile = profiles.get(left);
40849
- if (!profile || profile.length === 0) {
40850
- mergedZone.push(...pendingZones);
40851
- pendingZones = [];
40852
- continue;
40853
- }
40854
- let right = profilesStartingPosition[colIndex + 1];
40855
- if (right !== undefined) {
40856
- right--;
40857
- }
40858
- const nextPendingZones = [];
40859
- for (let i = 0; i < profile.length; i += 2) {
40860
- const top = profile[i];
40861
- let bottom = profile[i + 1];
40862
- if (bottom !== undefined) {
40863
- bottom--;
40864
- }
40865
- const profileZone = {
40866
- top,
40867
- left,
40868
- bottom,
40869
- right,
40870
- hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
40871
- };
40872
- let findCorrespondingZone = false;
40873
- for (let j = pendingZones.length - 1; j >= 0; j--) {
40874
- const pendingZone = pendingZones[j];
40875
- if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
40876
- pendingZone.right = profileZone.right;
40877
- pendingZones.splice(j, 1);
40878
- nextPendingZones.push(pendingZone);
40879
- findCorrespondingZone = true;
40880
- break;
40881
- }
40882
- }
40883
- if (!findCorrespondingZone) {
40884
- nextPendingZones.push(profileZone);
40885
- }
40886
- }
40887
- mergedZone.push(...pendingZones);
40888
- pendingZones = nextPendingZones;
40889
- }
40890
- mergedZone.push(...pendingZones);
40891
- return mergedZone;
40892
- }
40893
- function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
40894
- let end = arr.length - 1;
40895
- let result = -1;
40896
- while (start <= end) {
40897
- const mid = Math.floor((start + end) / 2);
40898
- if (arr[mid] === val && matchEqual) {
40899
- return mid;
40900
- }
40901
- else if (arr[mid] < val) {
40902
- result = mid;
40903
- start = mid + 1;
40904
- }
40905
- else {
40906
- end = mid - 1;
40907
- }
40908
- }
40909
- return result;
40910
- }
40911
- function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
40912
- let end = arr.length - 1;
40913
- let result = arr.length;
40914
- while (start <= end) {
40915
- const mid = Math.floor((start + end) / 2);
40916
- if (arr[mid] === val && matchEqual) {
40917
- return mid;
40918
- }
40919
- else if (arr[mid] > val) {
40920
- result = mid;
40921
- end = mid - 1;
40922
- }
40923
- else {
40924
- start = mid + 1;
40925
- }
40926
- }
40927
- return result;
40928
- }
40929
- /**
40930
- * Compares two arrays.
40931
- * For performance reasons, this function is to be preferred
40932
- * to 'deepEquals' in the case we know that the inputs are arrays.
40933
- */
40934
- function deepEqualsArray(arr1, arr2) {
40935
- if (arr1.length !== arr2.length) {
40936
- return false;
40937
- }
40938
- for (let i = 0; i < arr1.length; i++) {
40939
- if (!deepEquals(arr1[i], arr2[i])) {
40940
- return false;
40941
- }
40942
- }
40943
- return true;
40944
- }
40945
-
40946
40965
  function quickselect(arr, k, left, right, compare) {
40947
40966
  quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
40948
40967
  }
@@ -43539,25 +43558,6 @@ class AutofillPlugin extends UIPlugin {
43539
43558
  case "AUTOFILL_AUTO":
43540
43559
  this.autofillAuto();
43541
43560
  break;
43542
- case "AUTOFILL_CELL":
43543
- this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
43544
- const sheetId = this.getters.getActiveSheetId();
43545
- this.dispatch("UPDATE_CELL", {
43546
- sheetId,
43547
- col: cmd.col,
43548
- row: cmd.row,
43549
- style: cmd.style || null,
43550
- content: cmd.content || "",
43551
- format: cmd.format || "",
43552
- });
43553
- this.dispatch("SET_BORDER", {
43554
- sheetId,
43555
- col: cmd.col,
43556
- row: cmd.row,
43557
- border: cmd.border,
43558
- });
43559
- this.autofillCF(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
43560
- this.autofillDV(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
43561
43561
  }
43562
43562
  }
43563
43563
  // ---------------------------------------------------------------------------
@@ -43581,6 +43581,7 @@ class AutofillPlugin extends UIPlugin {
43581
43581
  }
43582
43582
  const source = this.getters.getSelectedZone();
43583
43583
  const target = this.autofillZone;
43584
+ const autofillCellsData = [];
43584
43585
  switch (this.direction) {
43585
43586
  case "down" /* DIRECTION.DOWN */:
43586
43587
  for (let col = source.left; col <= source.right; col++) {
@@ -43590,7 +43591,7 @@ class AutofillPlugin extends UIPlugin {
43590
43591
  }
43591
43592
  const generator = this.createGenerator(xcs);
43592
43593
  for (let row = target.top; row <= target.bottom; row++) {
43593
- this.computeNewCell(generator, col, row, apply);
43594
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43594
43595
  }
43595
43596
  }
43596
43597
  break;
@@ -43602,7 +43603,7 @@ class AutofillPlugin extends UIPlugin {
43602
43603
  }
43603
43604
  const generator = this.createGenerator(xcs);
43604
43605
  for (let row = target.bottom; row >= target.top; row--) {
43605
- this.computeNewCell(generator, col, row, apply);
43606
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43606
43607
  }
43607
43608
  }
43608
43609
  break;
@@ -43614,7 +43615,7 @@ class AutofillPlugin extends UIPlugin {
43614
43615
  }
43615
43616
  const generator = this.createGenerator(xcs);
43616
43617
  for (let col = target.right; col >= target.left; col--) {
43617
- this.computeNewCell(generator, col, row, apply);
43618
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43618
43619
  }
43619
43620
  }
43620
43621
  break;
@@ -43626,12 +43627,26 @@ class AutofillPlugin extends UIPlugin {
43626
43627
  }
43627
43628
  const generator = this.createGenerator(xcs);
43628
43629
  for (let col = target.left; col <= target.right; col++) {
43629
- this.computeNewCell(generator, col, row, apply);
43630
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
43630
43631
  }
43631
43632
  }
43632
43633
  break;
43633
43634
  }
43634
43635
  if (apply) {
43636
+ const bordersZones = {};
43637
+ const cfNewRanges = {};
43638
+ const dvNewZones = {};
43639
+ const sheetId = this.getters.getActiveSheetId();
43640
+ for (const data of autofillCellsData) {
43641
+ this.collectBordersData(data, bordersZones);
43642
+ this.autofillMerge(sheetId, data);
43643
+ this.autofillCell(sheetId, data);
43644
+ this.collectConditionalFormatsData(sheetId, data, cfNewRanges);
43645
+ this.collectDataValidationsData(sheetId, data, dvNewZones);
43646
+ }
43647
+ this.autofillBorders(sheetId, bordersZones);
43648
+ this.autofillConditionalFormats(sheetId, cfNewRanges);
43649
+ this.autofillDataValidations(sheetId, dvNewZones);
43635
43650
  this.autofillZone = undefined;
43636
43651
  this.selection.resizeAnchorZone(this.direction, this.steps);
43637
43652
  this.lastCellSelected = {};
@@ -43640,6 +43655,95 @@ class AutofillPlugin extends UIPlugin {
43640
43655
  this.tooltip = undefined;
43641
43656
  }
43642
43657
  }
43658
+ collectBordersData(data, bordersPositions) {
43659
+ const key = JSON.stringify(data.border);
43660
+ if (!(key in bordersPositions)) {
43661
+ bordersPositions[key] = [];
43662
+ }
43663
+ bordersPositions[key].push(positionToZone({ col: data.col, row: data.row }));
43664
+ }
43665
+ collectConditionalFormatsData(sheetId, data, cfNewRanges) {
43666
+ const { originCol, originRow, col, row } = data;
43667
+ const cfsAtOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
43668
+ const xc = toXC(col, row);
43669
+ for (const cf of cfsAtOrigin) {
43670
+ if (!(cf.id in cfNewRanges)) {
43671
+ cfNewRanges[cf.id] = [];
43672
+ }
43673
+ cfNewRanges[cf.id].push(xc);
43674
+ }
43675
+ }
43676
+ collectDataValidationsData(sheetId, data, dvNewZones) {
43677
+ const { originCol, originRow, col, row } = data;
43678
+ const cellPosition = { sheetId, col: originCol, row: originRow };
43679
+ const dvsAtOrigin = this.getters.getValidationRuleForCell(cellPosition);
43680
+ if (!dvsAtOrigin) {
43681
+ return;
43682
+ }
43683
+ if (!(dvsAtOrigin.id in dvNewZones)) {
43684
+ dvNewZones[dvsAtOrigin.id] = [];
43685
+ }
43686
+ dvNewZones[dvsAtOrigin.id].push(positionToZone({ col, row }));
43687
+ }
43688
+ autofillCell(sheetId, data) {
43689
+ this.dispatch("UPDATE_CELL", {
43690
+ sheetId,
43691
+ col: data.col,
43692
+ row: data.row,
43693
+ content: data.content || "",
43694
+ style: data.style || null,
43695
+ format: data.format || "",
43696
+ });
43697
+ // Still usefull in odoo ATM to autofill field sync
43698
+ this.dispatch("AUTOFILL_CELL", data);
43699
+ }
43700
+ autofillBorders(sheetId, bordersPositions) {
43701
+ for (const stringifiedBorder in bordersPositions) {
43702
+ const border = stringifiedBorder === "undefined" ? undefined : JSON.parse(stringifiedBorder);
43703
+ this.dispatch("SET_BORDERS_ON_TARGET", {
43704
+ sheetId,
43705
+ border,
43706
+ target: futureRecomputeZones(bordersPositions[stringifiedBorder]),
43707
+ });
43708
+ }
43709
+ }
43710
+ autofillConditionalFormats(sheetId, cfNewRanges) {
43711
+ for (const cfId in cfNewRanges) {
43712
+ const changes = cfNewRanges[cfId];
43713
+ const cf = this.getters.getConditionalFormats(sheetId).find((cf) => cf.id === cfId);
43714
+ if (!cf) {
43715
+ continue;
43716
+ }
43717
+ const newCfZones = this.getters.getAdaptedCfRanges(sheetId, cf, changes.map(toZone), []);
43718
+ if (newCfZones) {
43719
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
43720
+ cf: {
43721
+ id: cf.id,
43722
+ rule: cf.rule,
43723
+ stopIfTrue: cf.stopIfTrue,
43724
+ },
43725
+ ranges: newCfZones.map((xc) => this.getters.getRangeDataFromZone(sheetId, xc)),
43726
+ sheetId,
43727
+ });
43728
+ }
43729
+ }
43730
+ }
43731
+ autofillDataValidations(sheetId, dvNewZones) {
43732
+ for (const dvId in dvNewZones) {
43733
+ const changes = dvNewZones[dvId];
43734
+ const dvOrigin = this.getters.getDataValidationRule(sheetId, dvId);
43735
+ if (!dvOrigin) {
43736
+ continue;
43737
+ }
43738
+ const dvRangesXcs = dvOrigin.ranges.map((range) => range.zone);
43739
+ const newDvRanges = futureRecomputeZones(dvRangesXcs.concat(changes), []);
43740
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
43741
+ rule: dvOrigin,
43742
+ ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
43743
+ sheetId,
43744
+ });
43745
+ }
43746
+ }
43643
43747
  /**
43644
43748
  * Select a cell which becomes the last cell of the autofillZone
43645
43749
  */
@@ -43706,22 +43810,20 @@ class AutofillPlugin extends UIPlugin {
43706
43810
  /**
43707
43811
  * Generate the next cell
43708
43812
  */
43709
- computeNewCell(generator, col, row, apply) {
43813
+ computeNewCell(generator, col, row) {
43710
43814
  const { cellData, tooltip, origin } = generator.next();
43711
43815
  const { content, style, border, format } = cellData;
43712
43816
  this.tooltip = tooltip;
43713
- if (apply) {
43714
- this.dispatch("AUTOFILL_CELL", {
43715
- originCol: origin.col,
43716
- originRow: origin.row,
43717
- col,
43718
- row,
43719
- content,
43720
- style,
43721
- border,
43722
- format,
43723
- });
43724
- }
43817
+ return {
43818
+ originCol: origin.col,
43819
+ originRow: origin.row,
43820
+ col,
43821
+ row,
43822
+ content,
43823
+ style,
43824
+ border,
43825
+ format,
43826
+ };
43725
43827
  }
43726
43828
  /**
43727
43829
  * Get the rule associated to the current cell
@@ -43789,8 +43891,8 @@ class AutofillPlugin extends UIPlugin {
43789
43891
  ? position[first].value
43790
43892
  : position[second].value;
43791
43893
  }
43792
- autoFillMerge(originCol, originRow, col, row) {
43793
- const sheetId = this.getters.getActiveSheetId();
43894
+ autofillMerge(sheetId, data) {
43895
+ const { originCol, originRow, col, row } = data;
43794
43896
  const position = { sheetId, col, row };
43795
43897
  const originPosition = { sheetId, col: originCol, row: originRow };
43796
43898
  if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {
@@ -43817,35 +43919,6 @@ class AutofillPlugin extends UIPlugin {
43817
43919
  });
43818
43920
  }
43819
43921
  }
43820
- autofillCF(originCol, originRow, col, row) {
43821
- const sheetId = this.getters.getActiveSheetId();
43822
- const cfOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
43823
- for (const cf of cfOrigin) {
43824
- const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, [toXC(col, row)], []);
43825
- if (newCfRanges) {
43826
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
43827
- cf: deepCopy(cf),
43828
- ranges: newCfRanges.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
43829
- sheetId,
43830
- });
43831
- }
43832
- }
43833
- }
43834
- autofillDV(originCol, originRow, col, row) {
43835
- const sheetId = this.getters.getActiveSheetId();
43836
- const cellPosition = { sheetId, col: originCol, row: originRow };
43837
- const dvOrigin = this.getters.getValidationRuleForCell(cellPosition);
43838
- if (!dvOrigin) {
43839
- return;
43840
- }
43841
- const dvRangesXcs = dvOrigin.ranges.map((range) => this.getters.getRangeString(range, sheetId));
43842
- const newDvRanges = recomputeZones(dvRangesXcs.concat(toXC(col, row)), []);
43843
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
43844
- rule: dvOrigin,
43845
- ranges: newDvRanges.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
43846
- sheetId,
43847
- });
43848
- }
43849
43922
  // ---------------------------------------------------------------------------
43850
43923
  // Grid rendering
43851
43924
  // ---------------------------------------------------------------------------
@@ -44343,10 +44416,8 @@ function mergeTransformation(toTransform, executed) {
44343
44416
  }
44344
44417
  const target = [];
44345
44418
  for (const zone1 of toTransform.target) {
44346
- for (const zone2 of executed.target) {
44347
- if (!overlap(zone1, zone2)) {
44348
- target.push({ ...zone1 });
44349
- }
44419
+ if (executed.target.every((zone2) => !overlap(zone1, zone2))) {
44420
+ target.push(zone1);
44350
44421
  }
44351
44422
  }
44352
44423
  if (target.length) {
@@ -45232,6 +45303,9 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45232
45303
  copiedTables;
45233
45304
  zones;
45234
45305
  uuidGenerator = new UuidGenerator();
45306
+ queuedBordersToAdd = {};
45307
+ queuedCFChanges = {};
45308
+ queuedDVChanges = {};
45235
45309
  constructor(zones, operation, getters, dispatch, selection) {
45236
45310
  super(operation, getters, dispatch, selection);
45237
45311
  if (!zones.length) {
@@ -45341,6 +45415,8 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45341
45415
  * Paste the clipboard content in the given target
45342
45416
  */
45343
45417
  paste(target, options) {
45418
+ this.queuedCFChanges = {};
45419
+ this.queuedDVChanges = {};
45344
45420
  if (this.operation === "COPY") {
45345
45421
  this.pasteFromCopy(target, options);
45346
45422
  }
@@ -45353,6 +45429,7 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45353
45429
  if (options?.selectTarget) {
45354
45430
  this.selectPastedZone(width, height, isCutOperation, target);
45355
45431
  }
45432
+ this.executeQueuedChanges(this.getters.getActiveSheetId());
45356
45433
  }
45357
45434
  pasteFromCopy(target, options) {
45358
45435
  if (target.length === 1) {
@@ -45527,7 +45604,11 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45527
45604
  left: targetBorders?.left || originBorders?.left,
45528
45605
  right: targetBorders?.right || originBorders?.right,
45529
45606
  };
45530
- this.dispatch("SET_BORDER", { sheetId, col, row, border });
45607
+ const borderKey = JSON.stringify(border);
45608
+ if (!this.queuedBordersToAdd[borderKey]) {
45609
+ this.queuedBordersToAdd[borderKey] = [];
45610
+ }
45611
+ this.queuedBordersToAdd[borderKey].push(positionToZone(target));
45531
45612
  if (clipboardOption?.pasteOption === "onlyFormat") {
45532
45613
  this.dispatch("UPDATE_CELL", {
45533
45614
  ...target,
@@ -45673,7 +45754,8 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45673
45754
  }
45674
45755
  }
45675
45756
  pasteCf(origin, target) {
45676
- const xc = toXC(target.col, target.row);
45757
+ const zone = positionToZone(target);
45758
+ const originZone = positionToZone(origin);
45677
45759
  for (let rule of this.getters.getConditionalFormats(origin.sheetId)) {
45678
45760
  for (let range of rule.ranges) {
45679
45761
  if (isInside(origin.col, origin.row, this.getters.getRangeFromSheetXC(origin.sheetId, range).zone)) {
@@ -45681,15 +45763,15 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45681
45763
  const toRemoveRange = [];
45682
45764
  if (this.operation === "CUT") {
45683
45765
  //remove from current rule
45684
- toRemoveRange.push(toXC(origin.col, origin.row));
45766
+ toRemoveRange.push(originZone);
45685
45767
  }
45686
45768
  if (origin.sheetId === target.sheetId) {
45687
- this.adaptCFRules(origin.sheetId, cf, [xc], toRemoveRange);
45769
+ this.adaptCFRules(origin.sheetId, cf, [zone], toRemoveRange);
45688
45770
  }
45689
45771
  else {
45690
45772
  this.adaptCFRules(origin.sheetId, cf, [], toRemoveRange);
45691
45773
  const cfToCopyTo = this.getCFToCopyTo(target.sheetId, cf);
45692
- this.adaptCFRules(target.sheetId, cfToCopyTo, [xc], []);
45774
+ this.adaptCFRules(target.sheetId, cfToCopyTo, [zone], []);
45693
45775
  }
45694
45776
  }
45695
45777
  }
@@ -45699,79 +45781,130 @@ class ClipboardCellsState extends ClipboardCellsAbstractState {
45699
45781
  * Add or remove cells to a given conditional formatting rule.
45700
45782
  */
45701
45783
  adaptCFRules(sheetId, cf, toAdd, toRemove) {
45702
- const newRangesXC = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
45703
- if (!newRangesXC) {
45704
- return;
45784
+ if (!this.queuedCFChanges[sheetId]) {
45785
+ this.queuedCFChanges[sheetId] = [];
45705
45786
  }
45706
- if (newRangesXC.length === 0) {
45707
- this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
45708
- return;
45787
+ const queuedChange = this.queuedCFChanges[sheetId].find((queued) => queued.cf.id === cf.id);
45788
+ if (!queuedChange) {
45789
+ this.queuedCFChanges[sheetId].push({ toAdd, toRemove, cf });
45790
+ }
45791
+ else {
45792
+ queuedChange.toAdd.push(...toAdd);
45793
+ queuedChange.toRemove.push(...toRemove);
45709
45794
  }
45710
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
45711
- cf: {
45712
- id: cf.id,
45713
- rule: cf.rule,
45714
- stopIfTrue: cf.stopIfTrue,
45715
- },
45716
- ranges: newRangesXC.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
45717
- sheetId,
45718
- });
45719
45795
  }
45720
45796
  getCFToCopyTo(targetSheetId, originCF) {
45721
- const cfInTarget = this.getters
45797
+ let targetCF = this.getters
45722
45798
  .getConditionalFormats(targetSheetId)
45723
45799
  .find((cf) => cf.stopIfTrue === originCF.stopIfTrue && deepEquals(cf.rule, originCF.rule));
45724
- return cfInTarget
45725
- ? cfInTarget
45726
- : { ...originCF, id: this.uuidGenerator.smallUuid(), ranges: [] };
45800
+ const queuedCfs = this.queuedCFChanges[targetSheetId];
45801
+ if (!targetCF && queuedCfs) {
45802
+ targetCF = queuedCfs.find((queued) => queued.cf.stopIfTrue === originCF.stopIfTrue && deepEquals(queued.cf.rule, originCF.rule))?.cf;
45803
+ }
45804
+ return targetCF ? targetCF : { ...originCF, id: this.uuidGenerator.smallUuid(), ranges: [] };
45727
45805
  }
45728
45806
  pasteDataValidation(origin, target) {
45729
45807
  const rule = this.getters.getValidationRuleForCell(origin);
45808
+ const originZone = positionToZone(origin);
45730
45809
  if (!rule) {
45731
45810
  return;
45732
45811
  }
45733
- const xc = toXC(target.col, target.row);
45812
+ const zone = positionToZone(target);
45734
45813
  for (const range of rule.ranges) {
45735
45814
  if (isInside(origin.col, origin.row, range.zone)) {
45736
45815
  const toRemoveRange = [];
45737
45816
  if (this.operation === "CUT") {
45738
- toRemoveRange.push(toXC(origin.col, origin.row));
45817
+ toRemoveRange.push(originZone);
45739
45818
  }
45740
45819
  if (origin.sheetId === target.sheetId) {
45741
- this.adaptDataValidationRule(origin.sheetId, rule, [xc], toRemoveRange);
45820
+ const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
45821
+ this.adaptDataValidationRule(origin.sheetId, copyToRule, [zone], toRemoveRange);
45742
45822
  }
45743
45823
  else {
45744
- this.adaptDataValidationRule(origin.sheetId, rule, [], toRemoveRange);
45745
- let copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
45746
- this.adaptDataValidationRule(target.sheetId, copyToRule, [xc], []);
45824
+ const originRule = this.getters.getValidationRuleForCell(origin);
45825
+ if (originRule) {
45826
+ this.adaptDataValidationRule(origin.sheetId, originRule, [], toRemoveRange);
45827
+ }
45828
+ const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
45829
+ this.adaptDataValidationRule(target.sheetId, copyToRule, [zone], []);
45747
45830
  }
45748
45831
  }
45749
45832
  }
45750
45833
  }
45751
45834
  getDataValidationRuleToCopyTo(targetSheetId, originRule) {
45752
- const ruleInTargetSheet = this.getters
45835
+ let targetRule = this.getters
45753
45836
  .getDataValidationRules(targetSheetId)
45754
45837
  .find((rule) => deepEquals(originRule.criterion, rule.criterion) &&
45755
45838
  originRule.isBlocking === rule.isBlocking);
45756
- return ruleInTargetSheet
45757
- ? ruleInTargetSheet
45758
- : { ...originRule, id: this.uuidGenerator.smallUuid(), ranges: [] };
45839
+ const queuedRules = this.queuedDVChanges[targetSheetId];
45840
+ if (!targetRule && queuedRules) {
45841
+ targetRule = queuedRules.find((queued) => deepEquals(originRule.criterion, queued.rule.criterion) &&
45842
+ originRule.isBlocking === queued.rule.isBlocking)?.rule;
45843
+ }
45844
+ return targetRule || { ...originRule, id: this.uuidGenerator.smallUuid(), ranges: [] };
45759
45845
  }
45760
45846
  /**
45761
45847
  * Add or remove XCs to a given data validation rule.
45762
45848
  */
45763
45849
  adaptDataValidationRule(sheetId, rule, toAdd, toRemove) {
45764
- const dvRangesXcs = rule.ranges.map((range) => this.getters.getRangeString(range, sheetId));
45765
- const newRangesXC = recomputeZones([...dvRangesXcs, ...toAdd], toRemove);
45766
- if (newRangesXC.length === 0) {
45767
- this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: rule.id });
45768
- return;
45850
+ if (!this.queuedDVChanges[sheetId]) {
45851
+ this.queuedDVChanges[sheetId] = [];
45769
45852
  }
45770
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
45771
- rule,
45772
- ranges: newRangesXC.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
45773
- sheetId,
45774
- });
45853
+ const queuedChange = this.queuedDVChanges[sheetId].find((queued) => queued.rule.id === rule.id);
45854
+ if (!queuedChange) {
45855
+ this.queuedDVChanges[sheetId].push({ toAdd, toRemove, rule: deepCopy(rule) });
45856
+ }
45857
+ else {
45858
+ queuedChange.toAdd.push(...toAdd);
45859
+ queuedChange.toRemove.push(...toRemove);
45860
+ }
45861
+ }
45862
+ executeQueuedChanges(pasteSheetTarget) {
45863
+ for (const sheetId in this.queuedCFChanges) {
45864
+ for (const { toAdd, toRemove, cf } of this.queuedCFChanges[sheetId]) {
45865
+ const newRangesXc = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
45866
+ if (!newRangesXc) {
45867
+ continue;
45868
+ }
45869
+ if (newRangesXc.length === 0) {
45870
+ this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
45871
+ continue;
45872
+ }
45873
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
45874
+ cf: {
45875
+ id: cf.id,
45876
+ rule: cf.rule,
45877
+ stopIfTrue: cf.stopIfTrue,
45878
+ },
45879
+ ranges: newRangesXc.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
45880
+ sheetId,
45881
+ });
45882
+ }
45883
+ }
45884
+ for (const sheetId in this.queuedDVChanges) {
45885
+ for (const { toAdd, toRemove, rule: dv } of this.queuedDVChanges[sheetId]) {
45886
+ // Remove the zones first in case the same position is in toAdd and toRemove
45887
+ const dvZones = dv.ranges.map((range) => range.zone);
45888
+ const withRemovedZones = futureRecomputeZones(dvZones, toRemove);
45889
+ const newDvZones = futureRecomputeZones([...withRemovedZones, ...toAdd], []);
45890
+ if (newDvZones.length === 0) {
45891
+ this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: dv.id });
45892
+ continue;
45893
+ }
45894
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
45895
+ rule: { id: dv.id, criterion: dv.criterion, isBlocking: dv.isBlocking },
45896
+ ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
45897
+ sheetId,
45898
+ });
45899
+ }
45900
+ }
45901
+ for (const borderKey in this.queuedBordersToAdd) {
45902
+ const zones = this.queuedBordersToAdd[borderKey];
45903
+ const border = JSON.parse(borderKey);
45904
+ const target = futureRecomputeZones(zones, []);
45905
+ this.dispatch("SET_BORDERS_ON_TARGET", { sheetId: pasteSheetTarget, target, border });
45906
+ }
45907
+ this.queuedBordersToAdd = {};
45775
45908
  }
45776
45909
  }
45777
45910
 
@@ -58084,6 +58217,6 @@ const constants = {
58084
58217
  export { AbstractChart, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
58085
58218
 
58086
58219
 
58087
- __info__.version = "17.0.56";
58088
- __info__.date = "2025-04-04T08:36:58.258Z";
58089
- __info__.hash = "c484787";
58220
+ __info__.version = "17.0.58";
58221
+ __info__.date = "2025-04-18T16:20:26.099Z";
58222
+ __info__.hash = "88d92de";