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