@odoo/o-spreadsheet 19.0.5 → 19.0.7
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 +795 -397
- package/dist/o-spreadsheet.d.ts +24 -6
- package/dist/o-spreadsheet.esm.js +795 -397
- package/dist/o-spreadsheet.iife.js +795 -397
- package/dist/o-spreadsheet.iife.min.js +429 -428
- package/dist/o_spreadsheet.xml +13 -4
- package/package.json +1 -1
|
@@ -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 19.0.
|
|
6
|
-
* @date 2025-10-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 19.0.7
|
|
6
|
+
* @date 2025-10-23T08:19:01.764Z
|
|
7
|
+
* @hash 1c1d1ec
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
@@ -1893,6 +1893,29 @@ profilesStartingPosition, profiles, zones, toRemove = false) {
|
|
|
1893
1893
|
removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
|
|
1894
1894
|
}
|
|
1895
1895
|
}
|
|
1896
|
+
function profilesContainsZone(profilesStartingPosition, profiles, zone) {
|
|
1897
|
+
const leftValue = zone.left;
|
|
1898
|
+
const rightValue = zone.right;
|
|
1899
|
+
const topValue = zone.top;
|
|
1900
|
+
const bottomValue = zone.bottom + 1;
|
|
1901
|
+
const leftIndex = binaryPredecessorSearch(profilesStartingPosition, leftValue, 0);
|
|
1902
|
+
const rightIndex = binaryPredecessorSearch(profilesStartingPosition, rightValue, leftIndex);
|
|
1903
|
+
if (leftIndex === -1 || rightIndex === -1) {
|
|
1904
|
+
return false;
|
|
1905
|
+
}
|
|
1906
|
+
for (let i = leftIndex; i <= rightIndex; i++) {
|
|
1907
|
+
const profile = profiles.get(profilesStartingPosition[i]);
|
|
1908
|
+
const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, true);
|
|
1909
|
+
const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, true);
|
|
1910
|
+
if (topPredIndex === -1 || topPredIndex % 2 !== 0) {
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
if (topValue < profile[topPredIndex] || bottomValue > profile[bottomSuccIndex]) {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return true;
|
|
1918
|
+
}
|
|
1896
1919
|
function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
|
|
1897
1920
|
if (value === undefined) {
|
|
1898
1921
|
// this is only the case when the value correspond to a bottom value that could be undefined
|
|
@@ -1977,7 +2000,18 @@ function modifyProfile(profile, zone, toRemove = false) {
|
|
|
1977
2000
|
}
|
|
1978
2001
|
// add the top and bottom value to the profile and
|
|
1979
2002
|
// remove all information between the top and bottom index
|
|
1980
|
-
|
|
2003
|
+
const toDelete = bottomSuccIndex - topPredIndex - 1;
|
|
2004
|
+
const toInsert = newPoints.length;
|
|
2005
|
+
const start = topPredIndex + 1;
|
|
2006
|
+
// fast path and slow path
|
|
2007
|
+
if (start === profile.length - 1 && toDelete === 1 && toInsert === 1) {
|
|
2008
|
+
// fast path: we just need to replace the last element
|
|
2009
|
+
profile[start] = newPoints[0] ?? newPoints[1];
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
// equivalent but slower and with memory allocation
|
|
2013
|
+
profile.splice(start, toDelete, ...newPoints);
|
|
2014
|
+
}
|
|
1981
2015
|
}
|
|
1982
2016
|
function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
|
|
1983
2017
|
const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
|
|
@@ -2016,8 +2050,10 @@ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
|
|
|
2016
2050
|
left,
|
|
2017
2051
|
bottom,
|
|
2018
2052
|
right,
|
|
2019
|
-
hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
|
|
2020
2053
|
};
|
|
2054
|
+
if ((bottom === undefined && top !== 0) || (right === undefined && left !== 0)) {
|
|
2055
|
+
profileZone.hasHeader = true;
|
|
2056
|
+
}
|
|
2021
2057
|
let findCorrespondingZone = false;
|
|
2022
2058
|
for (let j = pendingZones.length - 1; j >= 0; j--) {
|
|
2023
2059
|
const pendingZone = pendingZones[j];
|
|
@@ -2502,17 +2538,6 @@ function excludeTopLeft(zone) {
|
|
|
2502
2538
|
}
|
|
2503
2539
|
return [leftColumnZone, rightPartZone];
|
|
2504
2540
|
}
|
|
2505
|
-
function aggregatePositionsToZones(positions) {
|
|
2506
|
-
const result = {};
|
|
2507
|
-
for (const position of positions) {
|
|
2508
|
-
result[position.sheetId] ??= [];
|
|
2509
|
-
result[position.sheetId].push(positionToZone(position));
|
|
2510
|
-
}
|
|
2511
|
-
for (const sheetId in result) {
|
|
2512
|
-
result[sheetId] = recomputeZones(result[sheetId]);
|
|
2513
|
-
}
|
|
2514
|
-
return result;
|
|
2515
|
-
}
|
|
2516
2541
|
/**
|
|
2517
2542
|
* Array of all positions in the zone.
|
|
2518
2543
|
*/
|
|
@@ -4515,7 +4540,17 @@ function toNumberMatrix(data, argName) {
|
|
|
4515
4540
|
return toMatrix(data).map((row) => {
|
|
4516
4541
|
return row.map((cell) => {
|
|
4517
4542
|
if (typeof cell.value !== "number") {
|
|
4518
|
-
|
|
4543
|
+
let message = "";
|
|
4544
|
+
if (typeof cell === "object") {
|
|
4545
|
+
message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got an empty value.", argName);
|
|
4546
|
+
}
|
|
4547
|
+
else if (typeof cell === "string") {
|
|
4548
|
+
message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a string.", argName);
|
|
4549
|
+
}
|
|
4550
|
+
else if (typeof cell === "boolean") {
|
|
4551
|
+
message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a boolean.", argName);
|
|
4552
|
+
}
|
|
4553
|
+
throw new EvaluationError(message);
|
|
4519
4554
|
}
|
|
4520
4555
|
return cell.value;
|
|
4521
4556
|
});
|
|
@@ -6934,6 +6969,10 @@ function getRangeParts(xc, zone) {
|
|
|
6934
6969
|
}
|
|
6935
6970
|
return parts;
|
|
6936
6971
|
}
|
|
6972
|
+
function positionToBoundedRange(position) {
|
|
6973
|
+
const zone = { left: position.col, top: position.row, right: position.col, bottom: position.row };
|
|
6974
|
+
return { sheetId: position.sheetId, zone };
|
|
6975
|
+
}
|
|
6937
6976
|
/**
|
|
6938
6977
|
* Check that a zone is valid regarding the order of top-bottom and left-right.
|
|
6939
6978
|
* Left should be smaller than right, top should be smaller than bottom.
|
|
@@ -9530,7 +9569,7 @@ class CellClipboardHandler extends AbstractCellClipboardHandler {
|
|
|
9530
9569
|
pasteCell(origin, target, clipboardOption) {
|
|
9531
9570
|
const { sheetId, col, row } = target;
|
|
9532
9571
|
const targetCell = this.getters.getEvaluatedCell(target);
|
|
9533
|
-
const originFormat = origin?.format
|
|
9572
|
+
const originFormat = origin?.format || origin.evaluatedCell.format;
|
|
9534
9573
|
if (clipboardOption?.pasteOption === "asValue") {
|
|
9535
9574
|
this.dispatch("UPDATE_CELL", {
|
|
9536
9575
|
...target,
|
|
@@ -13903,7 +13942,7 @@ const GROWTH = {
|
|
|
13903
13942
|
if (knownDataY.length === 0 || knownDataY[0].length === 0) {
|
|
13904
13943
|
return new EvaluationError(emptyDataErrorMessage("known_data_y"));
|
|
13905
13944
|
}
|
|
13906
|
-
return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, "
|
|
13945
|
+
return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, "known_data_y")), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b)));
|
|
13907
13946
|
},
|
|
13908
13947
|
};
|
|
13909
13948
|
// -----------------------------------------------------------------------------
|
|
@@ -13976,7 +14015,7 @@ const LINEST = {
|
|
|
13976
14015
|
if (dataY.length === 0 || dataY[0].length === 0) {
|
|
13977
14016
|
return new EvaluationError(emptyDataErrorMessage("data_y"));
|
|
13978
14017
|
}
|
|
13979
|
-
return fullLinearRegression(toNumberMatrix(dataX, "
|
|
14018
|
+
return fullLinearRegression(toNumberMatrix(dataX, "data_x"), toNumberMatrix(dataY, "data_y"), toBoolean(calculateB), toBoolean(verbose));
|
|
13980
14019
|
},
|
|
13981
14020
|
isExported: true,
|
|
13982
14021
|
};
|
|
@@ -13995,7 +14034,7 @@ const LOGEST = {
|
|
|
13995
14034
|
if (dataY.length === 0 || dataY[0].length === 0) {
|
|
13996
14035
|
return new EvaluationError(emptyDataErrorMessage("data_y"));
|
|
13997
14036
|
}
|
|
13998
|
-
const coeffs = fullLinearRegression(toNumberMatrix(dataX, "
|
|
14037
|
+
const coeffs = fullLinearRegression(toNumberMatrix(dataX, "data_x"), logM(toNumberMatrix(dataY, "data_y")), toBoolean(calculateB), toBoolean(verbose));
|
|
13999
14038
|
for (let i = 0; i < coeffs.length; i++) {
|
|
14000
14039
|
coeffs[i][0] = Math.exp(coeffs[i][0]);
|
|
14001
14040
|
}
|
|
@@ -14616,7 +14655,7 @@ const TREND = {
|
|
|
14616
14655
|
if (knownDataY.length === 0 || knownDataY[0].length === 0) {
|
|
14617
14656
|
return new EvaluationError(emptyDataErrorMessage("known_data_y"));
|
|
14618
14657
|
}
|
|
14619
|
-
return predictLinearValues(toNumberMatrix(knownDataY, "
|
|
14658
|
+
return predictLinearValues(toNumberMatrix(knownDataY, "known_data_y"), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b));
|
|
14620
14659
|
},
|
|
14621
14660
|
};
|
|
14622
14661
|
// -----------------------------------------------------------------------------
|
|
@@ -23137,6 +23176,10 @@ const chartShowValuesPlugin = {
|
|
|
23137
23176
|
}
|
|
23138
23177
|
const ctx = chart.ctx;
|
|
23139
23178
|
ctx.save();
|
|
23179
|
+
const { left, top, height, width } = chart.chartArea;
|
|
23180
|
+
ctx.beginPath();
|
|
23181
|
+
ctx.rect(left, top, width, height);
|
|
23182
|
+
ctx.clip();
|
|
23140
23183
|
ctx.textAlign = "center";
|
|
23141
23184
|
ctx.textBaseline = "middle";
|
|
23142
23185
|
ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
|
|
@@ -24180,7 +24223,7 @@ class ChartJsComponent extends owl.Component {
|
|
|
24180
24223
|
this.chart.update();
|
|
24181
24224
|
}
|
|
24182
24225
|
hasChartDataChanged() {
|
|
24183
|
-
return !deepEquals(this.currentRuntime
|
|
24226
|
+
return !deepEquals(this.getChartDataInRuntime(this.currentRuntime), this.getChartDataInRuntime(this.chartRuntime));
|
|
24184
24227
|
}
|
|
24185
24228
|
enableAnimationInChartData(chartData) {
|
|
24186
24229
|
return {
|
|
@@ -24188,6 +24231,17 @@ class ChartJsComponent extends owl.Component {
|
|
|
24188
24231
|
options: { ...chartData.options, animation: { animateRotate: true } },
|
|
24189
24232
|
};
|
|
24190
24233
|
}
|
|
24234
|
+
getChartDataInRuntime(runtime) {
|
|
24235
|
+
const data = runtime.chartJsConfig.data;
|
|
24236
|
+
return {
|
|
24237
|
+
labels: data.labels,
|
|
24238
|
+
dataset: data.datasets.map((dataset) => ({
|
|
24239
|
+
data: dataset.data,
|
|
24240
|
+
label: dataset.label,
|
|
24241
|
+
tree: dataset.tree,
|
|
24242
|
+
})),
|
|
24243
|
+
};
|
|
24244
|
+
}
|
|
24191
24245
|
get animationChartId() {
|
|
24192
24246
|
return this.props.isFullScreen ? this.props.chartId + "-fullscreen" : this.props.chartId;
|
|
24193
24247
|
}
|
|
@@ -25611,6 +25665,7 @@ function getChartTimeOptions(labels, labelFormat, locale) {
|
|
|
25611
25665
|
parser: luxonFormat,
|
|
25612
25666
|
displayFormats,
|
|
25613
25667
|
unit: timeUnit ?? false,
|
|
25668
|
+
tooltipFormat: luxonFormat,
|
|
25614
25669
|
};
|
|
25615
25670
|
}
|
|
25616
25671
|
/**
|
|
@@ -26679,6 +26734,7 @@ function getLineChartScales(definition, args) {
|
|
|
26679
26734
|
};
|
|
26680
26735
|
Object.assign(scales.x, axis);
|
|
26681
26736
|
scales.x.ticks.maxTicksLimit = 15;
|
|
26737
|
+
delete scales?.x?.ticks?.callback;
|
|
26682
26738
|
}
|
|
26683
26739
|
else if (axisType === "linear") {
|
|
26684
26740
|
scales.x.type = "linear";
|
|
@@ -32364,7 +32420,7 @@ class ChartDashboardMenu extends owl.Component {
|
|
|
32364
32420
|
}
|
|
32365
32421
|
openContextMenu(ev) {
|
|
32366
32422
|
this.menuState.isOpen = true;
|
|
32367
|
-
this.menuState.anchorRect =
|
|
32423
|
+
this.menuState.anchorRect = getBoundingRectAsPOJO(ev.currentTarget);
|
|
32368
32424
|
const figureId = this.env.model.getters.getFigureIdFromChartId(this.props.chartId);
|
|
32369
32425
|
this.menuState.menuItems = getChartMenuActions(figureId, () => { }, this.env);
|
|
32370
32426
|
}
|
|
@@ -32396,6 +32452,7 @@ class CarouselFigure extends owl.Component {
|
|
|
32396
32452
|
onFigureDeleted: Function,
|
|
32397
32453
|
editFigureStyle: { type: Function, optional: true },
|
|
32398
32454
|
isFullScreen: { type: Boolean, optional: true },
|
|
32455
|
+
openContextMenu: { type: Function, optional: true },
|
|
32399
32456
|
};
|
|
32400
32457
|
static components = { ChartDashboardMenu, MenuPopover };
|
|
32401
32458
|
carouselTabsRef = owl.useRef("carouselTabs");
|
|
@@ -32529,6 +32586,12 @@ class CarouselFigure extends owl.Component {
|
|
|
32529
32586
|
get visibleCarouselItems() {
|
|
32530
32587
|
return this.carousel.items.filter((item) => item.type === "carouselDataView" && this.props.isFullScreen ? false : true);
|
|
32531
32588
|
}
|
|
32589
|
+
openContextMenu(event) {
|
|
32590
|
+
const target = event.currentTarget;
|
|
32591
|
+
if (target) {
|
|
32592
|
+
this.props.openContextMenu?.(getBoundingRectAsPOJO(target));
|
|
32593
|
+
}
|
|
32594
|
+
}
|
|
32532
32595
|
}
|
|
32533
32596
|
|
|
32534
32597
|
class ChartFigure extends owl.Component {
|
|
@@ -32538,6 +32601,7 @@ class ChartFigure extends owl.Component {
|
|
|
32538
32601
|
onFigureDeleted: Function,
|
|
32539
32602
|
editFigureStyle: { type: Function, optional: true },
|
|
32540
32603
|
isFullScreen: { type: Boolean, optional: true },
|
|
32604
|
+
openContextMenu: { type: Function, optional: true },
|
|
32541
32605
|
};
|
|
32542
32606
|
static components = { ChartDashboardMenu };
|
|
32543
32607
|
onDoubleClick() {
|
|
@@ -32570,6 +32634,7 @@ class ImageFigure extends owl.Component {
|
|
|
32570
32634
|
figureUI: Object,
|
|
32571
32635
|
onFigureDeleted: Function,
|
|
32572
32636
|
editFigureStyle: { type: Function, optional: true },
|
|
32637
|
+
openContextMenu: { type: Function, optional: true },
|
|
32573
32638
|
};
|
|
32574
32639
|
static components = {};
|
|
32575
32640
|
// ---------------------------------------------------------------------------
|
|
@@ -34621,8 +34686,11 @@ class Composer extends owl.Component {
|
|
|
34621
34686
|
}
|
|
34622
34687
|
const newSelection = this.contentHelper.getCurrentSelection();
|
|
34623
34688
|
this.props.composerStore.stopComposerRangeSelection();
|
|
34624
|
-
this.props.
|
|
34625
|
-
this.props.
|
|
34689
|
+
const isCurrentlyInactive = this.props.composerStore.editionMode === "inactive";
|
|
34690
|
+
this.props.onComposerContentFocused(newSelection);
|
|
34691
|
+
if (!isCurrentlyInactive) {
|
|
34692
|
+
this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
|
|
34693
|
+
}
|
|
34626
34694
|
this.processTokenAtCursor();
|
|
34627
34695
|
}
|
|
34628
34696
|
onDblClick() {
|
|
@@ -35117,13 +35185,6 @@ class AbstractComposerStore extends SpreadsheetStore {
|
|
|
35117
35185
|
}
|
|
35118
35186
|
}
|
|
35119
35187
|
startEdition(text, selection) {
|
|
35120
|
-
if (selection) {
|
|
35121
|
-
const content = text || this.getComposerContent(this.getters.getActivePosition());
|
|
35122
|
-
const validSelection = this.isSelectionValid(content.length, selection.start, selection.end);
|
|
35123
|
-
if (!validSelection) {
|
|
35124
|
-
return;
|
|
35125
|
-
}
|
|
35126
|
-
}
|
|
35127
35188
|
const { col, row } = this.getters.getActivePosition();
|
|
35128
35189
|
this.model.dispatch("SELECT_FIGURE", { figureId: null });
|
|
35129
35190
|
this.model.dispatch("SCROLL_TO_CELL", { col, row });
|
|
@@ -35180,7 +35241,7 @@ class AbstractComposerStore extends SpreadsheetStore {
|
|
|
35180
35241
|
// ---------------------------------------------------------------------------
|
|
35181
35242
|
get currentContent() {
|
|
35182
35243
|
if (this.editionMode === "inactive") {
|
|
35183
|
-
return this.getComposerContent(this.getters.getActivePosition());
|
|
35244
|
+
return this.getComposerContent(this.getters.getActivePosition()).text;
|
|
35184
35245
|
}
|
|
35185
35246
|
return this._currentContent;
|
|
35186
35247
|
}
|
|
@@ -35379,8 +35440,9 @@ class AbstractComposerStore extends SpreadsheetStore {
|
|
|
35379
35440
|
this.sheetId = sheetId;
|
|
35380
35441
|
this.row = row;
|
|
35381
35442
|
this.editionMode = "editing";
|
|
35382
|
-
|
|
35383
|
-
this.
|
|
35443
|
+
const { text, adjustedSelection } = this.getComposerContent({ sheetId, col, row }, selection);
|
|
35444
|
+
this.initialContent = text;
|
|
35445
|
+
this.setContent(str || this.initialContent, adjustedSelection ?? selection);
|
|
35384
35446
|
this.colorIndexByRange = {};
|
|
35385
35447
|
const zone = positionToZone({ col: this.col, row: this.row });
|
|
35386
35448
|
this.captureSelection(zone, col, row);
|
|
@@ -35857,7 +35919,7 @@ class StandaloneComposerStore extends AbstractComposerStore {
|
|
|
35857
35919
|
constructor(get, args) {
|
|
35858
35920
|
super(get);
|
|
35859
35921
|
this.args = args;
|
|
35860
|
-
this._currentContent = this.getComposerContent();
|
|
35922
|
+
this._currentContent = this.getComposerContent().text;
|
|
35861
35923
|
}
|
|
35862
35924
|
getAutoCompleteProviders() {
|
|
35863
35925
|
const providersDefinitions = super.getAutoCompleteProviders();
|
|
@@ -35894,7 +35956,7 @@ class StandaloneComposerStore extends AbstractComposerStore {
|
|
|
35894
35956
|
})
|
|
35895
35957
|
.join("");
|
|
35896
35958
|
}
|
|
35897
|
-
return localizeContent(content, this.getters.getLocale());
|
|
35959
|
+
return { text: localizeContent(content, this.getters.getLocale()) };
|
|
35898
35960
|
}
|
|
35899
35961
|
stopEdition() {
|
|
35900
35962
|
this._stopEdition();
|
|
@@ -38664,12 +38726,23 @@ cellPopoverRegistry
|
|
|
38664
38726
|
.add("LinkEditor", LinkEditorPopoverBuilder)
|
|
38665
38727
|
.add("FilterMenu", FilterMenuPopoverBuilder);
|
|
38666
38728
|
|
|
38667
|
-
const
|
|
38668
|
-
|
|
38669
|
-
|
|
38670
|
-
|
|
38671
|
-
|
|
38672
|
-
|
|
38729
|
+
const DEFAULT_BAR_CHART_CONFIG = {
|
|
38730
|
+
type: "bar",
|
|
38731
|
+
title: {},
|
|
38732
|
+
dataSets: [],
|
|
38733
|
+
legendPosition: "none",
|
|
38734
|
+
dataSetsHaveTitle: false,
|
|
38735
|
+
stacked: false,
|
|
38736
|
+
};
|
|
38737
|
+
const DEFAULT_LINE_CHART_CONFIG = {
|
|
38738
|
+
type: "line",
|
|
38739
|
+
title: {},
|
|
38740
|
+
dataSets: [],
|
|
38741
|
+
legendPosition: "none",
|
|
38742
|
+
dataSetsHaveTitle: false,
|
|
38743
|
+
stacked: false,
|
|
38744
|
+
cumulative: false,
|
|
38745
|
+
labelsAsText: false,
|
|
38673
38746
|
};
|
|
38674
38747
|
function getUnboundRange(getters, zone) {
|
|
38675
38748
|
return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
|
|
@@ -38708,43 +38781,19 @@ function detectColumnType(cells) {
|
|
|
38708
38781
|
return detectedType;
|
|
38709
38782
|
}
|
|
38710
38783
|
function categorizeColumns(zones, getters) {
|
|
38711
|
-
const columns =
|
|
38712
|
-
number: [],
|
|
38713
|
-
text: [],
|
|
38714
|
-
date: [],
|
|
38715
|
-
};
|
|
38784
|
+
const columns = [];
|
|
38716
38785
|
for (const zone of getZonesByColumns(zones)) {
|
|
38717
38786
|
const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
|
|
38718
|
-
|
|
38719
|
-
if (type !== "empty") {
|
|
38720
|
-
const targetType = type === "percentage" ? "number" : type;
|
|
38721
|
-
columns[targetType].push({ zone, type });
|
|
38722
|
-
}
|
|
38787
|
+
columns.push({ zone, type: detectColumnType(cells) });
|
|
38723
38788
|
}
|
|
38724
38789
|
return columns;
|
|
38725
38790
|
}
|
|
38726
38791
|
function getCellStats(getters, zone) {
|
|
38727
38792
|
const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
|
|
38728
|
-
const
|
|
38729
|
-
let totalCount = 0;
|
|
38730
|
-
let percentageSum = 0;
|
|
38731
|
-
for (let i = 0; i < cells.length; i++) {
|
|
38732
|
-
const { value } = cells[i];
|
|
38733
|
-
const str = value?.toString().trim();
|
|
38734
|
-
if (!str) {
|
|
38735
|
-
continue;
|
|
38736
|
-
}
|
|
38737
|
-
uniqueValues.add(str);
|
|
38738
|
-
totalCount++;
|
|
38739
|
-
const num = Number(value);
|
|
38740
|
-
if (!isNaN(num)) {
|
|
38741
|
-
percentageSum += Math.abs(num) * 100;
|
|
38742
|
-
}
|
|
38743
|
-
}
|
|
38793
|
+
const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
|
|
38744
38794
|
return {
|
|
38745
|
-
uniqueCount:
|
|
38746
|
-
totalCount,
|
|
38747
|
-
percentageSum,
|
|
38795
|
+
uniqueCount: new Set(values).size,
|
|
38796
|
+
totalCount: values.length,
|
|
38748
38797
|
};
|
|
38749
38798
|
}
|
|
38750
38799
|
function isDatasetTitled(getters, column) {
|
|
@@ -38755,167 +38804,191 @@ function isDatasetTitled(getters, column) {
|
|
|
38755
38804
|
});
|
|
38756
38805
|
return ![CellValueType.number, CellValueType.empty].includes(titleCell.type);
|
|
38757
38806
|
}
|
|
38758
|
-
|
|
38759
|
-
|
|
38760
|
-
|
|
38761
|
-
|
|
38762
|
-
|
|
38763
|
-
|
|
38764
|
-
|
|
38765
|
-
|
|
38766
|
-
}
|
|
38807
|
+
/**
|
|
38808
|
+
* Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
|
|
38809
|
+
* - If the column contains a single cell, create a scorecard.
|
|
38810
|
+
* - If the column type is "percentage", create a pie chart.
|
|
38811
|
+
* - If the column type is "text", create a pie chart
|
|
38812
|
+
* - If the column type is "date", create a line chart.
|
|
38813
|
+
* - Otherwise, create a bar chart.
|
|
38814
|
+
*/
|
|
38767
38815
|
function buildSingleColumnChart(column, getters) {
|
|
38768
38816
|
const { type, zone } = column;
|
|
38769
38817
|
const sheetId = getters.getActiveSheetId();
|
|
38770
38818
|
const dataSetsHaveTitle = isDatasetTitled(getters, column);
|
|
38771
38819
|
const dataRange = getUnboundRange(getters, zone);
|
|
38772
38820
|
const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
|
|
38821
|
+
if (getZoneArea(zone) === 1) {
|
|
38822
|
+
return buildScorecard(zone, getters);
|
|
38823
|
+
}
|
|
38773
38824
|
switch (type) {
|
|
38774
38825
|
case "percentage":
|
|
38775
|
-
|
|
38776
|
-
|
|
38826
|
+
return {
|
|
38827
|
+
type: "pie",
|
|
38777
38828
|
title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
|
|
38829
|
+
dataSets: [{ dataRange }],
|
|
38830
|
+
legendPosition: "none",
|
|
38778
38831
|
dataSetsHaveTitle,
|
|
38779
|
-
|
|
38780
|
-
});
|
|
38832
|
+
};
|
|
38781
38833
|
case "text":
|
|
38782
38834
|
const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
|
|
38783
38835
|
const titleCount = cells.reduce((count, cell) => (cell.value === titleCell.value ? count + 1 : count), 0);
|
|
38784
38836
|
const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
|
|
38785
|
-
return
|
|
38837
|
+
return {
|
|
38838
|
+
type: "pie",
|
|
38786
38839
|
title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
|
|
38840
|
+
dataSets: [{ dataRange }],
|
|
38787
38841
|
labelRange: dataRange,
|
|
38788
38842
|
dataSetsHaveTitle: hasUniqueTitle,
|
|
38789
|
-
isDoughnut: false,
|
|
38790
38843
|
aggregated: true,
|
|
38791
38844
|
legendPosition: "top",
|
|
38792
|
-
}
|
|
38793
|
-
// TODO: Handle date column with matrix chart when matrix chart is supported
|
|
38845
|
+
};
|
|
38794
38846
|
case "date":
|
|
38795
|
-
return
|
|
38796
|
-
|
|
38847
|
+
return {
|
|
38848
|
+
...DEFAULT_LINE_CHART_CONFIG,
|
|
38849
|
+
type: "line",
|
|
38850
|
+
title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
|
|
38851
|
+
dataSets: [{ dataRange }],
|
|
38797
38852
|
dataSetsHaveTitle,
|
|
38798
|
-
|
|
38799
|
-
labelsAsText: false,
|
|
38800
|
-
});
|
|
38853
|
+
};
|
|
38801
38854
|
}
|
|
38802
|
-
return
|
|
38855
|
+
return {
|
|
38856
|
+
...DEFAULT_BAR_CHART_CONFIG,
|
|
38857
|
+
title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
|
|
38858
|
+
dataSets: [{ dataRange }],
|
|
38859
|
+
dataSetsHaveTitle,
|
|
38860
|
+
};
|
|
38803
38861
|
}
|
|
38862
|
+
/**
|
|
38863
|
+
* Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
|
|
38864
|
+
* columns left to right, and is as follows:
|
|
38865
|
+
* - any type + percentage columns: pie chart
|
|
38866
|
+
* - number + number columns: scatter chart
|
|
38867
|
+
* - date + number columns: line chart
|
|
38868
|
+
* - text + number columns: treemap if repetition in labels
|
|
38869
|
+
* - any other combination: bar chart
|
|
38870
|
+
*/
|
|
38804
38871
|
function buildTwoColumnChart(columns, getters) {
|
|
38805
|
-
|
|
38806
|
-
|
|
38807
|
-
return createBaseChart("scatter", [{ dataRange: getUnboundRange(getters, numberColumns[1].zone) }], {
|
|
38808
|
-
labelRange: getUnboundRange(getters, numberColumns[0].zone),
|
|
38809
|
-
dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[1]),
|
|
38810
|
-
labelsAsText: false,
|
|
38811
|
-
});
|
|
38872
|
+
if (columns.length !== 2) {
|
|
38873
|
+
throw new Error("buildTwoColumnChart expects exactly two columns");
|
|
38812
38874
|
}
|
|
38813
|
-
|
|
38814
|
-
|
|
38815
|
-
|
|
38816
|
-
|
|
38817
|
-
|
|
38818
|
-
|
|
38819
|
-
|
|
38875
|
+
if (columns[1].type === "percentage") {
|
|
38876
|
+
return {
|
|
38877
|
+
type: "pie",
|
|
38878
|
+
title: {},
|
|
38879
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38880
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38881
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
|
|
38882
|
+
aggregated: true,
|
|
38883
|
+
legendPosition: "none",
|
|
38884
|
+
};
|
|
38885
|
+
}
|
|
38886
|
+
if (columns[0].type === "number" && columns[1].type === "number") {
|
|
38887
|
+
return {
|
|
38888
|
+
type: "scatter",
|
|
38889
|
+
title: {},
|
|
38890
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38891
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38892
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
|
|
38820
38893
|
labelsAsText: false,
|
|
38821
|
-
|
|
38894
|
+
legendPosition: "none",
|
|
38895
|
+
};
|
|
38896
|
+
}
|
|
38897
|
+
// TODO: Handle date + number with calendar chart when implemented (and change the docstring)
|
|
38898
|
+
if (columns[0].type === "date" && columns[1].type === "number") {
|
|
38899
|
+
return {
|
|
38900
|
+
...DEFAULT_LINE_CHART_CONFIG,
|
|
38901
|
+
type: "line",
|
|
38902
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38903
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38904
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[0]),
|
|
38905
|
+
};
|
|
38822
38906
|
}
|
|
38823
|
-
if (
|
|
38824
|
-
const
|
|
38825
|
-
const
|
|
38907
|
+
if (columns[0].type === "text" && columns[1].type === "number") {
|
|
38908
|
+
const textColumn = columns[0];
|
|
38909
|
+
const numberColumn = columns[1];
|
|
38826
38910
|
const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
|
|
38827
38911
|
const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
|
|
38828
|
-
const maxCategories = dataSetsHaveTitle
|
|
38829
|
-
? CHART_LIMITS.MAX_PIE_CATEGORIES
|
|
38830
|
-
: CHART_LIMITS.MAX_PIE_CATEGORIES_NO_TITLE;
|
|
38831
|
-
const labelRange = getUnboundRange(getters, textColumn.zone);
|
|
38832
|
-
const dataRange = getUnboundRange(getters, numberColumn.zone);
|
|
38833
|
-
if (uniqueCount <= maxCategories) {
|
|
38834
|
-
const { percentageSum } = getCellStats(getters, numberColumn.zone);
|
|
38835
|
-
return createBaseChart("pie", [{ dataRange }], {
|
|
38836
|
-
labelRange,
|
|
38837
|
-
dataSetsHaveTitle,
|
|
38838
|
-
isDoughnut: numberColumn.type === "percentage" && percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
|
|
38839
|
-
aggregated: true,
|
|
38840
|
-
legendPosition: "top",
|
|
38841
|
-
});
|
|
38842
|
-
}
|
|
38843
|
-
// Use treemap when categories repeat, as pie chart would be cluttered
|
|
38844
38912
|
if (uniqueCount !== totalCount) {
|
|
38845
|
-
return
|
|
38846
|
-
|
|
38913
|
+
return {
|
|
38914
|
+
type: "treemap",
|
|
38915
|
+
title: {},
|
|
38916
|
+
dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }],
|
|
38917
|
+
labelRange: getUnboundRange(getters, numberColumn.zone),
|
|
38847
38918
|
dataSetsHaveTitle,
|
|
38848
|
-
|
|
38919
|
+
legendPosition: "none",
|
|
38920
|
+
};
|
|
38849
38921
|
}
|
|
38850
|
-
return createBaseChart("bar", [{ dataRange }], {
|
|
38851
|
-
labelRange,
|
|
38852
|
-
dataSetsHaveTitle,
|
|
38853
|
-
});
|
|
38854
38922
|
}
|
|
38855
|
-
|
|
38856
|
-
|
|
38857
|
-
|
|
38858
|
-
labelRange: getUnboundRange(getters,
|
|
38859
|
-
dataSetsHaveTitle: isDatasetTitled(getters,
|
|
38860
|
-
|
|
38861
|
-
labelsAsText: true,
|
|
38862
|
-
});
|
|
38923
|
+
return {
|
|
38924
|
+
...DEFAULT_BAR_CHART_CONFIG,
|
|
38925
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38926
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38927
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
|
|
38928
|
+
};
|
|
38863
38929
|
}
|
|
38930
|
+
/**
|
|
38931
|
+
* Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
|
|
38932
|
+
* the columns left to right, and is as follows:
|
|
38933
|
+
* - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
|
|
38934
|
+
* - any type + multiple percentage columns: pie chart
|
|
38935
|
+
* - date + multiple number columns: line chart
|
|
38936
|
+
* - any other combination: bar chart
|
|
38937
|
+
*/
|
|
38864
38938
|
function buildMultiColumnChart(columns, getters) {
|
|
38865
|
-
|
|
38866
|
-
|
|
38867
|
-
|
|
38868
|
-
|
|
38869
|
-
|
|
38939
|
+
if (columns.length < 3) {
|
|
38940
|
+
throw new Error("buildMultiColumnChart expects at least three columns");
|
|
38941
|
+
}
|
|
38942
|
+
const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
|
|
38943
|
+
const lastColumn = columns[columns.length - 1];
|
|
38944
|
+
const columnsExceptLast = columns.slice(0, columns.length - 1);
|
|
38945
|
+
if ((lastColumn.type === "percentage" || lastColumn.type === "number") &&
|
|
38946
|
+
columnsExceptLast.every((col) => col.type === "text")) {
|
|
38947
|
+
const dataSets = columnsExceptLast.map(({ zone }) => ({
|
|
38870
38948
|
dataRange: getUnboundRange(getters, zone),
|
|
38871
38949
|
}));
|
|
38872
|
-
return
|
|
38873
|
-
|
|
38950
|
+
return {
|
|
38951
|
+
type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
|
|
38952
|
+
title: {},
|
|
38953
|
+
dataSets,
|
|
38954
|
+
labelRange: getUnboundRange(getters, lastColumn.zone),
|
|
38874
38955
|
dataSetsHaveTitle,
|
|
38875
|
-
|
|
38956
|
+
legendPosition: "none",
|
|
38957
|
+
};
|
|
38876
38958
|
}
|
|
38877
|
-
const
|
|
38959
|
+
const firstColumn = columns[0];
|
|
38960
|
+
const columnsExceptFirst = columns.slice(1);
|
|
38961
|
+
const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({
|
|
38878
38962
|
dataRange: getUnboundRange(getters, zone),
|
|
38879
38963
|
}));
|
|
38880
|
-
if (
|
|
38881
|
-
return
|
|
38882
|
-
|
|
38964
|
+
if (columnsExceptFirst.every((col) => col.type === "percentage")) {
|
|
38965
|
+
return {
|
|
38966
|
+
type: "pie",
|
|
38967
|
+
title: {},
|
|
38968
|
+
dataSets: rangesOfColumnsExceptFirst,
|
|
38969
|
+
labelRange: getUnboundRange(getters, firstColumn.zone),
|
|
38883
38970
|
dataSetsHaveTitle,
|
|
38884
|
-
|
|
38885
|
-
labelsAsText: false,
|
|
38971
|
+
aggregated: false,
|
|
38886
38972
|
legendPosition: "top",
|
|
38887
|
-
}
|
|
38973
|
+
};
|
|
38888
38974
|
}
|
|
38889
|
-
if (
|
|
38890
|
-
|
|
38891
|
-
|
|
38892
|
-
|
|
38893
|
-
|
|
38894
|
-
|
|
38895
|
-
|
|
38896
|
-
|
|
38897
|
-
|
|
38898
|
-
const expectedDataCount = categoryCount * numberColumns.length + (dataSetsHaveTitle ? numberColumns.length : 0);
|
|
38899
|
-
const actualDataCount = numberColumns.reduce((sum, dataCol) => sum + getCellStats(getters, dataCol.zone).totalCount, 0);
|
|
38900
|
-
if (uniqueCount === totalCount &&
|
|
38901
|
-
uniqueCount >= CHART_LIMITS.MIN_RADAR_CATEGORIES &&
|
|
38902
|
-
uniqueCount <= CHART_LIMITS.MAX_RADAR_CATEGORIES &&
|
|
38903
|
-
expectedDataCount === actualDataCount) {
|
|
38904
|
-
return createBaseChart("radar", dataSets, {
|
|
38905
|
-
title: dataSetsHaveTitle && firstCell.value ? { text: String(firstCell.value) } : {},
|
|
38906
|
-
labelRange: getUnboundRange(getters, textColumn.zone),
|
|
38907
|
-
dataSetsHaveTitle,
|
|
38908
|
-
legendPosition: "top",
|
|
38909
|
-
});
|
|
38910
|
-
}
|
|
38975
|
+
if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
|
|
38976
|
+
return {
|
|
38977
|
+
...DEFAULT_LINE_CHART_CONFIG,
|
|
38978
|
+
type: "line",
|
|
38979
|
+
dataSets: rangesOfColumnsExceptFirst,
|
|
38980
|
+
labelRange: getUnboundRange(getters, firstColumn.zone),
|
|
38981
|
+
dataSetsHaveTitle,
|
|
38982
|
+
legendPosition: "top",
|
|
38983
|
+
};
|
|
38911
38984
|
}
|
|
38912
|
-
|
|
38913
|
-
|
|
38914
|
-
|
|
38985
|
+
return {
|
|
38986
|
+
...DEFAULT_BAR_CHART_CONFIG,
|
|
38987
|
+
dataSets: rangesOfColumnsExceptFirst,
|
|
38988
|
+
labelRange: getUnboundRange(getters, firstColumn.zone),
|
|
38915
38989
|
dataSetsHaveTitle,
|
|
38916
|
-
aggregated: true,
|
|
38917
38990
|
legendPosition: "top",
|
|
38918
|
-
}
|
|
38991
|
+
};
|
|
38919
38992
|
}
|
|
38920
38993
|
function buildScorecard(zone, getters) {
|
|
38921
38994
|
const cell = getters.getCell({
|
|
@@ -38938,22 +39011,18 @@ function buildScorecard(zone, getters) {
|
|
|
38938
39011
|
*/
|
|
38939
39012
|
function getSmartChartDefinition(zones, getters) {
|
|
38940
39013
|
const columns = categorizeColumns(zones, getters);
|
|
38941
|
-
|
|
38942
|
-
|
|
38943
|
-
|
|
38944
|
-
|
|
38945
|
-
|
|
38946
|
-
|
|
38947
|
-
});
|
|
39014
|
+
if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
|
|
39015
|
+
const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) }));
|
|
39016
|
+
return { ...DEFAULT_BAR_CHART_CONFIG, dataSets };
|
|
39017
|
+
}
|
|
39018
|
+
const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
|
|
39019
|
+
switch (nonEmptyColumns.length) {
|
|
38948
39020
|
case 1:
|
|
38949
|
-
|
|
38950
|
-
return getZoneArea(singleColumn.zone) === 1
|
|
38951
|
-
? buildScorecard(singleColumn.zone, getters)
|
|
38952
|
-
: buildSingleColumnChart(singleColumn, getters);
|
|
39021
|
+
return buildSingleColumnChart(nonEmptyColumns[0], getters);
|
|
38953
39022
|
case 2:
|
|
38954
|
-
return buildTwoColumnChart(
|
|
39023
|
+
return buildTwoColumnChart(nonEmptyColumns, getters);
|
|
38955
39024
|
default:
|
|
38956
|
-
return buildMultiColumnChart(
|
|
39025
|
+
return buildMultiColumnChart(nonEmptyColumns, getters);
|
|
38957
39026
|
}
|
|
38958
39027
|
}
|
|
38959
39028
|
|
|
@@ -39572,6 +39641,74 @@ function getPath2D(svgPath) {
|
|
|
39572
39641
|
return path2D;
|
|
39573
39642
|
}
|
|
39574
39643
|
|
|
39644
|
+
/**
|
|
39645
|
+
* Get the relative path between two files
|
|
39646
|
+
*
|
|
39647
|
+
* Eg.:
|
|
39648
|
+
* from "folder1/file1.txt" to "folder2/file2.txt" => "../folder2/file2.txt"
|
|
39649
|
+
*/
|
|
39650
|
+
function getRelativePath(from, to) {
|
|
39651
|
+
const fromPathParts = from.split("/");
|
|
39652
|
+
const toPathParts = to.split("/");
|
|
39653
|
+
let relPath = "";
|
|
39654
|
+
let startIndex = 0;
|
|
39655
|
+
for (let i = 0; i < fromPathParts.length - 1; i++) {
|
|
39656
|
+
if (fromPathParts[i] === toPathParts[i]) {
|
|
39657
|
+
startIndex++;
|
|
39658
|
+
}
|
|
39659
|
+
else {
|
|
39660
|
+
relPath += "../";
|
|
39661
|
+
}
|
|
39662
|
+
}
|
|
39663
|
+
relPath += toPathParts.slice(startIndex).join("/");
|
|
39664
|
+
return relPath;
|
|
39665
|
+
}
|
|
39666
|
+
/**
|
|
39667
|
+
* Convert an array of element into an object where the objects keys were the elements position in the array.
|
|
39668
|
+
* Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.
|
|
39669
|
+
*
|
|
39670
|
+
* eg. : ["a", "b"] => {0:"a", 1:"b"}
|
|
39671
|
+
*/
|
|
39672
|
+
function arrayToObject(array, indexOffset = 0) {
|
|
39673
|
+
const obj = {};
|
|
39674
|
+
for (let i = 0; i < array.length; i++) {
|
|
39675
|
+
if (array[i]) {
|
|
39676
|
+
obj[i + indexOffset] = array[i];
|
|
39677
|
+
}
|
|
39678
|
+
}
|
|
39679
|
+
return obj;
|
|
39680
|
+
}
|
|
39681
|
+
/**
|
|
39682
|
+
* In xlsx we can have string with unicode characters with the format _x00fa_.
|
|
39683
|
+
* Replace with characters understandable by JS
|
|
39684
|
+
*/
|
|
39685
|
+
function fixXlsxUnicode(str) {
|
|
39686
|
+
return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {
|
|
39687
|
+
return String.fromCharCode(parseInt(code, 16));
|
|
39688
|
+
});
|
|
39689
|
+
}
|
|
39690
|
+
/** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
|
|
39691
|
+
function getSheetDataHeader(sheetData, dimension, index) {
|
|
39692
|
+
if (dimension === "COL") {
|
|
39693
|
+
if (!sheetData.cols[index]) {
|
|
39694
|
+
sheetData.cols[index] = {};
|
|
39695
|
+
}
|
|
39696
|
+
return sheetData.cols[index];
|
|
39697
|
+
}
|
|
39698
|
+
if (!sheetData.rows[index]) {
|
|
39699
|
+
sheetData.rows[index] = {};
|
|
39700
|
+
}
|
|
39701
|
+
return sheetData.rows[index];
|
|
39702
|
+
}
|
|
39703
|
+
/** Prefix the string by "=" if the string looks like a formula */
|
|
39704
|
+
function prefixFormulaWithEqual(formula) {
|
|
39705
|
+
if (formula[0] === "=") {
|
|
39706
|
+
return formula;
|
|
39707
|
+
}
|
|
39708
|
+
const tokens = tokenize(formula);
|
|
39709
|
+
return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
|
|
39710
|
+
}
|
|
39711
|
+
|
|
39575
39712
|
/**
|
|
39576
39713
|
* Map of the different types of conversions warnings and their name in error messages
|
|
39577
39714
|
*/
|
|
@@ -40094,66 +40231,6 @@ function hexaToInt(hex) {
|
|
|
40094
40231
|
*/
|
|
40095
40232
|
const DEFAULT_SYSTEM_COLOR = "FF000000";
|
|
40096
40233
|
|
|
40097
|
-
/**
|
|
40098
|
-
* Get the relative path between two files
|
|
40099
|
-
*
|
|
40100
|
-
* Eg.:
|
|
40101
|
-
* from "folder1/file1.txt" to "folder2/file2.txt" => "../folder2/file2.txt"
|
|
40102
|
-
*/
|
|
40103
|
-
function getRelativePath(from, to) {
|
|
40104
|
-
const fromPathParts = from.split("/");
|
|
40105
|
-
const toPathParts = to.split("/");
|
|
40106
|
-
let relPath = "";
|
|
40107
|
-
let startIndex = 0;
|
|
40108
|
-
for (let i = 0; i < fromPathParts.length - 1; i++) {
|
|
40109
|
-
if (fromPathParts[i] === toPathParts[i]) {
|
|
40110
|
-
startIndex++;
|
|
40111
|
-
}
|
|
40112
|
-
else {
|
|
40113
|
-
relPath += "../";
|
|
40114
|
-
}
|
|
40115
|
-
}
|
|
40116
|
-
relPath += toPathParts.slice(startIndex).join("/");
|
|
40117
|
-
return relPath;
|
|
40118
|
-
}
|
|
40119
|
-
/**
|
|
40120
|
-
* Convert an array of element into an object where the objects keys were the elements position in the array.
|
|
40121
|
-
* Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.
|
|
40122
|
-
*
|
|
40123
|
-
* eg. : ["a", "b"] => {0:"a", 1:"b"}
|
|
40124
|
-
*/
|
|
40125
|
-
function arrayToObject(array, indexOffset = 0) {
|
|
40126
|
-
const obj = {};
|
|
40127
|
-
for (let i = 0; i < array.length; i++) {
|
|
40128
|
-
if (array[i]) {
|
|
40129
|
-
obj[i + indexOffset] = array[i];
|
|
40130
|
-
}
|
|
40131
|
-
}
|
|
40132
|
-
return obj;
|
|
40133
|
-
}
|
|
40134
|
-
/**
|
|
40135
|
-
* In xlsx we can have string with unicode characters with the format _x00fa_.
|
|
40136
|
-
* Replace with characters understandable by JS
|
|
40137
|
-
*/
|
|
40138
|
-
function fixXlsxUnicode(str) {
|
|
40139
|
-
return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {
|
|
40140
|
-
return String.fromCharCode(parseInt(code, 16));
|
|
40141
|
-
});
|
|
40142
|
-
}
|
|
40143
|
-
/** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
|
|
40144
|
-
function getSheetDataHeader(sheetData, dimension, index) {
|
|
40145
|
-
if (dimension === "COL") {
|
|
40146
|
-
if (!sheetData.cols[index]) {
|
|
40147
|
-
sheetData.cols[index] = {};
|
|
40148
|
-
}
|
|
40149
|
-
return sheetData.cols[index];
|
|
40150
|
-
}
|
|
40151
|
-
if (!sheetData.rows[index]) {
|
|
40152
|
-
sheetData.rows[index] = {};
|
|
40153
|
-
}
|
|
40154
|
-
return sheetData.rows[index];
|
|
40155
|
-
}
|
|
40156
|
-
|
|
40157
40234
|
const XLSX_DATE_FORMAT_REGEX = /^(yy|yyyy|m{1,5}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/m|\s|-|\/|\.|:)+$/i;
|
|
40158
40235
|
/**
|
|
40159
40236
|
* Convert excel format to o_spreadsheet format
|
|
@@ -40368,9 +40445,9 @@ function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
|
|
|
40368
40445
|
if (!rule.operator || !rule.formula || rule.formula.length === 0)
|
|
40369
40446
|
continue;
|
|
40370
40447
|
operator = CF_OPERATOR_TYPE_CONVERSION_MAP[rule.operator];
|
|
40371
|
-
values.push(
|
|
40448
|
+
values.push(prefixFormulaWithEqual(rule.formula[0]));
|
|
40372
40449
|
if (rule.formula.length === 2) {
|
|
40373
|
-
values.push(
|
|
40450
|
+
values.push(prefixFormulaWithEqual(rule.formula[1]));
|
|
40374
40451
|
}
|
|
40375
40452
|
break;
|
|
40376
40453
|
}
|
|
@@ -40528,11 +40605,6 @@ function convertIcons(xlsxIconSet, index) {
|
|
|
40528
40605
|
? ICON_SETS[iconSet].neutral
|
|
40529
40606
|
: ICON_SETS[iconSet].good;
|
|
40530
40607
|
}
|
|
40531
|
-
/** Prefix the string by "=" if the string looks like a formula */
|
|
40532
|
-
function prefixFormula(formula) {
|
|
40533
|
-
const tokens = tokenize(formula);
|
|
40534
|
-
return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
|
|
40535
|
-
}
|
|
40536
40608
|
// ---------------------------------------------------------------------------
|
|
40537
40609
|
// Warnings
|
|
40538
40610
|
// ---------------------------------------------------------------------------
|
|
@@ -41008,7 +41080,7 @@ function convertDataValidationRules(xlsxDataValidations, warningManager) {
|
|
|
41008
41080
|
dvRules.push(decimalRule);
|
|
41009
41081
|
break;
|
|
41010
41082
|
case "list":
|
|
41011
|
-
const listRule =
|
|
41083
|
+
const listRule = convertListRule(dvId++, dv);
|
|
41012
41084
|
dvRules.push(listRule);
|
|
41013
41085
|
break;
|
|
41014
41086
|
case "date":
|
|
@@ -41028,9 +41100,9 @@ function convertDataValidationRules(xlsxDataValidations, warningManager) {
|
|
|
41028
41100
|
return dvRules;
|
|
41029
41101
|
}
|
|
41030
41102
|
function convertDecimalRule(id, dv) {
|
|
41031
|
-
const values = [dv.formula1.toString()];
|
|
41103
|
+
const values = [prefixFormulaWithEqual(dv.formula1.toString())];
|
|
41032
41104
|
if (dv.formula2) {
|
|
41033
|
-
values.push(dv.formula2.toString());
|
|
41105
|
+
values.push(prefixFormulaWithEqual(dv.formula2.toString()));
|
|
41034
41106
|
}
|
|
41035
41107
|
return {
|
|
41036
41108
|
id: id.toString(),
|
|
@@ -41042,7 +41114,7 @@ function convertDecimalRule(id, dv) {
|
|
|
41042
41114
|
},
|
|
41043
41115
|
};
|
|
41044
41116
|
}
|
|
41045
|
-
function
|
|
41117
|
+
function convertListRule(id, dv) {
|
|
41046
41118
|
const formula1 = dv.formula1.toString();
|
|
41047
41119
|
const isRangeRule = rangeReference.test(formula1);
|
|
41048
41120
|
return {
|
|
@@ -41058,9 +41130,9 @@ function convertListrule(id, dv) {
|
|
|
41058
41130
|
}
|
|
41059
41131
|
function convertDateRule(id, dv) {
|
|
41060
41132
|
let criterion;
|
|
41061
|
-
const values = [dv.formula1.toString()];
|
|
41133
|
+
const values = [prefixFormulaWithEqual(dv.formula1.toString())];
|
|
41062
41134
|
if (dv.formula2) {
|
|
41063
|
-
values.push(dv.formula2.toString());
|
|
41135
|
+
values.push(prefixFormulaWithEqual(dv.formula2.toString()));
|
|
41064
41136
|
criterion = {
|
|
41065
41137
|
type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
|
|
41066
41138
|
values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
|
|
@@ -41087,7 +41159,7 @@ function convertCustomRule(id, dv) {
|
|
|
41087
41159
|
isBlocking: dv.errorStyle !== "warning",
|
|
41088
41160
|
criterion: {
|
|
41089
41161
|
type: "customFormula",
|
|
41090
|
-
values: [
|
|
41162
|
+
values: [prefixFormulaWithEqual(dv.formula1.toString())],
|
|
41091
41163
|
},
|
|
41092
41164
|
};
|
|
41093
41165
|
}
|
|
@@ -43991,6 +44063,7 @@ function forceUnicityOfFigure(data) {
|
|
|
43991
44063
|
return data;
|
|
43992
44064
|
}
|
|
43993
44065
|
const figureIds = new Set();
|
|
44066
|
+
const chartIds = new Set();
|
|
43994
44067
|
const uuidGenerator = new UuidGenerator();
|
|
43995
44068
|
for (const sheet of data.sheets || []) {
|
|
43996
44069
|
for (const figure of sheet.figures || []) {
|
|
@@ -43998,6 +44071,12 @@ function forceUnicityOfFigure(data) {
|
|
|
43998
44071
|
figure.id += uuidGenerator.smallUuid();
|
|
43999
44072
|
}
|
|
44000
44073
|
figureIds.add(figure.id);
|
|
44074
|
+
if (figure.tag === "chart") {
|
|
44075
|
+
if (chartIds.has(figure.data?.chartId)) {
|
|
44076
|
+
figure.data.chartId += uuidGenerator.smallUuid();
|
|
44077
|
+
}
|
|
44078
|
+
chartIds.add(figure.data?.chartId);
|
|
44079
|
+
}
|
|
44001
44080
|
}
|
|
44002
44081
|
}
|
|
44003
44082
|
data.uniqueFigureIds = true;
|
|
@@ -49751,39 +49830,63 @@ class CellComposerStore extends AbstractComposerStore {
|
|
|
49751
49830
|
this.model.dispatch("AUTOFILL_TABLE_COLUMN", { ...this.currentEditedCell });
|
|
49752
49831
|
this.setContent("");
|
|
49753
49832
|
}
|
|
49754
|
-
getComposerContent(position) {
|
|
49833
|
+
getComposerContent(position, selection) {
|
|
49755
49834
|
const locale = this.getters.getLocale();
|
|
49756
49835
|
const cell = this.getters.getCell(position);
|
|
49757
49836
|
if (cell?.isFormula) {
|
|
49758
49837
|
const prettifiedContent = this.getPrettifiedFormula(cell);
|
|
49759
|
-
|
|
49838
|
+
// when a formula is prettified (multi lines, indented), adapt the cursor position
|
|
49839
|
+
// to take into account line breaks and tabs
|
|
49840
|
+
function adjustCursorIndex(targetIndex) {
|
|
49841
|
+
let adjustedIndex = 0;
|
|
49842
|
+
let originalIndex = 0;
|
|
49843
|
+
while (originalIndex < targetIndex) {
|
|
49844
|
+
adjustedIndex++;
|
|
49845
|
+
const char = prettifiedContent[adjustedIndex];
|
|
49846
|
+
if (char !== "\n" && char !== "\t") {
|
|
49847
|
+
originalIndex++;
|
|
49848
|
+
}
|
|
49849
|
+
}
|
|
49850
|
+
return adjustedIndex;
|
|
49851
|
+
}
|
|
49852
|
+
let adjustedSelection = selection;
|
|
49853
|
+
if (selection) {
|
|
49854
|
+
adjustedSelection = {
|
|
49855
|
+
start: adjustCursorIndex(selection.start),
|
|
49856
|
+
end: adjustCursorIndex(selection.end),
|
|
49857
|
+
};
|
|
49858
|
+
}
|
|
49859
|
+
return {
|
|
49860
|
+
text: localizeFormula(prettifiedContent, locale),
|
|
49861
|
+
adjustedSelection,
|
|
49862
|
+
};
|
|
49760
49863
|
}
|
|
49761
49864
|
const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
|
|
49762
49865
|
if (spreader) {
|
|
49763
|
-
return "";
|
|
49866
|
+
return { text: "" };
|
|
49764
49867
|
}
|
|
49765
49868
|
const { format, value, type, formattedValue } = this.getters.getEvaluatedCell(position);
|
|
49766
49869
|
switch (type) {
|
|
49767
49870
|
case CellValueType.empty:
|
|
49768
|
-
return "";
|
|
49871
|
+
return { text: "" };
|
|
49769
49872
|
case CellValueType.text:
|
|
49770
49873
|
case CellValueType.error:
|
|
49771
|
-
return value;
|
|
49874
|
+
return { text: value };
|
|
49772
49875
|
case CellValueType.boolean:
|
|
49773
|
-
return formattedValue;
|
|
49876
|
+
return { text: formattedValue };
|
|
49774
49877
|
case CellValueType.number:
|
|
49775
49878
|
if (format && isDateTimeFormat(format)) {
|
|
49776
49879
|
if (parseDateTime(formattedValue, locale) !== null) {
|
|
49777
49880
|
// formatted string can be parsed again
|
|
49778
|
-
return formattedValue;
|
|
49881
|
+
return { text: formattedValue };
|
|
49779
49882
|
}
|
|
49780
49883
|
// display a simplified and parsable string otherwise
|
|
49781
49884
|
const timeFormat = Number.isInteger(value)
|
|
49782
49885
|
? locale.dateFormat
|
|
49783
49886
|
: getDateTimeFormat(locale);
|
|
49784
|
-
return formatValue(value, { locale, format: timeFormat });
|
|
49887
|
+
return { text: formatValue(value, { locale, format: timeFormat }) };
|
|
49785
49888
|
}
|
|
49786
|
-
return this.numberComposerContent(value, format, locale);
|
|
49889
|
+
return { text: this.numberComposerContent(value, format, locale) };
|
|
49787
49890
|
}
|
|
49788
49891
|
}
|
|
49789
49892
|
getPrettifiedFormula(cell) {
|
|
@@ -49952,8 +50055,9 @@ class GridComposer extends owl.Component {
|
|
|
49952
50055
|
},
|
|
49953
50056
|
focus: this.focus,
|
|
49954
50057
|
isDefaultFocus: true,
|
|
49955
|
-
onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {
|
|
50058
|
+
onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
|
|
49956
50059
|
focusMode: "contentFocus",
|
|
50060
|
+
selection,
|
|
49957
50061
|
}),
|
|
49958
50062
|
onComposerCellFocused: (content) => this.composerFocusStore.focusComposer(this.composerInterface, {
|
|
49959
50063
|
focusMode: "cellFocus",
|
|
@@ -57359,12 +57463,13 @@ class DataValidationEditor extends owl.Component {
|
|
|
57359
57463
|
onCloseSidePanel: { type: Function, optional: true },
|
|
57360
57464
|
};
|
|
57361
57465
|
state = owl.useState({ rule: this.defaultDataValidationRule, errors: [] });
|
|
57466
|
+
editingSheetId;
|
|
57362
57467
|
setup() {
|
|
57468
|
+
this.editingSheetId = this.env.model.getters.getActiveSheetId();
|
|
57363
57469
|
if (this.props.rule) {
|
|
57364
|
-
const sheetId = this.env.model.getters.getActiveSheetId();
|
|
57365
57470
|
this.state.rule = {
|
|
57366
57471
|
...this.props.rule,
|
|
57367
|
-
ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range,
|
|
57472
|
+
ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, this.editingSheetId)),
|
|
57368
57473
|
};
|
|
57369
57474
|
this.state.rule.criterion.type = this.props.rule.criterion.type;
|
|
57370
57475
|
}
|
|
@@ -57398,7 +57503,6 @@ class DataValidationEditor extends owl.Component {
|
|
|
57398
57503
|
const locale = this.env.model.getters.getLocale();
|
|
57399
57504
|
const criterion = rule.criterion;
|
|
57400
57505
|
const criterionEvaluator = criterionEvaluatorRegistry.get(criterion.type);
|
|
57401
|
-
const sheetId = this.env.model.getters.getActiveSheetId();
|
|
57402
57506
|
const values = criterion.values
|
|
57403
57507
|
.slice(0, criterionEvaluator.numberOfValues(criterion))
|
|
57404
57508
|
.map((value) => value?.trim())
|
|
@@ -57406,8 +57510,8 @@ class DataValidationEditor extends owl.Component {
|
|
|
57406
57510
|
.map((value) => canonicalizeContent(value, locale));
|
|
57407
57511
|
rule.criterion = { ...criterion, values };
|
|
57408
57512
|
return {
|
|
57409
|
-
sheetId,
|
|
57410
|
-
ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(
|
|
57513
|
+
sheetId: this.editingSheetId,
|
|
57514
|
+
ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(this.editingSheetId, xc)),
|
|
57411
57515
|
rule,
|
|
57412
57516
|
};
|
|
57413
57517
|
}
|
|
@@ -57934,6 +58038,7 @@ css /* scss */ `
|
|
|
57934
58038
|
.o-button {
|
|
57935
58039
|
height: 19px;
|
|
57936
58040
|
width: 19px;
|
|
58041
|
+
box-sizing: content-box;
|
|
57937
58042
|
.o-icon {
|
|
57938
58043
|
height: 14px;
|
|
57939
58044
|
width: 14px;
|
|
@@ -68707,6 +68812,281 @@ class ZoneRBush extends RBush {
|
|
|
68707
68812
|
}
|
|
68708
68813
|
}
|
|
68709
68814
|
|
|
68815
|
+
class ZoneSet {
|
|
68816
|
+
profilesStartingPosition = [0];
|
|
68817
|
+
profiles = new Map([[0, []]]);
|
|
68818
|
+
constructor(zones = []) {
|
|
68819
|
+
for (const zone of zones) {
|
|
68820
|
+
this.add(zone);
|
|
68821
|
+
}
|
|
68822
|
+
}
|
|
68823
|
+
isEmpty() {
|
|
68824
|
+
return this.profiles.size === 1 && this.profiles.get(0)?.length === 0;
|
|
68825
|
+
}
|
|
68826
|
+
add(zone) {
|
|
68827
|
+
modifyProfiles(this.profilesStartingPosition, this.profiles, [zone]);
|
|
68828
|
+
}
|
|
68829
|
+
delete(zone) {
|
|
68830
|
+
modifyProfiles(this.profilesStartingPosition, this.profiles, [zone], true);
|
|
68831
|
+
}
|
|
68832
|
+
has(zone) {
|
|
68833
|
+
return profilesContainsZone(this.profilesStartingPosition, this.profiles, zone);
|
|
68834
|
+
}
|
|
68835
|
+
difference(other) {
|
|
68836
|
+
const result = this.copy();
|
|
68837
|
+
for (const zone of other) {
|
|
68838
|
+
result.delete(zone);
|
|
68839
|
+
}
|
|
68840
|
+
return result;
|
|
68841
|
+
}
|
|
68842
|
+
copy() {
|
|
68843
|
+
const result = new ZoneSet();
|
|
68844
|
+
result.profilesStartingPosition = [...this.profilesStartingPosition];
|
|
68845
|
+
result.profiles = new Map();
|
|
68846
|
+
for (const [key, value] of this.profiles) {
|
|
68847
|
+
result.profiles.set(key, [...value]);
|
|
68848
|
+
}
|
|
68849
|
+
return result;
|
|
68850
|
+
}
|
|
68851
|
+
size() {
|
|
68852
|
+
let size = 0;
|
|
68853
|
+
for (const profile of this.profiles.values()) {
|
|
68854
|
+
size += profile.length;
|
|
68855
|
+
}
|
|
68856
|
+
return size / 2;
|
|
68857
|
+
}
|
|
68858
|
+
/**
|
|
68859
|
+
* iterator of all the zones in the ZoneSet
|
|
68860
|
+
*/
|
|
68861
|
+
[Symbol.iterator]() {
|
|
68862
|
+
return constructZonesFromProfiles(this.profilesStartingPosition, this.profiles)[Symbol.iterator]();
|
|
68863
|
+
}
|
|
68864
|
+
}
|
|
68865
|
+
|
|
68866
|
+
class RangeSet {
|
|
68867
|
+
setsBySheetId = {};
|
|
68868
|
+
constructor(ranges = []) {
|
|
68869
|
+
for (const range of ranges) {
|
|
68870
|
+
this.add(range);
|
|
68871
|
+
}
|
|
68872
|
+
}
|
|
68873
|
+
add(range) {
|
|
68874
|
+
if (!this.setsBySheetId[range.sheetId]) {
|
|
68875
|
+
this.setsBySheetId[range.sheetId] = new ZoneSet();
|
|
68876
|
+
}
|
|
68877
|
+
this.setsBySheetId[range.sheetId].add(range.zone);
|
|
68878
|
+
}
|
|
68879
|
+
addMany(ranges) {
|
|
68880
|
+
for (const range of ranges) {
|
|
68881
|
+
this.add(range);
|
|
68882
|
+
}
|
|
68883
|
+
}
|
|
68884
|
+
addPosition(position) {
|
|
68885
|
+
this.add(positionToBoundedRange(position));
|
|
68886
|
+
}
|
|
68887
|
+
addManyPositions(positions) {
|
|
68888
|
+
for (const position of positions) {
|
|
68889
|
+
this.addPosition(position);
|
|
68890
|
+
}
|
|
68891
|
+
}
|
|
68892
|
+
has(range) {
|
|
68893
|
+
if (!this.setsBySheetId[range.sheetId]) {
|
|
68894
|
+
return false;
|
|
68895
|
+
}
|
|
68896
|
+
return this.setsBySheetId[range.sheetId].has(range.zone);
|
|
68897
|
+
}
|
|
68898
|
+
hasPosition(position) {
|
|
68899
|
+
return this.has(positionToBoundedRange(position));
|
|
68900
|
+
}
|
|
68901
|
+
delete(range) {
|
|
68902
|
+
if (!this.setsBySheetId[range.sheetId]) {
|
|
68903
|
+
return;
|
|
68904
|
+
}
|
|
68905
|
+
this.setsBySheetId[range.sheetId].delete(range.zone);
|
|
68906
|
+
}
|
|
68907
|
+
deleteMany(ranges) {
|
|
68908
|
+
for (const range of ranges) {
|
|
68909
|
+
this.delete(range);
|
|
68910
|
+
}
|
|
68911
|
+
}
|
|
68912
|
+
deleteManyPositions(positions) {
|
|
68913
|
+
for (const position of positions) {
|
|
68914
|
+
this.delete(positionToBoundedRange(position));
|
|
68915
|
+
}
|
|
68916
|
+
}
|
|
68917
|
+
difference(other) {
|
|
68918
|
+
const result = new RangeSet();
|
|
68919
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68920
|
+
result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId];
|
|
68921
|
+
}
|
|
68922
|
+
for (const sheetId in other.setsBySheetId) {
|
|
68923
|
+
if (result.setsBySheetId[sheetId]) {
|
|
68924
|
+
result.setsBySheetId[sheetId] = result.setsBySheetId[sheetId].difference(other.setsBySheetId[sheetId]);
|
|
68925
|
+
}
|
|
68926
|
+
}
|
|
68927
|
+
return result;
|
|
68928
|
+
}
|
|
68929
|
+
copy() {
|
|
68930
|
+
const result = new RangeSet();
|
|
68931
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68932
|
+
result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId].copy();
|
|
68933
|
+
}
|
|
68934
|
+
return result;
|
|
68935
|
+
}
|
|
68936
|
+
clear() {
|
|
68937
|
+
this.setsBySheetId = {};
|
|
68938
|
+
}
|
|
68939
|
+
size() {
|
|
68940
|
+
let size = 0;
|
|
68941
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68942
|
+
size += this.setsBySheetId[sheetId].size();
|
|
68943
|
+
}
|
|
68944
|
+
return size;
|
|
68945
|
+
}
|
|
68946
|
+
isEmpty() {
|
|
68947
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68948
|
+
if (!this.setsBySheetId[sheetId].isEmpty()) {
|
|
68949
|
+
return false;
|
|
68950
|
+
}
|
|
68951
|
+
}
|
|
68952
|
+
return true;
|
|
68953
|
+
}
|
|
68954
|
+
/**
|
|
68955
|
+
* iterator of all the ranges in the RangeSet
|
|
68956
|
+
*/
|
|
68957
|
+
[Symbol.iterator]() {
|
|
68958
|
+
const result = [];
|
|
68959
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68960
|
+
for (const zone of this.setsBySheetId[sheetId]) {
|
|
68961
|
+
result.push({ sheetId: sheetId, zone });
|
|
68962
|
+
}
|
|
68963
|
+
}
|
|
68964
|
+
return result[Symbol.iterator]();
|
|
68965
|
+
}
|
|
68966
|
+
}
|
|
68967
|
+
|
|
68968
|
+
/**
|
|
68969
|
+
* R-Tree of ranges, mapping zones (r-tree bounding boxes) to ranges (data of the r-tree item).
|
|
68970
|
+
* Ranges associated to the exact same bounding box are grouped together
|
|
68971
|
+
* to reduce the number of nodes in the R-tree.
|
|
68972
|
+
*/
|
|
68973
|
+
class DependenciesRTree {
|
|
68974
|
+
rTree;
|
|
68975
|
+
constructor(items = []) {
|
|
68976
|
+
const compactedBoxes = groupSameBoundingBoxes(items);
|
|
68977
|
+
this.rTree = new SpreadsheetRTree(compactedBoxes);
|
|
68978
|
+
}
|
|
68979
|
+
insert(item) {
|
|
68980
|
+
const data = this.rTree.search(item.boundingBox);
|
|
68981
|
+
const itemBoundingBox = item.boundingBox;
|
|
68982
|
+
const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
|
|
68983
|
+
boundingBox.zone.left === itemBoundingBox.zone.left &&
|
|
68984
|
+
boundingBox.zone.top === itemBoundingBox.zone.top &&
|
|
68985
|
+
boundingBox.zone.right === itemBoundingBox.zone.right &&
|
|
68986
|
+
boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
|
|
68987
|
+
if (exactBoundingBox) {
|
|
68988
|
+
exactBoundingBox.data.add(item.data);
|
|
68989
|
+
}
|
|
68990
|
+
else {
|
|
68991
|
+
this.rTree.insert({ ...item, data: new RangeSet([item.data]) });
|
|
68992
|
+
}
|
|
68993
|
+
}
|
|
68994
|
+
search({ zone, sheetId }) {
|
|
68995
|
+
const results = new RangeSet();
|
|
68996
|
+
for (const { data } of this.rTree.search({ zone, sheetId })) {
|
|
68997
|
+
results.addMany(data);
|
|
68998
|
+
}
|
|
68999
|
+
return results;
|
|
69000
|
+
}
|
|
69001
|
+
remove(item) {
|
|
69002
|
+
const data = this.rTree.search(item.boundingBox);
|
|
69003
|
+
const itemBoundingBox = item.boundingBox;
|
|
69004
|
+
const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
|
|
69005
|
+
boundingBox.zone.left === itemBoundingBox.zone.left &&
|
|
69006
|
+
boundingBox.zone.top === itemBoundingBox.zone.top &&
|
|
69007
|
+
boundingBox.zone.right === itemBoundingBox.zone.right &&
|
|
69008
|
+
boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
|
|
69009
|
+
if (exactBoundingBox) {
|
|
69010
|
+
exactBoundingBox.data.delete(item.data);
|
|
69011
|
+
}
|
|
69012
|
+
else {
|
|
69013
|
+
this.rTree.remove({ ...item, data: new RangeSet([item.data]) });
|
|
69014
|
+
}
|
|
69015
|
+
}
|
|
69016
|
+
}
|
|
69017
|
+
/**
|
|
69018
|
+
* Group together all formulas pointing to the exact same dependency (bounding box).
|
|
69019
|
+
* The goal is to optimize the following case:
|
|
69020
|
+
* - if any cell in B1:B1000 changes, C1 must be recomputed
|
|
69021
|
+
* - if any cell in B1:B1000 changes, C2 must be recomputed
|
|
69022
|
+
* - if any cell in B1:B1000 changes, C3 must be recomputed
|
|
69023
|
+
* ...
|
|
69024
|
+
* - if any cell in B1:B1000 changes, C1000 must be recomputed
|
|
69025
|
+
*
|
|
69026
|
+
* Instead of having 1000 entries in the R-tree, we want to have a single entry
|
|
69027
|
+
* with B1:B1000 (bounding box) pointing to C1:C1000 (formulas).
|
|
69028
|
+
*/
|
|
69029
|
+
function groupSameBoundingBoxes(items) {
|
|
69030
|
+
// Important: this function must be as fast as possible. It is on the evaluation hot path.
|
|
69031
|
+
let maxCol = 0;
|
|
69032
|
+
let maxRow = 0;
|
|
69033
|
+
for (let i = 0; i < items.length; i++) {
|
|
69034
|
+
const zone = items[i].boundingBox.zone;
|
|
69035
|
+
if (zone.right > maxCol) {
|
|
69036
|
+
maxCol = zone.right;
|
|
69037
|
+
}
|
|
69038
|
+
if (zone.bottom > maxRow) {
|
|
69039
|
+
maxRow = zone.bottom;
|
|
69040
|
+
}
|
|
69041
|
+
}
|
|
69042
|
+
maxCol += 1;
|
|
69043
|
+
maxRow += 1;
|
|
69044
|
+
// in most real-world cases, we can use a fast numeric key
|
|
69045
|
+
// but if the zones are too far right or bottom, we fallback to a slower string key
|
|
69046
|
+
const maxPossibleKey = (((maxRow + 1) * maxCol + 1) * maxRow + 1) * maxCol;
|
|
69047
|
+
const useFastKey = maxPossibleKey <= Number.MAX_SAFE_INTEGER;
|
|
69048
|
+
if (!useFastKey) {
|
|
69049
|
+
console.warn("Max col/row size exceeded, using slow zone key");
|
|
69050
|
+
}
|
|
69051
|
+
const groupedByBBox = {};
|
|
69052
|
+
for (const item of items) {
|
|
69053
|
+
const sheetId = item.boundingBox.sheetId;
|
|
69054
|
+
if (!groupedByBBox[sheetId]) {
|
|
69055
|
+
groupedByBBox[sheetId] = {};
|
|
69056
|
+
}
|
|
69057
|
+
const bBox = item.boundingBox.zone;
|
|
69058
|
+
let bBoxKey = 0;
|
|
69059
|
+
if (useFastKey) {
|
|
69060
|
+
bBoxKey =
|
|
69061
|
+
bBox.left +
|
|
69062
|
+
bBox.top * maxCol +
|
|
69063
|
+
bBox.right * maxCol * maxRow +
|
|
69064
|
+
bBox.bottom * maxCol * maxRow * maxCol;
|
|
69065
|
+
}
|
|
69066
|
+
else {
|
|
69067
|
+
bBoxKey = `${bBox.left},${bBox.top},${bBox.right},${bBox.bottom}`;
|
|
69068
|
+
}
|
|
69069
|
+
if (groupedByBBox[sheetId][bBoxKey]) {
|
|
69070
|
+
const ranges = groupedByBBox[sheetId][bBoxKey].data;
|
|
69071
|
+
ranges.add(item.data);
|
|
69072
|
+
}
|
|
69073
|
+
else {
|
|
69074
|
+
groupedByBBox[sheetId][bBoxKey] = {
|
|
69075
|
+
boundingBox: item.boundingBox,
|
|
69076
|
+
data: new RangeSet([item.data]),
|
|
69077
|
+
};
|
|
69078
|
+
}
|
|
69079
|
+
}
|
|
69080
|
+
const result = [];
|
|
69081
|
+
for (const sheetId in groupedByBBox) {
|
|
69082
|
+
const map = groupedByBBox[sheetId];
|
|
69083
|
+
for (const key in map) {
|
|
69084
|
+
result.push(map[key]);
|
|
69085
|
+
}
|
|
69086
|
+
}
|
|
69087
|
+
return result;
|
|
69088
|
+
}
|
|
69089
|
+
|
|
68710
69090
|
/**
|
|
68711
69091
|
* Implementation of a dependency Graph.
|
|
68712
69092
|
* The graph is used to evaluate the cells in the correct
|
|
@@ -68715,12 +69095,10 @@ class ZoneRBush extends RBush {
|
|
|
68715
69095
|
* It uses an R-Tree data structure to efficiently find dependent cells.
|
|
68716
69096
|
*/
|
|
68717
69097
|
class FormulaDependencyGraph {
|
|
68718
|
-
createEmptyPositionSet;
|
|
68719
69098
|
dependencies = new PositionMap();
|
|
68720
69099
|
rTree;
|
|
68721
|
-
constructor(
|
|
68722
|
-
this.
|
|
68723
|
-
this.rTree = new SpreadsheetRTree(data);
|
|
69100
|
+
constructor(data = []) {
|
|
69101
|
+
this.rTree = new DependenciesRTree(data);
|
|
68724
69102
|
}
|
|
68725
69103
|
removeAllDependencies(formulaPosition) {
|
|
68726
69104
|
const ranges = this.dependencies.get(formulaPosition);
|
|
@@ -68734,7 +69112,10 @@ class FormulaDependencyGraph {
|
|
|
68734
69112
|
}
|
|
68735
69113
|
addDependencies(formulaPosition, dependencies) {
|
|
68736
69114
|
const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
|
|
68737
|
-
data:
|
|
69115
|
+
data: {
|
|
69116
|
+
sheetId: formulaPosition.sheetId,
|
|
69117
|
+
zone: positionToZone(formulaPosition),
|
|
69118
|
+
},
|
|
68738
69119
|
boundingBox: {
|
|
68739
69120
|
zone,
|
|
68740
69121
|
sheetId,
|
|
@@ -68752,46 +69133,20 @@ class FormulaDependencyGraph {
|
|
|
68752
69133
|
}
|
|
68753
69134
|
}
|
|
68754
69135
|
/**
|
|
68755
|
-
* Return all the cells that depend on the provided ranges
|
|
68756
|
-
* in the correct order they should be evaluated.
|
|
68757
|
-
* This is called a topological ordering (excluding cycles)
|
|
69136
|
+
* Return all the cells that depend on the provided ranges.
|
|
68758
69137
|
*/
|
|
68759
|
-
getCellsDependingOn(ranges) {
|
|
68760
|
-
|
|
69138
|
+
getCellsDependingOn(ranges, visited = new RangeSet()) {
|
|
69139
|
+
visited = visited.copy();
|
|
68761
69140
|
const queue = Array.from(ranges).reverse();
|
|
68762
69141
|
while (queue.length > 0) {
|
|
68763
69142
|
const range = queue.pop();
|
|
68764
|
-
|
|
68765
|
-
const
|
|
68766
|
-
|
|
68767
|
-
for (let row = zone.top; row <= zone.bottom; row++) {
|
|
68768
|
-
visited.add({ sheetId, col, row });
|
|
68769
|
-
}
|
|
68770
|
-
}
|
|
68771
|
-
const impactedPositions = this.rTree.search(range).map((dep) => dep.data);
|
|
68772
|
-
const nextInQueue = {};
|
|
68773
|
-
for (const position of impactedPositions) {
|
|
68774
|
-
if (!visited.has(position)) {
|
|
68775
|
-
if (!nextInQueue[position.sheetId]) {
|
|
68776
|
-
nextInQueue[position.sheetId] = [];
|
|
68777
|
-
}
|
|
68778
|
-
nextInQueue[position.sheetId].push(positionToZone(position));
|
|
68779
|
-
}
|
|
68780
|
-
}
|
|
68781
|
-
for (const sheetId in nextInQueue) {
|
|
68782
|
-
const zones = recomputeZones(nextInQueue[sheetId], []);
|
|
68783
|
-
queue.push(...zones.map((zone) => ({ sheetId, zone })));
|
|
68784
|
-
}
|
|
69143
|
+
visited.add(range);
|
|
69144
|
+
const impactedRanges = this.rTree.search(range);
|
|
69145
|
+
queue.push(...impactedRanges.difference(visited));
|
|
68785
69146
|
}
|
|
68786
69147
|
// remove initial ranges
|
|
68787
69148
|
for (const range of ranges) {
|
|
68788
|
-
|
|
68789
|
-
const sheetId = range.sheetId;
|
|
68790
|
-
for (let col = zone.left; col <= zone.right; col++) {
|
|
68791
|
-
for (let row = zone.top; row <= zone.bottom; row++) {
|
|
68792
|
-
visited.delete({ sheetId, col, row });
|
|
68793
|
-
}
|
|
68794
|
-
}
|
|
69149
|
+
visited.delete(range);
|
|
68795
69150
|
}
|
|
68796
69151
|
return visited;
|
|
68797
69152
|
}
|
|
@@ -69054,7 +69409,7 @@ class Evaluator {
|
|
|
69054
69409
|
getters;
|
|
69055
69410
|
compilationParams;
|
|
69056
69411
|
evaluatedCells = new PositionMap();
|
|
69057
|
-
formulaDependencies = lazy(new FormulaDependencyGraph(
|
|
69412
|
+
formulaDependencies = lazy(new FormulaDependencyGraph());
|
|
69058
69413
|
blockedArrayFormulas = new PositionSet({});
|
|
69059
69414
|
spreadingRelations = new SpreadingRelation();
|
|
69060
69415
|
constructor(context, getters) {
|
|
@@ -69089,7 +69444,7 @@ class Evaluator {
|
|
|
69089
69444
|
return undefined;
|
|
69090
69445
|
}
|
|
69091
69446
|
const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
|
|
69092
|
-
return
|
|
69447
|
+
return arrayFormulas.find((position) => !this.blockedArrayFormulas.has(position));
|
|
69093
69448
|
}
|
|
69094
69449
|
updateDependencies(position) {
|
|
69095
69450
|
// removing dependencies is slow because it requires
|
|
@@ -69133,57 +69488,72 @@ class Evaluator {
|
|
|
69133
69488
|
}
|
|
69134
69489
|
evaluateCells(positions) {
|
|
69135
69490
|
const start = performance.now();
|
|
69136
|
-
const
|
|
69137
|
-
|
|
69491
|
+
const rangesToCompute = new RangeSet();
|
|
69492
|
+
rangesToCompute.addManyPositions(positions);
|
|
69138
69493
|
const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);
|
|
69139
|
-
|
|
69140
|
-
|
|
69141
|
-
|
|
69142
|
-
this.evaluate(
|
|
69494
|
+
rangesToCompute.addMany(this.getCellsDependingOn(rangesToCompute));
|
|
69495
|
+
rangesToCompute.addMany(arrayFormulasPositions);
|
|
69496
|
+
rangesToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
|
|
69497
|
+
this.evaluate(rangesToCompute);
|
|
69143
69498
|
console.debug("evaluate Cells", performance.now() - start, "ms");
|
|
69144
69499
|
}
|
|
69145
69500
|
getArrayFormulasImpactedByChangesOf(positions) {
|
|
69146
|
-
const
|
|
69501
|
+
const impactedRanges = new RangeSet();
|
|
69147
69502
|
for (const position of positions) {
|
|
69148
69503
|
const content = this.getters.getCell(position)?.content;
|
|
69149
69504
|
const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
|
|
69150
69505
|
if (arrayFormulaPosition !== undefined) {
|
|
69151
69506
|
// take into account new collisions.
|
|
69152
|
-
|
|
69507
|
+
impactedRanges.addPosition(arrayFormulaPosition);
|
|
69153
69508
|
}
|
|
69154
69509
|
if (!content) {
|
|
69155
69510
|
// The previous content could have blocked some array formulas
|
|
69156
|
-
|
|
69511
|
+
impactedRanges.addPosition(position);
|
|
69157
69512
|
}
|
|
69158
69513
|
}
|
|
69159
|
-
const
|
|
69160
|
-
|
|
69161
|
-
for (const zone of zonesBySheetIds[sheetId]) {
|
|
69162
|
-
impactedPositions.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
|
|
69163
|
-
}
|
|
69514
|
+
for (const range of [...impactedRanges]) {
|
|
69515
|
+
impactedRanges.addMany(this.getArrayFormulasBlockedBy(range.sheetId, range.zone));
|
|
69164
69516
|
}
|
|
69165
|
-
return
|
|
69517
|
+
return impactedRanges;
|
|
69166
69518
|
}
|
|
69167
69519
|
buildDependencyGraph() {
|
|
69168
69520
|
this.blockedArrayFormulas = this.createEmptyPositionSet();
|
|
69169
69521
|
this.spreadingRelations = new SpreadingRelation();
|
|
69170
69522
|
this.formulaDependencies = lazy(() => {
|
|
69171
|
-
const
|
|
69172
|
-
|
|
69173
|
-
.
|
|
69174
|
-
|
|
69175
|
-
|
|
69176
|
-
|
|
69177
|
-
|
|
69178
|
-
|
|
69179
|
-
|
|
69180
|
-
|
|
69523
|
+
const rTreeItems = [];
|
|
69524
|
+
for (const sheetId of this.getters.getSheetIds()) {
|
|
69525
|
+
const cells = this.getters.getCells(sheetId);
|
|
69526
|
+
for (const cellId in cells) {
|
|
69527
|
+
const cell = cells[cellId];
|
|
69528
|
+
if (cell.isFormula) {
|
|
69529
|
+
const directDependencies = cell.compiledFormula.dependencies;
|
|
69530
|
+
for (const range of directDependencies) {
|
|
69531
|
+
if (range.invalidSheetName || range.invalidXc) {
|
|
69532
|
+
continue;
|
|
69533
|
+
}
|
|
69534
|
+
rTreeItems.push({
|
|
69535
|
+
data: {
|
|
69536
|
+
sheetId,
|
|
69537
|
+
zone: positionToZone(this.getters.getCellPosition(cellId)),
|
|
69538
|
+
},
|
|
69539
|
+
boundingBox: { sheetId: range.sheetId, zone: range.zone },
|
|
69540
|
+
});
|
|
69541
|
+
}
|
|
69542
|
+
}
|
|
69543
|
+
}
|
|
69544
|
+
}
|
|
69545
|
+
return new FormulaDependencyGraph(rTreeItems);
|
|
69181
69546
|
});
|
|
69182
69547
|
}
|
|
69183
69548
|
evaluateAllCells() {
|
|
69184
69549
|
const start = performance.now();
|
|
69185
69550
|
this.evaluatedCells = new PositionMap();
|
|
69186
|
-
|
|
69551
|
+
const ranges = [];
|
|
69552
|
+
for (const sheetId of this.getters.getSheetIds()) {
|
|
69553
|
+
const zone = this.getters.getSheetZone(sheetId);
|
|
69554
|
+
ranges.push({ sheetId, zone });
|
|
69555
|
+
}
|
|
69556
|
+
this.evaluate(ranges);
|
|
69187
69557
|
console.debug("evaluate all cells", performance.now() - start, "ms");
|
|
69188
69558
|
}
|
|
69189
69559
|
evaluateFormulaResult(sheetId, formulaString) {
|
|
@@ -69207,48 +69577,47 @@ class Evaluator {
|
|
|
69207
69577
|
return handleError(error, "");
|
|
69208
69578
|
}
|
|
69209
69579
|
}
|
|
69210
|
-
getAllCells() {
|
|
69211
|
-
const positions = this.createEmptyPositionSet();
|
|
69212
|
-
positions.fillAllPositions();
|
|
69213
|
-
return positions;
|
|
69214
|
-
}
|
|
69215
69580
|
/**
|
|
69216
69581
|
* Return the position of formulas blocked by the given positions
|
|
69217
69582
|
* as well as all their dependencies.
|
|
69218
69583
|
*/
|
|
69219
69584
|
getArrayFormulasBlockedBy(sheetId, zone) {
|
|
69220
|
-
const arrayFormulaPositions =
|
|
69585
|
+
const arrayFormulaPositions = new RangeSet();
|
|
69221
69586
|
const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);
|
|
69222
|
-
arrayFormulaPositions.
|
|
69587
|
+
arrayFormulaPositions.addManyPositions(arrayFormulas);
|
|
69223
69588
|
const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));
|
|
69224
69589
|
if (spilledPositions.length) {
|
|
69225
69590
|
// ignore the formula spreading on the position. Keep only the blocked ones
|
|
69226
|
-
arrayFormulaPositions.
|
|
69591
|
+
arrayFormulaPositions.deleteManyPositions(spilledPositions);
|
|
69227
69592
|
}
|
|
69228
69593
|
arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));
|
|
69229
69594
|
return arrayFormulaPositions;
|
|
69230
69595
|
}
|
|
69231
|
-
|
|
69596
|
+
nextRangesToUpdate = new RangeSet();
|
|
69232
69597
|
cellsBeingComputed = new Set();
|
|
69233
69598
|
symbolsBeingComputed = new Set();
|
|
69234
|
-
evaluate(
|
|
69599
|
+
evaluate(ranges) {
|
|
69235
69600
|
this.cellsBeingComputed = new Set();
|
|
69236
|
-
this.
|
|
69601
|
+
this.nextRangesToUpdate = new RangeSet(ranges);
|
|
69237
69602
|
let currentIteration = 0;
|
|
69238
|
-
while (!this.
|
|
69603
|
+
while (!this.nextRangesToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
|
|
69239
69604
|
this.updateCompilationParameters();
|
|
69240
|
-
const
|
|
69241
|
-
|
|
69242
|
-
|
|
69243
|
-
|
|
69244
|
-
|
|
69245
|
-
|
|
69246
|
-
|
|
69247
|
-
|
|
69248
|
-
|
|
69249
|
-
|
|
69250
|
-
|
|
69251
|
-
|
|
69605
|
+
const ranges = [...this.nextRangesToUpdate];
|
|
69606
|
+
this.nextRangesToUpdate.clear();
|
|
69607
|
+
this.clearEvaluatedRanges(ranges);
|
|
69608
|
+
for (const range of ranges) {
|
|
69609
|
+
const { left, bottom, right, top } = range.zone;
|
|
69610
|
+
for (let col = left; col <= right; col++) {
|
|
69611
|
+
for (let row = top; row <= bottom; row++) {
|
|
69612
|
+
const position = { sheetId: range.sheetId, col, row };
|
|
69613
|
+
if (this.nextRangesToUpdate.hasPosition(position)) {
|
|
69614
|
+
continue;
|
|
69615
|
+
}
|
|
69616
|
+
const evaluatedCell = this.computeCell(position);
|
|
69617
|
+
if (evaluatedCell !== EMPTY_CELL) {
|
|
69618
|
+
this.evaluatedCells.set(position, evaluatedCell);
|
|
69619
|
+
}
|
|
69620
|
+
}
|
|
69252
69621
|
}
|
|
69253
69622
|
}
|
|
69254
69623
|
onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
|
|
@@ -69257,6 +69626,16 @@ class Evaluator {
|
|
|
69257
69626
|
console.warn("Maximum iteration reached while evaluating cells");
|
|
69258
69627
|
}
|
|
69259
69628
|
}
|
|
69629
|
+
clearEvaluatedRanges(ranges) {
|
|
69630
|
+
for (const range of ranges) {
|
|
69631
|
+
const { left, bottom, right, top } = range.zone;
|
|
69632
|
+
for (let col = left; col <= right; col++) {
|
|
69633
|
+
for (let row = top; row <= bottom; row++) {
|
|
69634
|
+
this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
|
|
69635
|
+
}
|
|
69636
|
+
}
|
|
69637
|
+
}
|
|
69638
|
+
}
|
|
69260
69639
|
computeCell(position) {
|
|
69261
69640
|
const evaluation = this.evaluatedCells.get(position);
|
|
69262
69641
|
if (evaluation) {
|
|
@@ -69329,9 +69708,9 @@ class Evaluator {
|
|
|
69329
69708
|
}
|
|
69330
69709
|
invalidatePositionsDependingOnSpread(sheetId, resultZone) {
|
|
69331
69710
|
// the result matrix is split in 2 zones to exclude the array formula position
|
|
69332
|
-
const invalidatedPositions = this.
|
|
69333
|
-
invalidatedPositions.delete({ sheetId,
|
|
69334
|
-
this.
|
|
69711
|
+
const invalidatedPositions = this.getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
|
|
69712
|
+
invalidatedPositions.delete({ sheetId, zone: resultZone });
|
|
69713
|
+
this.nextRangesToUpdate.addMany(invalidatedPositions);
|
|
69335
69714
|
}
|
|
69336
69715
|
assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
|
|
69337
69716
|
const numberOfCols = this.getters.getNumberCols(sheetId);
|
|
@@ -69406,7 +69785,7 @@ class Evaluator {
|
|
|
69406
69785
|
}
|
|
69407
69786
|
const sheetId = position.sheetId;
|
|
69408
69787
|
this.invalidatePositionsDependingOnSpread(sheetId, zone);
|
|
69409
|
-
this.
|
|
69788
|
+
this.nextRangesToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
|
|
69410
69789
|
}
|
|
69411
69790
|
/**
|
|
69412
69791
|
* Wraps a GetSymbolValue function to add cycle detection
|
|
@@ -69441,13 +69820,8 @@ class Evaluator {
|
|
|
69441
69820
|
}
|
|
69442
69821
|
return cell.compiledFormula.dependencies;
|
|
69443
69822
|
}
|
|
69444
|
-
getCellsDependingOn(
|
|
69445
|
-
|
|
69446
|
-
const zonesBySheetIds = aggregatePositionsToZones(positions);
|
|
69447
|
-
for (const sheetId in zonesBySheetIds) {
|
|
69448
|
-
ranges.push(...zonesBySheetIds[sheetId].map((zone) => ({ sheetId, zone })));
|
|
69449
|
-
}
|
|
69450
|
-
return this.formulaDependencies().getCellsDependingOn(ranges);
|
|
69823
|
+
getCellsDependingOn(ranges) {
|
|
69824
|
+
return this.formulaDependencies().getCellsDependingOn(ranges, this.nextRangesToUpdate);
|
|
69451
69825
|
}
|
|
69452
69826
|
}
|
|
69453
69827
|
function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
|
|
@@ -70958,7 +71332,8 @@ class DynamicTablesPlugin extends CoreViewPlugin {
|
|
|
70958
71332
|
const topLeft = { col: unionZone.left, row: unionZone.top, sheetId };
|
|
70959
71333
|
const parentSpreadingCell = this.getters.getArrayFormulaSpreadingOn(topLeft);
|
|
70960
71334
|
if (!parentSpreadingCell) {
|
|
70961
|
-
|
|
71335
|
+
const evaluatedCell = this.getters.getEvaluatedCell(topLeft);
|
|
71336
|
+
return (evaluatedCell.value === CellErrorType.SpilledBlocked && !evaluatedCell.errorOriginPosition);
|
|
70962
71337
|
}
|
|
70963
71338
|
else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
|
|
70964
71339
|
return true;
|
|
@@ -82130,6 +82505,7 @@ class RibbonMenu extends owl.Component {
|
|
|
82130
82505
|
static components = { Menu };
|
|
82131
82506
|
rootItems = topbarMenuRegistry.getMenuItems();
|
|
82132
82507
|
menuRef = owl.useRef("menu");
|
|
82508
|
+
containerRef = owl.useRef("container");
|
|
82133
82509
|
state = owl.useState({
|
|
82134
82510
|
menuItems: this.rootItems,
|
|
82135
82511
|
title: _t("Menu Bar"),
|
|
@@ -82137,6 +82513,7 @@ class RibbonMenu extends owl.Component {
|
|
|
82137
82513
|
});
|
|
82138
82514
|
setup() {
|
|
82139
82515
|
owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
|
|
82516
|
+
owl.onMounted(this.updateShadows);
|
|
82140
82517
|
}
|
|
82141
82518
|
onExternalClick(ev) {
|
|
82142
82519
|
if (!this.menuRef.el?.contains(ev.target)) {
|
|
@@ -82149,6 +82526,7 @@ class RibbonMenu extends owl.Component {
|
|
|
82149
82526
|
this.state.parentState = { ...this.state };
|
|
82150
82527
|
this.state.menuItems = children;
|
|
82151
82528
|
this.state.title = menu.name(this.env);
|
|
82529
|
+
this.containerRef.el?.scrollTo({ top: 0 });
|
|
82152
82530
|
}
|
|
82153
82531
|
else {
|
|
82154
82532
|
this.state.menuItems = this.rootItems;
|
|
@@ -82170,6 +82548,19 @@ class RibbonMenu extends owl.Component {
|
|
|
82170
82548
|
height: `${this.props.height}px`,
|
|
82171
82549
|
});
|
|
82172
82550
|
}
|
|
82551
|
+
updateShadows() {
|
|
82552
|
+
if (!this.containerRef.el) {
|
|
82553
|
+
return;
|
|
82554
|
+
}
|
|
82555
|
+
this.containerRef.el.classList.remove("scroll-top", "scroll-bottom");
|
|
82556
|
+
const maxScroll = this.containerRef.el.scrollHeight - this.containerRef.el.clientHeight || 0;
|
|
82557
|
+
if (this.containerRef.el.scrollTop < maxScroll - 1) {
|
|
82558
|
+
this.containerRef.el.classList.add("scroll-bottom");
|
|
82559
|
+
}
|
|
82560
|
+
if (this.containerRef.el.scrollTop > 0) {
|
|
82561
|
+
this.containerRef.el.classList.add("scroll-top");
|
|
82562
|
+
}
|
|
82563
|
+
}
|
|
82173
82564
|
onClickBack() {
|
|
82174
82565
|
if (!this.state.parentState) {
|
|
82175
82566
|
this.props.onClose();
|
|
@@ -82178,6 +82569,7 @@ class RibbonMenu extends owl.Component {
|
|
|
82178
82569
|
this.state.menuItems = this.state.parentState.menuItems;
|
|
82179
82570
|
this.state.title = this.state.parentState.title;
|
|
82180
82571
|
this.state.parentState = this.state.parentState.parentState;
|
|
82572
|
+
this.containerRef.el?.scrollTo({ top: 0 });
|
|
82181
82573
|
}
|
|
82182
82574
|
get backTitle() {
|
|
82183
82575
|
return this.state.parentState ? _t("Go to previous menu") : _t("Close menu bar");
|
|
@@ -82234,7 +82626,9 @@ class SmallBottomBar extends owl.Component {
|
|
|
82234
82626
|
: "inactive";
|
|
82235
82627
|
}
|
|
82236
82628
|
get showFxIcon() {
|
|
82237
|
-
return this.focus === "inactive" &&
|
|
82629
|
+
return (this.focus === "inactive" &&
|
|
82630
|
+
!this.composerStore.currentContent &&
|
|
82631
|
+
!this.composerStore.placeholder);
|
|
82238
82632
|
}
|
|
82239
82633
|
get rect() {
|
|
82240
82634
|
return this.composerRef.el
|
|
@@ -82251,8 +82645,9 @@ class SmallBottomBar extends owl.Component {
|
|
|
82251
82645
|
},
|
|
82252
82646
|
focus: this.focus,
|
|
82253
82647
|
composerStore: this.composerStore,
|
|
82254
|
-
onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {
|
|
82648
|
+
onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
|
|
82255
82649
|
focusMode: "contentFocus",
|
|
82650
|
+
selection,
|
|
82256
82651
|
}),
|
|
82257
82652
|
isDefaultFocus: false,
|
|
82258
82653
|
inputStyle: cssPropertiesToCss({
|
|
@@ -82260,6 +82655,7 @@ class SmallBottomBar extends owl.Component {
|
|
|
82260
82655
|
"max-height": `130px`,
|
|
82261
82656
|
}),
|
|
82262
82657
|
showAssistant: !isIOS(), // Hide assistant on iOS as it breaks visually
|
|
82658
|
+
placeholder: this.composerStore.placeholder,
|
|
82263
82659
|
};
|
|
82264
82660
|
}
|
|
82265
82661
|
get symbols() {
|
|
@@ -82322,7 +82718,9 @@ class TopBarComposer extends owl.Component {
|
|
|
82322
82718
|
: "inactive";
|
|
82323
82719
|
}
|
|
82324
82720
|
get showFxIcon() {
|
|
82325
|
-
return this.focus === "inactive" &&
|
|
82721
|
+
return (this.focus === "inactive" &&
|
|
82722
|
+
!this.composerStore.currentContent &&
|
|
82723
|
+
!this.composerStore.placeholder);
|
|
82326
82724
|
}
|
|
82327
82725
|
get composerStyle() {
|
|
82328
82726
|
const style = {
|
|
@@ -88538,6 +88936,6 @@ exports.tokenColors = tokenColors;
|
|
|
88538
88936
|
exports.tokenize = tokenize;
|
|
88539
88937
|
|
|
88540
88938
|
|
|
88541
|
-
__info__.version = "19.0.
|
|
88542
|
-
__info__.date = "2025-10-
|
|
88543
|
-
__info__.hash = "
|
|
88939
|
+
__info__.version = "19.0.7";
|
|
88940
|
+
__info__.date = "2025-10-23T08:19:01.764Z";
|
|
88941
|
+
__info__.hash = "1c1d1ec";
|