@odoo/o-spreadsheet 16.4.0-alpha.0 → 16.4.0-alpha.2

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.
@@ -1113,6 +1113,21 @@ function isConsecutive(iterable) {
1113
1113
  }
1114
1114
  }
1115
1115
  return true;
1116
+ }
1117
+ class JetSet extends Set {
1118
+ add(...iterable) {
1119
+ for (const element of iterable) {
1120
+ super.add(element);
1121
+ }
1122
+ return this;
1123
+ }
1124
+ delete(...iterable) {
1125
+ let deleted = false;
1126
+ for (const element of iterable) {
1127
+ deleted ||= super.delete(element);
1128
+ }
1129
+ return deleted;
1130
+ }
1116
1131
  }
1117
1132
 
1118
1133
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
@@ -2199,249 +2214,6 @@ function splitReference(ref) {
2199
2214
  return { sheetName, xc };
2200
2215
  }
2201
2216
 
2202
- class RangeImpl {
2203
- getSheetSize;
2204
- _zone;
2205
- parts;
2206
- invalidXc;
2207
- prefixSheet = false;
2208
- sheetId; // the sheet on which the range is defined
2209
- invalidSheetName; // the name of any sheet that is invalid
2210
- constructor(args, getSheetSize) {
2211
- this.getSheetSize = getSheetSize;
2212
- this._zone = args.zone;
2213
- this.parts = args.parts;
2214
- this.prefixSheet = args.prefixSheet;
2215
- this.invalidXc = args.invalidXc;
2216
- this.sheetId = args.sheetId;
2217
- this.invalidSheetName = args.invalidSheetName;
2218
- }
2219
- static fromRange(range, getters) {
2220
- if (range instanceof RangeImpl) {
2221
- return range;
2222
- }
2223
- return new RangeImpl(range, getters.getSheetSize);
2224
- }
2225
- get unboundedZone() {
2226
- return this._zone;
2227
- }
2228
- get zone() {
2229
- const { left, top, bottom, right } = this._zone;
2230
- if (right !== undefined && bottom !== undefined)
2231
- return { left, top, right, bottom };
2232
- else if (bottom === undefined && right !== undefined) {
2233
- return { right, top, left, bottom: this.getSheetSize(this.sheetId).numberOfRows - 1 };
2234
- }
2235
- else if (right === undefined && bottom !== undefined) {
2236
- return { bottom, left, top, right: this.getSheetSize(this.sheetId).numberOfCols - 1 };
2237
- }
2238
- throw new Error(_lt("Bad zone format"));
2239
- }
2240
- static getRangeParts(xc, zone) {
2241
- const parts = xc.split(":").map((p) => {
2242
- const isFullRow = isRowReference(p);
2243
- return {
2244
- colFixed: isFullRow ? false : p.startsWith("$"),
2245
- rowFixed: isFullRow ? p.startsWith("$") : p.includes("$", 1),
2246
- };
2247
- });
2248
- const isFullCol = zone.bottom === undefined;
2249
- const isFullRow = zone.right === undefined;
2250
- if (isFullCol) {
2251
- parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2252
- parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2253
- if (zone.left === zone.right) {
2254
- parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
2255
- parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
2256
- }
2257
- }
2258
- if (isFullRow) {
2259
- parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
2260
- parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
2261
- if (zone.top === zone.bottom) {
2262
- parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2263
- parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2264
- }
2265
- }
2266
- return parts;
2267
- }
2268
- get isFullCol() {
2269
- return this._zone.bottom === undefined;
2270
- }
2271
- get isFullRow() {
2272
- return this._zone.right === undefined;
2273
- }
2274
- get rangeData() {
2275
- return {
2276
- _zone: this._zone,
2277
- _sheetId: this.sheetId,
2278
- };
2279
- }
2280
- /**
2281
- * Check that a zone is valid regarding the order of top-bottom and left-right.
2282
- * Left should be smaller than right, top should be smaller than bottom.
2283
- * If it's not the case, simply invert them, and invert the linked parts
2284
- */
2285
- orderZone() {
2286
- const zone = { ...this._zone };
2287
- let parts = this.parts;
2288
- if (zone.right !== undefined && zone.right < zone.left) {
2289
- let right = zone.right;
2290
- zone.right = zone.left;
2291
- zone.left = right;
2292
- parts = [
2293
- {
2294
- colFixed: parts[1]?.colFixed || false,
2295
- rowFixed: parts[0]?.rowFixed || false,
2296
- },
2297
- {
2298
- colFixed: parts[0]?.colFixed || false,
2299
- rowFixed: parts[1]?.rowFixed || false,
2300
- },
2301
- ];
2302
- }
2303
- if (zone.bottom !== undefined && zone.bottom < zone.top) {
2304
- let bottom = zone.bottom;
2305
- zone.bottom = zone.top;
2306
- zone.top = bottom;
2307
- parts = [
2308
- {
2309
- colFixed: parts[0]?.colFixed || false,
2310
- rowFixed: parts[1]?.rowFixed || false,
2311
- },
2312
- {
2313
- colFixed: parts[1]?.colFixed || false,
2314
- rowFixed: parts[0]?.rowFixed || false,
2315
- },
2316
- ];
2317
- }
2318
- return this.clone({ zone, parts });
2319
- }
2320
- /**
2321
- *
2322
- * @param rangeParams optional, values to put in the cloned range instead of the current values of the range
2323
- */
2324
- clone(rangeParams) {
2325
- return new RangeImpl({
2326
- zone: rangeParams?.zone ? rangeParams.zone : { ...this._zone },
2327
- sheetId: rangeParams?.sheetId ? rangeParams.sheetId : this.sheetId,
2328
- invalidSheetName: rangeParams && "invalidSheetName" in rangeParams // 'attr in obj' instead of just 'obj.attr' because we accept undefined values
2329
- ? rangeParams.invalidSheetName
2330
- : this.invalidSheetName,
2331
- invalidXc: rangeParams && "invalidXc" in rangeParams ? rangeParams.invalidXc : this.invalidXc,
2332
- parts: rangeParams?.parts
2333
- ? rangeParams.parts
2334
- : this.parts.map((part) => {
2335
- return { rowFixed: part.rowFixed, colFixed: part.colFixed };
2336
- }),
2337
- prefixSheet: rangeParams?.prefixSheet ? rangeParams.prefixSheet : this.prefixSheet,
2338
- }, this.getSheetSize);
2339
- }
2340
- }
2341
- /**
2342
- * Copy a range. If the range is on the sheetIdFrom, the range will target
2343
- * sheetIdTo.
2344
- */
2345
- function copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {
2346
- const sheetId = range.sheetId === sheetIdFrom ? sheetIdTo : range.sheetId;
2347
- return range.clone({ sheetId });
2348
- }
2349
- /**
2350
- * Create a range from a xc. If the xc is empty, this function returns undefined.
2351
- */
2352
- function createRange(getters, sheetId, range) {
2353
- return range ? getters.getRangeFromSheetXC(sheetId, range) : undefined;
2354
- }
2355
-
2356
- /** Methods from Odoo Web Utils */
2357
- /**
2358
- * This function computes a score that represent the fact that the
2359
- * string contains the pattern, or not
2360
- *
2361
- * - If the score is 0, the string does not contain the letters of the pattern in
2362
- * the correct order.
2363
- * - if the score is > 0, it actually contains the letters.
2364
- *
2365
- * Better matches will get a higher score: consecutive letters are better,
2366
- * and a match closer to the beginning of the string is also scored higher.
2367
- */
2368
- function fuzzyMatch(pattern, str) {
2369
- pattern = pattern.toLocaleLowerCase();
2370
- str = str.toLocaleLowerCase();
2371
- let totalScore = 0;
2372
- let currentScore = 0;
2373
- let len = str.length;
2374
- let patternIndex = 0;
2375
- for (let i = 0; i < len; i++) {
2376
- if (str[i] === pattern[patternIndex]) {
2377
- patternIndex++;
2378
- currentScore += 100 + currentScore - i / 200;
2379
- }
2380
- else {
2381
- currentScore = 0;
2382
- }
2383
- totalScore = totalScore + currentScore;
2384
- }
2385
- return patternIndex === pattern.length ? totalScore : 0;
2386
- }
2387
- /**
2388
- * Return a list of things that matches a pattern, ordered by their 'score' (
2389
- * higher score first). An higher score means that the match is better. For
2390
- * example, consecutive letters are considered a better match.
2391
- */
2392
- function fuzzyLookup(pattern, list, fn) {
2393
- const results = [];
2394
- list.forEach((data) => {
2395
- const score = fuzzyMatch(pattern, fn(data));
2396
- if (score > 0) {
2397
- results.push({ score, elem: data });
2398
- }
2399
- });
2400
- // we want better matches first
2401
- results.sort((a, b) => b.score - a.score);
2402
- return results.map((r) => r.elem);
2403
- }
2404
-
2405
- function createDefaultRows(rowNumber) {
2406
- const rows = [];
2407
- for (let i = 0; i < rowNumber; i++) {
2408
- const row = {
2409
- cells: {},
2410
- };
2411
- rows.push(row);
2412
- }
2413
- return rows;
2414
- }
2415
-
2416
- /*
2417
- * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
2418
- * */
2419
- class UuidGenerator {
2420
- isFastIdStrategy = false;
2421
- fastIdStart = 0;
2422
- setIsFastStrategy(isFast) {
2423
- this.isFastIdStrategy = isFast;
2424
- }
2425
- uuidv4() {
2426
- if (this.isFastIdStrategy) {
2427
- this.fastIdStart++;
2428
- return String(this.fastIdStart);
2429
- //@ts-ignore
2430
- }
2431
- else if (window.crypto && window.crypto.getRandomValues) {
2432
- //@ts-ignore
2433
- return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
2434
- }
2435
- else {
2436
- // mainly for jest and other browsers that do not have the crypto functionality
2437
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
2438
- var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8;
2439
- return v.toString(16);
2440
- });
2441
- }
2442
- }
2443
- }
2444
-
2445
2217
  /**
2446
2218
  * Convert from a cartesian reference to a Zone
2447
2219
  * The range boundaries will be kept in the same order as the
@@ -2883,6 +2655,14 @@ function positions(zone) {
2883
2655
  }
2884
2656
  return positions;
2885
2657
  }
2658
+ function forEachPositionsInZone(zone, callback) {
2659
+ const { left, right, top, bottom } = zone;
2660
+ for (let col = left; col <= right; col++) {
2661
+ for (let row = top; row <= bottom; row++) {
2662
+ callback(col, row);
2663
+ }
2664
+ }
2665
+ }
2886
2666
  /**
2887
2667
  * This function returns a zone with coordinates modified according to the change
2888
2668
  * applied to the zone. It may be possible to change the zone by resizing or moving
@@ -3040,6 +2820,287 @@ function getZonesRows(zones) {
3040
2820
  return set;
3041
2821
  }
3042
2822
 
2823
+ class RangeImpl {
2824
+ getSheetSize;
2825
+ _zone;
2826
+ parts;
2827
+ invalidXc;
2828
+ prefixSheet = false;
2829
+ sheetId; // the sheet on which the range is defined
2830
+ invalidSheetName; // the name of any sheet that is invalid
2831
+ constructor(args, getSheetSize) {
2832
+ this.getSheetSize = getSheetSize;
2833
+ this._zone = args.zone;
2834
+ this.parts = args.parts;
2835
+ this.prefixSheet = args.prefixSheet;
2836
+ this.invalidXc = args.invalidXc;
2837
+ this.sheetId = args.sheetId;
2838
+ this.invalidSheetName = args.invalidSheetName;
2839
+ }
2840
+ static fromRange(range, getters) {
2841
+ if (range instanceof RangeImpl) {
2842
+ return range;
2843
+ }
2844
+ return new RangeImpl(range, getters.getSheetSize);
2845
+ }
2846
+ get unboundedZone() {
2847
+ return this._zone;
2848
+ }
2849
+ get zone() {
2850
+ const { left, top, bottom, right } = this._zone;
2851
+ if (right !== undefined && bottom !== undefined)
2852
+ return { left, top, right, bottom };
2853
+ else if (bottom === undefined && right !== undefined) {
2854
+ return { right, top, left, bottom: this.getSheetSize(this.sheetId).numberOfRows - 1 };
2855
+ }
2856
+ else if (right === undefined && bottom !== undefined) {
2857
+ return { bottom, left, top, right: this.getSheetSize(this.sheetId).numberOfCols - 1 };
2858
+ }
2859
+ throw new Error(_lt("Bad zone format"));
2860
+ }
2861
+ static getRangeParts(xc, zone) {
2862
+ const parts = xc.split(":").map((p) => {
2863
+ const isFullRow = isRowReference(p);
2864
+ return {
2865
+ colFixed: isFullRow ? false : p.startsWith("$"),
2866
+ rowFixed: isFullRow ? p.startsWith("$") : p.includes("$", 1),
2867
+ };
2868
+ });
2869
+ const isFullCol = zone.bottom === undefined;
2870
+ const isFullRow = zone.right === undefined;
2871
+ if (isFullCol) {
2872
+ parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2873
+ parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2874
+ if (zone.left === zone.right) {
2875
+ parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
2876
+ parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
2877
+ }
2878
+ }
2879
+ if (isFullRow) {
2880
+ parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
2881
+ parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
2882
+ if (zone.top === zone.bottom) {
2883
+ parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2884
+ parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2885
+ }
2886
+ }
2887
+ return parts;
2888
+ }
2889
+ get isFullCol() {
2890
+ return this._zone.bottom === undefined;
2891
+ }
2892
+ get isFullRow() {
2893
+ return this._zone.right === undefined;
2894
+ }
2895
+ get rangeData() {
2896
+ return {
2897
+ _zone: this._zone,
2898
+ _sheetId: this.sheetId,
2899
+ };
2900
+ }
2901
+ /**
2902
+ * Check that a zone is valid regarding the order of top-bottom and left-right.
2903
+ * Left should be smaller than right, top should be smaller than bottom.
2904
+ * If it's not the case, simply invert them, and invert the linked parts
2905
+ */
2906
+ orderZone() {
2907
+ const zone = { ...this._zone };
2908
+ let parts = this.parts;
2909
+ if (zone.right !== undefined && zone.right < zone.left) {
2910
+ let right = zone.right;
2911
+ zone.right = zone.left;
2912
+ zone.left = right;
2913
+ parts = [
2914
+ {
2915
+ colFixed: parts[1]?.colFixed || false,
2916
+ rowFixed: parts[0]?.rowFixed || false,
2917
+ },
2918
+ {
2919
+ colFixed: parts[0]?.colFixed || false,
2920
+ rowFixed: parts[1]?.rowFixed || false,
2921
+ },
2922
+ ];
2923
+ }
2924
+ if (zone.bottom !== undefined && zone.bottom < zone.top) {
2925
+ let bottom = zone.bottom;
2926
+ zone.bottom = zone.top;
2927
+ zone.top = bottom;
2928
+ parts = [
2929
+ {
2930
+ colFixed: parts[0]?.colFixed || false,
2931
+ rowFixed: parts[1]?.rowFixed || false,
2932
+ },
2933
+ {
2934
+ colFixed: parts[1]?.colFixed || false,
2935
+ rowFixed: parts[0]?.rowFixed || false,
2936
+ },
2937
+ ];
2938
+ }
2939
+ return this.clone({ zone, parts });
2940
+ }
2941
+ /**
2942
+ *
2943
+ * @param rangeParams optional, values to put in the cloned range instead of the current values of the range
2944
+ */
2945
+ clone(rangeParams) {
2946
+ return new RangeImpl({
2947
+ zone: rangeParams?.zone ? rangeParams.zone : { ...this._zone },
2948
+ sheetId: rangeParams?.sheetId ? rangeParams.sheetId : this.sheetId,
2949
+ invalidSheetName: rangeParams && "invalidSheetName" in rangeParams // 'attr in obj' instead of just 'obj.attr' because we accept undefined values
2950
+ ? rangeParams.invalidSheetName
2951
+ : this.invalidSheetName,
2952
+ invalidXc: rangeParams && "invalidXc" in rangeParams ? rangeParams.invalidXc : this.invalidXc,
2953
+ parts: rangeParams?.parts
2954
+ ? rangeParams.parts
2955
+ : this.parts.map((part) => {
2956
+ return { rowFixed: part.rowFixed, colFixed: part.colFixed };
2957
+ }),
2958
+ prefixSheet: rangeParams?.prefixSheet ? rangeParams.prefixSheet : this.prefixSheet,
2959
+ }, this.getSheetSize);
2960
+ }
2961
+ }
2962
+ /**
2963
+ * Copy a range. If the range is on the sheetIdFrom, the range will target
2964
+ * sheetIdTo.
2965
+ */
2966
+ function copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {
2967
+ const sheetId = range.sheetId === sheetIdFrom ? sheetIdTo : range.sheetId;
2968
+ return range.clone({ sheetId });
2969
+ }
2970
+ /**
2971
+ * Create a range from a xc. If the xc is empty, this function returns undefined.
2972
+ */
2973
+ function createRange(getters, sheetId, range) {
2974
+ return range ? getters.getRangeFromSheetXC(sheetId, range) : undefined;
2975
+ }
2976
+ /**
2977
+ * Spread multiple colrows zone to one row/col zone and add a many new input range as needed.
2978
+ * For example, A1:B4 will become [A1:A4, B1:B4]
2979
+ */
2980
+ function spreadRange(ranges) {
2981
+ const postProcessedRanges = [];
2982
+ for (const range of ranges) {
2983
+ const { sheetName } = splitReference(range);
2984
+ const sheetPrefix = sheetName ? `${sheetName}!` : "";
2985
+ const zone = toUnboundedZone(range);
2986
+ if (zone.bottom !== zone.top && zone.left != zone.right) {
2987
+ if (zone.right) {
2988
+ for (let j = zone.left; j <= zone.right; ++j) {
2989
+ postProcessedRanges.push(`${sheetPrefix}${zoneToXc({
2990
+ left: j,
2991
+ right: j,
2992
+ top: zone.top,
2993
+ bottom: zone.bottom,
2994
+ })}`);
2995
+ }
2996
+ }
2997
+ else {
2998
+ for (let j = zone.top; j <= zone.bottom; ++j) {
2999
+ postProcessedRanges.push(`${sheetPrefix}${zoneToXc({
3000
+ left: zone.left,
3001
+ right: zone.right,
3002
+ top: j,
3003
+ bottom: j,
3004
+ })}`);
3005
+ }
3006
+ }
3007
+ }
3008
+ else {
3009
+ postProcessedRanges.push(range);
3010
+ }
3011
+ }
3012
+ return postProcessedRanges;
3013
+ }
3014
+
3015
+ /** Methods from Odoo Web Utils */
3016
+ /**
3017
+ * This function computes a score that represent the fact that the
3018
+ * string contains the pattern, or not
3019
+ *
3020
+ * - If the score is 0, the string does not contain the letters of the pattern in
3021
+ * the correct order.
3022
+ * - if the score is > 0, it actually contains the letters.
3023
+ *
3024
+ * Better matches will get a higher score: consecutive letters are better,
3025
+ * and a match closer to the beginning of the string is also scored higher.
3026
+ */
3027
+ function fuzzyMatch(pattern, str) {
3028
+ pattern = pattern.toLocaleLowerCase();
3029
+ str = str.toLocaleLowerCase();
3030
+ let totalScore = 0;
3031
+ let currentScore = 0;
3032
+ let len = str.length;
3033
+ let patternIndex = 0;
3034
+ for (let i = 0; i < len; i++) {
3035
+ if (str[i] === pattern[patternIndex]) {
3036
+ patternIndex++;
3037
+ currentScore += 100 + currentScore - i / 200;
3038
+ }
3039
+ else {
3040
+ currentScore = 0;
3041
+ }
3042
+ totalScore = totalScore + currentScore;
3043
+ }
3044
+ return patternIndex === pattern.length ? totalScore : 0;
3045
+ }
3046
+ /**
3047
+ * Return a list of things that matches a pattern, ordered by their 'score' (
3048
+ * higher score first). An higher score means that the match is better. For
3049
+ * example, consecutive letters are considered a better match.
3050
+ */
3051
+ function fuzzyLookup(pattern, list, fn) {
3052
+ const results = [];
3053
+ list.forEach((data) => {
3054
+ const score = fuzzyMatch(pattern, fn(data));
3055
+ if (score > 0) {
3056
+ results.push({ score, elem: data });
3057
+ }
3058
+ });
3059
+ // we want better matches first
3060
+ results.sort((a, b) => b.score - a.score);
3061
+ return results.map((r) => r.elem);
3062
+ }
3063
+
3064
+ function createDefaultRows(rowNumber) {
3065
+ const rows = [];
3066
+ for (let i = 0; i < rowNumber; i++) {
3067
+ const row = {
3068
+ cells: {},
3069
+ };
3070
+ rows.push(row);
3071
+ }
3072
+ return rows;
3073
+ }
3074
+
3075
+ /*
3076
+ * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
3077
+ * */
3078
+ class UuidGenerator {
3079
+ isFastIdStrategy = false;
3080
+ fastIdStart = 0;
3081
+ setIsFastStrategy(isFast) {
3082
+ this.isFastIdStrategy = isFast;
3083
+ }
3084
+ uuidv4() {
3085
+ if (this.isFastIdStrategy) {
3086
+ this.fastIdStart++;
3087
+ return String(this.fastIdStart);
3088
+ //@ts-ignore
3089
+ }
3090
+ else if (window.crypto && window.crypto.getRandomValues) {
3091
+ //@ts-ignore
3092
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
3093
+ }
3094
+ else {
3095
+ // mainly for jest and other browsers that do not have the crypto functionality
3096
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
3097
+ var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8;
3098
+ return v.toString(16);
3099
+ });
3100
+ }
3101
+ }
3102
+ }
3103
+
3043
3104
  class ChartJsComponent extends Component {
3044
3105
  static template = "o-spreadsheet-ChartJsComponent";
3045
3106
  canvas = useRef("graphContainer");
@@ -4183,6 +4244,11 @@ const invalidateEvaluationCommands = new Set([
4183
4244
  "INSERT_CELL",
4184
4245
  "UNDO",
4185
4246
  "REDO",
4247
+ "ADD_MERGE",
4248
+ ]);
4249
+ const invalidateDependenciesCommands = new Set([
4250
+ ...invalidateEvaluationCommands,
4251
+ "MOVE_RANGES",
4186
4252
  ]);
4187
4253
  const invalidateCFEvaluationCommands = new Set([
4188
4254
  ...invalidateEvaluationCommands,
@@ -4400,6 +4466,9 @@ var CommandResult;
4400
4466
  })(CommandResult || (CommandResult = {}));
4401
4467
 
4402
4468
  const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"];
4469
+ function isMatrix(x) {
4470
+ return Array.isArray(x) && Array.isArray(x[0]);
4471
+ }
4403
4472
  var DIRECTION;
4404
4473
  (function (DIRECTION) {
4405
4474
  DIRECTION["UP"] = "up";
@@ -10365,7 +10434,7 @@ const PRODUCT = {
10365
10434
  ],
10366
10435
  returns: ["NUMBER"],
10367
10436
  computeFormat: (factor1) => {
10368
- return Array.isArray(factor1) ? factor1[0][0]?.format : factor1?.format;
10437
+ return Array.isArray(factor1?.format) ? factor1.format[0][0] : factor1?.format;
10369
10438
  },
10370
10439
  compute: function (...factors) {
10371
10440
  let count = 0;
@@ -10591,7 +10660,7 @@ const SUM = {
10591
10660
  ],
10592
10661
  returns: ["NUMBER"],
10593
10662
  computeFormat: (value1) => {
10594
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
10663
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
10595
10664
  },
10596
10665
  compute: function (...values) {
10597
10666
  return reduceNumbers(values, (acc, a) => acc + a, 0);
@@ -10863,7 +10932,7 @@ const AVERAGE = {
10863
10932
  ],
10864
10933
  returns: ["NUMBER"],
10865
10934
  computeFormat: (value1) => {
10866
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
10935
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
10867
10936
  },
10868
10937
  compute: function (...values) {
10869
10938
  let count = 0;
@@ -10891,7 +10960,7 @@ const AVERAGE_WEIGHTED = {
10891
10960
  ],
10892
10961
  returns: ["NUMBER"],
10893
10962
  computeFormat: (values) => {
10894
- return Array.isArray(values) ? values[0][0]?.format : values?.format;
10963
+ return Array.isArray(values?.format) ? values.format[0][0] : values?.format;
10895
10964
  },
10896
10965
  compute: function (...values) {
10897
10966
  let sum = 0;
@@ -10949,7 +11018,7 @@ const AVERAGEA = {
10949
11018
  ],
10950
11019
  returns: ["NUMBER"],
10951
11020
  computeFormat: (value1) => {
10952
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
11021
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
10953
11022
  },
10954
11023
  compute: function (...values) {
10955
11024
  let count = 0;
@@ -11122,7 +11191,7 @@ const LARGE = {
11122
11191
  ],
11123
11192
  returns: ["NUMBER"],
11124
11193
  computeFormat: (data) => {
11125
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11194
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11126
11195
  },
11127
11196
  compute: function (data, n) {
11128
11197
  const _n = Math.trunc(toNumber(n));
@@ -11158,7 +11227,7 @@ const MAX = {
11158
11227
  ],
11159
11228
  returns: ["NUMBER"],
11160
11229
  computeFormat: (value1) => {
11161
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
11230
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
11162
11231
  },
11163
11232
  compute: function (...values) {
11164
11233
  const result = reduceNumbers(values, (acc, a) => (acc < a ? a : acc), -Infinity);
@@ -11177,7 +11246,7 @@ const MAXA = {
11177
11246
  ],
11178
11247
  returns: ["NUMBER"],
11179
11248
  computeFormat: (value1) => {
11180
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
11249
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
11181
11250
  },
11182
11251
  compute: function (...values) {
11183
11252
  const maxa = reduceNumbersTextAs0(values, (acc, a) => {
@@ -11223,7 +11292,7 @@ const MEDIAN = {
11223
11292
  ],
11224
11293
  returns: ["NUMBER"],
11225
11294
  computeFormat: (value1) => {
11226
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
11295
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
11227
11296
  },
11228
11297
  compute: function (...values) {
11229
11298
  let data = [];
@@ -11245,7 +11314,7 @@ const MIN = {
11245
11314
  ],
11246
11315
  returns: ["NUMBER"],
11247
11316
  computeFormat: (value1) => {
11248
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
11317
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
11249
11318
  },
11250
11319
  compute: function (...values) {
11251
11320
  const result = reduceNumbers(values, (acc, a) => (a < acc ? a : acc), Infinity);
@@ -11264,7 +11333,7 @@ const MINA = {
11264
11333
  ],
11265
11334
  returns: ["NUMBER"],
11266
11335
  computeFormat: (value1) => {
11267
- return Array.isArray(value1) ? value1[0][0]?.format : value1?.format;
11336
+ return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
11268
11337
  },
11269
11338
  compute: function (...values) {
11270
11339
  const mina = reduceNumbersTextAs0(values, (acc, a) => {
@@ -11310,7 +11379,7 @@ const PERCENTILE = {
11310
11379
  ],
11311
11380
  returns: ["NUMBER"],
11312
11381
  computeFormat: (data) => {
11313
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11382
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11314
11383
  },
11315
11384
  compute: function (data, percentile) {
11316
11385
  return PERCENTILE_INC.compute(data, percentile);
@@ -11328,7 +11397,7 @@ const PERCENTILE_EXC = {
11328
11397
  ],
11329
11398
  returns: ["NUMBER"],
11330
11399
  computeFormat: (data) => {
11331
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11400
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11332
11401
  },
11333
11402
  compute: function (data, percentile) {
11334
11403
  return centile([data], percentile, false);
@@ -11346,7 +11415,7 @@ const PERCENTILE_INC = {
11346
11415
  ],
11347
11416
  returns: ["NUMBER"],
11348
11417
  computeFormat: (data) => {
11349
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11418
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11350
11419
  },
11351
11420
  compute: function (data, percentile) {
11352
11421
  return centile([data], percentile, true);
@@ -11364,7 +11433,7 @@ const QUARTILE = {
11364
11433
  ],
11365
11434
  returns: ["NUMBER"],
11366
11435
  computeFormat: (data) => {
11367
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11436
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11368
11437
  },
11369
11438
  compute: function (data, quartileNumber) {
11370
11439
  return QUARTILE_INC.compute(data, quartileNumber);
@@ -11382,7 +11451,7 @@ const QUARTILE_EXC = {
11382
11451
  ],
11383
11452
  returns: ["NUMBER"],
11384
11453
  computeFormat: (data) => {
11385
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11454
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11386
11455
  },
11387
11456
  compute: function (data, quartileNumber) {
11388
11457
  const _quartileNumber = Math.trunc(toNumber(quartileNumber));
@@ -11401,7 +11470,7 @@ const QUARTILE_INC = {
11401
11470
  ],
11402
11471
  returns: ["NUMBER"],
11403
11472
  computeFormat: (data) => {
11404
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11473
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11405
11474
  },
11406
11475
  compute: function (data, quartileNumber) {
11407
11476
  const _quartileNumber = Math.trunc(toNumber(quartileNumber));
@@ -11420,7 +11489,7 @@ const SMALL = {
11420
11489
  ],
11421
11490
  returns: ["NUMBER"],
11422
11491
  computeFormat: (data) => {
11423
- return Array.isArray(data) ? data[0][0]?.format : data?.format;
11492
+ return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
11424
11493
  },
11425
11494
  compute: function (data, n) {
11426
11495
  const _n = Math.trunc(toNumber(n));
@@ -16207,17 +16276,11 @@ function extractArgValuesFromArgs(args) {
16207
16276
  return undefined;
16208
16277
  }
16209
16278
  if (typeof arg === "function") {
16210
- return () => _extractArgValuesFromArgs(arg());
16279
+ return () => arg()?.value;
16211
16280
  }
16212
- return _extractArgValuesFromArgs(arg);
16281
+ return arg.value;
16213
16282
  });
16214
16283
  }
16215
- function _extractArgValuesFromArgs(arg) {
16216
- if (Array.isArray(arg)) {
16217
- return arg.map((col) => col.map((simpleArg) => simpleArg?.value));
16218
- }
16219
- return arg?.value;
16220
- }
16221
16284
  const functionRegistry = new FunctionRegistry();
16222
16285
  for (let category of categories) {
16223
16286
  const fns = category.functions;
@@ -16869,6 +16932,7 @@ const unFreezePane = {
16869
16932
  execute: (env) => env.model.dispatch("UNFREEZE_COLUMNS_ROWS", {
16870
16933
  sheetId: env.model.getters.getActiveSheetId(),
16871
16934
  }),
16935
+ icon: "o-spreadsheet-Icon.UNFREEZE",
16872
16936
  };
16873
16937
  const freezePane = {
16874
16938
  name: _lt("Freeze"),
@@ -17616,50 +17680,51 @@ const uuidGenerator$1 = new UuidGenerator();
17616
17680
  css /* scss */ `
17617
17681
  .o-selection {
17618
17682
  .o-selection-input {
17619
- display: flex;
17620
- flex-direction: row;
17683
+ padding: 2px 0px;
17621
17684
 
17622
17685
  input {
17623
17686
  padding: 4px 6px;
17624
17687
  border-radius: 4px;
17625
17688
  box-sizing: border-box;
17626
- flex-grow: 2;
17627
17689
  }
17628
17690
  input:focus {
17629
17691
  outline: none;
17630
17692
  }
17631
17693
  input.o-required,
17632
17694
  input.o-focused {
17633
- border-width: 2px;
17634
- padding: 3px 5px;
17695
+ border: 1px solid;
17635
17696
  }
17636
17697
  input.o-focused {
17637
- border-color: ${SELECTION_BORDER_COLOR};
17698
+ border-width: 2px;
17699
+ padding: 3px 5px;
17638
17700
  }
17639
17701
  input.o-invalid {
17640
- border-color: red;
17702
+ /* The background-color is similar to the bootstrap alert-danger class but, because of the commit 0358a76d,
17703
+ * which avoids being parasitized by the dark-mode in spreadsheet, we cannot use this class.
17704
+ * TODO: Replace with the bootstrap alert-danger class when we support dark mode
17705
+ */
17706
+ background-color: #ffdddd;
17707
+ border-width: 2px;
17641
17708
  }
17642
17709
  button.o-btn {
17643
- background: transparent;
17644
- border: none;
17645
17710
  color: #333;
17646
- cursor: pointer;
17647
17711
  }
17648
17712
  button.o-btn-action {
17649
17713
  margin: 8px 1px;
17650
17714
  border-radius: 4px;
17651
- background: transparent;
17652
17715
  border: 1px solid #dadce0;
17653
17716
  color: #188038;
17654
- font-weight: bold;
17655
17717
  font-size: 14px;
17656
17718
  height: 25px;
17657
17719
  }
17720
+ .error-icon {
17721
+ right: 7px;
17722
+ top: 7px;
17723
+ }
17658
17724
  }
17659
17725
  /** Make the character a bit bigger
17660
17726
  compared to its neighbor INPUT box */
17661
17727
  .o-remove-selection {
17662
- font-weight: bold;
17663
17728
  font-size: calc(100% + 4px);
17664
17729
  }
17665
17730
  }
@@ -17675,7 +17740,7 @@ css /* scss */ `
17675
17740
  class SelectionInput extends Component {
17676
17741
  static template = "o-spreadsheet-SelectionInput";
17677
17742
  id = uuidGenerator$1.uuidv4();
17678
- previousRanges = this.props.ranges || [];
17743
+ previousRanges = this.props.ranges() || [];
17679
17744
  originSheet = this.env.model.getters.getActiveSheetId();
17680
17745
  state = useState({
17681
17746
  isMissing: false,
@@ -17683,11 +17748,11 @@ class SelectionInput extends Component {
17683
17748
  });
17684
17749
  focusedInput = useRef("focusedInput");
17685
17750
  get ranges() {
17686
- const existingSelectionRange = this.env.model.getters.getSelectionInput(this.id);
17687
- const ranges = existingSelectionRange.length
17688
- ? existingSelectionRange
17689
- : this.props.ranges
17690
- ? this.props.ranges.map((xc, id) => ({
17751
+ const existingSelectionRanges = this.env.model.getters.getSelectionInput(this.id);
17752
+ const ranges = existingSelectionRanges.length
17753
+ ? existingSelectionRanges
17754
+ : this.props.ranges()
17755
+ ? this.props.ranges().map((xc, id) => ({
17691
17756
  xc,
17692
17757
  id,
17693
17758
  isFocused: false,
@@ -17707,6 +17772,12 @@ class SelectionInput extends Component {
17707
17772
  get isInvalid() {
17708
17773
  return this.props.isInvalid || this.state.isMissing;
17709
17774
  }
17775
+ get isConfirmable() {
17776
+ return this.hasFocus && this.ranges.every((range) => range.isValidRange);
17777
+ }
17778
+ get isResettable() {
17779
+ return this.previousRanges.join() !== this.ranges.map((r) => r.xc).join();
17780
+ }
17710
17781
  setup() {
17711
17782
  useEffect(() => this.focusedInput.el?.focus(), () => [this.focusedInput.el]);
17712
17783
  onMounted(() => this.enableNewSelectionInput());
@@ -17716,7 +17787,7 @@ class SelectionInput extends Component {
17716
17787
  enableNewSelectionInput() {
17717
17788
  this.env.model.dispatch("ENABLE_NEW_SELECTION_INPUT", {
17718
17789
  id: this.id,
17719
- initialRanges: this.props.ranges,
17790
+ initialRanges: this.props.ranges(),
17720
17791
  hasSingleRange: this.props.hasSingleRange,
17721
17792
  });
17722
17793
  }
@@ -17725,7 +17796,8 @@ class SelectionInput extends Component {
17725
17796
  }
17726
17797
  checkChange() {
17727
17798
  const value = this.env.model.getters.getSelectionInputValue(this.id);
17728
- if (this.previousRanges.join() !== value.join()) {
17799
+ const valid = !this.isInvalid && this.ranges.every((range) => range.isValidRange);
17800
+ if (valid && this.previousRanges.join() !== value.join()) {
17729
17801
  this.triggerChange();
17730
17802
  }
17731
17803
  }
@@ -17736,7 +17808,6 @@ class SelectionInput extends Component {
17736
17808
  triggerChange() {
17737
17809
  const ranges = this.env.model.getters.getSelectionInputValue(this.id);
17738
17810
  this.props.onSelectionChanged?.(ranges);
17739
- this.previousRanges = ranges;
17740
17811
  }
17741
17812
  onKeydown(ev) {
17742
17813
  if (ev.key === "F2") {
@@ -17786,10 +17857,37 @@ class SelectionInput extends Component {
17786
17857
  });
17787
17858
  this.triggerChange();
17788
17859
  }
17860
+ reset() {
17861
+ const existingSelectionRanges = this.env.model.getters.getSelectionInput(this.id);
17862
+ for (const range of existingSelectionRanges) {
17863
+ this.env.model.dispatch("REMOVE_RANGE", {
17864
+ id: this.id,
17865
+ rangeId: range.id,
17866
+ });
17867
+ }
17868
+ for (const range of this.previousRanges) {
17869
+ this.env.model.dispatch("ADD_RANGE", { id: this.id, value: range });
17870
+ }
17871
+ this.confirm();
17872
+ }
17789
17873
  confirm() {
17790
- this.env.model.dispatch("UNFOCUS_SELECTION_INPUT");
17791
- const ranges = this.env.model.getters.getSelectionInputValue(this.id);
17792
- if (this.props.required && ranges.length === 0) {
17874
+ let anyValidInput = false;
17875
+ const existingSelectionRanges = this.env.model.getters.getSelectionInput(this.id);
17876
+ const existingSelectionXcs = [];
17877
+ for (const range of existingSelectionRanges) {
17878
+ if (range.xc === "") {
17879
+ this.env.model.dispatch("REMOVE_RANGE", {
17880
+ id: this.id,
17881
+ rangeId: range.id,
17882
+ });
17883
+ continue;
17884
+ }
17885
+ existingSelectionXcs.push(range.xc);
17886
+ if (this.env.model.getters.isRangeValid(range.xc)) {
17887
+ anyValidInput = true;
17888
+ }
17889
+ }
17890
+ if (this.props.required && !anyValidInput) {
17793
17891
  this.state.isMissing = true;
17794
17892
  }
17795
17893
  const activeSheetId = this.env.model.getters.getActiveSheetId();
@@ -17799,11 +17897,17 @@ class SelectionInput extends Component {
17799
17897
  sheetIdTo: this.originSheet,
17800
17898
  });
17801
17899
  }
17900
+ this.props.onSelectionChanged?.(existingSelectionXcs);
17802
17901
  this.props.onSelectionConfirmed?.();
17902
+ this.previousRanges = this.props.ranges();
17903
+ if (existingSelectionXcs.join() !== this.previousRanges.join()) {
17904
+ this.enableNewSelectionInput();
17905
+ }
17906
+ this.env.model.dispatch("UNFOCUS_SELECTION_INPUT");
17803
17907
  }
17804
17908
  }
17805
17909
  SelectionInput.props = {
17806
- ranges: Array,
17910
+ ranges: Function,
17807
17911
  hasSingleRange: { type: Boolean, optional: true },
17808
17912
  required: { type: Boolean, optional: true },
17809
17913
  isInvalid: { type: Boolean, optional: true },
@@ -17851,10 +17955,14 @@ class LineBarPieConfigPanel extends Component {
17851
17955
  this.dataSeriesRanges = ranges;
17852
17956
  }
17853
17957
  onDataSeriesConfirmed() {
17958
+ this.dataSeriesRanges = spreadRange(this.dataSeriesRanges);
17854
17959
  this.state.datasetDispatchResult = this.props.updateChart({
17855
17960
  dataSets: this.dataSeriesRanges,
17856
17961
  });
17857
17962
  }
17963
+ getDataSeriesRanges() {
17964
+ return this.dataSeriesRanges;
17965
+ }
17858
17966
  /**
17859
17967
  * Change the local labelRange. The model should be updated when the
17860
17968
  * button "confirm" is clicked
@@ -17867,6 +17975,9 @@ class LineBarPieConfigPanel extends Component {
17867
17975
  labelRange: this.labelRange,
17868
17976
  });
17869
17977
  }
17978
+ getLabelRange() {
17979
+ return this.labelRange || "";
17980
+ }
17870
17981
  onUpdateAggregated(ev) {
17871
17982
  this.props.updateChart({
17872
17983
  aggregated: ev.target.checked,
@@ -17940,6 +18051,18 @@ css /* scss */ `
17940
18051
  grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
17941
18052
  grid-gap: ${ITEM_HORIZONTAL_MARGIN * 2}px;
17942
18053
  }
18054
+ .o-color-picker-toggler-button {
18055
+ display: flex;
18056
+ .o-color-picker-toggler-sign {
18057
+ margin: auto auto;
18058
+ width: 55%;
18059
+ height: 55%;
18060
+ .o-icon {
18061
+ width: 100%;
18062
+ height: 100%;
18063
+ }
18064
+ }
18065
+ }
17943
18066
  .o-color-picker-line-item {
17944
18067
  width: ${ITEM_EDGE_LENGTH}px;
17945
18068
  height: ${ITEM_EDGE_LENGTH}px;
@@ -18275,6 +18398,9 @@ class GaugeChartConfigPanel extends Component {
18275
18398
  dataRange: this.dataRange,
18276
18399
  });
18277
18400
  }
18401
+ getDataRange() {
18402
+ return this.dataRange || "";
18403
+ }
18278
18404
  }
18279
18405
  GaugeChartConfigPanel.props = {
18280
18406
  figureId: String,
@@ -18473,6 +18599,9 @@ class ScorecardChartConfigPanel extends Component {
18473
18599
  keyValue: this.keyValue,
18474
18600
  });
18475
18601
  }
18602
+ getKeyValueRange() {
18603
+ return this.keyValue || "";
18604
+ }
18476
18605
  onBaselineRangeChanged(ranges) {
18477
18606
  this.baseline = ranges[0];
18478
18607
  }
@@ -18481,6 +18610,9 @@ class ScorecardChartConfigPanel extends Component {
18481
18610
  baseline: this.baseline,
18482
18611
  });
18483
18612
  }
18613
+ getBaselineRange() {
18614
+ return this.baseline || "";
18615
+ }
18484
18616
  updateBaselineMode(ev) {
18485
18617
  this.props.updateChart({ baselineMode: ev.target.value });
18486
18618
  }
@@ -20612,7 +20744,14 @@ class ContentEditableHelper {
20612
20744
  current = it.next();
20613
20745
  }
20614
20746
  if (current.value !== nodeToFind) {
20615
- throw new Error("Cannot find the node in the children of the element");
20747
+ /** This situation can happen if the code is called while the selection is not currently on the ContentEditableHelper.
20748
+ * In this case, we return 0 because we don't know the size of the text before the selection.
20749
+ *
20750
+ * A known occurence is triggered since the introduction of commit d4663158 (PR #2038).
20751
+ *
20752
+ * FIXME: find a way to test eventhough the selection API is not available in jsDOM.
20753
+ */
20754
+ return 0;
20616
20755
  }
20617
20756
  else {
20618
20757
  if (!current.value.hasChildNodes()) {
@@ -20737,7 +20876,7 @@ FunctionDescriptionProvider.props = {
20737
20876
  argToFocus: Number,
20738
20877
  };
20739
20878
 
20740
- const functions$2 = functionRegistry.content;
20879
+ const functions$3 = functionRegistry.content;
20741
20880
  const ASSISTANT_WIDTH = 300;
20742
20881
  const selectionIndicatorClass = "selector-flag";
20743
20882
  const selectionIndicatorColor = "#a9a9a9";
@@ -21239,7 +21378,7 @@ class Composer extends Component {
21239
21378
  // initialize Formula Assistant
21240
21379
  const tokenContext = tokenAtCursor.functionContext;
21241
21380
  const parentFunction = tokenContext.parent.toUpperCase();
21242
- const description = functions$2[parentFunction];
21381
+ const description = functions$3[parentFunction];
21243
21382
  const argPosition = tokenContext.argPosition;
21244
21383
  this.functionDescriptionState.functionName = parentFunction;
21245
21384
  this.functionDescriptionState.functionDescription = description;
@@ -22110,6 +22249,21 @@ FiguresContainer.props = {
22110
22249
  onFigureDeleted: Function,
22111
22250
  };
22112
22251
 
22252
+ /**
22253
+ * Manages an event listener on a ref. Useful for hooks that want to manage
22254
+ * event listeners, especially more than one. Prefer using t-on directly in
22255
+ * components. If your hook only needs a single event listener, consider simply
22256
+ * returning it from the hook and letting the user attach it with t-on.
22257
+ *
22258
+ * Adapted from Odoo Community - See https://github.com/odoo/odoo/blob/saas-16.2/addons/web/static/src/core/utils/hooks.js
22259
+ */
22260
+ function useRefListener(ref, ...listener) {
22261
+ useEffect((el) => {
22262
+ el?.addEventListener(...listener);
22263
+ return () => el?.removeEventListener(...listener);
22264
+ }, () => [ref.el]);
22265
+ }
22266
+
22113
22267
  /**
22114
22268
  * Repeatedly calls a callback function with a time delay between calls.
22115
22269
  */
@@ -22172,20 +22326,10 @@ function useCellHovered(env, gridRef, callback) {
22172
22326
  setPosition(undefined, undefined);
22173
22327
  }
22174
22328
  }
22175
- onMounted(() => {
22176
- const grid = gridRef.el;
22177
- grid.addEventListener("mousemove", updateMousePosition);
22178
- grid.addEventListener("mouseleave", pause);
22179
- grid.addEventListener("mouseenter", resume);
22180
- grid.addEventListener("mousedown", recompute);
22181
- });
22182
- onWillUnmount(() => {
22183
- const grid = gridRef.el;
22184
- grid.removeEventListener("mousemove", updateMousePosition);
22185
- grid.removeEventListener("mouseleave", pause);
22186
- grid.removeEventListener("mouseenter", resume);
22187
- grid.removeEventListener("mousedown", recompute);
22188
- });
22329
+ useRefListener(gridRef, "mousemove", updateMousePosition);
22330
+ useRefListener(gridRef, "mouseleave", pause);
22331
+ useRefListener(gridRef, "mouseenter", resume);
22332
+ useRefListener(gridRef, "mousedown", recompute);
22189
22333
  function setPosition(col, row) {
22190
22334
  if (col !== hoveredPosition.col || row !== hoveredPosition.row) {
22191
22335
  hoveredPosition.col = col;
@@ -22224,16 +22368,9 @@ function useTouchMove(gridRef, handler, canMoveUp) {
22224
22368
  x = currentX;
22225
22369
  y = currentY;
22226
22370
  }
22227
- onMounted(() => {
22228
- gridRef.el.addEventListener("touchstart", onTouchStart);
22229
- gridRef.el.addEventListener("touchend", onTouchEnd);
22230
- gridRef.el.addEventListener("touchmove", onTouchMove);
22231
- });
22232
- onWillUnmount(() => {
22233
- gridRef.el.removeEventListener("touchstart", onTouchStart);
22234
- gridRef.el.removeEventListener("touchend", onTouchEnd);
22235
- gridRef.el.removeEventListener("touchmove", onTouchMove);
22236
- });
22371
+ useRefListener(gridRef, "touchstart", onTouchStart);
22372
+ useRefListener(gridRef, "touchend", onTouchEnd);
22373
+ useRefListener(gridRef, "touchmove", onTouchMove);
22237
22374
  }
22238
22375
  class GridOverlay extends Component {
22239
22376
  static template = "o-spreadsheet-GridOverlay";
@@ -25084,7 +25221,7 @@ function indentCode(code) {
25084
25221
  * is useful for the composer, which needs to be able to work with incomplete
25085
25222
  * formulas.
25086
25223
  */
25087
- const functions$1 = functionRegistry.content;
25224
+ const functions$2 = functionRegistry.content;
25088
25225
  const POSTFIX_UNARY_OPERATORS = ["%"];
25089
25226
  const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
25090
25227
  function tokenize(str) {
@@ -25207,7 +25344,7 @@ function tokenizeSymbol(chars) {
25207
25344
  }
25208
25345
  if (result.length) {
25209
25346
  const value = result;
25210
- const isFunction = value.toUpperCase() in functions$1;
25347
+ const isFunction = value.toUpperCase() in functions$2;
25211
25348
  if (isFunction) {
25212
25349
  return { type: "FUNCTION", value };
25213
25350
  }
@@ -25677,7 +25814,7 @@ function rangeTokenize(formula) {
25677
25814
  return result;
25678
25815
  }
25679
25816
 
25680
- const functions = functionRegistry.content;
25817
+ const functions$1 = functionRegistry.content;
25681
25818
  const OPERATOR_MAP = {
25682
25819
  "=": "EQ",
25683
25820
  "+": "ADD",
@@ -25740,56 +25877,32 @@ function compile(formula) {
25740
25877
  * between a cell value and a non cell value.
25741
25878
  */
25742
25879
  function compileFunctionArgs(ast) {
25743
- const functionDefinition = functions[ast.value.toUpperCase()];
25744
- const currentFunctionArguments = ast.args;
25745
- // check if arguments are supplied in the correct quantities
25746
- const nbrArg = currentFunctionArguments.length;
25747
- if (nbrArg < functionDefinition.minArgRequired) {
25748
- throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", ast.value.toUpperCase(), functionDefinition.minArgRequired.toString(), nbrArg.toString()));
25749
- }
25750
- if (nbrArg > functionDefinition.maxArgPossible) {
25751
- throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", ast.value.toUpperCase(), functionDefinition.maxArgPossible.toString(), nbrArg.toString()));
25752
- }
25753
- const repeatingArg = functionDefinition.nbrArgRepeating;
25754
- if (repeatingArg > 1) {
25755
- const argBeforeRepeat = functionDefinition.args.length - repeatingArg;
25756
- const nbrRepeatingArg = nbrArg - argBeforeRepeat;
25757
- if (nbrRepeatingArg % repeatingArg !== 0) {
25758
- throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", ast.value.toUpperCase(), argBeforeRepeat.toString(), repeatingArg.toString()));
25759
- }
25760
- }
25761
- let compiledArgs = [];
25762
- for (let i = 0; i < nbrArg; i++) {
25763
- const argPosition = functionDefinition.getArgToFocus(i + 1) - 1;
25764
- if (0 <= argPosition && argPosition < functionDefinition.args.length) {
25765
- const currentArg = currentFunctionArguments[i];
25766
- const argDefinition = functionDefinition.args[argPosition];
25767
- const argTypes = argDefinition.type || [];
25768
- // detect when an argument need to be evaluated as a meta argument
25769
- const isMeta = argTypes.includes("META");
25770
- // detect when an argument need to be evaluated as a lazy argument
25771
- const isLazy = argDefinition.lazy;
25772
- const hasRange = argTypes.some((t) => t === "RANGE" ||
25773
- t === "RANGE<BOOLEAN>" ||
25774
- t === "RANGE<DATE>" ||
25775
- t === "RANGE<NUMBER>" ||
25776
- t === "RANGE<STRING>");
25777
- const isRangeOnly = argTypes.every((t) => t === "RANGE" ||
25778
- t === "RANGE<BOOLEAN>" ||
25779
- t === "RANGE<DATE>" ||
25780
- t === "RANGE<NUMBER>" ||
25781
- t === "RANGE<STRING>");
25782
- if (isRangeOnly) {
25783
- if (currentArg.type !== "REFERENCE") {
25784
- throw new BadExpressionError(_lt("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", ast.value.toUpperCase(), (i + 1).toString(), currentArg.type.toLowerCase()));
25785
- }
25880
+ const { args } = ast;
25881
+ const functionName = ast.value.toUpperCase();
25882
+ const functionDefinition = functions$1[functionName];
25883
+ assertEnoughArgs(ast);
25884
+ const compiledArgs = [];
25885
+ for (let i = 0; i < args.length; i++) {
25886
+ const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1;
25887
+ const argDefinition = functionDefinition.args[argToFocus];
25888
+ const currentArg = args[i];
25889
+ const argTypes = argDefinition.type || [];
25890
+ // detect when an argument need to be evaluated as a meta argument
25891
+ const isMeta = argTypes.includes("META");
25892
+ // detect when an argument need to be evaluated as a lazy argument
25893
+ const isLazy = argDefinition.lazy;
25894
+ const hasRange = argTypes.some((t) => isRangeType(t));
25895
+ const isRangeOnly = argTypes.every((t) => isRangeType(t));
25896
+ if (isRangeOnly) {
25897
+ if (!isRangeInput(currentArg)) {
25898
+ throw new BadExpressionError(_lt("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", functionName, (i + 1).toString(), currentArg.type.toLowerCase()));
25786
25899
  }
25787
- const compiledAST = compileAST(currentArg, isMeta, hasRange, {
25788
- functionName: ast.value.toUpperCase(),
25789
- paramIndex: i + 1,
25790
- });
25791
- compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST);
25792
25900
  }
25901
+ const compiledAST = compileAST(currentArg, isMeta, hasRange, {
25902
+ functionName,
25903
+ paramIndex: i + 1,
25904
+ });
25905
+ compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST);
25793
25906
  }
25794
25907
  return compiledArgs;
25795
25908
  }
@@ -25933,6 +26046,41 @@ function formulaArguments(tokens) {
25933
26046
  dependencies,
25934
26047
  constantValues,
25935
26048
  };
26049
+ }
26050
+ /**
26051
+ * Check if arguments are supplied in the correct quantities
26052
+ */
26053
+ function assertEnoughArgs(ast) {
26054
+ const nbrArg = ast.args.length;
26055
+ const functionName = ast.value.toUpperCase();
26056
+ const functionDefinition = functions$1[functionName];
26057
+ if (nbrArg < functionDefinition.minArgRequired) {
26058
+ throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString()));
26059
+ }
26060
+ if (nbrArg > functionDefinition.maxArgPossible) {
26061
+ throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString()));
26062
+ }
26063
+ const repeatableArgs = functionDefinition.nbrArgRepeating;
26064
+ if (repeatableArgs > 1) {
26065
+ const unrepeatableArgs = functionDefinition.args.length - repeatableArgs;
26066
+ const repeatingArgs = nbrArg - unrepeatableArgs;
26067
+ if (repeatingArgs % repeatableArgs !== 0) {
26068
+ throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", functionName, unrepeatableArgs.toString(), repeatableArgs.toString()));
26069
+ }
26070
+ }
26071
+ }
26072
+ function isRangeType(type) {
26073
+ return type.startsWith("RANGE");
26074
+ }
26075
+ function isRangeInput(arg) {
26076
+ if (arg.type === "REFERENCE") {
26077
+ return true;
26078
+ }
26079
+ if (arg.type === "FUNCALL") {
26080
+ const fnDef = functions$1[arg.value.toUpperCase()];
26081
+ return fnDef && isRangeType(fnDef.returns[0]);
26082
+ }
26083
+ return false;
25936
26084
  }
25937
26085
 
25938
26086
  /**
@@ -26049,16 +26197,6 @@ function convertOperator(operator) {
26049
26197
  // -------------------------------------
26050
26198
  // WORKSHEET HELPERS
26051
26199
  // -------------------------------------
26052
- function getCellType(value) {
26053
- switch (typeof value) {
26054
- case "boolean":
26055
- return "b";
26056
- case "string":
26057
- return "str";
26058
- case "number":
26059
- return "n";
26060
- }
26061
- }
26062
26200
  function convertHeightToExcel(height) {
26063
26201
  return Math.round(HEIGHT_FACTOR * height * 100) / 100;
26064
26202
  }
@@ -28807,11 +28945,9 @@ class BasePlugin {
28807
28945
  */
28808
28946
  class CorePlugin extends BasePlugin {
28809
28947
  getters;
28810
- range;
28811
28948
  uuidGenerator;
28812
28949
  constructor({ getters, stateObserver, range, dispatch, uuidGenerator }) {
28813
28950
  super(stateObserver, dispatch);
28814
- this.range = range;
28815
28951
  range.addRangeProvider(this.adaptRanges.bind(this));
28816
28952
  this.getters = getters;
28817
28953
  this.uuidGenerator = uuidGenerator;
@@ -29745,23 +29881,7 @@ class CellPlugin extends CorePlugin {
29745
29881
  */
29746
29882
  createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId) {
29747
29883
  const dependencies = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
29748
- const buildFormulaContent = this.buildFormulaContent.bind(this);
29749
- // Only for formulas with dependencies because
29750
- // **the closure is expensive memory-wise**
29751
- return {
29752
- id,
29753
- get content() {
29754
- return buildFormulaContent(sheetId, {
29755
- dependencies: this.dependencies,
29756
- compiledFormula: this.compiledFormula,
29757
- });
29758
- },
29759
- style,
29760
- format,
29761
- isFormula: true,
29762
- compiledFormula,
29763
- dependencies,
29764
- };
29884
+ return new FormulaCellWithDependencies(id, compiledFormula, format, style, dependencies, sheetId, this.buildFormulaContent.bind(this));
29765
29885
  }
29766
29886
  createErrorFormula(id, content, format, style, error) {
29767
29887
  return {
@@ -29787,6 +29907,31 @@ class CellPlugin extends CorePlugin {
29787
29907
  const sheetZone = this.getters.getSheetZone(sheetId);
29788
29908
  return isInside(col, row, sheetZone) ? 0 /* CommandResult.Success */ : 18 /* CommandResult.TargetOutOfSheet */;
29789
29909
  }
29910
+ }
29911
+ class FormulaCellWithDependencies {
29912
+ id;
29913
+ compiledFormula;
29914
+ format;
29915
+ style;
29916
+ dependencies;
29917
+ sheetId;
29918
+ buildFormulaContent;
29919
+ isFormula = true;
29920
+ constructor(id, compiledFormula, format, style, dependencies, sheetId, buildFormulaContent) {
29921
+ this.id = id;
29922
+ this.compiledFormula = compiledFormula;
29923
+ this.format = format;
29924
+ this.style = style;
29925
+ this.dependencies = dependencies;
29926
+ this.sheetId = sheetId;
29927
+ this.buildFormulaContent = buildFormulaContent;
29928
+ }
29929
+ get content() {
29930
+ return this.buildFormulaContent(this.sheetId, {
29931
+ dependencies: this.dependencies,
29932
+ compiledFormula: this.compiledFormula,
29933
+ });
29934
+ }
29790
29935
  }
29791
29936
 
29792
29937
  class ChartPlugin extends CorePlugin {
@@ -33078,6 +33223,931 @@ class UIPlugin extends BasePlugin {
33078
33223
  drawGrid(ctx, layer) { }
33079
33224
  }
33080
33225
 
33226
+ const functionMap = functionRegistry.mapping;
33227
+ /**
33228
+ * Return all functions necessary to properly evaluate a formula:
33229
+ * - a refFn function to read any reference, cell or range of a normalized formula
33230
+ * - a range function to convert any reference to a proper value array
33231
+ * - an evaluation context
33232
+ */
33233
+ function buildCompilationParameters(context, getters, computeCell) {
33234
+ const builder = new CompilationParametersBuilder(context, getters, computeCell);
33235
+ return builder.getParameters();
33236
+ }
33237
+ class CompilationParametersBuilder {
33238
+ getters;
33239
+ computeCell;
33240
+ evalContext;
33241
+ constructor(context, getters, computeCell) {
33242
+ this.getters = getters;
33243
+ this.computeCell = computeCell;
33244
+ this.evalContext = Object.assign(Object.create(functionMap), context, {
33245
+ getters: this.getters,
33246
+ });
33247
+ }
33248
+ getParameters() {
33249
+ return [this.refFn.bind(this), this.range.bind(this), this.evalContext];
33250
+ }
33251
+ /**
33252
+ * Returns the value of the cell(s) used in reference
33253
+ *
33254
+ * @param range the references used
33255
+ * @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the
33256
+ * function for which this parameter is used, we just return the string of the parameter.
33257
+ * The `compute` of the formula's function must process it completely
33258
+ */
33259
+ refFn(range, isMeta, functionName, paramNumber) {
33260
+ if (isMeta) {
33261
+ // Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
33262
+ return { value: zoneToXc(range.zone) };
33263
+ }
33264
+ if (!isZoneValid(range.zone)) {
33265
+ throw new InvalidReferenceError();
33266
+ }
33267
+ // if the formula definition could have accepted a range, we would pass through the _range function and not here
33268
+ if (range.zone.bottom !== range.zone.top || range.zone.left !== range.zone.right) {
33269
+ throw new Error(paramNumber
33270
+ ? _lt("Function %s expects the parameter %s to be a single value or a single cell reference, not a range.", functionName.toString(), paramNumber.toString())
33271
+ : _lt("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
33272
+ }
33273
+ if (range.invalidSheetName) {
33274
+ throw new Error(_lt("Invalid sheet name: %s", range.invalidSheetName));
33275
+ }
33276
+ return this.readCell(range);
33277
+ }
33278
+ readCell(range) {
33279
+ if (!this.getters.tryGetSheet(range.sheetId)) {
33280
+ throw new Error(_lt("Invalid sheet name"));
33281
+ }
33282
+ const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
33283
+ const evaluatedCell = this.getEvaluatedCellIfNotEmpty(position);
33284
+ if (evaluatedCell === undefined) {
33285
+ return { value: null, format: this.getters.getCell(position)?.format };
33286
+ }
33287
+ return evaluatedCell;
33288
+ }
33289
+ getEvaluatedCellIfNotEmpty(position) {
33290
+ const evaluatedCell = this.getEvaluatedCell(position);
33291
+ if (evaluatedCell.type === CellValueType.empty) {
33292
+ const cell = this.getters.getCell(position);
33293
+ if (!cell || cell.content === "") {
33294
+ return undefined;
33295
+ }
33296
+ }
33297
+ return evaluatedCell;
33298
+ }
33299
+ getEvaluatedCell(position) {
33300
+ const evaluatedCell = this.computeCell(position);
33301
+ if (evaluatedCell.type === CellValueType.error) {
33302
+ throw evaluatedCell.error;
33303
+ }
33304
+ return evaluatedCell;
33305
+ }
33306
+ /**
33307
+ * Return the values of the cell(s) used in reference, but always in the format of a range even
33308
+ * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
33309
+ * range<number> etc.
33310
+ *
33311
+ * Note that each col is possibly sparse: it only contain the values of cells
33312
+ * that are actually present in the grid.
33313
+ */
33314
+ range({ sheetId, zone }) {
33315
+ if (!isZoneValid(zone)) {
33316
+ throw new InvalidReferenceError();
33317
+ }
33318
+ // Performance issue: Avoid fetching data on positions that are out of the spreadsheet
33319
+ // e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array
33320
+ const sheetZone = this.getters.getSheetZone(sheetId);
33321
+ const _zone = intersection(zone, sheetZone);
33322
+ if (!_zone) {
33323
+ return { value: [[]], format: [[]] };
33324
+ }
33325
+ const height = _zone.bottom - _zone.top + 1;
33326
+ const width = _zone.right - _zone.left + 1;
33327
+ const value = Array.from({ length: width }, () => Array.from({ length: height }));
33328
+ const format = Array.from({ length: width }, () => Array.from({ length: height }));
33329
+ // Performance issue: nested loop is faster than a map here
33330
+ for (let col = _zone.left; col <= _zone.right; col++) {
33331
+ for (let row = _zone.top; row <= _zone.bottom; row++) {
33332
+ const evaluatedCell = this.getEvaluatedCellIfNotEmpty({ sheetId: sheetId, col, row });
33333
+ if (evaluatedCell) {
33334
+ const colIndex = col - _zone.left;
33335
+ const rowIndex = row - _zone.top;
33336
+ value[colIndex][rowIndex] = evaluatedCell.value;
33337
+ if (evaluatedCell.format !== undefined) {
33338
+ format[colIndex][rowIndex] = evaluatedCell.format;
33339
+ }
33340
+ }
33341
+ }
33342
+ }
33343
+ return { value, format };
33344
+ }
33345
+ }
33346
+
33347
+ /**
33348
+ * This class is an implementation of a dependency Graph.
33349
+ * The graph is used to evaluate the cells in the correct
33350
+ * order, and should be updated each time a cell's content is modified
33351
+ *
33352
+ */
33353
+ class FormulaDependencyGraph {
33354
+ /**
33355
+ * Internal structure:
33356
+ * - key: a cell position (encoded as an integer)
33357
+ * - value: a set of cell positions that depends on the key
33358
+ *
33359
+ * Given
33360
+ * - A1:"= B1 + SQRT(B2)"
33361
+ * - C1:"= B1";
33362
+ * - C2:"= C1"
33363
+ *
33364
+ * we will have something like:
33365
+ * - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
33366
+ * - B2 ---> (A1) meaning A1 depends on B2
33367
+ * - C1 ---> (C2) meaning C2 depends on C1
33368
+ */
33369
+ inverseDependencies = new Map();
33370
+ dependencies = new Map();
33371
+ removeAllDependencies(formulaPositionId) {
33372
+ const dependencies = this.dependencies.get(formulaPositionId);
33373
+ if (!dependencies) {
33374
+ return;
33375
+ }
33376
+ for (const dependency of dependencies) {
33377
+ this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
33378
+ }
33379
+ this.dependencies.delete(formulaPositionId);
33380
+ }
33381
+ addDependencies(formulaPositionId, dependencies) {
33382
+ for (const dependency of dependencies) {
33383
+ const inverseDependencies = this.inverseDependencies.get(dependency);
33384
+ if (inverseDependencies) {
33385
+ inverseDependencies.add(formulaPositionId);
33386
+ }
33387
+ else {
33388
+ this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
33389
+ }
33390
+ }
33391
+ const existingDependencies = this.dependencies.get(formulaPositionId);
33392
+ if (existingDependencies) {
33393
+ existingDependencies.push(...dependencies);
33394
+ }
33395
+ else {
33396
+ this.dependencies.set(formulaPositionId, dependencies);
33397
+ }
33398
+ }
33399
+ /**
33400
+ * Return the cell and all cells that depend on it,
33401
+ * in the correct order they should be evaluated.
33402
+ * This is called a topological ordering (excluding cycles)
33403
+ */
33404
+ getCellsDependingOn(positionIds) {
33405
+ const visited = new JetSet();
33406
+ const queue = Array.from(positionIds);
33407
+ while (queue.length > 0) {
33408
+ const node = queue.shift();
33409
+ visited.add(node);
33410
+ const adjacentNodes = this.inverseDependencies.get(node) || new Set();
33411
+ for (const adjacentNode of adjacentNodes) {
33412
+ if (!visited.has(adjacentNode)) {
33413
+ queue.push(adjacentNode);
33414
+ }
33415
+ }
33416
+ }
33417
+ visited.delete(...positionIds);
33418
+ return visited;
33419
+ }
33420
+ }
33421
+
33422
+ /**
33423
+ * Contains, for each cell, the array
33424
+ * formulas that could potentially spread on it
33425
+ * This is essentially a two way mapping between array formulas
33426
+ * and their results.
33427
+ *
33428
+ * As we don't allow two array formulas to spread on the same cell, this structure
33429
+ * is used to force the reevaluation of the potential spreaders of a cell when the
33430
+ * content of this cell is modified. This structure should be updated each time
33431
+ * an array formula is evaluated and try to spread on another cell.
33432
+ *
33433
+ */
33434
+ class SpreadingRelation {
33435
+ /**
33436
+ * Internal structure:
33437
+ * For something like
33438
+ * - A2:'=SPLIT("KAYAK", "A")'
33439
+ * - B1:'=TRANSPOSE(SPLIT("COYOTE", "O"))'
33440
+ *
33441
+ * Resulting in:
33442
+ * ```
33443
+ * -----------------
33444
+ * | | A | B | C |
33445
+ * |---+---+---+---|
33446
+ * | 1 | | C | |
33447
+ * | 2 | K | Y | K |
33448
+ * | 3 | | T | |
33449
+ * | 4 | | E | |
33450
+ * -----------------
33451
+ * ```
33452
+ * We will have `resultsToArrayFormulas` looking like:
33453
+ * - (B2) --> (A2, B1) meaning B2 can be the result of A2 OR B1
33454
+ * - (C2) --> (A2) meaning C2 is the result of A2
33455
+ * - (B3) --> (B1) meaning B3 is the result of B1
33456
+ * - (B4) --> (B1) meaning B4 is the result of B1
33457
+ *
33458
+ * We will have `arrayFormulasToResults` looking like:
33459
+ * - (A2) --> (B2, C2) meaning A2 spreads on B2 and C2
33460
+ * - (B1) --> (B2, B3, B4) meaning B1 spreads on B2, B3 and B4
33461
+ *
33462
+ */
33463
+ resultsToArrayFormulas = new Map();
33464
+ arrayFormulasToResults = new Map();
33465
+ getFormulaPositionsSpreadingOn(resultPositionId) {
33466
+ return this.resultsToArrayFormulas.get(resultPositionId) || EMPTY_ARRAY;
33467
+ }
33468
+ getArrayResultPositionIds(formulasPositionId) {
33469
+ return this.arrayFormulasToResults.get(formulasPositionId) || EMPTY_ARRAY;
33470
+ }
33471
+ /**
33472
+ * Remove a node, also remove it from other nodes adjacency list
33473
+ */
33474
+ removeNode(positionId) {
33475
+ this.resultsToArrayFormulas.delete(positionId);
33476
+ this.arrayFormulasToResults.delete(positionId);
33477
+ }
33478
+ /**
33479
+ * Create a spreading relation between two cells
33480
+ */
33481
+ addRelation({ arrayFormulaPositionId, resultPositionId, }) {
33482
+ if (!this.resultsToArrayFormulas.has(resultPositionId)) {
33483
+ this.resultsToArrayFormulas.set(resultPositionId, new Set());
33484
+ }
33485
+ this.resultsToArrayFormulas.get(resultPositionId)?.add(arrayFormulaPositionId);
33486
+ if (!this.arrayFormulasToResults.has(arrayFormulaPositionId)) {
33487
+ this.arrayFormulasToResults.set(arrayFormulaPositionId, new Set());
33488
+ }
33489
+ this.arrayFormulasToResults.get(arrayFormulaPositionId)?.add(resultPositionId);
33490
+ }
33491
+ hasArrayFormulaResult(positionId) {
33492
+ return this.resultsToArrayFormulas.has(positionId);
33493
+ }
33494
+ isArrayFormula(positionId) {
33495
+ return this.arrayFormulasToResults.has(positionId);
33496
+ }
33497
+ }
33498
+ const EMPTY_ARRAY = [];
33499
+
33500
+ const MAX_ITERATION = 30;
33501
+ class Evaluator {
33502
+ getters;
33503
+ compilationParams;
33504
+ positionEncoder = new PositionBitsEncoder();
33505
+ evaluatedCells = new Map();
33506
+ formulaDependencies = lazy(new FormulaDependencyGraph());
33507
+ blockedArrayFormulas = new Set();
33508
+ spreadingRelations = new SpreadingRelation();
33509
+ constructor(context, getters) {
33510
+ this.getters = getters;
33511
+ this.compilationParams = buildCompilationParameters(context, getters, (position) => this.computeCell(this.encodePosition(position)));
33512
+ }
33513
+ getEvaluatedCell(position) {
33514
+ return this.evaluatedCells.get(this.encodePosition(position)) || createEvaluatedCell("");
33515
+ }
33516
+ getArrayFormulaSpreadingOn(position) {
33517
+ const positionId = this.encodePosition(position);
33518
+ const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
33519
+ return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
33520
+ }
33521
+ getEvaluatedPositions() {
33522
+ return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
33523
+ }
33524
+ getArrayFormulaSpreadingOnId(positionId) {
33525
+ if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
33526
+ return undefined;
33527
+ }
33528
+ const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
33529
+ return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
33530
+ }
33531
+ updateDependencies(position) {
33532
+ const positionId = this.encodePosition(position);
33533
+ this.formulaDependencies().removeAllDependencies(positionId);
33534
+ const dependencies = this.getDirectDependencies(positionId);
33535
+ this.formulaDependencies().addDependencies(positionId, dependencies);
33536
+ }
33537
+ evaluateCells(positions) {
33538
+ const cells = positions.map(this.encodePosition.bind(this));
33539
+ const cellsToCompute = new JetSet(cells);
33540
+ const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
33541
+ cellsToCompute.add(...this.getCellsDependingOn(cells));
33542
+ cellsToCompute.add(...arrayFormulas);
33543
+ cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
33544
+ this.evaluate(cellsToCompute);
33545
+ }
33546
+ getArrayFormulasImpactedByChangesOf(positionIds) {
33547
+ const impactedPositionIds = new JetSet();
33548
+ for (const positionId of positionIds) {
33549
+ const content = this.getCell(positionId)?.content;
33550
+ const arrayFormulaPositionId = this.getArrayFormulaSpreadingOnId(positionId);
33551
+ if (arrayFormulaPositionId !== undefined) {
33552
+ // take into account new collisions.
33553
+ impactedPositionIds.add(arrayFormulaPositionId);
33554
+ }
33555
+ if (!content) {
33556
+ // The previous content could have blocked some array formulas
33557
+ impactedPositionIds.add(...this.getArrayFormulasBlockedByOrSpreadingOn(positionId));
33558
+ }
33559
+ }
33560
+ return impactedPositionIds;
33561
+ }
33562
+ buildDependencyGraph() {
33563
+ this.blockedArrayFormulas = new Set();
33564
+ this.spreadingRelations = new SpreadingRelation();
33565
+ this.formulaDependencies = lazy(() => {
33566
+ const dependencyGraph = new FormulaDependencyGraph();
33567
+ for (const positionId of this.getAllCells()) {
33568
+ const dependencies = this.getDirectDependencies(positionId);
33569
+ dependencyGraph.addDependencies(positionId, dependencies);
33570
+ }
33571
+ return dependencyGraph;
33572
+ });
33573
+ }
33574
+ evaluateAllCells() {
33575
+ this.evaluatedCells = new Map();
33576
+ this.evaluate(this.getAllCells());
33577
+ }
33578
+ getAllCells() {
33579
+ const positionIds = new JetSet();
33580
+ for (const sheetId of this.getters.getSheetIds()) {
33581
+ const cellIds = this.getters.getCells(sheetId);
33582
+ for (const cellId in cellIds) {
33583
+ positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
33584
+ }
33585
+ }
33586
+ return positionIds;
33587
+ }
33588
+ getArrayFormulasBlockedByOrSpreadingOn(positionId) {
33589
+ if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
33590
+ return [];
33591
+ }
33592
+ const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
33593
+ const cells = new JetSet(arrayFormulas);
33594
+ cells.add(...this.getCellsDependingOn(arrayFormulas));
33595
+ return cells;
33596
+ }
33597
+ nextPositionsToUpdate = new JetSet();
33598
+ cellsBeingComputed = new Set();
33599
+ evaluate(cells) {
33600
+ this.cellsBeingComputed = new Set();
33601
+ this.nextPositionsToUpdate = cells;
33602
+ let currentIteration = 0;
33603
+ while (this.nextPositionsToUpdate.size && currentIteration++ < MAX_ITERATION) {
33604
+ const positionIds = Array.from(this.nextPositionsToUpdate);
33605
+ this.nextPositionsToUpdate.clear();
33606
+ for (let i = 0; i < positionIds.length; ++i) {
33607
+ const cell = positionIds[i];
33608
+ this.evaluatedCells.delete(cell);
33609
+ }
33610
+ for (let i = 0; i < positionIds.length; ++i) {
33611
+ const cell = positionIds[i];
33612
+ this.setEvaluatedCell(cell, this.computeCell(cell));
33613
+ }
33614
+ }
33615
+ }
33616
+ setEvaluatedCell(positionId, evaluatedCell) {
33617
+ if (this.nextPositionsToUpdate.has(positionId)) {
33618
+ this.nextPositionsToUpdate.delete(positionId);
33619
+ }
33620
+ this.evaluatedCells.set(positionId, evaluatedCell);
33621
+ }
33622
+ computeCell(positionId) {
33623
+ const evaluation = this.evaluatedCells.get(positionId);
33624
+ if (evaluation) {
33625
+ return evaluation; // already computed
33626
+ }
33627
+ if (!this.blockedArrayFormulas.has(positionId)) {
33628
+ this.invalidateSpreading(positionId);
33629
+ }
33630
+ const cell = this.getCell(positionId);
33631
+ if (cell === undefined) {
33632
+ return createEvaluatedCell("");
33633
+ }
33634
+ const cellId = cell.id;
33635
+ try {
33636
+ if (this.cellsBeingComputed.has(cellId)) {
33637
+ throw new CircularDependencyError();
33638
+ }
33639
+ this.cellsBeingComputed.add(cellId);
33640
+ return cell.isFormula
33641
+ ? this.computeFormulaCell(cell)
33642
+ : evaluateLiteral(cell.content, cell.format);
33643
+ }
33644
+ catch (e) {
33645
+ return this.handleError(e, cell);
33646
+ }
33647
+ finally {
33648
+ this.cellsBeingComputed.delete(cellId);
33649
+ }
33650
+ }
33651
+ handleError(e, cell) {
33652
+ if (!(e instanceof Error)) {
33653
+ e = new Error(e);
33654
+ }
33655
+ const msg = e?.errorType || CellErrorType.GenericError;
33656
+ // apply function name
33657
+ const __lastFnCalled = this.compilationParams[2].__lastFnCalled || "";
33658
+ const error = new EvaluationError(msg, e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error);
33659
+ return errorCell(cell.content, error);
33660
+ }
33661
+ computeFormulaCell(cellData) {
33662
+ const cellId = cellData.id;
33663
+ this.compilationParams[2].__originCellXC = () => {
33664
+ // compute the value lazily for performance reasons
33665
+ const position = this.compilationParams[2].getters.getCellPosition(cellId);
33666
+ return toXC(position.col, position.row);
33667
+ };
33668
+ const formulaReturn = cellData.compiledFormula.execute(cellData.dependencies, ...this.compilationParams);
33669
+ assertFormulaReturnHasConsistentDimensions(formulaReturn);
33670
+ const { value: computedValue, format: computedFormat } = formulaReturn;
33671
+ if (!isMatrix(computedValue)) {
33672
+ return createEvaluatedCell(computedValue, cellData.format || computedFormat);
33673
+ }
33674
+ const formulaPosition = this.getters.getCellPosition(cellId);
33675
+ this.assertSheetHasEnoughSpaceToSpreadFormulaResult(formulaPosition, computedValue);
33676
+ forEachSpreadPositionInMatrix(computedValue, this.updateSpreadRelation(formulaPosition));
33677
+ forEachSpreadPositionInMatrix(computedValue, this.checkCollision(formulaPosition));
33678
+ forEachSpreadPositionInMatrix(computedValue,
33679
+ // due the isMatrix check above, we know that formulaReturn is MatrixFunctionReturn
33680
+ this.spreadValues(formulaPosition, formulaReturn));
33681
+ const formatFromPosition = formatFromPositionAccess(computedFormat);
33682
+ return createEvaluatedCell(computedValue[0][0], cellData.format || formatFromPosition(0, 0));
33683
+ }
33684
+ assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
33685
+ const numberOfCols = this.getters.getNumberCols(sheetId);
33686
+ const numberOfRows = this.getters.getNumberRows(sheetId);
33687
+ const enoughCols = col + matrixResult.length <= numberOfCols;
33688
+ const enoughRows = row + matrixResult[0].length <= numberOfRows;
33689
+ if (enoughCols && enoughRows) {
33690
+ return;
33691
+ }
33692
+ if (enoughCols) {
33693
+ throw new Error(_lt("Result couldn't be automatically expanded. Please insert more rows."));
33694
+ }
33695
+ if (enoughRows) {
33696
+ throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns."));
33697
+ }
33698
+ throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns and rows."));
33699
+ }
33700
+ updateSpreadRelation({ sheetId, col, row, }) {
33701
+ const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
33702
+ return (i, j) => {
33703
+ const position = { sheetId, col: i + col, row: j + row };
33704
+ const resultPositionId = this.encodePosition(position);
33705
+ this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
33706
+ };
33707
+ }
33708
+ checkCollision({ sheetId, col, row }) {
33709
+ const formulaPositionId = this.encodePosition({ sheetId, col, row });
33710
+ return (i, j) => {
33711
+ const position = { sheetId: sheetId, col: i + col, row: j + row };
33712
+ const rawCell = this.getters.getCell(position);
33713
+ if (rawCell?.content ||
33714
+ this.getters.getEvaluatedCell(position).type !== CellValueType.empty) {
33715
+ this.blockedArrayFormulas.add(formulaPositionId);
33716
+ throw new Error(_lt("Array result was not expanded because it would overwrite data in %s.", toXC(position.col, position.row)));
33717
+ }
33718
+ this.blockedArrayFormulas.delete(formulaPositionId);
33719
+ };
33720
+ }
33721
+ spreadValues({ sheetId, col, row }, matrixResult) {
33722
+ const formatFromPosition = formatFromPositionAccess(matrixResult.format);
33723
+ return (i, j) => {
33724
+ const position = { sheetId, col: i + col, row: j + row };
33725
+ const cell = this.getters.getCell(position);
33726
+ const format = cell?.format;
33727
+ const evaluatedCell = createEvaluatedCell(matrixResult.value[i][j], format || formatFromPosition(i, j));
33728
+ const positionId = this.encodePosition(position);
33729
+ this.setEvaluatedCell(positionId, evaluatedCell);
33730
+ // check if formula dependencies present in the spread zone
33731
+ // if so, they need to be recomputed
33732
+ this.nextPositionsToUpdate.add(...this.getCellsDependingOn([positionId]));
33733
+ };
33734
+ }
33735
+ invalidateSpreading(positionId) {
33736
+ if (!this.spreadingRelations.isArrayFormula(positionId)) {
33737
+ return;
33738
+ }
33739
+ for (const child of this.spreadingRelations.getArrayResultPositionIds(positionId)) {
33740
+ const content = this.getCell(child)?.content;
33741
+ if (content) {
33742
+ // there's no point at re-evaluating overlapping array formulas,
33743
+ // there's still a collision
33744
+ continue;
33745
+ }
33746
+ this.evaluatedCells.delete(child);
33747
+ this.nextPositionsToUpdate.add(...this.getCellsDependingOn([child]));
33748
+ this.nextPositionsToUpdate.add(...this.getArrayFormulasBlockedByOrSpreadingOn(child));
33749
+ }
33750
+ this.spreadingRelations.removeNode(positionId);
33751
+ }
33752
+ // ----------------------------------------------------------
33753
+ // COMMON FUNCTIONALITY
33754
+ // ----------------------------------------------------------
33755
+ getDirectDependencies(positionId) {
33756
+ const cell = this.getCell(positionId);
33757
+ if (!cell?.isFormula) {
33758
+ return [];
33759
+ }
33760
+ const dependencies = [];
33761
+ for (const range of cell.dependencies) {
33762
+ if (range.invalidSheetName || range.invalidXc) {
33763
+ continue;
33764
+ }
33765
+ const sheetId = range.sheetId;
33766
+ forEachPositionsInZone(range.zone, (col, row) => {
33767
+ dependencies.push(this.encodePosition({ sheetId, col, row }));
33768
+ });
33769
+ }
33770
+ return dependencies;
33771
+ }
33772
+ getCellsDependingOn(positionIds) {
33773
+ return this.formulaDependencies().getCellsDependingOn(positionIds);
33774
+ }
33775
+ getCell(positionId) {
33776
+ return this.getters.getCell(this.decodePosition(positionId));
33777
+ }
33778
+ encodePosition(position) {
33779
+ return this.positionEncoder.encode(position);
33780
+ }
33781
+ decodePosition(positionId) {
33782
+ return this.positionEncoder.decode(positionId);
33783
+ }
33784
+ }
33785
+ function forEachSpreadPositionInMatrix(matrix, callback) {
33786
+ for (let i = 0; i < matrix.length; ++i) {
33787
+ for (let j = 0; j < matrix[i].length; ++j) {
33788
+ if (i === 0 && j === 0) {
33789
+ continue;
33790
+ }
33791
+ callback(i, j);
33792
+ }
33793
+ }
33794
+ }
33795
+ function formatFromPositionAccess(format) {
33796
+ return isMatrix(format) ? (i, j) => format[i][j] : () => format;
33797
+ }
33798
+ function assertFormulaReturnHasConsistentDimensions(formulaReturn) {
33799
+ const { value: computedValue, format: computedFormat } = formulaReturn;
33800
+ if (!isMatrix(computedValue)) {
33801
+ if (isMatrix(computedFormat)) {
33802
+ throw new Error("A format matrix should never be associated with a scalar value");
33803
+ }
33804
+ return;
33805
+ }
33806
+ if (isMatrix(computedFormat)) {
33807
+ const sameDimensions = computedValue.length === computedFormat.length &&
33808
+ computedValue[0].length === computedFormat[0].length;
33809
+ if (!sameDimensions) {
33810
+ throw new Error("Formats and values should have the same dimensions!");
33811
+ }
33812
+ }
33813
+ }
33814
+ /**
33815
+ * Encode (and decode) cell positions { sheetId, col, row }
33816
+ * to a single integer.
33817
+ *
33818
+ * `col` and `row` values are encoded on 21 bits each (max 2^21 = 2_097_152),
33819
+ * An incremental integer id is assigned to each different sheet id, starting at 0.
33820
+ *
33821
+ * e.g.
33822
+ * Given { col: 10, row: 4, sheetId: "abcde" }
33823
+ * we have:
33824
+ * - row "4" encoded on 21 bits: 000000000000000000100
33825
+ * - col "10" encoded on 21 bits: 000000000000000001010
33826
+ * - sheetId: let's say it's the 4th sheetId met, encoded to: 11
33827
+ *
33828
+ * The final encoded value is found by concatenating the 3 bit sequences:
33829
+ *
33830
+ * sheetId: 11
33831
+ * col: 000000000000000001010
33832
+ * row: 000000000000000000100
33833
+ * => 11000000000000000001010000000000000000000100
33834
+ *
33835
+ * this binary sequence is the integer 13194160504836
33836
+ */
33837
+ class PositionBitsEncoder {
33838
+ sheetMapping = {};
33839
+ inverseSheetMapping = new Map();
33840
+ constructor() {
33841
+ try {
33842
+ // @ts-ignore
33843
+ o_spreadsheet.__DEBUG__ = o_spreadsheet.__DEBUG__ || {};
33844
+ // @ts-ignore
33845
+ o_spreadsheet.__DEBUG__.decodePosition = this.decode.bind(this);
33846
+ // @ts-ignore
33847
+ o_spreadsheet.__DEBUG__.encodePosition = this.encode.bind(this);
33848
+ }
33849
+ catch (error) { }
33850
+ }
33851
+ /**
33852
+ * Encode a cell position to a single integer.
33853
+ */
33854
+ encode({ sheetId, col, row }) {
33855
+ return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
33856
+ }
33857
+ decode(id) {
33858
+ // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
33859
+ const row = Number(id & 2097151n);
33860
+ const col = Number((id >> 21n) & 2097151n);
33861
+ const sheetId = this.decodeSheet(id >> 42n);
33862
+ return { sheetId, col, row };
33863
+ }
33864
+ encodeSheet(sheetId) {
33865
+ const sheetKey = this.sheetMapping[sheetId];
33866
+ if (sheetKey === undefined) {
33867
+ const newSheetKey = BigInt(Object.keys(this.sheetMapping).length);
33868
+ this.sheetMapping[sheetId] = newSheetKey;
33869
+ this.inverseSheetMapping.set(newSheetKey, sheetId);
33870
+ return newSheetKey;
33871
+ }
33872
+ return sheetKey;
33873
+ }
33874
+ decodeSheet(sheetKey) {
33875
+ const sheetId = this.inverseSheetMapping.get(sheetKey);
33876
+ if (sheetId === undefined) {
33877
+ throw new Error("Sheet id not found");
33878
+ }
33879
+ return sheetId;
33880
+ }
33881
+ }
33882
+
33883
+ const functions = functionRegistry.content;
33884
+ //#region
33885
+ // ---------------------------------------------------------------------------
33886
+ // INTRODUCTION
33887
+ // ---------------------------------------------------------------------------
33888
+ // The evaluation plugin is in charge of computing the values of the cells.
33889
+ // This is a fairly complex task for several reasons:
33890
+ // Reason n°1: Cells can contain formulas that must be interpreted to know
33891
+ // the final value of the cell. And these formulas can depend on other cells.
33892
+ // ex A1:"=SUM(B1:B2)" we have to evaluate B1:B2 first to be able to evaluate A1.
33893
+ // We say here that we have a 'formula dependency' between A1 and B1:B2.
33894
+ // Reason n°2: A cell can assign value to other cells that haven't content.
33895
+ // This concerns cells containing a formula that returns an array of values.
33896
+ // ex A1:"=SPLIT('Odoo','d')" Evaluating A1 must assign the value "O" to A1 and
33897
+ // "oo" to B1. We say here that we have a 'spread relation' between A1 and B1.
33898
+ // B1 have a spread value from A1.
33899
+ // Note that a cell can contain a formula which depends on other cells which
33900
+ // themselves can:
33901
+ // - contain formulas which depends on other cells (and so on).
33902
+ // - contain a spread value from other formulas which depends on other cells
33903
+ // (and so on).
33904
+ // I - How to build the evaluation ?
33905
+ // If we had only formulas dependencies to treat, the evaluation would be
33906
+ // simple: the formulas dependencies are directly deduced from the content
33907
+ // of the formulas. With the dependencies we are able to calculate which
33908
+ // formula must be evaluated before another.
33909
+ // Cycles
33910
+ // ------
33911
+ // We can also easily detect if the cells are included in reference cycles
33912
+ // and return an error in this case. ex: A1:"=B1" B1:"=C1" C1:"=A1"
33913
+ // The "#CYCLE" error must be returned for
33914
+ // all three cells.
33915
+ // But there's more! There are formulas referring to other cells but never
33916
+ // use them. This is the case for example
33917
+ // with the "IF" formula. ex:
33918
+ // A1:"=IF(D1,A2,B1)"
33919
+ // A2:"=A1"
33920
+ // In this case it is obvious that we have a cyclic dependency. But in practice
33921
+ // this will only exist if D1 is true.
33922
+ // For this reason, we believe that the evaluation should be **partly recursive**:
33923
+ // The function computing a formula cell starts by marking them as 'being evaluated'
33924
+ // and then call itself on the dependencies of the concerned cell. This allows
33925
+ // to evaluate the dependencies before the cell itself and to detect
33926
+ // if the cell that is being evaluated isn't part of a cycle.
33927
+ // II - The spread relation anticipation problem
33928
+ // The biggest difficulty to solve with the evaluation lies in the fact that
33929
+ // we cannot anticipate the spread relations: cells impacted by the result array
33930
+ // of a formula are only determined after the array formula has been
33931
+ // evaluated. In the case where the impacted cells are used in other formulas,
33932
+ // this will require to re-evaluation other formulas (and so on...). ex:
33933
+ // A1:"=B2"
33934
+ // A2:"=SPLIT('Odoo','d')"
33935
+ // in the example above, A2 spreads on B2, but we will know it only after
33936
+ // the evaluation of A2. To be able to evaluate A1 correctly, we must therefore
33937
+ // reevaluate A1 after the evaluation of A2.
33938
+ // We could evaluate which formula spreads first. Except that the array formulas
33939
+ // can themselves depend on the spreads of other formulas. ex:
33940
+ // A1:"=SPLIT(B3,'d')"
33941
+ // A2:="odoo odoo"
33942
+ // A3:"=SPLIT(A2,' ')"
33943
+ // In the example above, A3 must be evaluated before A1 because A1 needs B3 which
33944
+ // can be modified by A3.
33945
+ // Therefore, there would be a spatial evaluation order to be respected between
33946
+ // the array formulas. We could imagine that, when an array formula depends
33947
+ // on a cell, then we evaluate the first formula that spreads located in the upper
33948
+ // left corner of this cell.
33949
+ // Although this possibility has been explored, it remains complicated to spatially
33950
+ // predict which formula should be evaluated before another, especially when
33951
+ // the array formulas are located in different sheets or when the array formulas
33952
+ // depends on the spreads of each other. ex:
33953
+ //
33954
+ // A1:"=ARRAY_FORMULA_ALPHA(B2)"
33955
+ // A2:"=ARRAY_FORMULA_BETA(B1)"
33956
+ // In the example above, ARRAY_FORMULA_ALPHA and ARRAY_FORMULA_BETA are some
33957
+ // formulas that could spread depending on the value of B2 and B1. This could be a
33958
+ // cyclic dependency that we cannot anticipate.
33959
+ // And as with the "IF" formula, array formulas may not use their dependency.
33960
+ // It then becomes very difficult to manage...
33961
+ // Also, if we have a cycle, that doesn't mean it's bad. The cycle can converge to
33962
+ // a stable state at the scale of the sheets. Functionally, we don't want to forbid
33963
+ // convergent cycles. It is an interesting feature but which requires to re-evaluate
33964
+ // the cycle as many times as convergence is not reached.
33965
+ // Thus, in order to respect the relations between the cells (formula dependencies and
33966
+ // spread relations), the evaluation of the cells must:
33967
+ // - respect a precise order (cells values used by another must be evaluated first) : As
33968
+ // we cannot anticipate which dependencies are really used by the formulas, we must
33969
+ // evaluate the cells in a recursive way;
33970
+ // - be done as many times as necessary to ensure that all the cells have been correctly
33971
+ // evaluated in the correct order (in case of, for example, spreading relation cycles).
33972
+ // The chosen solution is to reevaluate the formulas impacted by spreads as many times
33973
+ // as necessary in several iterations, where evaluated cells can trigger the evaluation
33974
+ // of other cells depending on it, at the next iteration.
33975
+ //#endregion
33976
+ class EvaluationPlugin extends UIPlugin {
33977
+ static getters = [
33978
+ "evaluateFormula",
33979
+ "getRangeFormattedValues",
33980
+ "getRangeValues",
33981
+ "getRangeFormats",
33982
+ "getEvaluatedCell",
33983
+ "getEvaluatedCells",
33984
+ "getEvaluatedCellsInZone",
33985
+ ];
33986
+ shouldRebuildDependenciesGraph = true;
33987
+ evaluator;
33988
+ compilationParams;
33989
+ positionsToUpdate = [];
33990
+ constructor(config) {
33991
+ super(config);
33992
+ this.evaluator = new Evaluator(config.custom, this.getters);
33993
+ this.compilationParams = buildCompilationParameters(config.custom, this.getters, (position) => this.evaluator.getEvaluatedCell(position));
33994
+ }
33995
+ // ---------------------------------------------------------------------------
33996
+ // Command Handling
33997
+ // ---------------------------------------------------------------------------
33998
+ beforeHandle(cmd) {
33999
+ if (invalidateDependenciesCommands.has(cmd.type)) {
34000
+ this.shouldRebuildDependenciesGraph = true;
34001
+ }
34002
+ }
34003
+ handle(cmd) {
34004
+ switch (cmd.type) {
34005
+ case "UPDATE_CELL":
34006
+ if (!("content" in cmd || "format" in cmd) || this.shouldRebuildDependenciesGraph) {
34007
+ return;
34008
+ }
34009
+ this.positionsToUpdate.push(cmd);
34010
+ if ("content" in cmd) {
34011
+ this.evaluator.updateDependencies(cmd);
34012
+ }
34013
+ break;
34014
+ case "EVALUATE_CELLS":
34015
+ this.evaluator.evaluateAllCells();
34016
+ break;
34017
+ }
34018
+ }
34019
+ finalize() {
34020
+ if (this.shouldRebuildDependenciesGraph) {
34021
+ this.evaluator.buildDependencyGraph();
34022
+ this.evaluator.evaluateAllCells();
34023
+ this.shouldRebuildDependenciesGraph = false;
34024
+ }
34025
+ else if (this.positionsToUpdate.length) {
34026
+ this.evaluator.evaluateCells(this.positionsToUpdate);
34027
+ }
34028
+ this.positionsToUpdate = [];
34029
+ }
34030
+ // ---------------------------------------------------------------------------
34031
+ // Getters
34032
+ // ---------------------------------------------------------------------------
34033
+ evaluateFormula(formulaString, sheetId = this.getters.getActiveSheetId()) {
34034
+ const compiledFormula = compile(formulaString);
34035
+ const ranges = [];
34036
+ for (let xc of compiledFormula.dependencies) {
34037
+ ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));
34038
+ }
34039
+ return compiledFormula.execute(ranges, ...this.compilationParams).value;
34040
+ }
34041
+ /**
34042
+ * Return the value of each cell in the range as they are displayed in the grid.
34043
+ */
34044
+ getRangeFormattedValues(range) {
34045
+ const sheet = this.getters.tryGetSheet(range.sheetId);
34046
+ if (sheet === undefined)
34047
+ return [];
34048
+ return this.getters
34049
+ .getEvaluatedCellsInZone(sheet.id, range.zone)
34050
+ .map((cell) => cell.formattedValue);
34051
+ }
34052
+ /**
34053
+ * Return the value of each cell in the range.
34054
+ */
34055
+ getRangeValues(range) {
34056
+ const sheet = this.getters.tryGetSheet(range.sheetId);
34057
+ if (sheet === undefined)
34058
+ return [];
34059
+ return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.value);
34060
+ }
34061
+ /**
34062
+ * Return the format of each cell in the range.
34063
+ */
34064
+ getRangeFormats(range) {
34065
+ const sheet = this.getters.tryGetSheet(range.sheetId);
34066
+ if (sheet === undefined)
34067
+ return [];
34068
+ return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.format);
34069
+ }
34070
+ getEvaluatedCell(position) {
34071
+ return this.evaluator.getEvaluatedCell(position);
34072
+ }
34073
+ getEvaluatedCells(sheetId) {
34074
+ const rawCells = this.getters.getCells(sheetId) || {};
34075
+ const record = {};
34076
+ for (let cellId of Object.keys(rawCells)) {
34077
+ const position = this.getters.getCellPosition(cellId);
34078
+ record[cellId] = this.getEvaluatedCell(position);
34079
+ }
34080
+ return record;
34081
+ }
34082
+ getEvaluatedCellsInZone(sheetId, zone) {
34083
+ return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
34084
+ }
34085
+ // ---------------------------------------------------------------------------
34086
+ // Export
34087
+ // ---------------------------------------------------------------------------
34088
+ exportForExcel(data) {
34089
+ for (const position of this.evaluator.getEvaluatedPositions()) {
34090
+ const evaluatedCell = this.evaluator.getEvaluatedCell(position);
34091
+ const xc = toXC(position.col, position.row);
34092
+ const value = evaluatedCell.value;
34093
+ let isFormula = false;
34094
+ let newContent = undefined;
34095
+ let newFormat = undefined;
34096
+ let isExported = true;
34097
+ const formulaCell = this.getCorrespondingFormulaCell(position);
34098
+ if (formulaCell) {
34099
+ isExported = formulaCell.compiledFormula.tokens
34100
+ .filter((tk) => tk.type === "FUNCTION")
34101
+ .every((tk) => functions[tk.value.toUpperCase()].isExported);
34102
+ isFormula = isExported;
34103
+ if (!isExported) {
34104
+ newContent = value.toString();
34105
+ newFormat = evaluatedCell.format;
34106
+ }
34107
+ }
34108
+ const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
34109
+ const exportedCellData = exportedSheetData.cells[xc] || {};
34110
+ const format = newFormat
34111
+ ? getItemId(newFormat, data.formats)
34112
+ : exportedCellData.format;
34113
+ const content = !isExported ? newContent : exportedCellData.content;
34114
+ exportedSheetData.cells[xc] = { ...exportedCellData, value, isFormula, content, format };
34115
+ }
34116
+ }
34117
+ /**
34118
+ * Returns the corresponding formula cell of a given cell
34119
+ * It could be the formula present in the cell itself or the
34120
+ * formula of the array formula that spreads to the cell
34121
+ */
34122
+ getCorrespondingFormulaCell(position) {
34123
+ const cell = this.getters.getCell(position);
34124
+ if (cell && cell.content) {
34125
+ if (cell.isFormula && !isBadExpression(cell.content)) {
34126
+ return cell;
34127
+ }
34128
+ return undefined;
34129
+ }
34130
+ const spreadingFormulaPosition = this.evaluator.getArrayFormulaSpreadingOn(position);
34131
+ if (spreadingFormulaPosition === undefined) {
34132
+ return undefined;
34133
+ }
34134
+ const spreadingFormulaCell = this.getters.getCell(spreadingFormulaPosition);
34135
+ if (spreadingFormulaCell?.isFormula) {
34136
+ return spreadingFormulaCell;
34137
+ }
34138
+ return undefined;
34139
+ }
34140
+ }
34141
+ function isBadExpression(formula) {
34142
+ try {
34143
+ compile(formula);
34144
+ return false;
34145
+ }
34146
+ catch (error) {
34147
+ return true;
34148
+ }
34149
+ }
34150
+
33081
34151
  /**
33082
34152
  * https://tomekdev.com/posts/sorting-colors-in-js
33083
34153
  */
@@ -33228,329 +34298,6 @@ class CustomColorsPlugin extends UIPlugin {
33228
34298
  }
33229
34299
  }
33230
34300
 
33231
- const functionMap = functionRegistry.mapping;
33232
- class EvaluationPlugin extends UIPlugin {
33233
- static getters = [
33234
- "evaluateFormula",
33235
- "getRangeFormattedValues",
33236
- "getRangeValues",
33237
- "getRangeFormats",
33238
- "getEvaluatedCell",
33239
- "getEvaluatedCells",
33240
- "getColEvaluatedCells",
33241
- "getEvaluatedCellsInZone",
33242
- ];
33243
- isUpToDate = false;
33244
- evaluatedCells = {};
33245
- evalContext;
33246
- lazyEvaluation;
33247
- constructor(config) {
33248
- super(config);
33249
- this.evalContext = config.custom;
33250
- this.lazyEvaluation = config.lazyEvaluation;
33251
- }
33252
- // ---------------------------------------------------------------------------
33253
- // Command Handling
33254
- // ---------------------------------------------------------------------------
33255
- handle(cmd) {
33256
- if (invalidateEvaluationCommands.has(cmd.type)) {
33257
- this.isUpToDate = false;
33258
- }
33259
- switch (cmd.type) {
33260
- case "UPDATE_CELL":
33261
- if ("content" in cmd || "format" in cmd) {
33262
- this.isUpToDate = false;
33263
- }
33264
- break;
33265
- case "EVALUATE_CELLS":
33266
- this.evaluate();
33267
- break;
33268
- }
33269
- }
33270
- finalize() {
33271
- if (!this.isUpToDate) {
33272
- this.evaluate();
33273
- this.isUpToDate = true;
33274
- }
33275
- }
33276
- // ---------------------------------------------------------------------------
33277
- // Getters
33278
- // ---------------------------------------------------------------------------
33279
- evaluateFormula(formulaString, sheetId = this.getters.getActiveSheetId()) {
33280
- const compiledFormula = compile(formulaString);
33281
- const params = this.getCompilationParameters((cell) => this.getEvaluatedCell(this.getters.getCellPosition(cell.id)));
33282
- const ranges = [];
33283
- for (let xc of compiledFormula.dependencies) {
33284
- ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));
33285
- }
33286
- return compiledFormula.execute(ranges, ...params).value;
33287
- }
33288
- /**
33289
- * Return the value of each cell in the range as they are displayed in the grid.
33290
- */
33291
- getRangeFormattedValues(range) {
33292
- const sheet = this.getters.tryGetSheet(range.sheetId);
33293
- if (sheet === undefined)
33294
- return [];
33295
- return this.getters
33296
- .getEvaluatedCellsInZone(sheet.id, range.zone)
33297
- .map((cell) => cell.formattedValue);
33298
- }
33299
- /**
33300
- * Return the value of each cell in the range.
33301
- */
33302
- getRangeValues(range) {
33303
- const sheet = this.getters.tryGetSheet(range.sheetId);
33304
- if (sheet === undefined)
33305
- return [];
33306
- return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.value);
33307
- }
33308
- /**
33309
- * Return the format of each cell in the range.
33310
- */
33311
- getRangeFormats(range) {
33312
- const sheet = this.getters.tryGetSheet(range.sheetId);
33313
- if (sheet === undefined)
33314
- return [];
33315
- return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.format);
33316
- }
33317
- getEvaluatedCell({ sheetId, col, row }) {
33318
- const cell = this.getters.getCell({ sheetId, col, row });
33319
- if (cell === undefined) {
33320
- return createEvaluatedCell("");
33321
- }
33322
- // the cell might have been created by a command in the current
33323
- // dispatch but the evaluation is not done yet.
33324
- return this.evaluatedCells[sheetId]?.[col]?.[row]?.() || createEvaluatedCell("");
33325
- }
33326
- getEvaluatedCells(sheetId) {
33327
- const rawCells = this.getters.getCells(sheetId) || {};
33328
- const record = {};
33329
- for (let cellId of Object.keys(rawCells)) {
33330
- const position = this.getters.getCellPosition(cellId);
33331
- record[cellId] = this.getEvaluatedCell(position);
33332
- }
33333
- return record;
33334
- }
33335
- /**
33336
- * Returns all the evaluated cells of a col
33337
- */
33338
- getColEvaluatedCells(sheetId, col) {
33339
- return Object.values(this.evaluatedCells[sheetId]?.[col] || [])
33340
- .filter(isDefined$1)
33341
- .map((lazyCell) => lazyCell());
33342
- }
33343
- getEvaluatedCellsInZone(sheetId, zone) {
33344
- return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
33345
- }
33346
- // ---------------------------------------------------------------------------
33347
- // Evaluator
33348
- // ---------------------------------------------------------------------------
33349
- setEvaluatedCell(cellId, evaluatedCell) {
33350
- const { col, row, sheetId } = this.getters.getCellPosition(cellId);
33351
- if (!this.evaluatedCells[sheetId]) {
33352
- this.evaluatedCells[sheetId] = {};
33353
- }
33354
- if (!this.evaluatedCells[sheetId][col]) {
33355
- this.evaluatedCells[sheetId][col] = {};
33356
- }
33357
- this.evaluatedCells[sheetId][col][row] = evaluatedCell;
33358
- if (!this.lazyEvaluation) {
33359
- this.evaluatedCells[sheetId][col][row]();
33360
- }
33361
- }
33362
- *getAllCells() {
33363
- // use a generator function to avoid re-building a new object
33364
- for (const sheetId of this.getters.getSheetIds()) {
33365
- const cells = this.getters.getCells(sheetId);
33366
- for (const cellId in cells) {
33367
- yield cells[cellId];
33368
- }
33369
- }
33370
- }
33371
- evaluate() {
33372
- this.evaluatedCells = {};
33373
- const cellsBeingComputed = new Set();
33374
- const computeCell = (cell) => {
33375
- const cellId = cell.id;
33376
- const { col, row, sheetId } = this.getters.getCellPosition(cellId);
33377
- const lazyEvaluation = this.evaluatedCells[sheetId]?.[col]?.[row];
33378
- if (lazyEvaluation) {
33379
- return lazyEvaluation; // already computed
33380
- }
33381
- return lazy(() => {
33382
- try {
33383
- switch (cell.isFormula) {
33384
- case true:
33385
- return computeFormulaCell(cell);
33386
- case false:
33387
- return evaluateLiteral(cell.content, cell.format);
33388
- }
33389
- }
33390
- catch (e) {
33391
- return handleError(e, cell);
33392
- }
33393
- });
33394
- };
33395
- const handleError = (e, cell) => {
33396
- if (!(e instanceof Error)) {
33397
- e = new Error(e);
33398
- }
33399
- const msg = e?.errorType || CellErrorType.GenericError;
33400
- // apply function name
33401
- const __lastFnCalled = compilationParameters[2].__lastFnCalled || "";
33402
- const error = new EvaluationError(msg, e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error);
33403
- return errorCell(cell.content, error);
33404
- };
33405
- const computeFormulaCell = (cellData) => {
33406
- const cellId = cellData.id;
33407
- if (cellsBeingComputed.has(cellId)) {
33408
- throw new CircularDependencyError();
33409
- }
33410
- compilationParameters[2].__originCellXC = () => {
33411
- // compute the value lazily for performance reasons
33412
- const position = compilationParameters[2].getters.getCellPosition(cellId);
33413
- return toXC(position.col, position.row);
33414
- };
33415
- cellsBeingComputed.add(cellId);
33416
- const computedCell = cellData.compiledFormula.execute(cellData.dependencies, ...compilationParameters);
33417
- cellsBeingComputed.delete(cellId);
33418
- if (Array.isArray(computedCell.value)) {
33419
- // if a value returns an array (like =A1:A3)
33420
- throw new Error(_lt("This formula depends on invalid values"));
33421
- }
33422
- return createEvaluatedCell(computedCell.value, cellData.format || computedCell.format);
33423
- };
33424
- const compilationParameters = this.getCompilationParameters((cell) => computeCell(cell)());
33425
- for (const cell of this.getAllCells()) {
33426
- this.setEvaluatedCell(cell.id, computeCell(cell));
33427
- }
33428
- }
33429
- /**
33430
- * Return all functions necessary to properly evaluate a formula:
33431
- * - a refFn function to read any reference, cell or range of a normalized formula
33432
- * - a range function to convert any reference to a proper value array
33433
- * - an evaluation context
33434
- */
33435
- getCompilationParameters(computeCell) {
33436
- const evalContext = Object.assign(Object.create(functionMap), this.evalContext, {
33437
- getters: this.getters,
33438
- });
33439
- const getters = this.getters;
33440
- function readCell(range) {
33441
- let cell;
33442
- if (!getters.tryGetSheet(range.sheetId)) {
33443
- throw new Error(_lt("Invalid sheet name"));
33444
- }
33445
- cell = getters.getCell({ sheetId: range.sheetId, col: range.zone.left, row: range.zone.top });
33446
- if (!cell || cell.content === "") {
33447
- // magic "empty" value
33448
- // Returning {value: null} instead of undefined will ensure that we don't
33449
- // fall back on the default value of the argument provided to the formula's compute function
33450
- return { value: null, format: cell?.format };
33451
- }
33452
- return getEvaluatedCell(cell);
33453
- }
33454
- const getEvaluatedCell = (cell) => {
33455
- const evaluatedCell = computeCell(cell);
33456
- if (evaluatedCell.type === CellValueType.error) {
33457
- throw evaluatedCell.error;
33458
- }
33459
- return evaluatedCell;
33460
- };
33461
- /**
33462
- * Return the values of the cell(s) used in reference, but always in the format of a range even
33463
- * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
33464
- * range<number> etc.
33465
- *
33466
- * Note that each col is possibly sparse: it only contain the values of cells
33467
- * that are actually present in the grid.
33468
- */
33469
- function range(range) {
33470
- const sheetId = range.sheetId;
33471
- if (!isZoneValid(range.zone)) {
33472
- throw new InvalidReferenceError();
33473
- }
33474
- // Performance issue: Avoid fetching data on positions that are out of the spreadsheet
33475
- // e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array
33476
- const sheetZone = getters.getSheetZone(sheetId);
33477
- const result = [];
33478
- const zone = intersection(range.zone, sheetZone);
33479
- if (!zone) {
33480
- result.push([]);
33481
- return result;
33482
- }
33483
- // Performance issue: nested loop is faster than a map here
33484
- for (let col = zone.left; col <= zone.right; col++) {
33485
- const rowValues = [];
33486
- for (let row = zone.top; row <= zone.bottom; row++) {
33487
- const cell = evalContext.getters.getCell({ sheetId: range.sheetId, col, row });
33488
- rowValues.push(cell ? getEvaluatedCell(cell) : undefined);
33489
- }
33490
- result.push(rowValues);
33491
- }
33492
- return result;
33493
- }
33494
- /**
33495
- * Returns the value of the cell(s) used in reference
33496
- *
33497
- * @param range the references used
33498
- * @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the
33499
- * function for which this parameter is used, we just return the string of the parameter.
33500
- * The `compute` of the formula's function must process it completely
33501
- */
33502
- function refFn(range, isMeta, functionName, paramNumber) {
33503
- if (isMeta) {
33504
- // Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
33505
- return { value: zoneToXc(range.zone) };
33506
- }
33507
- if (!isZoneValid(range.zone)) {
33508
- throw new InvalidReferenceError();
33509
- }
33510
- // if the formula definition could have accepted a range, we would pass through the _range function and not here
33511
- if (range.zone.bottom !== range.zone.top || range.zone.left !== range.zone.right) {
33512
- throw new Error(paramNumber
33513
- ? _lt("Function %s expects the parameter %s to be a single value or a single cell reference, not a range.", functionName.toString(), paramNumber.toString())
33514
- : _lt("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
33515
- }
33516
- if (range.invalidSheetName) {
33517
- throw new Error(_lt("Invalid sheet name: %s", range.invalidSheetName));
33518
- }
33519
- return readCell(range);
33520
- }
33521
- return [refFn, range, evalContext];
33522
- }
33523
- // ---------------------------------------------------------------------------
33524
- // Export
33525
- // ---------------------------------------------------------------------------
33526
- exportForExcel(data) {
33527
- for (let sheet of data.sheets) {
33528
- for (const xc in sheet.cells) {
33529
- const position = { sheetId: sheet.id, ...toCartesian(xc) };
33530
- const cell = this.getters.getCell(position);
33531
- if (cell) {
33532
- const exportedCellData = sheet.cells[xc];
33533
- const evaluatedCell = this.getEvaluatedCell(position);
33534
- exportedCellData.value = evaluatedCell.value;
33535
- exportedCellData.isFormula = cell.isFormula && !this.isBadExpression(cell.content);
33536
- if (cell.format !== evaluatedCell.format) {
33537
- exportedCellData.computedFormat = evaluatedCell.format;
33538
- }
33539
- }
33540
- }
33541
- }
33542
- }
33543
- isBadExpression(formula) {
33544
- try {
33545
- compile(formula);
33546
- return false;
33547
- }
33548
- catch (error) {
33549
- return true;
33550
- }
33551
- }
33552
- }
33553
-
33554
34301
  class EvaluationChartPlugin extends UIPlugin {
33555
34302
  static getters = ["getChartRuntime", "getBackgroundOfSingleCellChart"];
33556
34303
  charts = {};
@@ -37963,6 +38710,7 @@ class SelectionInputPlugin extends UIPlugin {
37963
38710
  // ---------------------------------------------------------------------------
37964
38711
  allowDispatch(cmd) {
37965
38712
  switch (cmd.type) {
38713
+ case "ADD_RANGE":
37966
38714
  case "ADD_EMPTY_RANGE":
37967
38715
  if (this.inputHasSingleRange && this.ranges.length === 1) {
37968
38716
  return 30 /* CommandResult.MaximumRangesReached */;
@@ -38007,6 +38755,10 @@ class SelectionInputPlugin extends UIPlugin {
38007
38755
  this.insertNewRange(this.ranges.length, [""]);
38008
38756
  this.focusLast();
38009
38757
  break;
38758
+ case "ADD_RANGE":
38759
+ this.insertNewRange(this.ranges.length, [cmd.value]);
38760
+ this.focusLast();
38761
+ break;
38010
38762
  case "REMOVE_RANGE":
38011
38763
  const index = this.getIndex(cmd.rangeId);
38012
38764
  if (index !== null) {
@@ -38218,6 +38970,7 @@ class SelectionInputsManagerPlugin extends UIPlugin {
38218
38970
  case "UNFOCUS_SELECTION_INPUT":
38219
38971
  this.unfocus();
38220
38972
  break;
38973
+ case "ADD_RANGE":
38221
38974
  case "ADD_EMPTY_RANGE":
38222
38975
  case "REMOVE_RANGE":
38223
38976
  if (cmd.id !== this.focusedInputId) {
@@ -38231,9 +38984,11 @@ class SelectionInputsManagerPlugin extends UIPlugin {
38231
38984
  if (cmd.id !== this.focusedInputId) {
38232
38985
  const input = this.inputs[cmd.id];
38233
38986
  const range = input.ranges.find((range) => range.id === cmd.rangeId);
38234
- const sheetId = this.getters.getActiveSheetId();
38235
- const zone = this.getters.getRangeFromSheetXC(sheetId, range?.xc || "A1").zone;
38236
- this.selection.capture(input, { cell: { col: zone.left, row: zone.top }, zone }, { handleEvent: input.handleEvent.bind(input) });
38987
+ if (this.isRangeValid(range?.xc || "A1")) {
38988
+ const sheetId = this.getters.getActiveSheetId();
38989
+ const zone = this.getters.getRangeFromSheetXC(sheetId, range?.xc || "A1").zone;
38990
+ this.selection.capture(input, { cell: { col: zone.left, row: zone.top }, zone }, { handleEvent: input.handleEvent.bind(input) });
38991
+ }
38237
38992
  this.focusedInputId = cmd.id;
38238
38993
  }
38239
38994
  break;
@@ -38664,7 +39419,8 @@ class SheetUIPlugin extends UIPlugin {
38664
39419
  getCellWidth(position) {
38665
39420
  const text = this.getCellText(position);
38666
39421
  const style = this.getters.getCellComputedStyle(position);
38667
- let contentWidth = this.getTextWidth(text, style);
39422
+ const multiLineText = text.split(NEWLINE);
39423
+ let contentWidth = Math.max(...multiLineText.map((line) => this.getTextWidth(line, style)));
38668
39424
  const icon = this.getters.getConditionalIcon(position);
38669
39425
  if (icon) {
38670
39426
  contentWidth += computeIconWidth(this.getters.getCellStyle(position));
@@ -43793,6 +44549,10 @@ class Spreadsheet extends Component {
43793
44549
  }
43794
44550
  checkViewportSize() {
43795
44551
  const { xRatio, yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());
44552
+ if (!isFinite(xRatio) || !isFinite(yRatio)) {
44553
+ // before mounting, the ratios can be NaN or Infinity if the viewport size is 0
44554
+ return;
44555
+ }
43796
44556
  if (yRatio > MAXIMAL_FREEZABLE_RATIO || xRatio > MAXIMAL_FREEZABLE_RATIO) {
43797
44557
  if (this.isViewportTooSmall) {
43798
44558
  return;
@@ -45764,44 +46524,30 @@ function numberRef(reference) {
45764
46524
 
45765
46525
  function addFormula(cell) {
45766
46526
  const formula = cell.content;
45767
- const functions = functionRegistry.content;
45768
- const tokens = tokenize(formula);
45769
46527
  const attrs = [];
45770
46528
  let node = escapeXml ``;
45771
- const isExported = tokens
45772
- .filter((tk) => tk.type === "FUNCTION")
45773
- .every((tk) => functions[tk.value.toUpperCase()].isExported);
45774
- if (isExported) {
45775
- let cycle = escapeXml ``;
45776
- const XlsxFormula = adaptFormulaToExcel(formula);
45777
- // hack for cycles : if we don't set a value (be it 0 or #VALUE!), it will appear as invisible on excel,
45778
- // Making it very hard for the client to find where the recursion is.
45779
- if (cell.value === CellErrorType.CircularDependency) {
45780
- attrs.push(["t", "str"]);
45781
- cycle = escapeXml /*xml*/ `<v>${cell.value}</v>`;
45782
- }
45783
- node = escapeXml /*xml*/ `
46529
+ let cycle = escapeXml ``;
46530
+ const XlsxFormula = adaptFormulaToExcel(formula);
46531
+ // hack for cycles : if we don't set a value (be it 0 or #VALUE!), it will appear as invisible on excel,
46532
+ // Making it very hard for the client to find where the recursion is.
46533
+ if (cell.value === CellErrorType.CircularDependency) {
46534
+ attrs.push(["t", "str"]);
46535
+ cycle = escapeXml /*xml*/ `<v>${cell.value}</v>`;
46536
+ }
46537
+ node = escapeXml /*xml*/ `
45784
46538
  <f>
45785
46539
  ${XlsxFormula}
45786
46540
  </f>
45787
46541
  ${cycle}
45788
46542
  `;
45789
- return { attrs, node };
45790
- }
45791
- else {
45792
- // Shouldn't we always output the value then ?
45793
- const value = cell.value;
45794
- const type = getCellType(value);
45795
- attrs.push(["t", type]);
45796
- node = escapeXml /*xml*/ `<v>${value}</v>`;
45797
- return { attrs, node };
45798
- }
46543
+ return { attrs, node };
45799
46544
  }
45800
46545
  function addContent(content, sharedStrings, forceString = false) {
45801
46546
  let value = content;
45802
46547
  const attrs = [];
45803
- if (!forceString && ["TRUE", "FALSE"].includes(value.trim())) {
45804
- value = value === "TRUE" ? "1" : "0";
46548
+ const clearValue = value.trim().toUpperCase();
46549
+ if (!forceString && ["TRUE", "FALSE"].includes(clearValue)) {
46550
+ value = clearValue === "TRUE" ? "1" : "0";
45805
46551
  attrs.push(["t", "b"]);
45806
46552
  }
45807
46553
  else if (forceString || !isNumber(value)) {
@@ -47192,7 +47938,6 @@ class Model extends EventBus {
47192
47938
  moveClient: () => { },
47193
47939
  snapshotRequested: false,
47194
47940
  notifyUI: (payload) => this.trigger("notify-ui", payload),
47195
- lazyEvaluation: "lazyEvaluation" in config ? config.lazyEvaluation : true,
47196
47941
  };
47197
47942
  }
47198
47943
  setupCorePluginConfig() {
@@ -47215,7 +47960,6 @@ class Model extends EventBus {
47215
47960
  moveClient: this.session.move.bind(this.session),
47216
47961
  custom: this.config.custom,
47217
47962
  uiActions: this.config,
47218
- lazyEvaluation: this.config.lazyEvaluation,
47219
47963
  session: this.session,
47220
47964
  };
47221
47965
  }
@@ -47523,9 +48267,9 @@ function addFunction(functionName, functionDescription) {
47523
48267
  functionRegistry.add(functionName, functionDescription);
47524
48268
  }
47525
48269
 
47526
- export { AbstractChart, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, components, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, invalidateEvaluationCommands, links, load, parse, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
48270
+ export { AbstractChart, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, components, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, links, load, parse, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
47527
48271
 
47528
48272
 
47529
- __info__.version = '16.4.0-alpha.0';
47530
- __info__.date = '2023-05-17T15:49:04.448Z';
47531
- __info__.hash = '3a3aab7';
48273
+ __info__.version = '16.4.0-alpha.2';
48274
+ __info__.date = '2023-05-26T13:40:49.561Z';
48275
+ __info__.hash = '82328ad';