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