@odoo/o-spreadsheet 17.0.16 → 17.0.17
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 +90 -50
- package/dist/o-spreadsheet.esm.js +90 -50
- package/dist/o-spreadsheet.iife.js +90 -50
- package/dist/o-spreadsheet.iife.min.js +266 -266
- package/dist/o_spreadsheet.xml +5 -5
- package/package.json +2 -2
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* This file is generated by o-spreadsheet build tools. Do not edit it.
|
|
4
4
|
* @see https://github.com/odoo/o-spreadsheet
|
|
5
|
-
* @version 17.0.
|
|
6
|
-
* @date 2024-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 17.0.17
|
|
6
|
+
* @date 2024-04-05T13:59:37.139Z
|
|
7
|
+
* @hash 33f09d7
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
(function (exports, owl) {
|
|
@@ -558,7 +558,7 @@
|
|
|
558
558
|
}
|
|
559
559
|
// Generate new Id if the item didn't exist in the dictionary
|
|
560
560
|
const ids = Object.keys(itemsDic);
|
|
561
|
-
const maxId = ids.length === 0 ? 0 :
|
|
561
|
+
const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
|
|
562
562
|
itemsDic[maxId + 1] = item;
|
|
563
563
|
return maxId + 1;
|
|
564
564
|
}
|
|
@@ -775,6 +775,34 @@
|
|
|
775
775
|
}
|
|
776
776
|
return value >= min && value <= max;
|
|
777
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* Alternative to Math.max that works with large arrays.
|
|
780
|
+
* Typically useful for arrays bigger than 100k elements.
|
|
781
|
+
*/
|
|
782
|
+
function largeMax(array) {
|
|
783
|
+
let len = array.length;
|
|
784
|
+
if (len < 100000)
|
|
785
|
+
return Math.max(...array);
|
|
786
|
+
let max = -Infinity;
|
|
787
|
+
while (len--) {
|
|
788
|
+
max = array[len] > max ? array[len] : max;
|
|
789
|
+
}
|
|
790
|
+
return max;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Alternative to Math.min that works with large arrays.
|
|
794
|
+
* Typically useful for arrays bigger than 100k elements.
|
|
795
|
+
*/
|
|
796
|
+
function largeMin(array) {
|
|
797
|
+
let len = array.length;
|
|
798
|
+
if (len < 100000)
|
|
799
|
+
return Math.min(...array);
|
|
800
|
+
let min = +Infinity;
|
|
801
|
+
while (len--) {
|
|
802
|
+
min = array[len] < min ? array[len] : min;
|
|
803
|
+
}
|
|
804
|
+
return min;
|
|
805
|
+
}
|
|
778
806
|
|
|
779
807
|
const RBA_REGEX = /rgba?\(|\s+|\)/gi;
|
|
780
808
|
const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
|
|
@@ -4421,8 +4449,9 @@
|
|
|
4421
4449
|
* Get the default height of the cell given its style.
|
|
4422
4450
|
*/
|
|
4423
4451
|
function getDefaultCellHeight(ctx, cell, colSize) {
|
|
4424
|
-
if (!cell || !cell.content)
|
|
4452
|
+
if (!cell || (!cell.isFormula && !cell.content)) {
|
|
4425
4453
|
return DEFAULT_CELL_HEIGHT;
|
|
4454
|
+
}
|
|
4426
4455
|
const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
|
|
4427
4456
|
const numberOfLines = cell.isFormula
|
|
4428
4457
|
? 1
|
|
@@ -8325,10 +8354,10 @@
|
|
|
8325
8354
|
}
|
|
8326
8355
|
}
|
|
8327
8356
|
return {
|
|
8328
|
-
labels:
|
|
8357
|
+
labels: Array.from(labelSet),
|
|
8329
8358
|
dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
|
|
8330
8359
|
...dataset,
|
|
8331
|
-
data:
|
|
8360
|
+
data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
|
|
8332
8361
|
})),
|
|
8333
8362
|
};
|
|
8334
8363
|
}
|
|
@@ -8379,7 +8408,7 @@
|
|
|
8379
8408
|
const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
|
|
8380
8409
|
// tooltipItem.parsed.y can be an object or a number for pie charts
|
|
8381
8410
|
const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;
|
|
8382
|
-
const toolTipFormat = !format && yLabel
|
|
8411
|
+
const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
|
|
8383
8412
|
const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
|
|
8384
8413
|
return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
|
|
8385
8414
|
},
|
|
@@ -8665,7 +8694,10 @@
|
|
|
8665
8694
|
if (isNaN(value))
|
|
8666
8695
|
return value;
|
|
8667
8696
|
const { locale, format } = localeFormat;
|
|
8668
|
-
return formatValue(value, {
|
|
8697
|
+
return formatValue(value, {
|
|
8698
|
+
locale,
|
|
8699
|
+
format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
|
|
8700
|
+
});
|
|
8669
8701
|
},
|
|
8670
8702
|
},
|
|
8671
8703
|
},
|
|
@@ -9079,7 +9111,7 @@
|
|
|
9079
9111
|
return undefined;
|
|
9080
9112
|
}
|
|
9081
9113
|
const labelsTimestamps = labelDates.map((date) => date.getTime());
|
|
9082
|
-
const period =
|
|
9114
|
+
const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);
|
|
9083
9115
|
const minUnit = getFormatMinDisplayUnit(format);
|
|
9084
9116
|
if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
|
|
9085
9117
|
return "second";
|
|
@@ -9316,7 +9348,10 @@
|
|
|
9316
9348
|
if (isNaN(value))
|
|
9317
9349
|
return value;
|
|
9318
9350
|
const { locale, format } = localeFormat;
|
|
9319
|
-
return formatValue(value, {
|
|
9351
|
+
return formatValue(value, {
|
|
9352
|
+
locale,
|
|
9353
|
+
format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
|
|
9354
|
+
});
|
|
9320
9355
|
},
|
|
9321
9356
|
},
|
|
9322
9357
|
},
|
|
@@ -9543,7 +9578,7 @@
|
|
|
9543
9578
|
const percentage = calculatePercentage(data, dataIndex);
|
|
9544
9579
|
const xLabel = tooltipItem.label || tooltipItem.dataset.label;
|
|
9545
9580
|
const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;
|
|
9546
|
-
const toolTipFormat = !format && yLabel
|
|
9581
|
+
const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
|
|
9547
9582
|
const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
|
|
9548
9583
|
return xLabel ? `${xLabel}: ${yLabelStr} (${percentage}%)` : `${yLabelStr} (${percentage}%)`;
|
|
9549
9584
|
};
|
|
@@ -9551,7 +9586,7 @@
|
|
|
9551
9586
|
}
|
|
9552
9587
|
function getPieColors(colors, dataSetsValues) {
|
|
9553
9588
|
const pieColors = [];
|
|
9554
|
-
const maxLength =
|
|
9589
|
+
const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
|
|
9555
9590
|
for (let i = 0; i <= maxLength; i++) {
|
|
9556
9591
|
pieColors.push(colors.next());
|
|
9557
9592
|
}
|
|
@@ -10453,8 +10488,8 @@
|
|
|
10453
10488
|
let last;
|
|
10454
10489
|
const activesRows = env.model.getters.getActiveRows();
|
|
10455
10490
|
if (activesRows.size !== 0) {
|
|
10456
|
-
first =
|
|
10457
|
-
last =
|
|
10491
|
+
first = largeMin([...activesRows]);
|
|
10492
|
+
last = largeMax([...activesRows]);
|
|
10458
10493
|
}
|
|
10459
10494
|
else {
|
|
10460
10495
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -10482,8 +10517,8 @@
|
|
|
10482
10517
|
let last;
|
|
10483
10518
|
const activeCols = env.model.getters.getActiveCols();
|
|
10484
10519
|
if (activeCols.size !== 0) {
|
|
10485
|
-
first =
|
|
10486
|
-
last =
|
|
10520
|
+
first = largeMin([...activeCols]);
|
|
10521
|
+
last = largeMax([...activeCols]);
|
|
10487
10522
|
}
|
|
10488
10523
|
else {
|
|
10489
10524
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -10511,8 +10546,8 @@
|
|
|
10511
10546
|
let last;
|
|
10512
10547
|
const activesRows = env.model.getters.getActiveRows();
|
|
10513
10548
|
if (activesRows.size !== 0) {
|
|
10514
|
-
first =
|
|
10515
|
-
last =
|
|
10549
|
+
first = largeMin([...activesRows]);
|
|
10550
|
+
last = largeMax([...activesRows]);
|
|
10516
10551
|
}
|
|
10517
10552
|
else {
|
|
10518
10553
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -10553,8 +10588,8 @@
|
|
|
10553
10588
|
let last;
|
|
10554
10589
|
const activeCols = env.model.getters.getActiveCols();
|
|
10555
10590
|
if (activeCols.size !== 0) {
|
|
10556
|
-
first =
|
|
10557
|
-
last =
|
|
10591
|
+
first = largeMin([...activeCols]);
|
|
10592
|
+
last = largeMax([...activeCols]);
|
|
10558
10593
|
}
|
|
10559
10594
|
else {
|
|
10560
10595
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -10595,7 +10630,7 @@
|
|
|
10595
10630
|
let row;
|
|
10596
10631
|
let quantity;
|
|
10597
10632
|
if (activeRows.size) {
|
|
10598
|
-
row =
|
|
10633
|
+
row = largeMin([...activeRows]);
|
|
10599
10634
|
quantity = activeRows.size;
|
|
10600
10635
|
}
|
|
10601
10636
|
else {
|
|
@@ -10616,7 +10651,7 @@
|
|
|
10616
10651
|
let row;
|
|
10617
10652
|
let quantity;
|
|
10618
10653
|
if (activeRows.size) {
|
|
10619
|
-
row =
|
|
10654
|
+
row = largeMax([...activeRows]);
|
|
10620
10655
|
quantity = activeRows.size;
|
|
10621
10656
|
}
|
|
10622
10657
|
else {
|
|
@@ -10637,7 +10672,7 @@
|
|
|
10637
10672
|
let column;
|
|
10638
10673
|
let quantity;
|
|
10639
10674
|
if (activeCols.size) {
|
|
10640
|
-
column =
|
|
10675
|
+
column = largeMin([...activeCols]);
|
|
10641
10676
|
quantity = activeCols.size;
|
|
10642
10677
|
}
|
|
10643
10678
|
else {
|
|
@@ -10658,7 +10693,7 @@
|
|
|
10658
10693
|
let column;
|
|
10659
10694
|
let quantity;
|
|
10660
10695
|
if (activeCols.size) {
|
|
10661
|
-
column =
|
|
10696
|
+
column = largeMax([...activeCols]);
|
|
10662
10697
|
quantity = activeCols.size;
|
|
10663
10698
|
}
|
|
10664
10699
|
else {
|
|
@@ -31855,7 +31890,7 @@
|
|
|
31855
31890
|
function getSheetDims(sheet) {
|
|
31856
31891
|
const dims = [0, 0];
|
|
31857
31892
|
for (let row of sheet.rows) {
|
|
31858
|
-
dims[0] = Math.max(dims[0],
|
|
31893
|
+
dims[0] = Math.max(dims[0], largeMax(row.cells.map((cell) => toCartesian(cell.xc).col)));
|
|
31859
31894
|
dims[1] = Math.max(dims[1], row.index);
|
|
31860
31895
|
}
|
|
31861
31896
|
dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
|
|
@@ -36745,6 +36780,9 @@
|
|
|
36745
36780
|
if (newRule.criterion.type === "isBoolean") {
|
|
36746
36781
|
this.setCenterStyleToBooleanCells(newRule);
|
|
36747
36782
|
}
|
|
36783
|
+
else if (newRule.criterion.type === "isValueInList") {
|
|
36784
|
+
newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
|
|
36785
|
+
}
|
|
36748
36786
|
const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
|
|
36749
36787
|
const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
|
|
36750
36788
|
if (ruleIndex !== -1) {
|
|
@@ -37471,7 +37509,7 @@
|
|
|
37471
37509
|
if (hiddenElements.size >= elements) {
|
|
37472
37510
|
return "TooManyHiddenElements" /* CommandResult.TooManyHiddenElements */;
|
|
37473
37511
|
}
|
|
37474
|
-
else if (
|
|
37512
|
+
else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
|
|
37475
37513
|
return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
|
|
37476
37514
|
}
|
|
37477
37515
|
else {
|
|
@@ -38286,8 +38324,8 @@
|
|
|
38286
38324
|
let newRange = range;
|
|
38287
38325
|
let changeType = "NONE";
|
|
38288
38326
|
for (let group of groups) {
|
|
38289
|
-
const min =
|
|
38290
|
-
const max =
|
|
38327
|
+
const min = largeMin(group);
|
|
38328
|
+
const max = largeMax(group);
|
|
38291
38329
|
if (range.zone[start] <= min && min <= range.zone[end]) {
|
|
38292
38330
|
const toRemove = Math.min(range.zone[end], max) - min + 1;
|
|
38293
38331
|
changeType = "RESIZE";
|
|
@@ -38710,8 +38748,8 @@
|
|
|
38710
38748
|
}
|
|
38711
38749
|
return "Success" /* CommandResult.Success */;
|
|
38712
38750
|
case "REMOVE_COLUMNS_ROWS": {
|
|
38713
|
-
const min =
|
|
38714
|
-
const max =
|
|
38751
|
+
const min = largeMin(cmd.elements);
|
|
38752
|
+
const max = largeMax(cmd.elements);
|
|
38715
38753
|
if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
|
|
38716
38754
|
return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
|
|
38717
38755
|
}
|
|
@@ -42043,13 +42081,13 @@
|
|
|
42043
42081
|
.map((cell) => cell.value);
|
|
42044
42082
|
switch (threshold.type) {
|
|
42045
42083
|
case "value":
|
|
42046
|
-
const result = functionName === "max" ?
|
|
42084
|
+
const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
|
|
42047
42085
|
return result;
|
|
42048
42086
|
case "number":
|
|
42049
42087
|
return Number(threshold.value);
|
|
42050
42088
|
case "percentage":
|
|
42051
|
-
const min =
|
|
42052
|
-
const max =
|
|
42089
|
+
const min = largeMin(rangeValues);
|
|
42090
|
+
const max = largeMax(rangeValues);
|
|
42053
42091
|
const delta = max - min;
|
|
42054
42092
|
return min + (delta * Number(threshold.value)) / 100;
|
|
42055
42093
|
case "percentile":
|
|
@@ -43101,13 +43139,13 @@
|
|
|
43101
43139
|
const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
|
|
43102
43140
|
const cellPositions = range(end, -1, -1);
|
|
43103
43141
|
const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
|
|
43104
|
-
const maxValidPosition =
|
|
43142
|
+
const maxValidPosition = largeMax(invalidCells);
|
|
43105
43143
|
const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
|
|
43106
43144
|
const firstSequence = numberSequences[0] || [];
|
|
43107
|
-
if (
|
|
43145
|
+
if (largeMax(firstSequence) < maxValidPosition) {
|
|
43108
43146
|
return Infinity;
|
|
43109
43147
|
}
|
|
43110
|
-
return
|
|
43148
|
+
return largeMin(firstSequence);
|
|
43111
43149
|
}
|
|
43112
43150
|
shouldFindData(sheetId, zone) {
|
|
43113
43151
|
return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
|
|
@@ -45511,7 +45549,7 @@
|
|
|
45511
45549
|
* column of the current viewport
|
|
45512
45550
|
*/
|
|
45513
45551
|
getColDimensionsInViewport(sheetId, col) {
|
|
45514
|
-
const left =
|
|
45552
|
+
const left = largeMin(this.getters.getSheetViewVisibleCols());
|
|
45515
45553
|
const start = this.getters.getColRowOffsetInViewport("COL", left, col);
|
|
45516
45554
|
const size = this.getters.getColSize(sheetId, col);
|
|
45517
45555
|
const isColHidden = this.getters.isColHidden(sheetId, col);
|
|
@@ -45526,7 +45564,7 @@
|
|
|
45526
45564
|
* of the current viewport
|
|
45527
45565
|
*/
|
|
45528
45566
|
getRowDimensionsInViewport(sheetId, row) {
|
|
45529
|
-
const top =
|
|
45567
|
+
const top = largeMin(this.getters.getSheetViewVisibleRows());
|
|
45530
45568
|
const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
|
|
45531
45569
|
const size = this.getters.getRowSize(sheetId, row);
|
|
45532
45570
|
const isRowHidden = this.getters.isRowHidden(sheetId, row);
|
|
@@ -46990,7 +47028,7 @@
|
|
|
46990
47028
|
getColMaxWidth(sheetId, index) {
|
|
46991
47029
|
const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
|
|
46992
47030
|
const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
|
|
46993
|
-
return Math.max(0,
|
|
47031
|
+
return Math.max(0, largeMax(sizes));
|
|
46994
47032
|
}
|
|
46995
47033
|
/**
|
|
46996
47034
|
* Check that any "sheetId" in the command matches an existing
|
|
@@ -47755,7 +47793,7 @@
|
|
|
47755
47793
|
}
|
|
47756
47794
|
getPasteZone(target) {
|
|
47757
47795
|
const height = this.values.length;
|
|
47758
|
-
const width =
|
|
47796
|
+
const width = largeMax(this.values.map((a) => a.length));
|
|
47759
47797
|
const { left: activeCol, top: activeRow } = target[0];
|
|
47760
47798
|
return {
|
|
47761
47799
|
top: activeRow,
|
|
@@ -48792,11 +48830,11 @@
|
|
|
48792
48830
|
}
|
|
48793
48831
|
else {
|
|
48794
48832
|
const range = this.getters.getRangeFromSheetXC(this.sheetId, rule.criterion.values[0]);
|
|
48795
|
-
values = this.getters
|
|
48833
|
+
values = Array.from(new Set(this.getters
|
|
48796
48834
|
.getRangeValues(range)
|
|
48797
48835
|
.filter(isNotNull)
|
|
48798
48836
|
.map((value) => value.toString())
|
|
48799
|
-
.filter((val) => val !== "");
|
|
48837
|
+
.filter((val) => val !== "")));
|
|
48800
48838
|
}
|
|
48801
48839
|
const composerContent = this.getCurrentContent();
|
|
48802
48840
|
if (composerContent && composerContent !== this.getInitialComposerContent()) {
|
|
@@ -51253,12 +51291,14 @@
|
|
|
51253
51291
|
this.editionState = "initializing";
|
|
51254
51292
|
}
|
|
51255
51293
|
stopEdition() {
|
|
51256
|
-
|
|
51294
|
+
const input = this.sheetNameRef.el;
|
|
51295
|
+
if (!this.state.isEditing || !input)
|
|
51257
51296
|
return;
|
|
51258
51297
|
this.state.isEditing = false;
|
|
51259
51298
|
this.editionState = "initializing";
|
|
51260
|
-
|
|
51299
|
+
input.blur();
|
|
51261
51300
|
const inputValue = this.getInputContent() || "";
|
|
51301
|
+
input.innerText = inputValue;
|
|
51262
51302
|
interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
|
|
51263
51303
|
}
|
|
51264
51304
|
cancelEdition() {
|
|
@@ -51552,7 +51592,7 @@
|
|
|
51552
51592
|
this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
|
|
51553
51593
|
}
|
|
51554
51594
|
onSheetMouseDown(sheetId, event) {
|
|
51555
|
-
if (event.button !== 0)
|
|
51595
|
+
if (event.button !== 0 || this.env.model.getters.isReadonly())
|
|
51556
51596
|
return;
|
|
51557
51597
|
this.closeMenu();
|
|
51558
51598
|
const visibleSheets = this.getVisibleSheets();
|
|
@@ -55057,7 +55097,7 @@
|
|
|
55057
55097
|
}
|
|
55058
55098
|
function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
|
|
55059
55099
|
const colors = new ChartColors();
|
|
55060
|
-
const maxLength =
|
|
55100
|
+
const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
|
|
55061
55101
|
const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
|
|
55062
55102
|
const dataSetsNodes = [];
|
|
55063
55103
|
for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
|
|
@@ -57011,9 +57051,9 @@
|
|
|
57011
57051
|
exports.tokenize = tokenize;
|
|
57012
57052
|
|
|
57013
57053
|
|
|
57014
|
-
__info__.version = "17.0.
|
|
57015
|
-
__info__.date = "2024-
|
|
57016
|
-
__info__.hash = "
|
|
57054
|
+
__info__.version = "17.0.17";
|
|
57055
|
+
__info__.date = "2024-04-05T13:59:37.139Z";
|
|
57056
|
+
__info__.hash = "33f09d7";
|
|
57017
57057
|
|
|
57018
57058
|
|
|
57019
57059
|
})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);
|