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