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