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