@odoo/o-spreadsheet 18.2.1 → 18.2.3
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 +1351 -1002
- package/dist/o-spreadsheet.d.ts +188 -92
- package/dist/o-spreadsheet.esm.js +1351 -1002
- package/dist/o-spreadsheet.iife.js +1351 -1002
- package/dist/o-spreadsheet.iife.min.js +409 -387
- package/dist/o_spreadsheet.xml +124 -70
- 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 18.2.
|
|
6
|
-
* @date 2025-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 18.2.3
|
|
6
|
+
* @date 2025-03-12T15:32:36.274Z
|
|
7
|
+
* @hash 81b0e08
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
@@ -425,7 +425,6 @@ function escapeRegExp(str) {
|
|
|
425
425
|
* Sparse arrays remain sparse.
|
|
426
426
|
*/
|
|
427
427
|
function deepCopy(obj) {
|
|
428
|
-
const result = Array.isArray(obj) ? [] : {};
|
|
429
428
|
switch (typeof obj) {
|
|
430
429
|
case "object": {
|
|
431
430
|
if (obj === null) {
|
|
@@ -437,8 +436,18 @@ function deepCopy(obj) {
|
|
|
437
436
|
else if (!(isPlainObject(obj) || obj instanceof Array)) {
|
|
438
437
|
throw new Error("Unsupported type: only objects and arrays are supported");
|
|
439
438
|
}
|
|
440
|
-
|
|
441
|
-
|
|
439
|
+
const result = Array.isArray(obj) ? new Array(obj.length) : {};
|
|
440
|
+
if (Array.isArray(obj)) {
|
|
441
|
+
for (let i = 0, len = obj.length; i < len; i++) {
|
|
442
|
+
if (i in obj) {
|
|
443
|
+
result[i] = deepCopy(obj[i]);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
for (const key in obj) {
|
|
449
|
+
result[key] = deepCopy(obj[key]);
|
|
450
|
+
}
|
|
442
451
|
}
|
|
443
452
|
return result;
|
|
444
453
|
}
|
|
@@ -2701,21 +2710,30 @@ function mergeContiguousZones(zones) {
|
|
|
2701
2710
|
return mergedZones;
|
|
2702
2711
|
}
|
|
2703
2712
|
|
|
2713
|
+
const globalReverseLookup$1 = new WeakMap();
|
|
2714
|
+
const globalIdCounter = new WeakMap();
|
|
2704
2715
|
/**
|
|
2705
2716
|
* Get the id of the given item (its key in the given dictionary).
|
|
2706
2717
|
* If the given item does not exist in the dictionary, it creates one with a new id.
|
|
2707
2718
|
*/
|
|
2708
2719
|
function getItemId(item, itemsDic) {
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2720
|
+
if (!globalReverseLookup$1.has(itemsDic)) {
|
|
2721
|
+
globalReverseLookup$1.set(itemsDic, new Map());
|
|
2722
|
+
globalIdCounter.set(itemsDic, 0);
|
|
2723
|
+
}
|
|
2724
|
+
const reverseLookup = globalReverseLookup$1.get(itemsDic);
|
|
2725
|
+
const canonical = getCanonicalRepresentation(item);
|
|
2726
|
+
if (reverseLookup.has(canonical)) {
|
|
2727
|
+
const id = reverseLookup.get(canonical);
|
|
2728
|
+
itemsDic[id] = item;
|
|
2729
|
+
return id;
|
|
2713
2730
|
}
|
|
2714
2731
|
// Generate new Id if the item didn't exist in the dictionary
|
|
2715
|
-
const
|
|
2716
|
-
|
|
2717
|
-
itemsDic
|
|
2718
|
-
|
|
2732
|
+
const newId = globalIdCounter.get(itemsDic) + 1;
|
|
2733
|
+
reverseLookup.set(canonical, newId);
|
|
2734
|
+
globalIdCounter.set(itemsDic, newId);
|
|
2735
|
+
itemsDic[newId] = item;
|
|
2736
|
+
return newId;
|
|
2719
2737
|
}
|
|
2720
2738
|
function groupItemIdsByZones(positionsByItemId) {
|
|
2721
2739
|
const result = {};
|
|
@@ -2739,6 +2757,33 @@ function* iterateItemIdsPositions(sheetId, itemIdsByZones) {
|
|
|
2739
2757
|
}
|
|
2740
2758
|
}
|
|
2741
2759
|
}
|
|
2760
|
+
function getCanonicalRepresentation(item) {
|
|
2761
|
+
if (item === null)
|
|
2762
|
+
return "null";
|
|
2763
|
+
if (item === undefined)
|
|
2764
|
+
return "undefined";
|
|
2765
|
+
if (typeof item !== "object")
|
|
2766
|
+
return String(item);
|
|
2767
|
+
if (Array.isArray(item)) {
|
|
2768
|
+
const len = item.length;
|
|
2769
|
+
let result = "[";
|
|
2770
|
+
for (let i = 0; i < len; i++) {
|
|
2771
|
+
if (i > 0)
|
|
2772
|
+
result += ",";
|
|
2773
|
+
result += getCanonicalRepresentation(item[i]);
|
|
2774
|
+
}
|
|
2775
|
+
return result + "]";
|
|
2776
|
+
}
|
|
2777
|
+
const keys = Object.keys(item).sort();
|
|
2778
|
+
let repr = "{";
|
|
2779
|
+
for (const key of keys) {
|
|
2780
|
+
if (item[key] !== undefined) {
|
|
2781
|
+
repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
repr += "}";
|
|
2785
|
+
return repr;
|
|
2786
|
+
}
|
|
2742
2787
|
|
|
2743
2788
|
// -----------------------------------------------------------------------------
|
|
2744
2789
|
// Date Type
|
|
@@ -6102,8 +6147,9 @@ function spreadRange(getters, dataSets) {
|
|
|
6102
6147
|
if (zone.bottom !== zone.top && zone.left != zone.right) {
|
|
6103
6148
|
if (zone.right) {
|
|
6104
6149
|
for (let j = zone.left; j <= zone.right; ++j) {
|
|
6150
|
+
const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6105
6151
|
postProcessedRanges.push({
|
|
6106
|
-
...
|
|
6152
|
+
...datasetOptions,
|
|
6107
6153
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6108
6154
|
left: j,
|
|
6109
6155
|
right: j,
|
|
@@ -6115,8 +6161,9 @@ function spreadRange(getters, dataSets) {
|
|
|
6115
6161
|
}
|
|
6116
6162
|
else {
|
|
6117
6163
|
for (let j = zone.top; j <= zone.bottom; ++j) {
|
|
6164
|
+
const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6118
6165
|
postProcessedRanges.push({
|
|
6119
|
-
...
|
|
6166
|
+
...datasetOptions,
|
|
6120
6167
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6121
6168
|
left: zone.left,
|
|
6122
6169
|
right: zone.right,
|
|
@@ -8289,7 +8336,8 @@ function isSortedColumnValid(sortedColumn, pivot) {
|
|
|
8289
8336
|
const possibleValues = pivot
|
|
8290
8337
|
.getPossibleFieldValues(columns[i])
|
|
8291
8338
|
.map((v) => v.value);
|
|
8292
|
-
if (!possibleValues.includes(sortedColumn.domain[i].value)
|
|
8339
|
+
if (!possibleValues.includes(sortedColumn.domain[i].value) &&
|
|
8340
|
+
!(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
|
|
8293
8341
|
return false;
|
|
8294
8342
|
}
|
|
8295
8343
|
}
|
|
@@ -9534,150 +9582,6 @@ class ComposerFocusStore extends SpreadsheetStore {
|
|
|
9534
9582
|
}
|
|
9535
9583
|
}
|
|
9536
9584
|
|
|
9537
|
-
/**
|
|
9538
|
-
* This file is largely inspired by owl 1.
|
|
9539
|
-
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
9540
|
-
* So, the solution was to import the behavior of owl 1 directly in our
|
|
9541
|
-
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
9542
|
-
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
9543
|
-
* created for the first time.
|
|
9544
|
-
*/
|
|
9545
|
-
const STYLESHEETS = {};
|
|
9546
|
-
let nextId = 0;
|
|
9547
|
-
/**
|
|
9548
|
-
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
9549
|
-
* an inline stylesheet with just the following code:
|
|
9550
|
-
* ```js
|
|
9551
|
-
* css`.component-a { color: red; }`;
|
|
9552
|
-
* ```
|
|
9553
|
-
*/
|
|
9554
|
-
function css(strings, ...args) {
|
|
9555
|
-
const name = `__sheet__${nextId++}`;
|
|
9556
|
-
const value = String.raw(strings, ...args);
|
|
9557
|
-
registerSheet(name, value);
|
|
9558
|
-
activateSheet(name);
|
|
9559
|
-
return name;
|
|
9560
|
-
}
|
|
9561
|
-
function processSheet(str) {
|
|
9562
|
-
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
9563
|
-
const selectorStack = [];
|
|
9564
|
-
const parts = [];
|
|
9565
|
-
let rules = [];
|
|
9566
|
-
function generateSelector(stackIndex, parentSelector) {
|
|
9567
|
-
const parts = [];
|
|
9568
|
-
for (const selector of selectorStack[stackIndex]) {
|
|
9569
|
-
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
9570
|
-
if (part.includes("&")) {
|
|
9571
|
-
part = selector.replace(/&/g, parentSelector || "");
|
|
9572
|
-
}
|
|
9573
|
-
if (stackIndex < selectorStack.length - 1) {
|
|
9574
|
-
part = generateSelector(stackIndex + 1, part);
|
|
9575
|
-
}
|
|
9576
|
-
parts.push(part);
|
|
9577
|
-
}
|
|
9578
|
-
return parts.join(", ");
|
|
9579
|
-
}
|
|
9580
|
-
function generateRules() {
|
|
9581
|
-
if (rules.length) {
|
|
9582
|
-
parts.push(generateSelector(0) + " {");
|
|
9583
|
-
parts.push(...rules);
|
|
9584
|
-
parts.push("}");
|
|
9585
|
-
rules = [];
|
|
9586
|
-
}
|
|
9587
|
-
}
|
|
9588
|
-
while (tokens.length) {
|
|
9589
|
-
let token = tokens.shift();
|
|
9590
|
-
if (token === "}") {
|
|
9591
|
-
generateRules();
|
|
9592
|
-
selectorStack.pop();
|
|
9593
|
-
}
|
|
9594
|
-
else {
|
|
9595
|
-
if (tokens[0] === "{") {
|
|
9596
|
-
generateRules();
|
|
9597
|
-
selectorStack.push(token.split(/\s*,\s*/));
|
|
9598
|
-
tokens.shift();
|
|
9599
|
-
}
|
|
9600
|
-
if (tokens[0] === ";") {
|
|
9601
|
-
rules.push(" " + token + ";");
|
|
9602
|
-
}
|
|
9603
|
-
}
|
|
9604
|
-
}
|
|
9605
|
-
return parts.join("\n");
|
|
9606
|
-
}
|
|
9607
|
-
function registerSheet(id, css) {
|
|
9608
|
-
const sheet = document.createElement("style");
|
|
9609
|
-
sheet.textContent = processSheet(css);
|
|
9610
|
-
STYLESHEETS[id] = sheet;
|
|
9611
|
-
}
|
|
9612
|
-
function activateSheet(id) {
|
|
9613
|
-
const sheet = STYLESHEETS[id];
|
|
9614
|
-
sheet.setAttribute("component", id);
|
|
9615
|
-
document.head.appendChild(sheet);
|
|
9616
|
-
}
|
|
9617
|
-
function getTextDecoration({ strikethrough, underline, }) {
|
|
9618
|
-
if (!strikethrough && !underline) {
|
|
9619
|
-
return "none";
|
|
9620
|
-
}
|
|
9621
|
-
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
9622
|
-
}
|
|
9623
|
-
/**
|
|
9624
|
-
* Convert the cell style to CSS properties.
|
|
9625
|
-
*/
|
|
9626
|
-
function cellStyleToCss(style) {
|
|
9627
|
-
const attributes = cellTextStyleToCss(style);
|
|
9628
|
-
if (!style)
|
|
9629
|
-
return attributes;
|
|
9630
|
-
if (style.fillColor) {
|
|
9631
|
-
attributes["background"] = style.fillColor;
|
|
9632
|
-
}
|
|
9633
|
-
return attributes;
|
|
9634
|
-
}
|
|
9635
|
-
/**
|
|
9636
|
-
* Convert the cell text style to CSS properties.
|
|
9637
|
-
*/
|
|
9638
|
-
function cellTextStyleToCss(style) {
|
|
9639
|
-
const attributes = {};
|
|
9640
|
-
if (!style)
|
|
9641
|
-
return attributes;
|
|
9642
|
-
if (style.bold) {
|
|
9643
|
-
attributes["font-weight"] = "bold";
|
|
9644
|
-
}
|
|
9645
|
-
if (style.italic) {
|
|
9646
|
-
attributes["font-style"] = "italic";
|
|
9647
|
-
}
|
|
9648
|
-
if (style.strikethrough || style.underline) {
|
|
9649
|
-
let decoration = style.strikethrough ? "line-through" : "";
|
|
9650
|
-
decoration = style.underline ? decoration + " underline" : decoration;
|
|
9651
|
-
attributes["text-decoration"] = decoration;
|
|
9652
|
-
}
|
|
9653
|
-
if (style.textColor) {
|
|
9654
|
-
attributes["color"] = style.textColor;
|
|
9655
|
-
}
|
|
9656
|
-
return attributes;
|
|
9657
|
-
}
|
|
9658
|
-
/**
|
|
9659
|
-
* Transform CSS properties into a CSS string.
|
|
9660
|
-
*/
|
|
9661
|
-
function cssPropertiesToCss(attributes) {
|
|
9662
|
-
let styleStr = "";
|
|
9663
|
-
for (const attName in attributes) {
|
|
9664
|
-
if (!attributes[attName]) {
|
|
9665
|
-
continue;
|
|
9666
|
-
}
|
|
9667
|
-
styleStr += `${attName}:${attributes[attName]}; `;
|
|
9668
|
-
}
|
|
9669
|
-
return styleStr;
|
|
9670
|
-
}
|
|
9671
|
-
function getElementMargins(el) {
|
|
9672
|
-
const style = window.getComputedStyle(el);
|
|
9673
|
-
return {
|
|
9674
|
-
top: parseInt(style.marginTop, 10) || 0,
|
|
9675
|
-
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
9676
|
-
left: parseInt(style.marginLeft, 10) || 0,
|
|
9677
|
-
right: parseInt(style.marginRight, 10) || 0,
|
|
9678
|
-
};
|
|
9679
|
-
}
|
|
9680
|
-
|
|
9681
9585
|
const TREND_LINE_XAXIS_ID = "x1";
|
|
9682
9586
|
/**
|
|
9683
9587
|
* This file contains helpers that are common to different charts (mainly
|
|
@@ -10222,79 +10126,341 @@ function getNextNonEmptyBar(bars, startIndex) {
|
|
|
10222
10126
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10223
10127
|
}
|
|
10224
10128
|
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10129
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
10130
|
+
const GAUGE_PADDING_TOP = 10;
|
|
10131
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
10132
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
10133
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
10134
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
10135
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10136
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10137
|
+
function drawGaugeChart(canvas, runtime) {
|
|
10138
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10139
|
+
canvas.width = canvasBoundingRect.width;
|
|
10140
|
+
canvas.height = canvasBoundingRect.height;
|
|
10141
|
+
const ctx = canvas.getContext("2d");
|
|
10142
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10143
|
+
drawBackground(ctx, config);
|
|
10144
|
+
drawGauge(ctx, config);
|
|
10145
|
+
drawInflectionValues(ctx, config);
|
|
10146
|
+
drawLabels(ctx, config);
|
|
10147
|
+
drawTitle(ctx, config);
|
|
10148
|
+
}
|
|
10149
|
+
function drawGauge(ctx, config) {
|
|
10150
|
+
ctx.save();
|
|
10151
|
+
const gauge = config.gauge;
|
|
10152
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10153
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10154
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10155
|
+
if (arcRadius < 0) {
|
|
10156
|
+
return;
|
|
10233
10157
|
}
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10158
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10159
|
+
// Gauge background
|
|
10160
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10161
|
+
ctx.beginPath();
|
|
10162
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
10163
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10164
|
+
ctx.stroke();
|
|
10165
|
+
// Gauge value
|
|
10166
|
+
ctx.strokeStyle = gauge.color;
|
|
10167
|
+
ctx.beginPath();
|
|
10168
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10169
|
+
ctx.stroke();
|
|
10170
|
+
ctx.restore();
|
|
10171
|
+
}
|
|
10172
|
+
function drawBackground(ctx, config) {
|
|
10173
|
+
ctx.save();
|
|
10174
|
+
ctx.fillStyle = config.backgroundColor;
|
|
10175
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
10176
|
+
ctx.restore();
|
|
10177
|
+
}
|
|
10178
|
+
function drawLabels(ctx, config) {
|
|
10179
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10180
|
+
ctx.save();
|
|
10181
|
+
ctx.textAlign = "center";
|
|
10182
|
+
ctx.fillStyle = label.color;
|
|
10183
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10184
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10185
|
+
ctx.restore();
|
|
10186
|
+
}
|
|
10187
|
+
}
|
|
10188
|
+
function drawInflectionValues(ctx, config) {
|
|
10189
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10190
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
10191
|
+
ctx.save();
|
|
10192
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10193
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10194
|
+
ctx.lineWidth = 2;
|
|
10195
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10196
|
+
ctx.beginPath();
|
|
10197
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10198
|
+
ctx.lineTo(0, -height - 3);
|
|
10199
|
+
ctx.stroke();
|
|
10200
|
+
ctx.textAlign = "center";
|
|
10201
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10202
|
+
ctx.fillStyle = inflectionValue.color;
|
|
10203
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10204
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10205
|
+
ctx.restore();
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
function drawTitle(ctx, config) {
|
|
10209
|
+
ctx.save();
|
|
10210
|
+
const title = config.title;
|
|
10211
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10212
|
+
ctx.textBaseline = "middle";
|
|
10213
|
+
ctx.fillStyle = title.color;
|
|
10214
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10215
|
+
ctx.restore();
|
|
10216
|
+
}
|
|
10217
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10218
|
+
const maxValue = runtime.maxValue;
|
|
10219
|
+
const minValue = runtime.minValue;
|
|
10220
|
+
const gaugeValue = runtime.gaugeValue;
|
|
10221
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10222
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10223
|
+
const gaugePercentage = gaugeValue
|
|
10224
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10225
|
+
: 0;
|
|
10226
|
+
const gaugeValuePosition = {
|
|
10227
|
+
x: boundingRect.width / 2,
|
|
10228
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10240
10229
|
};
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
return this.chartRuntime.background;
|
|
10230
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10231
|
+
// Scale down the font size if the gaugeRect is too small
|
|
10232
|
+
if (gaugeRect.height < 300) {
|
|
10233
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10246
10234
|
}
|
|
10247
|
-
|
|
10248
|
-
|
|
10235
|
+
// Scale down the font size if the text is too long
|
|
10236
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
10237
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
10238
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10239
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10249
10240
|
}
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10241
|
+
const minLabelPosition = {
|
|
10242
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10243
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10244
|
+
};
|
|
10245
|
+
const maxLabelPosition = {
|
|
10246
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10247
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10248
|
+
};
|
|
10249
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
10250
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
10251
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
10252
|
+
if (runtime.title.text) {
|
|
10253
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
10256
10254
|
}
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10255
|
+
switch (runtime.title.align) {
|
|
10256
|
+
case "right":
|
|
10257
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10258
|
+
break;
|
|
10259
|
+
case "center":
|
|
10260
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
10261
|
+
break;
|
|
10262
|
+
case "left":
|
|
10263
|
+
default:
|
|
10264
|
+
x = CHART_PADDING$1;
|
|
10265
|
+
break;
|
|
10266
|
+
}
|
|
10267
|
+
return {
|
|
10268
|
+
width: boundingRect.width,
|
|
10269
|
+
height: boundingRect.height,
|
|
10270
|
+
title: {
|
|
10271
|
+
label: runtime.title.text ?? "",
|
|
10272
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10273
|
+
textPosition: {
|
|
10274
|
+
x,
|
|
10275
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10276
|
+
},
|
|
10277
|
+
color: runtime.title.color ?? textColor,
|
|
10278
|
+
bold: runtime.title.bold,
|
|
10279
|
+
italic: runtime.title.italic,
|
|
10280
|
+
},
|
|
10281
|
+
backgroundColor: runtime.background,
|
|
10282
|
+
gauge: {
|
|
10283
|
+
rect: gaugeRect,
|
|
10284
|
+
arcWidth: gaugeArcWidth,
|
|
10285
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
10286
|
+
color: getGaugeColor(runtime),
|
|
10287
|
+
},
|
|
10288
|
+
inflectionValues,
|
|
10289
|
+
gaugeValue: {
|
|
10290
|
+
label: gaugeLabel,
|
|
10291
|
+
textPosition: gaugeValuePosition,
|
|
10292
|
+
fontSize: gaugeValueFontSize,
|
|
10293
|
+
color: textColor,
|
|
10294
|
+
},
|
|
10295
|
+
minLabel: {
|
|
10296
|
+
label: runtime.minValue.label,
|
|
10297
|
+
textPosition: minLabelPosition,
|
|
10298
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10299
|
+
color: textColor,
|
|
10300
|
+
},
|
|
10301
|
+
maxLabel: {
|
|
10302
|
+
label: runtime.maxValue.label,
|
|
10303
|
+
textPosition: maxLabelPosition,
|
|
10304
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10305
|
+
color: textColor,
|
|
10306
|
+
},
|
|
10307
|
+
};
|
|
10308
|
+
}
|
|
10309
|
+
/**
|
|
10310
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10311
|
+
* space for the title and labels.
|
|
10312
|
+
*/
|
|
10313
|
+
function getGaugeRect(boundingRect, title) {
|
|
10314
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10315
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10316
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10317
|
+
let gaugeWidth;
|
|
10318
|
+
let gaugeHeight;
|
|
10319
|
+
if (drawWidth > 2 * drawHeight) {
|
|
10320
|
+
gaugeWidth = 2 * drawHeight;
|
|
10321
|
+
gaugeHeight = drawHeight;
|
|
10322
|
+
}
|
|
10323
|
+
else {
|
|
10324
|
+
gaugeWidth = drawWidth;
|
|
10325
|
+
gaugeHeight = drawWidth / 2;
|
|
10326
|
+
}
|
|
10327
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
10328
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
10329
|
+
return {
|
|
10330
|
+
x: gaugeX,
|
|
10331
|
+
y: gaugeY,
|
|
10332
|
+
width: gaugeWidth,
|
|
10333
|
+
height: gaugeHeight,
|
|
10334
|
+
};
|
|
10335
|
+
}
|
|
10336
|
+
/**
|
|
10337
|
+
* Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
|
|
10338
|
+
*
|
|
10339
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
10340
|
+
*/
|
|
10341
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
10342
|
+
const maxValue = runtime.maxValue;
|
|
10343
|
+
const minValue = runtime.minValue;
|
|
10344
|
+
const gaugeCircleCenter = {
|
|
10345
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
10346
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
10347
|
+
};
|
|
10348
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10349
|
+
const inflectionValues = [];
|
|
10350
|
+
const inflectionValuesTextRects = [];
|
|
10351
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
10352
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10353
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10354
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
10355
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10356
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10357
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
10358
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
10359
|
+
labelWidth + 2, // width of the text + some margin
|
|
10360
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10361
|
+
);
|
|
10362
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10363
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
10364
|
+
: 0;
|
|
10365
|
+
inflectionValuesTextRects.push(textRect);
|
|
10366
|
+
inflectionValues.push({
|
|
10367
|
+
rotation: angle,
|
|
10368
|
+
label: inflectionValue.label,
|
|
10369
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10370
|
+
color: textColor,
|
|
10371
|
+
offset,
|
|
10277
10372
|
});
|
|
10278
10373
|
}
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
|
|
10282
|
-
|
|
10374
|
+
return inflectionValues;
|
|
10375
|
+
}
|
|
10376
|
+
function getGaugeColor(runtime) {
|
|
10377
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
10378
|
+
if (gaugeValue === undefined) {
|
|
10379
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
10283
10380
|
}
|
|
10284
|
-
|
|
10285
|
-
const
|
|
10286
|
-
if (
|
|
10287
|
-
|
|
10288
|
-
if (chartData.options?.plugins?.title) {
|
|
10289
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10290
|
-
}
|
|
10381
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10382
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
10383
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10384
|
+
return runtime.colors[i];
|
|
10291
10385
|
}
|
|
10292
|
-
else {
|
|
10293
|
-
|
|
10386
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10387
|
+
return runtime.colors[i];
|
|
10294
10388
|
}
|
|
10295
|
-
this.chart.config.options = chartData.options;
|
|
10296
|
-
this.chart.update();
|
|
10297
10389
|
}
|
|
10390
|
+
return runtime.colors.at(-1);
|
|
10391
|
+
}
|
|
10392
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
10393
|
+
return [
|
|
10394
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10395
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10396
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10397
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10398
|
+
];
|
|
10399
|
+
}
|
|
10400
|
+
/**
|
|
10401
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10402
|
+
* is not handled.
|
|
10403
|
+
*/
|
|
10404
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
10405
|
+
const A = segment1.start;
|
|
10406
|
+
const B = segment1.end;
|
|
10407
|
+
const C = segment2.start;
|
|
10408
|
+
const D = segment2.end;
|
|
10409
|
+
/**
|
|
10410
|
+
* Line segment intersection algorithm
|
|
10411
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10412
|
+
*/
|
|
10413
|
+
function ccw(a, b, c) {
|
|
10414
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10415
|
+
}
|
|
10416
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
10417
|
+
}
|
|
10418
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
10419
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
10420
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
10421
|
+
for (const segment1 of segments1) {
|
|
10422
|
+
for (const segment2 of segments2) {
|
|
10423
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
10424
|
+
return true;
|
|
10425
|
+
}
|
|
10426
|
+
}
|
|
10427
|
+
}
|
|
10428
|
+
return false;
|
|
10429
|
+
}
|
|
10430
|
+
/**
|
|
10431
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10432
|
+
*
|
|
10433
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10434
|
+
*/
|
|
10435
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10436
|
+
const cos = Math.cos(angle);
|
|
10437
|
+
const sin = Math.sin(angle);
|
|
10438
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10439
|
+
const x = cos * radius;
|
|
10440
|
+
const y = sin * radius;
|
|
10441
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10442
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10443
|
+
const y2 = cos * (rectWidth / 2);
|
|
10444
|
+
const bottomRight = {
|
|
10445
|
+
x: x + x2 + circleCenterX,
|
|
10446
|
+
y: circleCenterY - (y - y2),
|
|
10447
|
+
};
|
|
10448
|
+
const bottomLeft = {
|
|
10449
|
+
x: x - x2 + circleCenterX,
|
|
10450
|
+
y: circleCenterY - (y + y2),
|
|
10451
|
+
};
|
|
10452
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10453
|
+
const xp = cos * (radius + rectHeight);
|
|
10454
|
+
const yp = sin * (radius + rectHeight);
|
|
10455
|
+
const topLeft = {
|
|
10456
|
+
x: xp - x2 + circleCenterX,
|
|
10457
|
+
y: circleCenterY - (yp + y2),
|
|
10458
|
+
};
|
|
10459
|
+
const topRight = {
|
|
10460
|
+
x: xp + x2 + circleCenterX,
|
|
10461
|
+
y: circleCenterY - (yp - y2),
|
|
10462
|
+
};
|
|
10463
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10298
10464
|
}
|
|
10299
10465
|
|
|
10300
10466
|
/**
|
|
@@ -10876,6 +11042,299 @@ class ScorecardChartConfigBuilder {
|
|
|
10876
11042
|
}
|
|
10877
11043
|
}
|
|
10878
11044
|
|
|
11045
|
+
const CHART_COMMON_OPTIONS = {
|
|
11046
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11047
|
+
responsive: true, // will resize when its container is resized
|
|
11048
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
11049
|
+
elements: {
|
|
11050
|
+
line: {
|
|
11051
|
+
fill: false, // do not fill the area under line charts
|
|
11052
|
+
},
|
|
11053
|
+
point: {
|
|
11054
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11055
|
+
},
|
|
11056
|
+
},
|
|
11057
|
+
animation: false,
|
|
11058
|
+
};
|
|
11059
|
+
function chartToImage(runtime, figure, type) {
|
|
11060
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11061
|
+
// fill the whole page otherwise
|
|
11062
|
+
const div = document.createElement("div");
|
|
11063
|
+
div.style.width = `${figure.width}px`;
|
|
11064
|
+
div.style.height = `${figure.height}px`;
|
|
11065
|
+
const canvas = document.createElement("canvas");
|
|
11066
|
+
div.append(canvas);
|
|
11067
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
11068
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
11069
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11070
|
+
document.body.append(div);
|
|
11071
|
+
if ("chartJsConfig" in runtime) {
|
|
11072
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
11073
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
11074
|
+
const Chart = getChartJSConstructor();
|
|
11075
|
+
const chart = new Chart(canvas, config);
|
|
11076
|
+
const imgContent = chart.toBase64Image();
|
|
11077
|
+
chart.destroy();
|
|
11078
|
+
div.remove();
|
|
11079
|
+
return imgContent;
|
|
11080
|
+
}
|
|
11081
|
+
else if (type === "scorecard") {
|
|
11082
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
11083
|
+
drawScoreChart(design, canvas);
|
|
11084
|
+
const imgContent = canvas.toDataURL();
|
|
11085
|
+
div.remove();
|
|
11086
|
+
return imgContent;
|
|
11087
|
+
}
|
|
11088
|
+
else if (type === "gauge") {
|
|
11089
|
+
drawGaugeChart(canvas, runtime);
|
|
11090
|
+
const imgContent = canvas.toDataURL();
|
|
11091
|
+
div.remove();
|
|
11092
|
+
return imgContent;
|
|
11093
|
+
}
|
|
11094
|
+
return undefined;
|
|
11095
|
+
}
|
|
11096
|
+
/**
|
|
11097
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
11098
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11099
|
+
*/
|
|
11100
|
+
const backgroundColorChartJSPlugin = {
|
|
11101
|
+
id: "customCanvasBackgroundColor",
|
|
11102
|
+
beforeDraw: (chart) => {
|
|
11103
|
+
const { ctx } = chart;
|
|
11104
|
+
ctx.save();
|
|
11105
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
11106
|
+
ctx.fillStyle = "#ffffff";
|
|
11107
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11108
|
+
ctx.restore();
|
|
11109
|
+
},
|
|
11110
|
+
};
|
|
11111
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11112
|
+
function getChartJSConstructor() {
|
|
11113
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11114
|
+
window.Chart.register(chartShowValuesPlugin);
|
|
11115
|
+
window.Chart.register(waterfallLinesPlugin);
|
|
11116
|
+
}
|
|
11117
|
+
return window.Chart;
|
|
11118
|
+
}
|
|
11119
|
+
|
|
11120
|
+
/**
|
|
11121
|
+
* This file is largely inspired by owl 1.
|
|
11122
|
+
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
11123
|
+
* So, the solution was to import the behavior of owl 1 directly in our
|
|
11124
|
+
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
11125
|
+
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
11126
|
+
* created for the first time.
|
|
11127
|
+
*/
|
|
11128
|
+
const STYLESHEETS = {};
|
|
11129
|
+
let nextId = 0;
|
|
11130
|
+
/**
|
|
11131
|
+
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
11132
|
+
* an inline stylesheet with just the following code:
|
|
11133
|
+
* ```js
|
|
11134
|
+
* css`.component-a { color: red; }`;
|
|
11135
|
+
* ```
|
|
11136
|
+
*/
|
|
11137
|
+
function css(strings, ...args) {
|
|
11138
|
+
const name = `__sheet__${nextId++}`;
|
|
11139
|
+
const value = String.raw(strings, ...args);
|
|
11140
|
+
registerSheet(name, value);
|
|
11141
|
+
activateSheet(name);
|
|
11142
|
+
return name;
|
|
11143
|
+
}
|
|
11144
|
+
function processSheet(str) {
|
|
11145
|
+
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
11146
|
+
const selectorStack = [];
|
|
11147
|
+
const parts = [];
|
|
11148
|
+
let rules = [];
|
|
11149
|
+
function generateSelector(stackIndex, parentSelector) {
|
|
11150
|
+
const parts = [];
|
|
11151
|
+
for (const selector of selectorStack[stackIndex]) {
|
|
11152
|
+
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
11153
|
+
if (part.includes("&")) {
|
|
11154
|
+
part = selector.replace(/&/g, parentSelector || "");
|
|
11155
|
+
}
|
|
11156
|
+
if (stackIndex < selectorStack.length - 1) {
|
|
11157
|
+
part = generateSelector(stackIndex + 1, part);
|
|
11158
|
+
}
|
|
11159
|
+
parts.push(part);
|
|
11160
|
+
}
|
|
11161
|
+
return parts.join(", ");
|
|
11162
|
+
}
|
|
11163
|
+
function generateRules() {
|
|
11164
|
+
if (rules.length) {
|
|
11165
|
+
parts.push(generateSelector(0) + " {");
|
|
11166
|
+
parts.push(...rules);
|
|
11167
|
+
parts.push("}");
|
|
11168
|
+
rules = [];
|
|
11169
|
+
}
|
|
11170
|
+
}
|
|
11171
|
+
while (tokens.length) {
|
|
11172
|
+
let token = tokens.shift();
|
|
11173
|
+
if (token === "}") {
|
|
11174
|
+
generateRules();
|
|
11175
|
+
selectorStack.pop();
|
|
11176
|
+
}
|
|
11177
|
+
else {
|
|
11178
|
+
if (tokens[0] === "{") {
|
|
11179
|
+
generateRules();
|
|
11180
|
+
selectorStack.push(token.split(/\s*,\s*/));
|
|
11181
|
+
tokens.shift();
|
|
11182
|
+
}
|
|
11183
|
+
if (tokens[0] === ";") {
|
|
11184
|
+
rules.push(" " + token + ";");
|
|
11185
|
+
}
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
return parts.join("\n");
|
|
11189
|
+
}
|
|
11190
|
+
function registerSheet(id, css) {
|
|
11191
|
+
const sheet = document.createElement("style");
|
|
11192
|
+
sheet.textContent = processSheet(css);
|
|
11193
|
+
STYLESHEETS[id] = sheet;
|
|
11194
|
+
}
|
|
11195
|
+
function activateSheet(id) {
|
|
11196
|
+
const sheet = STYLESHEETS[id];
|
|
11197
|
+
sheet.setAttribute("component", id);
|
|
11198
|
+
document.head.appendChild(sheet);
|
|
11199
|
+
}
|
|
11200
|
+
function getTextDecoration({ strikethrough, underline, }) {
|
|
11201
|
+
if (!strikethrough && !underline) {
|
|
11202
|
+
return "none";
|
|
11203
|
+
}
|
|
11204
|
+
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
11205
|
+
}
|
|
11206
|
+
/**
|
|
11207
|
+
* Convert the cell style to CSS properties.
|
|
11208
|
+
*/
|
|
11209
|
+
function cellStyleToCss(style) {
|
|
11210
|
+
const attributes = cellTextStyleToCss(style);
|
|
11211
|
+
if (!style)
|
|
11212
|
+
return attributes;
|
|
11213
|
+
if (style.fillColor) {
|
|
11214
|
+
attributes["background"] = style.fillColor;
|
|
11215
|
+
}
|
|
11216
|
+
return attributes;
|
|
11217
|
+
}
|
|
11218
|
+
/**
|
|
11219
|
+
* Convert the cell text style to CSS properties.
|
|
11220
|
+
*/
|
|
11221
|
+
function cellTextStyleToCss(style) {
|
|
11222
|
+
const attributes = {};
|
|
11223
|
+
if (!style)
|
|
11224
|
+
return attributes;
|
|
11225
|
+
if (style.bold) {
|
|
11226
|
+
attributes["font-weight"] = "bold";
|
|
11227
|
+
}
|
|
11228
|
+
if (style.italic) {
|
|
11229
|
+
attributes["font-style"] = "italic";
|
|
11230
|
+
}
|
|
11231
|
+
if (style.strikethrough || style.underline) {
|
|
11232
|
+
let decoration = style.strikethrough ? "line-through" : "";
|
|
11233
|
+
decoration = style.underline ? decoration + " underline" : decoration;
|
|
11234
|
+
attributes["text-decoration"] = decoration;
|
|
11235
|
+
}
|
|
11236
|
+
if (style.textColor) {
|
|
11237
|
+
attributes["color"] = style.textColor;
|
|
11238
|
+
}
|
|
11239
|
+
return attributes;
|
|
11240
|
+
}
|
|
11241
|
+
/**
|
|
11242
|
+
* Transform CSS properties into a CSS string.
|
|
11243
|
+
*/
|
|
11244
|
+
function cssPropertiesToCss(attributes) {
|
|
11245
|
+
let styleStr = "";
|
|
11246
|
+
for (const attName in attributes) {
|
|
11247
|
+
if (!attributes[attName]) {
|
|
11248
|
+
continue;
|
|
11249
|
+
}
|
|
11250
|
+
styleStr += `${attName}:${attributes[attName]}; `;
|
|
11251
|
+
}
|
|
11252
|
+
return styleStr;
|
|
11253
|
+
}
|
|
11254
|
+
function getElementMargins(el) {
|
|
11255
|
+
const style = window.getComputedStyle(el);
|
|
11256
|
+
return {
|
|
11257
|
+
top: parseInt(style.marginTop, 10) || 0,
|
|
11258
|
+
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
11259
|
+
left: parseInt(style.marginLeft, 10) || 0,
|
|
11260
|
+
right: parseInt(style.marginRight, 10) || 0,
|
|
11261
|
+
};
|
|
11262
|
+
}
|
|
11263
|
+
|
|
11264
|
+
css /* scss */ `
|
|
11265
|
+
.o-spreadsheet {
|
|
11266
|
+
.o-chart-custom-tooltip {
|
|
11267
|
+
font-size: 12px;
|
|
11268
|
+
background-color: #fff;
|
|
11269
|
+
z-index: ${ComponentsImportance.FigureTooltip};
|
|
11270
|
+
}
|
|
11271
|
+
}
|
|
11272
|
+
`;
|
|
11273
|
+
class ChartJsComponent extends owl.Component {
|
|
11274
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
11275
|
+
static props = {
|
|
11276
|
+
figure: Object,
|
|
11277
|
+
};
|
|
11278
|
+
canvas = owl.useRef("graphContainer");
|
|
11279
|
+
chart;
|
|
11280
|
+
currentRuntime;
|
|
11281
|
+
get background() {
|
|
11282
|
+
return this.chartRuntime.background;
|
|
11283
|
+
}
|
|
11284
|
+
get canvasStyle() {
|
|
11285
|
+
return `background-color: ${this.background}`;
|
|
11286
|
+
}
|
|
11287
|
+
get chartRuntime() {
|
|
11288
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11289
|
+
if (!("chartJsConfig" in runtime)) {
|
|
11290
|
+
throw new Error("Unsupported chart runtime");
|
|
11291
|
+
}
|
|
11292
|
+
return runtime;
|
|
11293
|
+
}
|
|
11294
|
+
setup() {
|
|
11295
|
+
owl.onMounted(() => {
|
|
11296
|
+
const runtime = this.chartRuntime;
|
|
11297
|
+
this.currentRuntime = runtime;
|
|
11298
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11299
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11300
|
+
});
|
|
11301
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
11302
|
+
owl.useEffect(() => {
|
|
11303
|
+
const runtime = this.chartRuntime;
|
|
11304
|
+
if (runtime !== this.currentRuntime) {
|
|
11305
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11306
|
+
this.chart?.destroy();
|
|
11307
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11308
|
+
}
|
|
11309
|
+
else {
|
|
11310
|
+
this.updateChartJs(deepCopy(runtime));
|
|
11311
|
+
}
|
|
11312
|
+
this.currentRuntime = runtime;
|
|
11313
|
+
}
|
|
11314
|
+
});
|
|
11315
|
+
}
|
|
11316
|
+
createChart(chartData) {
|
|
11317
|
+
const canvas = this.canvas.el;
|
|
11318
|
+
const ctx = canvas.getContext("2d");
|
|
11319
|
+
const Chart = getChartJSConstructor();
|
|
11320
|
+
this.chart = new Chart(ctx, chartData);
|
|
11321
|
+
}
|
|
11322
|
+
updateChartJs(chartRuntime) {
|
|
11323
|
+
const chartData = chartRuntime.chartJsConfig;
|
|
11324
|
+
if (chartData.data && chartData.data.datasets) {
|
|
11325
|
+
this.chart.data = chartData.data;
|
|
11326
|
+
if (chartData.options?.plugins?.title) {
|
|
11327
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11328
|
+
}
|
|
11329
|
+
}
|
|
11330
|
+
else {
|
|
11331
|
+
this.chart.data.datasets = [];
|
|
11332
|
+
}
|
|
11333
|
+
this.chart.config.options = chartData.options;
|
|
11334
|
+
this.chart.update();
|
|
11335
|
+
}
|
|
11336
|
+
}
|
|
11337
|
+
|
|
10879
11338
|
class ScorecardChart extends owl.Component {
|
|
10880
11339
|
static template = "o-spreadsheet-ScorecardChart";
|
|
10881
11340
|
static props = {
|
|
@@ -10907,6 +11366,7 @@ class ScorecardChart extends owl.Component {
|
|
|
10907
11366
|
const autoCompleteProviders = new Registry();
|
|
10908
11367
|
|
|
10909
11368
|
autoCompleteProviders.add("dataValidation", {
|
|
11369
|
+
displayAllOnInitialContent: true,
|
|
10910
11370
|
getProposals(tokenAtCursor, content) {
|
|
10911
11371
|
if (content.startsWith("=")) {
|
|
10912
11372
|
return [];
|
|
@@ -21203,6 +21663,15 @@ class AbstractComposerStore extends SpreadsheetStore {
|
|
|
21203
21663
|
const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
|
|
21204
21664
|
// remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
|
|
21205
21665
|
const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
|
|
21666
|
+
if (this._currentContent === this.initialContent &&
|
|
21667
|
+
provider.displayAllOnInitialContent &&
|
|
21668
|
+
proposals?.length) {
|
|
21669
|
+
return {
|
|
21670
|
+
proposals,
|
|
21671
|
+
selectProposal: provider.selectProposal,
|
|
21672
|
+
autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
|
|
21673
|
+
};
|
|
21674
|
+
}
|
|
21206
21675
|
if (exactMatch && this._currentContent !== this.initialContent) {
|
|
21207
21676
|
// this means the user has chosen a proposal
|
|
21208
21677
|
return;
|
|
@@ -22203,586 +22672,255 @@ const alphaNumericValueRegExp = /^(.*\D+)(\d+)$/;
|
|
|
22203
22672
|
/**
|
|
22204
22673
|
* Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
|
|
22205
22674
|
* Return the one which contains the given cell
|
|
22206
|
-
*/
|
|
22207
|
-
function getGroup(cell, cells, filter) {
|
|
22208
|
-
let group = [];
|
|
22209
|
-
let found = false;
|
|
22210
|
-
for (let x of cells) {
|
|
22211
|
-
if (x === cell) {
|
|
22212
|
-
found = true;
|
|
22213
|
-
}
|
|
22214
|
-
const cellValue = x === undefined || x.isFormula
|
|
22215
|
-
? undefined
|
|
22216
|
-
: evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
|
|
22217
|
-
if (cellValue && filter(cellValue)) {
|
|
22218
|
-
group.push(cellValue);
|
|
22219
|
-
}
|
|
22220
|
-
else {
|
|
22221
|
-
if (found) {
|
|
22222
|
-
return group;
|
|
22223
|
-
}
|
|
22224
|
-
group = [];
|
|
22225
|
-
}
|
|
22226
|
-
}
|
|
22227
|
-
return group;
|
|
22228
|
-
}
|
|
22229
|
-
/**
|
|
22230
|
-
* Get the average steps between numbers
|
|
22231
|
-
*/
|
|
22232
|
-
function getAverageIncrement(group) {
|
|
22233
|
-
const averages = [];
|
|
22234
|
-
let last = group[0];
|
|
22235
|
-
for (let i = 1; i < group.length; i++) {
|
|
22236
|
-
const current = group[i];
|
|
22237
|
-
averages.push(current - last);
|
|
22238
|
-
last = current;
|
|
22239
|
-
}
|
|
22240
|
-
return averages.reduce((a, b) => a + b, 0) / averages.length;
|
|
22241
|
-
}
|
|
22242
|
-
/**
|
|
22243
|
-
* Get the step for a group
|
|
22244
|
-
*/
|
|
22245
|
-
function calculateIncrementBasedOnGroup(group) {
|
|
22246
|
-
let increment = 1;
|
|
22247
|
-
if (group.length >= 2) {
|
|
22248
|
-
increment = getAverageIncrement(group) * group.length;
|
|
22249
|
-
}
|
|
22250
|
-
return increment;
|
|
22251
|
-
}
|
|
22252
|
-
/**
|
|
22253
|
-
* Iterates on a list of date intervals.
|
|
22254
|
-
* if every interval is the same, return the interval
|
|
22255
|
-
* Otherwise return undefined
|
|
22256
|
-
*
|
|
22257
|
-
*/
|
|
22258
|
-
function getEqualInterval(intervals) {
|
|
22259
|
-
if (intervals.length < 2) {
|
|
22260
|
-
return intervals[0] || { years: 0, months: 0, days: 0 };
|
|
22261
|
-
}
|
|
22262
|
-
const equal = intervals.every((interval) => interval.years === intervals[0].years &&
|
|
22263
|
-
interval.months === intervals[0].months &&
|
|
22264
|
-
interval.days === intervals[0].days);
|
|
22265
|
-
return equal ? intervals[0] : undefined;
|
|
22266
|
-
}
|
|
22267
|
-
/**
|
|
22268
|
-
* Based on a group of dates, calculate the increment that should be applied
|
|
22269
|
-
* to the next date.
|
|
22270
|
-
*
|
|
22271
|
-
* This will compute the date difference in calendar terms (years, months, days)
|
|
22272
|
-
* In order to make abstraction of leap years and months with different number of days.
|
|
22273
|
-
*
|
|
22274
|
-
* In case the dates are not equidistant in calendar terms, no rule can be extrapolated
|
|
22275
|
-
* In case of equidistant dates, we either have in that order:
|
|
22276
|
-
* - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
|
|
22277
|
-
* - exact day interval (e.g. +n days) in which case we increment by the same day interval
|
|
22278
|
-
* - equidistant dates but not the same interval, in which case we return increment of the same interval
|
|
22279
|
-
*
|
|
22280
|
-
* */
|
|
22281
|
-
function calculateDateIncrementBasedOnGroup(group) {
|
|
22282
|
-
if (group.length < 2) {
|
|
22283
|
-
return 1;
|
|
22284
|
-
}
|
|
22285
|
-
const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
|
|
22286
|
-
const datesIntervals = getDateIntervals(jsDates);
|
|
22287
|
-
const datesEquidistantInterval = getEqualInterval(datesIntervals);
|
|
22288
|
-
if (datesEquidistantInterval === undefined) {
|
|
22289
|
-
// dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
|
|
22290
|
-
return undefined;
|
|
22291
|
-
}
|
|
22292
|
-
// The dates are apart by an exact interval of years, months or days
|
|
22293
|
-
// but not a combination of them
|
|
22294
|
-
const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
|
|
22295
|
-
const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
|
|
22296
|
-
if (!exactDateInterval || isSameDay) {
|
|
22297
|
-
const timeIntervals = jsDates
|
|
22298
|
-
.map((date, index) => {
|
|
22299
|
-
if (index === 0) {
|
|
22300
|
-
return 0;
|
|
22301
|
-
}
|
|
22302
|
-
const previous = jsDates[index - 1];
|
|
22303
|
-
return Math.floor(date.getTime()) - Math.floor(previous.getTime());
|
|
22304
|
-
})
|
|
22305
|
-
.slice(1);
|
|
22306
|
-
const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
|
|
22307
|
-
if (equidistantDates) {
|
|
22308
|
-
return group.length * (group[1] - group[0]);
|
|
22309
|
-
}
|
|
22310
|
-
}
|
|
22311
|
-
return {
|
|
22312
|
-
years: datesEquidistantInterval.years * group.length,
|
|
22313
|
-
months: datesEquidistantInterval.months * group.length,
|
|
22314
|
-
days: datesEquidistantInterval.days * group.length,
|
|
22315
|
-
};
|
|
22316
|
-
}
|
|
22317
|
-
autofillRulesRegistry
|
|
22318
|
-
.add("simple_value_copy", {
|
|
22319
|
-
condition: (cell, cells) => {
|
|
22320
|
-
return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
|
|
22321
|
-
},
|
|
22322
|
-
generateRule: () => {
|
|
22323
|
-
return { type: "COPY_MODIFIER" };
|
|
22324
|
-
},
|
|
22325
|
-
sequence: 10,
|
|
22326
|
-
})
|
|
22327
|
-
.add("increment_alphanumeric_value", {
|
|
22328
|
-
condition: (cell) => !cell.isFormula &&
|
|
22329
|
-
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
|
|
22330
|
-
alphaNumericValueRegExp.test(cell.content),
|
|
22331
|
-
generateRule: (cell, cells) => {
|
|
22332
|
-
const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
|
|
22333
|
-
const prefix = cell.content.match(stringPrefixRegExp)[0];
|
|
22334
|
-
const numberPostfixLength = cell.content.length - prefix.length;
|
|
22335
|
-
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
|
|
22336
|
-
alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
|
|
22337
|
-
.filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
|
|
22338
|
-
.map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
|
|
22339
|
-
const increment = calculateIncrementBasedOnGroup(group);
|
|
22340
|
-
return {
|
|
22341
|
-
type: "ALPHANUMERIC_INCREMENT_MODIFIER",
|
|
22342
|
-
prefix,
|
|
22343
|
-
current: numberPostfix,
|
|
22344
|
-
increment,
|
|
22345
|
-
numberPostfixLength,
|
|
22346
|
-
};
|
|
22347
|
-
},
|
|
22348
|
-
sequence: 15,
|
|
22349
|
-
})
|
|
22350
|
-
.add("copy_text", {
|
|
22351
|
-
condition: (cell) => !cell.isFormula &&
|
|
22352
|
-
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
|
|
22353
|
-
generateRule: () => {
|
|
22354
|
-
return { type: "COPY_MODIFIER" };
|
|
22355
|
-
},
|
|
22356
|
-
sequence: 20,
|
|
22357
|
-
})
|
|
22358
|
-
.add("update_formula", {
|
|
22359
|
-
condition: (cell) => cell.isFormula,
|
|
22360
|
-
generateRule: (_, cells) => {
|
|
22361
|
-
return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
|
|
22362
|
-
},
|
|
22363
|
-
sequence: 30,
|
|
22364
|
-
})
|
|
22365
|
-
.add("increment_dates", {
|
|
22366
|
-
condition: (cell, cells) => {
|
|
22367
|
-
return (!cell.isFormula &&
|
|
22368
|
-
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
|
|
22369
|
-
!!cell.format &&
|
|
22370
|
-
isDateTimeFormat(cell.format));
|
|
22371
|
-
},
|
|
22372
|
-
generateRule: (cell, cells) => {
|
|
22373
|
-
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22374
|
-
!!evaluatedCell.format &&
|
|
22375
|
-
isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
|
|
22376
|
-
const increment = calculateDateIncrementBasedOnGroup(group);
|
|
22377
|
-
if (increment === undefined) {
|
|
22378
|
-
return { type: "COPY_MODIFIER" };
|
|
22379
|
-
}
|
|
22380
|
-
/** requires to detect the current date (requires to be an integer value with the right format)
|
|
22381
|
-
* detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
|
|
22382
|
-
*/
|
|
22383
|
-
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22384
|
-
if (typeof increment === "object") {
|
|
22385
|
-
return {
|
|
22386
|
-
type: "DATE_INCREMENT_MODIFIER",
|
|
22387
|
-
increment,
|
|
22388
|
-
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
|
|
22389
|
-
};
|
|
22390
|
-
}
|
|
22391
|
-
return {
|
|
22392
|
-
type: "INCREMENT_MODIFIER",
|
|
22393
|
-
increment,
|
|
22394
|
-
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
|
|
22395
|
-
};
|
|
22396
|
-
},
|
|
22397
|
-
sequence: 25,
|
|
22398
|
-
})
|
|
22399
|
-
.add("increment_number", {
|
|
22400
|
-
condition: (cell) => !cell.isFormula &&
|
|
22401
|
-
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
|
|
22402
|
-
generateRule: (cell, cells) => {
|
|
22403
|
-
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22404
|
-
!isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
|
|
22405
|
-
const increment = calculateIncrementBasedOnGroup(group);
|
|
22406
|
-
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22407
|
-
return {
|
|
22408
|
-
type: "INCREMENT_MODIFIER",
|
|
22409
|
-
increment,
|
|
22410
|
-
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
|
|
22411
|
-
};
|
|
22412
|
-
},
|
|
22413
|
-
sequence: 40,
|
|
22414
|
-
});
|
|
22415
|
-
/**
|
|
22416
|
-
* Returns the date intervals between consecutive dates of an array
|
|
22417
|
-
* in the format of { years: number, months: number, days: number }
|
|
22418
|
-
*
|
|
22419
|
-
* The split is necessary to make abstraction of leap years and
|
|
22420
|
-
* months with different number of days.
|
|
22421
|
-
*
|
|
22422
|
-
* @param dates
|
|
22423
|
-
*/
|
|
22424
|
-
function getDateIntervals(dates) {
|
|
22425
|
-
if (dates.length < 2) {
|
|
22426
|
-
return [{ years: 0, months: 0, days: 0 }];
|
|
22427
|
-
}
|
|
22428
|
-
const res = dates.map((date, index) => {
|
|
22429
|
-
if (index === 0) {
|
|
22430
|
-
return { years: 0, months: 0, days: 0 };
|
|
22431
|
-
}
|
|
22432
|
-
const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
|
|
22433
|
-
const years = getTimeDifferenceInWholeYears(previous, date);
|
|
22434
|
-
const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
|
|
22435
|
-
previous.setFullYear(previous.getFullYear() + years);
|
|
22436
|
-
previous.setMonth(previous.getMonth() + months);
|
|
22437
|
-
const days = getTimeDifferenceInWholeDays(previous, date);
|
|
22438
|
-
return {
|
|
22439
|
-
years,
|
|
22440
|
-
months,
|
|
22441
|
-
days,
|
|
22442
|
-
};
|
|
22443
|
-
});
|
|
22444
|
-
return res.slice(1);
|
|
22445
|
-
}
|
|
22446
|
-
|
|
22447
|
-
const cellPopoverRegistry = new Registry();
|
|
22448
|
-
|
|
22449
|
-
const GAUGE_PADDING_SIDE = 30;
|
|
22450
|
-
const GAUGE_PADDING_TOP = 10;
|
|
22451
|
-
const GAUGE_PADDING_BOTTOM = 20;
|
|
22452
|
-
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
22453
|
-
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
22454
|
-
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
22455
|
-
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
22456
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
22457
|
-
function drawGaugeChart(canvas, runtime) {
|
|
22458
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
22459
|
-
canvas.width = canvasBoundingRect.width;
|
|
22460
|
-
canvas.height = canvasBoundingRect.height;
|
|
22461
|
-
const ctx = canvas.getContext("2d");
|
|
22462
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
22463
|
-
drawBackground(ctx, config);
|
|
22464
|
-
drawGauge(ctx, config);
|
|
22465
|
-
drawInflectionValues(ctx, config);
|
|
22466
|
-
drawLabels(ctx, config);
|
|
22467
|
-
drawTitle(ctx, config);
|
|
22468
|
-
}
|
|
22469
|
-
function drawGauge(ctx, config) {
|
|
22470
|
-
ctx.save();
|
|
22471
|
-
const gauge = config.gauge;
|
|
22472
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
22473
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
22474
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
22475
|
-
if (arcRadius < 0) {
|
|
22476
|
-
return;
|
|
22477
|
-
}
|
|
22478
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
22479
|
-
// Gauge background
|
|
22480
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
22481
|
-
ctx.beginPath();
|
|
22482
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
22483
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
22484
|
-
ctx.stroke();
|
|
22485
|
-
// Gauge value
|
|
22486
|
-
ctx.strokeStyle = gauge.color;
|
|
22487
|
-
ctx.beginPath();
|
|
22488
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
22489
|
-
ctx.stroke();
|
|
22490
|
-
ctx.restore();
|
|
22491
|
-
}
|
|
22492
|
-
function drawBackground(ctx, config) {
|
|
22493
|
-
ctx.save();
|
|
22494
|
-
ctx.fillStyle = config.backgroundColor;
|
|
22495
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
22496
|
-
ctx.restore();
|
|
22497
|
-
}
|
|
22498
|
-
function drawLabels(ctx, config) {
|
|
22499
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
22500
|
-
ctx.save();
|
|
22501
|
-
ctx.textAlign = "center";
|
|
22502
|
-
ctx.fillStyle = label.color;
|
|
22503
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
22504
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
22505
|
-
ctx.restore();
|
|
22506
|
-
}
|
|
22507
|
-
}
|
|
22508
|
-
function drawInflectionValues(ctx, config) {
|
|
22509
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
22510
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
22511
|
-
ctx.save();
|
|
22512
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
22513
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
22514
|
-
ctx.lineWidth = 2;
|
|
22515
|
-
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
22516
|
-
ctx.beginPath();
|
|
22517
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
22518
|
-
ctx.lineTo(0, -height - 3);
|
|
22519
|
-
ctx.stroke();
|
|
22520
|
-
ctx.textAlign = "center";
|
|
22521
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
22522
|
-
ctx.fillStyle = inflectionValue.color;
|
|
22523
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
22524
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
22525
|
-
ctx.restore();
|
|
22526
|
-
}
|
|
22527
|
-
}
|
|
22528
|
-
function drawTitle(ctx, config) {
|
|
22529
|
-
ctx.save();
|
|
22530
|
-
const title = config.title;
|
|
22531
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
22532
|
-
ctx.textBaseline = "middle";
|
|
22533
|
-
ctx.fillStyle = title.color;
|
|
22534
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
22535
|
-
ctx.restore();
|
|
22536
|
-
}
|
|
22537
|
-
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
22538
|
-
const maxValue = runtime.maxValue;
|
|
22539
|
-
const minValue = runtime.minValue;
|
|
22540
|
-
const gaugeValue = runtime.gaugeValue;
|
|
22541
|
-
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
22542
|
-
const gaugeArcWidth = gaugeRect.width / 6;
|
|
22543
|
-
const gaugePercentage = gaugeValue
|
|
22544
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
22545
|
-
: 0;
|
|
22546
|
-
const gaugeValuePosition = {
|
|
22547
|
-
x: boundingRect.width / 2,
|
|
22548
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
22549
|
-
};
|
|
22550
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
22551
|
-
// Scale down the font size if the gaugeRect is too small
|
|
22552
|
-
if (gaugeRect.height < 300) {
|
|
22553
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
22554
|
-
}
|
|
22555
|
-
// Scale down the font size if the text is too long
|
|
22556
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
22557
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
22558
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
22559
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
22560
|
-
}
|
|
22561
|
-
const minLabelPosition = {
|
|
22562
|
-
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
22563
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22564
|
-
};
|
|
22565
|
-
const maxLabelPosition = {
|
|
22566
|
-
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
22567
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22568
|
-
};
|
|
22569
|
-
const textColor = chartMutedFontColor(runtime.background);
|
|
22570
|
-
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
22571
|
-
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
22572
|
-
if (runtime.title.text) {
|
|
22573
|
-
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
22574
|
-
}
|
|
22575
|
-
switch (runtime.title.align) {
|
|
22576
|
-
case "right":
|
|
22577
|
-
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
22578
|
-
break;
|
|
22579
|
-
case "center":
|
|
22580
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
22581
|
-
break;
|
|
22582
|
-
case "left":
|
|
22583
|
-
default:
|
|
22584
|
-
x = CHART_PADDING$1;
|
|
22585
|
-
break;
|
|
22586
|
-
}
|
|
22587
|
-
return {
|
|
22588
|
-
width: boundingRect.width,
|
|
22589
|
-
height: boundingRect.height,
|
|
22590
|
-
title: {
|
|
22591
|
-
label: runtime.title.text ?? "",
|
|
22592
|
-
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
22593
|
-
textPosition: {
|
|
22594
|
-
x,
|
|
22595
|
-
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
22596
|
-
},
|
|
22597
|
-
color: runtime.title.color ?? textColor,
|
|
22598
|
-
bold: runtime.title.bold,
|
|
22599
|
-
italic: runtime.title.italic,
|
|
22600
|
-
},
|
|
22601
|
-
backgroundColor: runtime.background,
|
|
22602
|
-
gauge: {
|
|
22603
|
-
rect: gaugeRect,
|
|
22604
|
-
arcWidth: gaugeArcWidth,
|
|
22605
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
22606
|
-
color: getGaugeColor(runtime),
|
|
22607
|
-
},
|
|
22608
|
-
inflectionValues,
|
|
22609
|
-
gaugeValue: {
|
|
22610
|
-
label: gaugeLabel,
|
|
22611
|
-
textPosition: gaugeValuePosition,
|
|
22612
|
-
fontSize: gaugeValueFontSize,
|
|
22613
|
-
color: textColor,
|
|
22614
|
-
},
|
|
22615
|
-
minLabel: {
|
|
22616
|
-
label: runtime.minValue.label,
|
|
22617
|
-
textPosition: minLabelPosition,
|
|
22618
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22619
|
-
color: textColor,
|
|
22620
|
-
},
|
|
22621
|
-
maxLabel: {
|
|
22622
|
-
label: runtime.maxValue.label,
|
|
22623
|
-
textPosition: maxLabelPosition,
|
|
22624
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22625
|
-
color: textColor,
|
|
22626
|
-
},
|
|
22627
|
-
};
|
|
22628
|
-
}
|
|
22629
|
-
/**
|
|
22630
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
22631
|
-
* space for the title and labels.
|
|
22632
|
-
*/
|
|
22633
|
-
function getGaugeRect(boundingRect, title) {
|
|
22634
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
22635
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
22636
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
22637
|
-
let gaugeWidth;
|
|
22638
|
-
let gaugeHeight;
|
|
22639
|
-
if (drawWidth > 2 * drawHeight) {
|
|
22640
|
-
gaugeWidth = 2 * drawHeight;
|
|
22641
|
-
gaugeHeight = drawHeight;
|
|
22642
|
-
}
|
|
22643
|
-
else {
|
|
22644
|
-
gaugeWidth = drawWidth;
|
|
22645
|
-
gaugeHeight = drawWidth / 2;
|
|
22646
|
-
}
|
|
22647
|
-
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
22648
|
-
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
22649
|
-
return {
|
|
22650
|
-
x: gaugeX,
|
|
22651
|
-
y: gaugeY,
|
|
22652
|
-
width: gaugeWidth,
|
|
22653
|
-
height: gaugeHeight,
|
|
22654
|
-
};
|
|
22655
|
-
}
|
|
22656
|
-
/**
|
|
22657
|
-
* Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
|
|
22658
|
-
*
|
|
22659
|
-
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
22660
|
-
*/
|
|
22661
|
-
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
22662
|
-
const maxValue = runtime.maxValue;
|
|
22663
|
-
const minValue = runtime.minValue;
|
|
22664
|
-
const gaugeCircleCenter = {
|
|
22665
|
-
x: gaugeRect.x + gaugeRect.width / 2,
|
|
22666
|
-
y: gaugeRect.y + gaugeRect.height,
|
|
22667
|
-
};
|
|
22668
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
22669
|
-
const inflectionValues = [];
|
|
22670
|
-
const inflectionValuesTextRects = [];
|
|
22671
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
22672
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
22673
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
22674
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
22675
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
22676
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
22677
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
22678
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
22679
|
-
labelWidth + 2, // width of the text + some margin
|
|
22680
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
22681
|
-
);
|
|
22682
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
22683
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
22684
|
-
: 0;
|
|
22685
|
-
inflectionValuesTextRects.push(textRect);
|
|
22686
|
-
inflectionValues.push({
|
|
22687
|
-
rotation: angle,
|
|
22688
|
-
label: inflectionValue.label,
|
|
22689
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22690
|
-
color: textColor,
|
|
22691
|
-
offset,
|
|
22692
|
-
});
|
|
22693
|
-
}
|
|
22694
|
-
return inflectionValues;
|
|
22695
|
-
}
|
|
22696
|
-
function getGaugeColor(runtime) {
|
|
22697
|
-
const gaugeValue = runtime.gaugeValue?.value;
|
|
22698
|
-
if (gaugeValue === undefined) {
|
|
22699
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
22700
|
-
}
|
|
22701
|
-
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
22702
|
-
const inflectionValue = runtime.inflectionValues[i];
|
|
22703
|
-
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
22704
|
-
return runtime.colors[i];
|
|
22675
|
+
*/
|
|
22676
|
+
function getGroup(cell, cells, filter) {
|
|
22677
|
+
let group = [];
|
|
22678
|
+
let found = false;
|
|
22679
|
+
for (let x of cells) {
|
|
22680
|
+
if (x === cell) {
|
|
22681
|
+
found = true;
|
|
22705
22682
|
}
|
|
22706
|
-
|
|
22707
|
-
|
|
22683
|
+
const cellValue = x === undefined || x.isFormula
|
|
22684
|
+
? undefined
|
|
22685
|
+
: evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
|
|
22686
|
+
if (cellValue && filter(cellValue)) {
|
|
22687
|
+
group.push(cellValue);
|
|
22688
|
+
}
|
|
22689
|
+
else {
|
|
22690
|
+
if (found) {
|
|
22691
|
+
return group;
|
|
22692
|
+
}
|
|
22693
|
+
group = [];
|
|
22708
22694
|
}
|
|
22709
22695
|
}
|
|
22710
|
-
return
|
|
22696
|
+
return group;
|
|
22711
22697
|
}
|
|
22712
|
-
|
|
22713
|
-
|
|
22714
|
-
|
|
22715
|
-
|
|
22716
|
-
|
|
22717
|
-
|
|
22718
|
-
|
|
22698
|
+
/**
|
|
22699
|
+
* Get the average steps between numbers
|
|
22700
|
+
*/
|
|
22701
|
+
function getAverageIncrement(group) {
|
|
22702
|
+
const averages = [];
|
|
22703
|
+
let last = group[0];
|
|
22704
|
+
for (let i = 1; i < group.length; i++) {
|
|
22705
|
+
const current = group[i];
|
|
22706
|
+
averages.push(current - last);
|
|
22707
|
+
last = current;
|
|
22708
|
+
}
|
|
22709
|
+
return averages.reduce((a, b) => a + b, 0) / averages.length;
|
|
22719
22710
|
}
|
|
22720
22711
|
/**
|
|
22721
|
-
*
|
|
22722
|
-
* is not handled.
|
|
22712
|
+
* Get the step for a group
|
|
22723
22713
|
*/
|
|
22724
|
-
function
|
|
22725
|
-
|
|
22726
|
-
|
|
22727
|
-
|
|
22728
|
-
const D = segment2.end;
|
|
22729
|
-
/**
|
|
22730
|
-
* Line segment intersection algorithm
|
|
22731
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
22732
|
-
*/
|
|
22733
|
-
function ccw(a, b, c) {
|
|
22734
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
22714
|
+
function calculateIncrementBasedOnGroup(group) {
|
|
22715
|
+
let increment = 1;
|
|
22716
|
+
if (group.length >= 2) {
|
|
22717
|
+
increment = getAverageIncrement(group) * group.length;
|
|
22735
22718
|
}
|
|
22736
|
-
return
|
|
22719
|
+
return increment;
|
|
22737
22720
|
}
|
|
22738
|
-
|
|
22739
|
-
|
|
22740
|
-
|
|
22741
|
-
|
|
22742
|
-
|
|
22743
|
-
|
|
22744
|
-
|
|
22721
|
+
/**
|
|
22722
|
+
* Iterates on a list of date intervals.
|
|
22723
|
+
* if every interval is the same, return the interval
|
|
22724
|
+
* Otherwise return undefined
|
|
22725
|
+
*
|
|
22726
|
+
*/
|
|
22727
|
+
function getEqualInterval(intervals) {
|
|
22728
|
+
if (intervals.length < 2) {
|
|
22729
|
+
return intervals[0] || { years: 0, months: 0, days: 0 };
|
|
22730
|
+
}
|
|
22731
|
+
const equal = intervals.every((interval) => interval.years === intervals[0].years &&
|
|
22732
|
+
interval.months === intervals[0].months &&
|
|
22733
|
+
interval.days === intervals[0].days);
|
|
22734
|
+
return equal ? intervals[0] : undefined;
|
|
22735
|
+
}
|
|
22736
|
+
/**
|
|
22737
|
+
* Based on a group of dates, calculate the increment that should be applied
|
|
22738
|
+
* to the next date.
|
|
22739
|
+
*
|
|
22740
|
+
* This will compute the date difference in calendar terms (years, months, days)
|
|
22741
|
+
* In order to make abstraction of leap years and months with different number of days.
|
|
22742
|
+
*
|
|
22743
|
+
* In case the dates are not equidistant in calendar terms, no rule can be extrapolated
|
|
22744
|
+
* In case of equidistant dates, we either have in that order:
|
|
22745
|
+
* - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
|
|
22746
|
+
* - exact day interval (e.g. +n days) in which case we increment by the same day interval
|
|
22747
|
+
* - equidistant dates but not the same interval, in which case we return increment of the same interval
|
|
22748
|
+
*
|
|
22749
|
+
* */
|
|
22750
|
+
function calculateDateIncrementBasedOnGroup(group) {
|
|
22751
|
+
if (group.length < 2) {
|
|
22752
|
+
return 1;
|
|
22753
|
+
}
|
|
22754
|
+
const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
|
|
22755
|
+
const datesIntervals = getDateIntervals(jsDates);
|
|
22756
|
+
const datesEquidistantInterval = getEqualInterval(datesIntervals);
|
|
22757
|
+
if (datesEquidistantInterval === undefined) {
|
|
22758
|
+
// dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
|
|
22759
|
+
return undefined;
|
|
22760
|
+
}
|
|
22761
|
+
// The dates are apart by an exact interval of years, months or days
|
|
22762
|
+
// but not a combination of them
|
|
22763
|
+
const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
|
|
22764
|
+
const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
|
|
22765
|
+
if (!exactDateInterval || isSameDay) {
|
|
22766
|
+
const timeIntervals = jsDates
|
|
22767
|
+
.map((date, index) => {
|
|
22768
|
+
if (index === 0) {
|
|
22769
|
+
return 0;
|
|
22745
22770
|
}
|
|
22771
|
+
const previous = jsDates[index - 1];
|
|
22772
|
+
return Math.floor(date.getTime()) - Math.floor(previous.getTime());
|
|
22773
|
+
})
|
|
22774
|
+
.slice(1);
|
|
22775
|
+
const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
|
|
22776
|
+
if (equidistantDates) {
|
|
22777
|
+
return group.length * (group[1] - group[0]);
|
|
22746
22778
|
}
|
|
22747
22779
|
}
|
|
22748
|
-
return
|
|
22780
|
+
return {
|
|
22781
|
+
years: datesEquidistantInterval.years * group.length,
|
|
22782
|
+
months: datesEquidistantInterval.months * group.length,
|
|
22783
|
+
days: datesEquidistantInterval.days * group.length,
|
|
22784
|
+
};
|
|
22749
22785
|
}
|
|
22786
|
+
autofillRulesRegistry
|
|
22787
|
+
.add("simple_value_copy", {
|
|
22788
|
+
condition: (cell, cells) => {
|
|
22789
|
+
return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
|
|
22790
|
+
},
|
|
22791
|
+
generateRule: () => {
|
|
22792
|
+
return { type: "COPY_MODIFIER" };
|
|
22793
|
+
},
|
|
22794
|
+
sequence: 10,
|
|
22795
|
+
})
|
|
22796
|
+
.add("increment_alphanumeric_value", {
|
|
22797
|
+
condition: (cell) => !cell.isFormula &&
|
|
22798
|
+
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
|
|
22799
|
+
alphaNumericValueRegExp.test(cell.content),
|
|
22800
|
+
generateRule: (cell, cells, direction) => {
|
|
22801
|
+
const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
|
|
22802
|
+
const prefix = cell.content.match(stringPrefixRegExp)[0];
|
|
22803
|
+
const numberPostfixLength = cell.content.length - prefix.length;
|
|
22804
|
+
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
|
|
22805
|
+
alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
|
|
22806
|
+
.filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
|
|
22807
|
+
.map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
|
|
22808
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22809
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22810
|
+
increment = -increment;
|
|
22811
|
+
}
|
|
22812
|
+
return {
|
|
22813
|
+
type: "ALPHANUMERIC_INCREMENT_MODIFIER",
|
|
22814
|
+
prefix,
|
|
22815
|
+
current: numberPostfix,
|
|
22816
|
+
increment,
|
|
22817
|
+
numberPostfixLength,
|
|
22818
|
+
};
|
|
22819
|
+
},
|
|
22820
|
+
sequence: 15,
|
|
22821
|
+
})
|
|
22822
|
+
.add("copy_text", {
|
|
22823
|
+
condition: (cell) => !cell.isFormula &&
|
|
22824
|
+
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
|
|
22825
|
+
generateRule: () => {
|
|
22826
|
+
return { type: "COPY_MODIFIER" };
|
|
22827
|
+
},
|
|
22828
|
+
sequence: 20,
|
|
22829
|
+
})
|
|
22830
|
+
.add("update_formula", {
|
|
22831
|
+
condition: (cell) => cell.isFormula,
|
|
22832
|
+
generateRule: (_, cells) => {
|
|
22833
|
+
return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
|
|
22834
|
+
},
|
|
22835
|
+
sequence: 30,
|
|
22836
|
+
})
|
|
22837
|
+
.add("increment_dates", {
|
|
22838
|
+
condition: (cell, cells) => {
|
|
22839
|
+
return (!cell.isFormula &&
|
|
22840
|
+
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
|
|
22841
|
+
!!cell.format &&
|
|
22842
|
+
isDateTimeFormat(cell.format));
|
|
22843
|
+
},
|
|
22844
|
+
generateRule: (cell, cells) => {
|
|
22845
|
+
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22846
|
+
!!evaluatedCell.format &&
|
|
22847
|
+
isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
|
|
22848
|
+
const increment = calculateDateIncrementBasedOnGroup(group);
|
|
22849
|
+
if (increment === undefined) {
|
|
22850
|
+
return { type: "COPY_MODIFIER" };
|
|
22851
|
+
}
|
|
22852
|
+
/** requires to detect the current date (requires to be an integer value with the right format)
|
|
22853
|
+
* detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
|
|
22854
|
+
*/
|
|
22855
|
+
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22856
|
+
if (typeof increment === "object") {
|
|
22857
|
+
return {
|
|
22858
|
+
type: "DATE_INCREMENT_MODIFIER",
|
|
22859
|
+
increment,
|
|
22860
|
+
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
|
|
22861
|
+
};
|
|
22862
|
+
}
|
|
22863
|
+
return {
|
|
22864
|
+
type: "INCREMENT_MODIFIER",
|
|
22865
|
+
increment,
|
|
22866
|
+
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
|
|
22867
|
+
};
|
|
22868
|
+
},
|
|
22869
|
+
sequence: 25,
|
|
22870
|
+
})
|
|
22871
|
+
.add("increment_number", {
|
|
22872
|
+
condition: (cell) => !cell.isFormula &&
|
|
22873
|
+
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
|
|
22874
|
+
generateRule: (cell, cells, direction) => {
|
|
22875
|
+
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22876
|
+
!isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
|
|
22877
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22878
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22879
|
+
increment = -increment;
|
|
22880
|
+
}
|
|
22881
|
+
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22882
|
+
return {
|
|
22883
|
+
type: "INCREMENT_MODIFIER",
|
|
22884
|
+
increment,
|
|
22885
|
+
current: evaluation.type === CellValueType.number ? evaluation.value : 0,
|
|
22886
|
+
};
|
|
22887
|
+
},
|
|
22888
|
+
sequence: 40,
|
|
22889
|
+
});
|
|
22750
22890
|
/**
|
|
22751
|
-
*
|
|
22891
|
+
* Returns the date intervals between consecutive dates of an array
|
|
22892
|
+
* in the format of { years: number, months: number, days: number }
|
|
22752
22893
|
*
|
|
22753
|
-
*
|
|
22894
|
+
* The split is necessary to make abstraction of leap years and
|
|
22895
|
+
* months with different number of days.
|
|
22896
|
+
*
|
|
22897
|
+
* @param dates
|
|
22754
22898
|
*/
|
|
22755
|
-
function
|
|
22756
|
-
|
|
22757
|
-
|
|
22758
|
-
|
|
22759
|
-
const
|
|
22760
|
-
|
|
22761
|
-
|
|
22762
|
-
|
|
22763
|
-
|
|
22764
|
-
|
|
22765
|
-
|
|
22766
|
-
|
|
22767
|
-
|
|
22768
|
-
|
|
22769
|
-
|
|
22770
|
-
|
|
22771
|
-
|
|
22772
|
-
|
|
22773
|
-
|
|
22774
|
-
|
|
22775
|
-
|
|
22776
|
-
x: xp - x2 + circleCenterX,
|
|
22777
|
-
y: circleCenterY - (yp + y2),
|
|
22778
|
-
};
|
|
22779
|
-
const topRight = {
|
|
22780
|
-
x: xp + x2 + circleCenterX,
|
|
22781
|
-
y: circleCenterY - (yp - y2),
|
|
22782
|
-
};
|
|
22783
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
22899
|
+
function getDateIntervals(dates) {
|
|
22900
|
+
if (dates.length < 2) {
|
|
22901
|
+
return [{ years: 0, months: 0, days: 0 }];
|
|
22902
|
+
}
|
|
22903
|
+
const res = dates.map((date, index) => {
|
|
22904
|
+
if (index === 0) {
|
|
22905
|
+
return { years: 0, months: 0, days: 0 };
|
|
22906
|
+
}
|
|
22907
|
+
const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
|
|
22908
|
+
const years = getTimeDifferenceInWholeYears(previous, date);
|
|
22909
|
+
const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
|
|
22910
|
+
previous.setFullYear(previous.getFullYear() + years);
|
|
22911
|
+
previous.setMonth(previous.getMonth() + months);
|
|
22912
|
+
const days = getTimeDifferenceInWholeDays(previous, date);
|
|
22913
|
+
return {
|
|
22914
|
+
years,
|
|
22915
|
+
months,
|
|
22916
|
+
days,
|
|
22917
|
+
};
|
|
22918
|
+
});
|
|
22919
|
+
return res.slice(1);
|
|
22784
22920
|
}
|
|
22785
22921
|
|
|
22922
|
+
const cellPopoverRegistry = new Registry();
|
|
22923
|
+
|
|
22786
22924
|
class GaugeChartComponent extends owl.Component {
|
|
22787
22925
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22788
22926
|
canvas = owl.useRef("chartContainer");
|
|
@@ -22815,72 +22953,6 @@ function toXlsxHexColor(color) {
|
|
|
22815
22953
|
return color;
|
|
22816
22954
|
}
|
|
22817
22955
|
|
|
22818
|
-
const CHART_COMMON_OPTIONS = {
|
|
22819
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
22820
|
-
responsive: true, // will resize when its container is resized
|
|
22821
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
22822
|
-
elements: {
|
|
22823
|
-
line: {
|
|
22824
|
-
fill: false, // do not fill the area under line charts
|
|
22825
|
-
},
|
|
22826
|
-
point: {
|
|
22827
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
22828
|
-
},
|
|
22829
|
-
},
|
|
22830
|
-
animation: false,
|
|
22831
|
-
};
|
|
22832
|
-
function chartToImage(runtime, figure, type) {
|
|
22833
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
22834
|
-
// fill the whole page otherwise
|
|
22835
|
-
const div = document.createElement("div");
|
|
22836
|
-
div.style.width = `${figure.width}px`;
|
|
22837
|
-
div.style.height = `${figure.height}px`;
|
|
22838
|
-
const canvas = document.createElement("canvas");
|
|
22839
|
-
div.append(canvas);
|
|
22840
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
22841
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
22842
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
22843
|
-
document.body.append(div);
|
|
22844
|
-
if ("chartJsConfig" in runtime) {
|
|
22845
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
22846
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
22847
|
-
const chart = new window.Chart(canvas, config);
|
|
22848
|
-
const imgContent = chart.toBase64Image();
|
|
22849
|
-
chart.destroy();
|
|
22850
|
-
div.remove();
|
|
22851
|
-
return imgContent;
|
|
22852
|
-
}
|
|
22853
|
-
else if (type === "scorecard") {
|
|
22854
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
22855
|
-
drawScoreChart(design, canvas);
|
|
22856
|
-
const imgContent = canvas.toDataURL();
|
|
22857
|
-
div.remove();
|
|
22858
|
-
return imgContent;
|
|
22859
|
-
}
|
|
22860
|
-
else if (type === "gauge") {
|
|
22861
|
-
drawGaugeChart(canvas, runtime);
|
|
22862
|
-
const imgContent = canvas.toDataURL();
|
|
22863
|
-
div.remove();
|
|
22864
|
-
return imgContent;
|
|
22865
|
-
}
|
|
22866
|
-
return undefined;
|
|
22867
|
-
}
|
|
22868
|
-
/**
|
|
22869
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
22870
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
22871
|
-
*/
|
|
22872
|
-
const backgroundColorChartJSPlugin = {
|
|
22873
|
-
id: "customCanvasBackgroundColor",
|
|
22874
|
-
beforeDraw: (chart) => {
|
|
22875
|
-
const { ctx } = chart;
|
|
22876
|
-
ctx.save();
|
|
22877
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
22878
|
-
ctx.fillStyle = "#ffffff";
|
|
22879
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
22880
|
-
ctx.restore();
|
|
22881
|
-
},
|
|
22882
|
-
};
|
|
22883
|
-
|
|
22884
22956
|
/**
|
|
22885
22957
|
* Represent a raw XML string
|
|
22886
22958
|
*/
|
|
@@ -22946,6 +23018,7 @@ const CONTENT_TYPES = {
|
|
|
22946
23018
|
macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
|
|
22947
23019
|
excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
|
|
22948
23020
|
sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
|
23021
|
+
metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
|
|
22949
23022
|
sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
|
|
22950
23023
|
styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
22951
23024
|
drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
|
|
@@ -22958,6 +23031,7 @@ const CONTENT_TYPES = {
|
|
|
22958
23031
|
const XLSX_RELATION_TYPE = {
|
|
22959
23032
|
document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
22960
23033
|
sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
23034
|
+
metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
|
|
22961
23035
|
sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
|
|
22962
23036
|
styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
22963
23037
|
drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
|
|
@@ -22967,6 +23041,7 @@ const XLSX_RELATION_TYPE = {
|
|
|
22967
23041
|
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
22968
23042
|
image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
22969
23043
|
};
|
|
23044
|
+
const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
|
|
22970
23045
|
const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
22971
23046
|
const HEIGHT_FACTOR = 0.75; // 100px => 75 u
|
|
22972
23047
|
/**
|
|
@@ -24342,16 +24417,25 @@ function addRelsToFile(relsFiles, path, rel) {
|
|
|
24342
24417
|
}
|
|
24343
24418
|
return id;
|
|
24344
24419
|
}
|
|
24420
|
+
const globalReverseLookup = new WeakMap();
|
|
24345
24421
|
function pushElement(property, propertyList) {
|
|
24346
|
-
let
|
|
24347
|
-
|
|
24348
|
-
|
|
24349
|
-
|
|
24350
|
-
|
|
24422
|
+
let reverseLookup = globalReverseLookup.get(propertyList);
|
|
24423
|
+
if (!reverseLookup) {
|
|
24424
|
+
reverseLookup = new Map();
|
|
24425
|
+
for (let i = 0; i < propertyList.length; i++) {
|
|
24426
|
+
const canonical = getCanonicalRepresentation(propertyList[i]);
|
|
24427
|
+
reverseLookup.set(canonical, i);
|
|
24351
24428
|
}
|
|
24429
|
+
globalReverseLookup.set(propertyList, reverseLookup);
|
|
24430
|
+
}
|
|
24431
|
+
const canonical = getCanonicalRepresentation(property);
|
|
24432
|
+
if (reverseLookup.has(canonical)) {
|
|
24433
|
+
return reverseLookup.get(canonical);
|
|
24352
24434
|
}
|
|
24353
|
-
propertyList
|
|
24354
|
-
|
|
24435
|
+
const maxId = propertyList.length;
|
|
24436
|
+
propertyList.push(property);
|
|
24437
|
+
reverseLookup.set(canonical, maxId);
|
|
24438
|
+
return maxId;
|
|
24355
24439
|
}
|
|
24356
24440
|
const chartIds = [];
|
|
24357
24441
|
/**
|
|
@@ -25388,29 +25472,34 @@ function convertPivotTableConfig(pivotTable) {
|
|
|
25388
25472
|
* In all the sheets, replace the table-only references in the formula cells with standard references.
|
|
25389
25473
|
*/
|
|
25390
25474
|
function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
25391
|
-
for (let
|
|
25392
|
-
const tables = xlsxSheets.find((s) => s.sheetName ===
|
|
25475
|
+
for (let tableSheet of convertedSheets) {
|
|
25476
|
+
const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
|
|
25393
25477
|
for (let table of tables) {
|
|
25394
25478
|
const tabRef = table.name + "[";
|
|
25395
|
-
for (let
|
|
25396
|
-
|
|
25397
|
-
|
|
25398
|
-
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
|
|
25402
|
-
|
|
25403
|
-
|
|
25404
|
-
|
|
25405
|
-
|
|
25406
|
-
|
|
25479
|
+
for (let sheet of convertedSheets) {
|
|
25480
|
+
for (let xc in sheet.cells) {
|
|
25481
|
+
const cell = sheet.cells[xc];
|
|
25482
|
+
let cellContent = sheet.cells[xc];
|
|
25483
|
+
if (cell && cellContent && cellContent.startsWith("=")) {
|
|
25484
|
+
let refIndex;
|
|
25485
|
+
while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
|
|
25486
|
+
let endIndex = refIndex + tabRef.length;
|
|
25487
|
+
let openBrackets = 1;
|
|
25488
|
+
while (openBrackets > 0 && endIndex < cellContent.length) {
|
|
25489
|
+
if (cellContent[endIndex] === "[") {
|
|
25490
|
+
openBrackets++;
|
|
25491
|
+
}
|
|
25492
|
+
else if (cellContent[endIndex] === "]") {
|
|
25493
|
+
openBrackets--;
|
|
25494
|
+
}
|
|
25495
|
+
endIndex++;
|
|
25496
|
+
}
|
|
25497
|
+
let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
|
|
25498
|
+
const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
|
|
25499
|
+
const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
|
|
25500
|
+
cellContent =
|
|
25501
|
+
cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
|
|
25407
25502
|
}
|
|
25408
|
-
reference = reference.slice(0, endIndex);
|
|
25409
|
-
const convertedRef = convertTableReference(reference, table, xc);
|
|
25410
|
-
cellContent =
|
|
25411
|
-
cellContent.slice(0, refIndex) +
|
|
25412
|
-
convertedRef +
|
|
25413
|
-
cellContent.slice(tabRef.length + refIndex + endIndex + 1);
|
|
25414
25503
|
}
|
|
25415
25504
|
sheet.cells[xc] = cellContent;
|
|
25416
25505
|
}
|
|
@@ -25419,11 +25508,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
25419
25508
|
}
|
|
25420
25509
|
}
|
|
25421
25510
|
/**
|
|
25422
|
-
* Convert table-specific references in formulas into standard references.
|
|
25511
|
+
* Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
|
|
25512
|
+
* and of keywords determining the rows of the table to reference.
|
|
25423
25513
|
*
|
|
25424
25514
|
* A reference in a table can have the form (only the part between brackets should be given to this function):
|
|
25425
25515
|
* - tableName[colName] : reference to the whole column "colName"
|
|
25516
|
+
* - tableName[#keyword] : reference to the whatever row the keyword refers to
|
|
25426
25517
|
* - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
|
|
25518
|
+
* - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
|
|
25519
|
+
* - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
|
|
25520
|
+
* - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
|
|
25521
|
+
*
|
|
25427
25522
|
*
|
|
25428
25523
|
* The available keywords are :
|
|
25429
25524
|
* - #All : all the column (including totals)
|
|
@@ -25431,58 +25526,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
25431
25526
|
* - #Headers : only the header of the column
|
|
25432
25527
|
* - #Totals : only the totals of the column
|
|
25433
25528
|
* - #This Row : only the element in the same row as the cell
|
|
25529
|
+
*
|
|
25530
|
+
* Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
|
|
25434
25531
|
*/
|
|
25435
|
-
function convertTableReference(expr, table, cellXc) {
|
|
25436
|
-
|
|
25532
|
+
function convertTableReference(sheetPrefix, expr, table, cellXc) {
|
|
25533
|
+
// TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
|
|
25534
|
+
// contain # or , characters. But that's probably an edge case that we can ignore for now.
|
|
25535
|
+
const parts = expr.split(",").map((part) => part.trim());
|
|
25437
25536
|
const tableZone = toZone(table.ref);
|
|
25438
|
-
const
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
|
|
25443
|
-
|
|
25444
|
-
|
|
25445
|
-
|
|
25446
|
-
|
|
25447
|
-
|
|
25448
|
-
|
|
25537
|
+
const colIndexes = [];
|
|
25538
|
+
const rowIndexes = [];
|
|
25539
|
+
const foundKeywords = [];
|
|
25540
|
+
for (const part of parts) {
|
|
25541
|
+
if (removeBrackets(part).startsWith("#")) {
|
|
25542
|
+
const keyWord = removeBrackets(part);
|
|
25543
|
+
foundKeywords.push(keyWord);
|
|
25544
|
+
switch (keyWord) {
|
|
25545
|
+
case "#All":
|
|
25546
|
+
rowIndexes.push(tableZone.top, tableZone.bottom);
|
|
25547
|
+
break;
|
|
25548
|
+
case "#Data":
|
|
25549
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25550
|
+
const bottom = table.totalsRowCount
|
|
25551
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25552
|
+
: tableZone.bottom;
|
|
25553
|
+
rowIndexes.push(top, bottom);
|
|
25554
|
+
break;
|
|
25555
|
+
case "#This Row":
|
|
25556
|
+
rowIndexes.push(toCartesian(cellXc).row);
|
|
25557
|
+
break;
|
|
25558
|
+
case "#Headers":
|
|
25559
|
+
if (!table.headerRowCount) {
|
|
25560
|
+
return CellErrorType.InvalidReference;
|
|
25561
|
+
}
|
|
25562
|
+
rowIndexes.push(tableZone.top);
|
|
25563
|
+
break;
|
|
25564
|
+
case "#Totals":
|
|
25565
|
+
if (!table.totalsRowCount) {
|
|
25566
|
+
return CellErrorType.InvalidReference;
|
|
25567
|
+
}
|
|
25568
|
+
rowIndexes.push(tableZone.bottom);
|
|
25569
|
+
break;
|
|
25570
|
+
}
|
|
25449
25571
|
}
|
|
25450
|
-
|
|
25451
|
-
|
|
25452
|
-
|
|
25453
|
-
|
|
25454
|
-
|
|
25455
|
-
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
|
|
25464
|
-
|
|
25465
|
-
|
|
25466
|
-
|
|
25467
|
-
if (!table.headerRowCount) {
|
|
25468
|
-
isReferencedZoneValid = false;
|
|
25469
|
-
}
|
|
25470
|
-
break;
|
|
25471
|
-
case "#Totals":
|
|
25472
|
-
refZone.top = refZone.bottom = tableZone.bottom;
|
|
25473
|
-
if (!table.totalsRowCount) {
|
|
25474
|
-
isReferencedZoneValid = false;
|
|
25572
|
+
else {
|
|
25573
|
+
const columns = part
|
|
25574
|
+
.split(":")
|
|
25575
|
+
.map((part) => part.trim())
|
|
25576
|
+
.map(removeBrackets);
|
|
25577
|
+
if (colIndexes.length) {
|
|
25578
|
+
return CellErrorType.InvalidReference;
|
|
25579
|
+
}
|
|
25580
|
+
const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
|
|
25581
|
+
if (colRelativeIndex === -1) {
|
|
25582
|
+
return CellErrorType.InvalidReference;
|
|
25583
|
+
}
|
|
25584
|
+
colIndexes.push(colRelativeIndex + tableZone.left);
|
|
25585
|
+
if (columns[1]) {
|
|
25586
|
+
const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
|
|
25587
|
+
if (colRelativeIndex2 === -1) {
|
|
25588
|
+
return CellErrorType.InvalidReference;
|
|
25475
25589
|
}
|
|
25476
|
-
|
|
25590
|
+
colIndexes.push(colRelativeIndex2 + tableZone.left);
|
|
25591
|
+
}
|
|
25477
25592
|
}
|
|
25478
|
-
const colRef = refElements[1].slice(1, refElements[1].length - 1);
|
|
25479
|
-
const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
|
|
25480
|
-
refZone.left = refZone.right = colRelativeIndex + tableZone.left;
|
|
25481
25593
|
}
|
|
25482
|
-
if (!
|
|
25594
|
+
if (!areKeywordsCompatible(foundKeywords)) {
|
|
25483
25595
|
return CellErrorType.InvalidReference;
|
|
25484
25596
|
}
|
|
25485
|
-
|
|
25597
|
+
if (rowIndexes.length === 0) {
|
|
25598
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25599
|
+
const bottom = table.totalsRowCount
|
|
25600
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25601
|
+
: tableZone.bottom;
|
|
25602
|
+
rowIndexes.push(top, bottom);
|
|
25603
|
+
}
|
|
25604
|
+
if (colIndexes.length === 0) {
|
|
25605
|
+
colIndexes.push(tableZone.left, tableZone.right);
|
|
25606
|
+
}
|
|
25607
|
+
const refZone = {
|
|
25608
|
+
top: Math.min(...rowIndexes),
|
|
25609
|
+
left: Math.min(...colIndexes),
|
|
25610
|
+
bottom: Math.max(...rowIndexes),
|
|
25611
|
+
right: Math.max(...colIndexes),
|
|
25612
|
+
};
|
|
25613
|
+
return sheetPrefix + zoneToXc(refZone);
|
|
25614
|
+
}
|
|
25615
|
+
function removeBrackets(str) {
|
|
25616
|
+
return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
|
|
25617
|
+
}
|
|
25618
|
+
function areKeywordsCompatible(keywords) {
|
|
25619
|
+
if (keywords.length < 2) {
|
|
25620
|
+
return true;
|
|
25621
|
+
}
|
|
25622
|
+
else if (keywords.length > 2) {
|
|
25623
|
+
return false;
|
|
25624
|
+
}
|
|
25625
|
+
else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
|
|
25626
|
+
return true;
|
|
25627
|
+
}
|
|
25628
|
+
else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
|
|
25629
|
+
return true;
|
|
25630
|
+
}
|
|
25631
|
+
return false;
|
|
25486
25632
|
}
|
|
25487
25633
|
|
|
25488
25634
|
// -------------------------------------
|
|
@@ -26136,7 +26282,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
26136
26282
|
title: { text: chartTitle },
|
|
26137
26283
|
type: CHART_TYPE_CONVERSION_MAP[chartType],
|
|
26138
26284
|
dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
|
|
26139
|
-
labelRange: this.
|
|
26285
|
+
labelRange: this.extractLabelRange(chartType, rootChartElement),
|
|
26140
26286
|
backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
|
|
26141
26287
|
default: "ffffff",
|
|
26142
26288
|
}).asString(),
|
|
@@ -26148,6 +26294,13 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
26148
26294
|
};
|
|
26149
26295
|
})[0];
|
|
26150
26296
|
}
|
|
26297
|
+
extractLabelRange(chartType, rootChartElement) {
|
|
26298
|
+
if (chartType === "scatterChart") {
|
|
26299
|
+
return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
|
|
26300
|
+
this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
|
|
26301
|
+
}
|
|
26302
|
+
return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
|
|
26303
|
+
}
|
|
26151
26304
|
extractComboChart(chartElement) {
|
|
26152
26305
|
// Title can be separated into multiple xml elements (for styling and such), we only import the text
|
|
26153
26306
|
const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
|
|
@@ -28650,11 +28803,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
|
|
|
28650
28803
|
}
|
|
28651
28804
|
let missingTimeAdapterAlreadyWarned = false;
|
|
28652
28805
|
function isLuxonTimeAdapterInstalled() {
|
|
28653
|
-
|
|
28806
|
+
const Chart = getChartJSConstructor();
|
|
28807
|
+
if (!Chart) {
|
|
28654
28808
|
return false;
|
|
28655
28809
|
}
|
|
28656
28810
|
// @ts-ignore
|
|
28657
|
-
const adapter = new
|
|
28811
|
+
const adapter = new Chart._adapters._date({});
|
|
28658
28812
|
const isInstalled = adapter._id === "luxon";
|
|
28659
28813
|
if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
|
|
28660
28814
|
missingTimeAdapterAlreadyWarned = true;
|
|
@@ -32508,10 +32662,6 @@ class Popover extends owl.Component {
|
|
|
32508
32662
|
this.currentDisplayValue = newDisplay;
|
|
32509
32663
|
if (!anchor)
|
|
32510
32664
|
return;
|
|
32511
|
-
el.style.top = "";
|
|
32512
|
-
el.style.left = "";
|
|
32513
|
-
el.style["max-height"] = "";
|
|
32514
|
-
el.style["max-width"] = "";
|
|
32515
32665
|
const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
|
|
32516
32666
|
let elDims = {
|
|
32517
32667
|
width: el.getBoundingClientRect().width,
|
|
@@ -34063,6 +34213,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
|
|
|
34063
34213
|
duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
|
|
34064
34214
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
34065
34215
|
formatTickValue: formatTickValue,
|
|
34216
|
+
getChartJSConstructor: getChartJSConstructor,
|
|
34066
34217
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
34067
34218
|
getDefinedAxis: getDefinedAxis,
|
|
34068
34219
|
getPieColors: getPieColors,
|
|
@@ -36322,6 +36473,7 @@ const irregularityMap = {
|
|
|
36322
36473
|
fingerprintStore.enable();
|
|
36323
36474
|
}
|
|
36324
36475
|
},
|
|
36476
|
+
isReadonlyAllowed: true,
|
|
36325
36477
|
icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
|
|
36326
36478
|
};
|
|
36327
36479
|
const viewFormulas = {
|
|
@@ -37863,6 +38015,11 @@ class SelectionInputStore extends SpreadsheetStore {
|
|
|
37863
38015
|
}
|
|
37864
38016
|
updateColors(colors) {
|
|
37865
38017
|
this.colors = colors;
|
|
38018
|
+
const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
|
|
38019
|
+
this.ranges = this.ranges.map((range) => ({
|
|
38020
|
+
...range,
|
|
38021
|
+
color: colorGenerator.next(),
|
|
38022
|
+
}));
|
|
37866
38023
|
}
|
|
37867
38024
|
confirm() {
|
|
37868
38025
|
for (const range of this.selectionInputs) {
|
|
@@ -37897,12 +38054,11 @@ class SelectionInputStore extends SpreadsheetStore {
|
|
|
37897
38054
|
* e.g. ["A1", "Sheet2!B3", "E12"]
|
|
37898
38055
|
*/
|
|
37899
38056
|
get selectionInputs() {
|
|
37900
|
-
const generator = new ColorGenerator(this.ranges.length, this.colors);
|
|
37901
38057
|
return this.ranges.map((input, index) => Object.assign({}, input, {
|
|
37902
38058
|
color: this.hasMainFocus &&
|
|
37903
38059
|
this.focusedRangeIndex !== null &&
|
|
37904
38060
|
this.getters.isRangeValid(input.xc)
|
|
37905
|
-
?
|
|
38061
|
+
? input.color
|
|
37906
38062
|
: null,
|
|
37907
38063
|
isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
|
|
37908
38064
|
isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
|
|
@@ -38172,10 +38328,10 @@ class SelectionInput extends owl.Component {
|
|
|
38172
38328
|
if (originalIndex === finalIndex) {
|
|
38173
38329
|
return;
|
|
38174
38330
|
}
|
|
38175
|
-
const
|
|
38176
|
-
|
|
38177
|
-
|
|
38178
|
-
this.props.onSelectionReordered?.(
|
|
38331
|
+
const indexes = range(0, draggableIds.length);
|
|
38332
|
+
indexes.splice(originalIndex, 1);
|
|
38333
|
+
indexes.splice(finalIndex, 0, originalIndex);
|
|
38334
|
+
this.props.onSelectionReordered?.(indexes);
|
|
38179
38335
|
this.props.onSelectionConfirmed?.();
|
|
38180
38336
|
this.store.confirm();
|
|
38181
38337
|
},
|
|
@@ -38453,6 +38609,9 @@ class GenericChartConfigPanel extends owl.Component {
|
|
|
38453
38609
|
this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
|
|
38454
38610
|
dataSets: this.dataSets,
|
|
38455
38611
|
});
|
|
38612
|
+
if (this.state.datasetDispatchResult.isSuccessful) {
|
|
38613
|
+
this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
|
|
38614
|
+
}
|
|
38456
38615
|
}
|
|
38457
38616
|
getDataSeriesRanges() {
|
|
38458
38617
|
return this.dataSets;
|
|
@@ -39869,8 +40028,16 @@ class ContentEditableHelper {
|
|
|
39869
40028
|
}
|
|
39870
40029
|
let startNode = this.findChildAtCharacterIndex(start);
|
|
39871
40030
|
let endNode = this.findChildAtCharacterIndex(end);
|
|
39872
|
-
|
|
39873
|
-
|
|
40031
|
+
// setEnd (setStart) will result in a collapsed range if the end point is before the start point
|
|
40032
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
|
|
40033
|
+
if (start <= end) {
|
|
40034
|
+
range.setStart(startNode.node, startNode.offset);
|
|
40035
|
+
range.setEnd(endNode.node, endNode.offset);
|
|
40036
|
+
}
|
|
40037
|
+
else {
|
|
40038
|
+
range.setStart(endNode.node, endNode.offset);
|
|
40039
|
+
range.setEnd(startNode.node, startNode.offset);
|
|
40040
|
+
}
|
|
39874
40041
|
}
|
|
39875
40042
|
}
|
|
39876
40043
|
/**
|
|
@@ -40172,8 +40339,7 @@ css /* scss */ `
|
|
|
40172
40339
|
}
|
|
40173
40340
|
|
|
40174
40341
|
.o-composer-assistant {
|
|
40175
|
-
|
|
40176
|
-
margin: 1px 4px;
|
|
40342
|
+
margin-top: 1px;
|
|
40177
40343
|
|
|
40178
40344
|
.o-semi-bold {
|
|
40179
40345
|
/* FIXME: to remove in favor of Bootstrap
|
|
@@ -40224,10 +40390,11 @@ class Composer extends owl.Component {
|
|
|
40224
40390
|
});
|
|
40225
40391
|
compositionActive = false;
|
|
40226
40392
|
spreadsheetRect = useSpreadsheetRect();
|
|
40227
|
-
get
|
|
40393
|
+
get assistantStyleProperties() {
|
|
40228
40394
|
const composerRect = this.composerRef.el.getBoundingClientRect();
|
|
40229
40395
|
const assistantStyle = {};
|
|
40230
|
-
|
|
40396
|
+
const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
|
|
40397
|
+
assistantStyle["min-width"] = `${minWidth}px`;
|
|
40231
40398
|
const proposals = this.autoCompleteState.provider?.proposals;
|
|
40232
40399
|
const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
|
|
40233
40400
|
if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
|
|
@@ -40251,13 +40418,29 @@ class Composer extends owl.Component {
|
|
|
40251
40418
|
}
|
|
40252
40419
|
}
|
|
40253
40420
|
else {
|
|
40254
|
-
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
|
|
40421
|
+
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
|
|
40255
40422
|
if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
|
|
40256
40423
|
this.spreadsheetRect.width) {
|
|
40257
40424
|
assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
|
|
40258
40425
|
}
|
|
40259
40426
|
}
|
|
40260
|
-
return
|
|
40427
|
+
return assistantStyle;
|
|
40428
|
+
}
|
|
40429
|
+
get assistantStyle() {
|
|
40430
|
+
const allProperties = this.assistantStyleProperties;
|
|
40431
|
+
return cssPropertiesToCss({
|
|
40432
|
+
"max-height": allProperties["max-height"],
|
|
40433
|
+
width: allProperties["width"],
|
|
40434
|
+
"min-width": allProperties["min-width"],
|
|
40435
|
+
});
|
|
40436
|
+
}
|
|
40437
|
+
get assistantContainerStyle() {
|
|
40438
|
+
const allProperties = this.assistantStyleProperties;
|
|
40439
|
+
return cssPropertiesToCss({
|
|
40440
|
+
top: allProperties["top"],
|
|
40441
|
+
right: allProperties["right"],
|
|
40442
|
+
transform: allProperties["transform"],
|
|
40443
|
+
});
|
|
40261
40444
|
}
|
|
40262
40445
|
// we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
|
|
40263
40446
|
shouldProcessInputEvents = false;
|
|
@@ -46731,9 +46914,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46731
46914
|
pivot: this.draft,
|
|
46732
46915
|
});
|
|
46733
46916
|
this.draft = null;
|
|
46734
|
-
if (!this.alreadyNotified &&
|
|
46735
|
-
!this.isDynamicPivotInViewport() &&
|
|
46736
|
-
this.isStaticPivotInViewport()) {
|
|
46917
|
+
if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
|
|
46737
46918
|
const formulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46738
46919
|
const pivotExample = `=PIVOT(${formulaId})`;
|
|
46739
46920
|
this.alreadyNotified = true;
|
|
@@ -46789,26 +46970,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46789
46970
|
this.applyUpdate();
|
|
46790
46971
|
}
|
|
46791
46972
|
}
|
|
46792
|
-
|
|
46793
|
-
|
|
46794
|
-
|
|
46795
|
-
|
|
46796
|
-
|
|
46797
|
-
|
|
46798
|
-
|
|
46799
|
-
return false;
|
|
46800
|
-
}
|
|
46801
|
-
isStaticPivotInViewport() {
|
|
46973
|
+
/**
|
|
46974
|
+
* @returns true if the updated pivot is visible in the viewport only as a
|
|
46975
|
+
* static pivot and not as a dynamic pivot
|
|
46976
|
+
*/
|
|
46977
|
+
isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
|
|
46978
|
+
let staticPivotCount = 0;
|
|
46979
|
+
const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46802
46980
|
for (const position of this.getters.getVisibleCellPositions()) {
|
|
46803
46981
|
const cell = this.getters.getCell(position);
|
|
46804
46982
|
if (cell?.isFormula) {
|
|
46805
46983
|
const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
|
|
46806
|
-
|
|
46807
|
-
|
|
46984
|
+
const pivotFormulaId = pivotFunction?.args[0]?.value;
|
|
46985
|
+
if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
|
|
46986
|
+
if (pivotFunction.functionName === "PIVOT") {
|
|
46987
|
+
// if we have at least one dynamic pivot visible inserted the viewport
|
|
46988
|
+
// we return false
|
|
46989
|
+
return false;
|
|
46990
|
+
}
|
|
46991
|
+
else {
|
|
46992
|
+
staticPivotCount++;
|
|
46993
|
+
}
|
|
46808
46994
|
}
|
|
46809
46995
|
}
|
|
46810
46996
|
}
|
|
46811
|
-
return
|
|
46997
|
+
// we return true if there are only static pivots visible inserted the viewport,
|
|
46998
|
+
// otherwise false
|
|
46999
|
+
return staticPivotCount > 0;
|
|
46812
47000
|
}
|
|
46813
47001
|
addDefaultDateTimeGranularity(fields, definition) {
|
|
46814
47002
|
const { columns, rows } = definition;
|
|
@@ -50347,6 +50535,71 @@ class GridPopover extends owl.Component {
|
|
|
50347
50535
|
}
|
|
50348
50536
|
}
|
|
50349
50537
|
|
|
50538
|
+
class UnhideRowHeaders extends owl.Component {
|
|
50539
|
+
static template = "o-spreadsheet-UnhideRowHeaders";
|
|
50540
|
+
static props = {
|
|
50541
|
+
headersGroups: Array,
|
|
50542
|
+
headerRange: Object,
|
|
50543
|
+
offset: { type: Number, optional: true },
|
|
50544
|
+
};
|
|
50545
|
+
static defaultProps = { offset: 0 };
|
|
50546
|
+
get sheetId() {
|
|
50547
|
+
return this.env.model.getters.getActiveSheetId();
|
|
50548
|
+
}
|
|
50549
|
+
getUnhidePreviousButtonStyle(hiddenIndex) {
|
|
50550
|
+
const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
|
|
50551
|
+
const y = rect.y + rect.height - HEADER_HEIGHT;
|
|
50552
|
+
return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
|
|
50553
|
+
}
|
|
50554
|
+
getUnhideNextButtonStyle(hiddenIndex) {
|
|
50555
|
+
const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
|
|
50556
|
+
const y = rect.y - HEADER_HEIGHT;
|
|
50557
|
+
return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
|
|
50558
|
+
}
|
|
50559
|
+
unhide(hiddenElements) {
|
|
50560
|
+
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
50561
|
+
sheetId: this.sheetId,
|
|
50562
|
+
dimension: "ROW",
|
|
50563
|
+
elements: hiddenElements,
|
|
50564
|
+
});
|
|
50565
|
+
}
|
|
50566
|
+
isVisible(header) {
|
|
50567
|
+
return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
|
|
50568
|
+
}
|
|
50569
|
+
}
|
|
50570
|
+
class UnhideColumnHeaders extends owl.Component {
|
|
50571
|
+
static template = "o-spreadsheet-UnhideColumnHeaders";
|
|
50572
|
+
static props = {
|
|
50573
|
+
headersGroups: Array,
|
|
50574
|
+
headerRange: Object,
|
|
50575
|
+
offset: { type: Number, optional: true },
|
|
50576
|
+
};
|
|
50577
|
+
static defaultProps = { offset: 0 };
|
|
50578
|
+
get sheetId() {
|
|
50579
|
+
return this.env.model.getters.getActiveSheetId();
|
|
50580
|
+
}
|
|
50581
|
+
getUnhidePreviousButtonStyle(hiddenIndex) {
|
|
50582
|
+
const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
|
|
50583
|
+
const x = rect.x + rect.width - HEADER_WIDTH;
|
|
50584
|
+
return cssPropertiesToCss({ left: x - this.props.offset + "px" });
|
|
50585
|
+
}
|
|
50586
|
+
getUnhideNextButtonStyle(hiddenIndex) {
|
|
50587
|
+
const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
|
|
50588
|
+
const x = rect.x - HEADER_WIDTH;
|
|
50589
|
+
return cssPropertiesToCss({ left: x - this.props.offset + "px" });
|
|
50590
|
+
}
|
|
50591
|
+
unhide(hiddenElements) {
|
|
50592
|
+
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
50593
|
+
sheetId: this.sheetId,
|
|
50594
|
+
dimension: "COL",
|
|
50595
|
+
elements: hiddenElements,
|
|
50596
|
+
});
|
|
50597
|
+
}
|
|
50598
|
+
isVisible(header) {
|
|
50599
|
+
return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
|
|
50600
|
+
}
|
|
50601
|
+
}
|
|
50602
|
+
|
|
50350
50603
|
class AbstractResizer extends owl.Component {
|
|
50351
50604
|
static props = {
|
|
50352
50605
|
onOpenContextMenu: Function,
|
|
@@ -50565,6 +50818,7 @@ css /* scss */ `
|
|
|
50565
50818
|
left: ${HEADER_WIDTH}px;
|
|
50566
50819
|
right: 0;
|
|
50567
50820
|
height: ${HEADER_HEIGHT}px;
|
|
50821
|
+
width: calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}px);
|
|
50568
50822
|
&.o-dragging {
|
|
50569
50823
|
cursor: grabbing;
|
|
50570
50824
|
}
|
|
@@ -50614,6 +50868,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50614
50868
|
onOpenContextMenu: Function,
|
|
50615
50869
|
};
|
|
50616
50870
|
static template = "o-spreadsheet-ColResizer";
|
|
50871
|
+
static components = { UnhideColumnHeaders };
|
|
50617
50872
|
colResizerRef;
|
|
50618
50873
|
setup() {
|
|
50619
50874
|
super.setup();
|
|
@@ -50622,6 +50877,9 @@ class ColResizer extends AbstractResizer {
|
|
|
50622
50877
|
this.MAX_SIZE_MARGIN = 90;
|
|
50623
50878
|
this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
|
|
50624
50879
|
}
|
|
50880
|
+
get sheetId() {
|
|
50881
|
+
return this.env.model.getters.getActiveSheetId();
|
|
50882
|
+
}
|
|
50625
50883
|
_getEvOffset(ev) {
|
|
50626
50884
|
return ev.offsetX;
|
|
50627
50885
|
}
|
|
@@ -50644,10 +50902,10 @@ class ColResizer extends AbstractResizer {
|
|
|
50644
50902
|
return this.env.model.getters.getEdgeScrollCol(position, position, position);
|
|
50645
50903
|
}
|
|
50646
50904
|
_getDimensionsInViewport(index) {
|
|
50647
|
-
return this.env.model.getters.getColDimensionsInViewport(this.
|
|
50905
|
+
return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
|
|
50648
50906
|
}
|
|
50649
50907
|
_getElementSize(index) {
|
|
50650
|
-
return this.env.model.getters.getColSize(this.
|
|
50908
|
+
return this.env.model.getters.getColSize(this.sheetId, index);
|
|
50651
50909
|
}
|
|
50652
50910
|
_getMaxSize() {
|
|
50653
50911
|
return this.colResizerRef.el.clientWidth;
|
|
@@ -50658,7 +50916,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50658
50916
|
const cols = this.env.model.getters.getActiveCols();
|
|
50659
50917
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
50660
50918
|
dimension: "COL",
|
|
50661
|
-
sheetId: this.
|
|
50919
|
+
sheetId: this.sheetId,
|
|
50662
50920
|
elements: cols.has(index) ? [...cols] : [index],
|
|
50663
50921
|
size,
|
|
50664
50922
|
});
|
|
@@ -50671,7 +50929,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50671
50929
|
elements.push(colIndex);
|
|
50672
50930
|
}
|
|
50673
50931
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
50674
|
-
sheetId: this.
|
|
50932
|
+
sheetId: this.sheetId,
|
|
50675
50933
|
dimension: "COL",
|
|
50676
50934
|
base: this.state.base,
|
|
50677
50935
|
elements,
|
|
@@ -50690,7 +50948,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50690
50948
|
_fitElementSize(index) {
|
|
50691
50949
|
const cols = this.env.model.getters.getActiveCols();
|
|
50692
50950
|
this.env.model.dispatch("AUTORESIZE_COLUMNS", {
|
|
50693
|
-
sheetId: this.
|
|
50951
|
+
sheetId: this.sheetId,
|
|
50694
50952
|
cols: cols.has(index) ? [...cols] : [index],
|
|
50695
50953
|
});
|
|
50696
50954
|
}
|
|
@@ -50701,7 +50959,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50701
50959
|
return this.env.model.getters.getActiveCols();
|
|
50702
50960
|
}
|
|
50703
50961
|
_getPreviousVisibleElement(index) {
|
|
50704
|
-
const sheetId = this.
|
|
50962
|
+
const sheetId = this.sheetId;
|
|
50705
50963
|
let row;
|
|
50706
50964
|
for (row = index - 1; row >= 0; row--) {
|
|
50707
50965
|
if (!this.env.model.getters.isColHidden(sheetId, row)) {
|
|
@@ -50712,13 +50970,38 @@ class ColResizer extends AbstractResizer {
|
|
|
50712
50970
|
}
|
|
50713
50971
|
unhide(hiddenElements) {
|
|
50714
50972
|
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
50715
|
-
sheetId: this.
|
|
50973
|
+
sheetId: this.sheetId,
|
|
50716
50974
|
elements: hiddenElements,
|
|
50717
50975
|
dimension: "COL",
|
|
50718
50976
|
});
|
|
50719
50977
|
}
|
|
50720
|
-
|
|
50721
|
-
|
|
50978
|
+
get mainUnhideHeadersProps() {
|
|
50979
|
+
const { left, right } = this.env.model.getters.getActiveMainViewport();
|
|
50980
|
+
const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
|
|
50981
|
+
const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
|
|
50982
|
+
const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
|
|
50983
|
+
return {
|
|
50984
|
+
headersGroups: hiddenGroups.slice(index),
|
|
50985
|
+
offset: this.env.model.getters.getMainViewportCoordinates().x,
|
|
50986
|
+
headerRange: { start: left, end: right },
|
|
50987
|
+
};
|
|
50988
|
+
}
|
|
50989
|
+
get frozenUnhideHeadersProps() {
|
|
50990
|
+
const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
|
|
50991
|
+
const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
|
|
50992
|
+
const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
|
|
50993
|
+
return {
|
|
50994
|
+
headersGroups: hiddenGroups.slice(0, index + 1),
|
|
50995
|
+
headerRange: { start: 0, end: xSplit - 1 },
|
|
50996
|
+
};
|
|
50997
|
+
}
|
|
50998
|
+
get frozenContainerStyle() {
|
|
50999
|
+
return cssPropertiesToCss({
|
|
51000
|
+
width: this.env.model.getters.getMainViewportCoordinates().x + "px",
|
|
51001
|
+
});
|
|
51002
|
+
}
|
|
51003
|
+
get hasFrozenPane() {
|
|
51004
|
+
return this.env.model.getters.getPaneDivisions(this.sheetId).xSplit > 0;
|
|
50722
51005
|
}
|
|
50723
51006
|
}
|
|
50724
51007
|
css /* scss */ `
|
|
@@ -50728,7 +51011,7 @@ css /* scss */ `
|
|
|
50728
51011
|
left: 0;
|
|
50729
51012
|
right: 0;
|
|
50730
51013
|
width: ${HEADER_WIDTH}px;
|
|
50731
|
-
height: 100
|
|
51014
|
+
height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
|
|
50732
51015
|
&.o-dragging {
|
|
50733
51016
|
cursor: grabbing;
|
|
50734
51017
|
}
|
|
@@ -50778,6 +51061,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50778
51061
|
onOpenContextMenu: Function,
|
|
50779
51062
|
};
|
|
50780
51063
|
static template = "o-spreadsheet-RowResizer";
|
|
51064
|
+
static components = { UnhideRowHeaders };
|
|
50781
51065
|
setup() {
|
|
50782
51066
|
super.setup();
|
|
50783
51067
|
this.rowResizerRef = owl.useRef("rowResizer");
|
|
@@ -50786,6 +51070,9 @@ class RowResizer extends AbstractResizer {
|
|
|
50786
51070
|
this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
|
|
50787
51071
|
}
|
|
50788
51072
|
rowResizerRef;
|
|
51073
|
+
get sheetId() {
|
|
51074
|
+
return this.env.model.getters.getActiveSheetId();
|
|
51075
|
+
}
|
|
50789
51076
|
_getEvOffset(ev) {
|
|
50790
51077
|
return ev.offsetY;
|
|
50791
51078
|
}
|
|
@@ -50808,10 +51095,10 @@ class RowResizer extends AbstractResizer {
|
|
|
50808
51095
|
return this.env.model.getters.getEdgeScrollRow(position, position, position);
|
|
50809
51096
|
}
|
|
50810
51097
|
_getDimensionsInViewport(index) {
|
|
50811
|
-
return this.env.model.getters.getRowDimensionsInViewport(this.
|
|
51098
|
+
return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
|
|
50812
51099
|
}
|
|
50813
51100
|
_getElementSize(index) {
|
|
50814
|
-
return this.env.model.getters.getRowSize(this.
|
|
51101
|
+
return this.env.model.getters.getRowSize(this.sheetId, index);
|
|
50815
51102
|
}
|
|
50816
51103
|
_getMaxSize() {
|
|
50817
51104
|
return this.rowResizerRef.el.clientHeight;
|
|
@@ -50822,7 +51109,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50822
51109
|
const rows = this.env.model.getters.getActiveRows();
|
|
50823
51110
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
50824
51111
|
dimension: "ROW",
|
|
50825
|
-
sheetId: this.
|
|
51112
|
+
sheetId: this.sheetId,
|
|
50826
51113
|
elements: rows.has(index) ? [...rows] : [index],
|
|
50827
51114
|
size,
|
|
50828
51115
|
});
|
|
@@ -50835,7 +51122,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50835
51122
|
elements.push(rowIndex);
|
|
50836
51123
|
}
|
|
50837
51124
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
50838
|
-
sheetId: this.
|
|
51125
|
+
sheetId: this.sheetId,
|
|
50839
51126
|
dimension: "ROW",
|
|
50840
51127
|
base: this.state.base,
|
|
50841
51128
|
elements,
|
|
@@ -50854,7 +51141,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50854
51141
|
_fitElementSize(index) {
|
|
50855
51142
|
const rows = this.env.model.getters.getActiveRows();
|
|
50856
51143
|
this.env.model.dispatch("AUTORESIZE_ROWS", {
|
|
50857
|
-
sheetId: this.
|
|
51144
|
+
sheetId: this.sheetId,
|
|
50858
51145
|
rows: rows.has(index) ? [...rows] : [index],
|
|
50859
51146
|
});
|
|
50860
51147
|
}
|
|
@@ -50865,7 +51152,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50865
51152
|
return this.env.model.getters.getActiveRows();
|
|
50866
51153
|
}
|
|
50867
51154
|
_getPreviousVisibleElement(index) {
|
|
50868
|
-
const sheetId = this.
|
|
51155
|
+
const sheetId = this.sheetId;
|
|
50869
51156
|
let row;
|
|
50870
51157
|
for (row = index - 1; row >= 0; row--) {
|
|
50871
51158
|
if (!this.env.model.getters.isRowHidden(sheetId, row)) {
|
|
@@ -50874,15 +51161,33 @@ class RowResizer extends AbstractResizer {
|
|
|
50874
51161
|
}
|
|
50875
51162
|
return row;
|
|
50876
51163
|
}
|
|
50877
|
-
|
|
50878
|
-
this.env.model.
|
|
50879
|
-
|
|
50880
|
-
|
|
50881
|
-
|
|
51164
|
+
get mainUnhideHeadersProps() {
|
|
51165
|
+
const { top, bottom } = this.env.model.getters.getActiveMainViewport();
|
|
51166
|
+
const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
|
|
51167
|
+
const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
|
|
51168
|
+
const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
|
|
51169
|
+
return {
|
|
51170
|
+
headersGroups: hiddenGroups.slice(index),
|
|
51171
|
+
offset: this.env.model.getters.getMainViewportCoordinates().y,
|
|
51172
|
+
headerRange: { start: top, end: bottom },
|
|
51173
|
+
};
|
|
51174
|
+
}
|
|
51175
|
+
get frozenUnhideHeadersProps() {
|
|
51176
|
+
const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
|
|
51177
|
+
const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
|
|
51178
|
+
const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
|
|
51179
|
+
return {
|
|
51180
|
+
headersGroups: hiddenGroups.slice(0, index + 1),
|
|
51181
|
+
headerRange: { start: 0, end: ySplit - 1 },
|
|
51182
|
+
};
|
|
51183
|
+
}
|
|
51184
|
+
get frozenContainerStyle() {
|
|
51185
|
+
return cssPropertiesToCss({
|
|
51186
|
+
height: this.env.model.getters.getMainViewportCoordinates().y + "px",
|
|
50882
51187
|
});
|
|
50883
51188
|
}
|
|
50884
|
-
|
|
50885
|
-
return
|
|
51189
|
+
get hasFrozenPane() {
|
|
51190
|
+
return this.env.model.getters.getPaneDivisions(this.sheetId).ySplit > 0;
|
|
50886
51191
|
}
|
|
50887
51192
|
}
|
|
50888
51193
|
css /* scss */ `
|
|
@@ -60662,6 +60967,7 @@ class EvaluationPlugin extends CoreViewPlugin {
|
|
|
60662
60967
|
exportForExcel(data) {
|
|
60663
60968
|
for (const sheet of data.sheets) {
|
|
60664
60969
|
sheet.cellValues = {};
|
|
60970
|
+
sheet.formulaSpillRanges = {};
|
|
60665
60971
|
}
|
|
60666
60972
|
for (const position of this.evaluator.getEvaluatedPositions()) {
|
|
60667
60973
|
const evaluatedCell = this.evaluator.getEvaluatedCell(position);
|
|
@@ -60673,8 +60979,9 @@ class EvaluationPlugin extends CoreViewPlugin {
|
|
|
60673
60979
|
const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
|
|
60674
60980
|
const formulaCell = this.getCorrespondingFormulaCell(position);
|
|
60675
60981
|
if (formulaCell) {
|
|
60982
|
+
const cell = this.getters.getCell(position);
|
|
60676
60983
|
isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
|
|
60677
|
-
isFormula = isExported;
|
|
60984
|
+
isFormula = isExported && cell?.content === formulaCell.content;
|
|
60678
60985
|
// If the cell contains a non-exported formula and that is evaluates to
|
|
60679
60986
|
// nothing* ,we don't export it.
|
|
60680
60987
|
// * non-falsy value are relevant and so are 0 and FALSE, which only leaves
|
|
@@ -60697,7 +61004,11 @@ class EvaluationPlugin extends CoreViewPlugin {
|
|
|
60697
61004
|
content = !isExported ? newContent : exportedCellData;
|
|
60698
61005
|
}
|
|
60699
61006
|
exportedSheetData.cells[xc] = content;
|
|
60700
|
-
exportedSheetData.cellValues[xc] = value;
|
|
61007
|
+
exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
|
|
61008
|
+
const spillZone = this.getSpreadZone(position);
|
|
61009
|
+
if (spillZone) {
|
|
61010
|
+
exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
|
|
61011
|
+
}
|
|
60701
61012
|
}
|
|
60702
61013
|
}
|
|
60703
61014
|
/**
|
|
@@ -62925,7 +63236,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
62925
63236
|
getRule(cell, cells) {
|
|
62926
63237
|
const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
|
|
62927
63238
|
const rule = rules.find((rule) => rule.condition(cell, cells));
|
|
62928
|
-
return rule && rule.generateRule(cell, cells);
|
|
63239
|
+
return rule && this.direction && rule.generateRule(cell, cells, this.direction);
|
|
62929
63240
|
}
|
|
62930
63241
|
/**
|
|
62931
63242
|
* Create the generator to be able to autofill the next cells.
|
|
@@ -67646,7 +67957,7 @@ class InternalViewport {
|
|
|
67646
67957
|
*
|
|
67647
67958
|
*/
|
|
67648
67959
|
getFullRect(zone) {
|
|
67649
|
-
const targetZone = intersection(zone, this);
|
|
67960
|
+
const targetZone = intersection(zone, this.boundaries);
|
|
67650
67961
|
const scrollDeltaX = this.snapCorrection.x;
|
|
67651
67962
|
const scrollDeltaY = this.snapCorrection.y;
|
|
67652
67963
|
if (targetZone) {
|
|
@@ -68114,7 +68425,8 @@ class SheetViewPlugin extends UIPlugin {
|
|
|
68114
68425
|
? this.getters.getSheetViewVisibleCols()
|
|
68115
68426
|
: this.getters.getSheetViewVisibleRows();
|
|
68116
68427
|
const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
|
|
68117
|
-
|
|
68428
|
+
let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
|
|
68429
|
+
endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
|
|
68118
68430
|
const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
|
|
68119
68431
|
let offset = 0;
|
|
68120
68432
|
for (const i of relevantIndexes) {
|
|
@@ -68239,11 +68551,12 @@ class SheetViewPlugin extends UIPlugin {
|
|
|
68239
68551
|
* column of the current viewport
|
|
68240
68552
|
*/
|
|
68241
68553
|
getColDimensionsInViewport(sheetId, col) {
|
|
68554
|
+
const { top } = this.getMainInternalViewport(sheetId);
|
|
68242
68555
|
const zone = {
|
|
68243
68556
|
left: col,
|
|
68244
68557
|
right: col,
|
|
68245
|
-
top:
|
|
68246
|
-
bottom:
|
|
68558
|
+
top: top,
|
|
68559
|
+
bottom: top,
|
|
68247
68560
|
};
|
|
68248
68561
|
const { x, width } = this.getVisibleRect(zone);
|
|
68249
68562
|
const start = x - this.gridOffsetX;
|
|
@@ -68254,9 +68567,10 @@ class SheetViewPlugin extends UIPlugin {
|
|
|
68254
68567
|
* of the current viewport
|
|
68255
68568
|
*/
|
|
68256
68569
|
getRowDimensionsInViewport(sheetId, row) {
|
|
68570
|
+
const { left } = this.getMainInternalViewport(sheetId);
|
|
68257
68571
|
const zone = {
|
|
68258
68572
|
left: 0,
|
|
68259
|
-
right:
|
|
68573
|
+
right: left,
|
|
68260
68574
|
top: row,
|
|
68261
68575
|
bottom: row,
|
|
68262
68576
|
};
|
|
@@ -73407,7 +73721,7 @@ function numberRef(reference) {
|
|
|
73407
73721
|
`;
|
|
73408
73722
|
}
|
|
73409
73723
|
|
|
73410
|
-
function addFormula(formula, value) {
|
|
73724
|
+
function addFormula(formula, value, formulaSpillRange) {
|
|
73411
73725
|
if (!formula) {
|
|
73412
73726
|
return { attrs: [], node: escapeXml `` };
|
|
73413
73727
|
}
|
|
@@ -73415,10 +73729,17 @@ function addFormula(formula, value) {
|
|
|
73415
73729
|
if (type === undefined) {
|
|
73416
73730
|
return { attrs: [], node: escapeXml `` };
|
|
73417
73731
|
}
|
|
73418
|
-
const attrs = [
|
|
73732
|
+
const attrs = [
|
|
73733
|
+
["cm", "1"],
|
|
73734
|
+
["t", type],
|
|
73735
|
+
];
|
|
73419
73736
|
const XlsxFormula = adaptFormulaToExcel(formula);
|
|
73420
73737
|
const exportedValue = adaptFormulaValueToExcel(value);
|
|
73421
|
-
|
|
73738
|
+
// We treat all formulas as array formulas (a simple formula
|
|
73739
|
+
// is an array formula that spills on only one cell) to avoid
|
|
73740
|
+
// trying to detect spilling sub-formulas which is not a trivial task.
|
|
73741
|
+
let node;
|
|
73742
|
+
node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
|
|
73422
73743
|
return { attrs, node };
|
|
73423
73744
|
}
|
|
73424
73745
|
function addContent(content, sharedStrings, forceString = false) {
|
|
@@ -74201,7 +74522,7 @@ function addStyles(styles) {
|
|
|
74201
74522
|
}
|
|
74202
74523
|
if (alignAttrs.length > 0) {
|
|
74203
74524
|
attributes.push(["applyAlignment", "1"]); // for Libre Office
|
|
74204
|
-
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}
|
|
74525
|
+
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
|
|
74205
74526
|
}
|
|
74206
74527
|
else {
|
|
74207
74528
|
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
|
|
@@ -74369,6 +74690,9 @@ function addColumns(cols) {
|
|
|
74369
74690
|
}
|
|
74370
74691
|
function addRows(construct, data, sheet) {
|
|
74371
74692
|
const rowNodes = [];
|
|
74693
|
+
const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
|
|
74694
|
+
const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
|
|
74695
|
+
const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
|
|
74372
74696
|
for (let r = 0; r < sheet.rowNumber; r++) {
|
|
74373
74697
|
const rowAttrs = [["r", r + 1]];
|
|
74374
74698
|
const row = sheet.rows[r] || {};
|
|
@@ -74384,9 +74708,6 @@ function addRows(construct, data, sheet) {
|
|
|
74384
74708
|
if (row.collapsed) {
|
|
74385
74709
|
rowAttrs.push(["collapsed", 1]);
|
|
74386
74710
|
}
|
|
74387
|
-
const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
|
|
74388
|
-
const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
|
|
74389
|
-
const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
|
|
74390
74711
|
const cellNodes = [];
|
|
74391
74712
|
for (let c = 0; c < sheet.colNumber; c++) {
|
|
74392
74713
|
const xc = toXC(c, r);
|
|
@@ -74408,7 +74729,7 @@ function addRows(construct, data, sheet) {
|
|
|
74408
74729
|
let cellNode = escapeXml ``;
|
|
74409
74730
|
// Either formula or static value inside the cell
|
|
74410
74731
|
if (content?.startsWith("=") && value !== undefined) {
|
|
74411
|
-
const res = addFormula(content, value);
|
|
74732
|
+
const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
|
|
74412
74733
|
if (!res) {
|
|
74413
74734
|
continue;
|
|
74414
74735
|
}
|
|
@@ -74694,6 +75015,30 @@ function createWorksheets(data, construct) {
|
|
|
74694
75015
|
`;
|
|
74695
75016
|
files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
|
|
74696
75017
|
}
|
|
75018
|
+
const sheetMetadataXml = escapeXml /*xml*/ `
|
|
75019
|
+
<metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
|
|
75020
|
+
<metadataTypes count="1">
|
|
75021
|
+
<metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
|
|
75022
|
+
pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
|
|
75023
|
+
clearComments="1" assign="1" coerce="1" cellMeta="1" />
|
|
75024
|
+
</metadataTypes>
|
|
75025
|
+
<futureMetadata name="XLDAPR" count="1">
|
|
75026
|
+
<bk>
|
|
75027
|
+
<extLst>
|
|
75028
|
+
<ext uri="{${ARRAY_FORMULA_URI}}">
|
|
75029
|
+
<xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
|
|
75030
|
+
</ext>
|
|
75031
|
+
</extLst>
|
|
75032
|
+
</bk>
|
|
75033
|
+
</futureMetadata>
|
|
75034
|
+
<cellMetadata count="1">
|
|
75035
|
+
<bk>
|
|
75036
|
+
<rc t="1" v="0" />
|
|
75037
|
+
</bk>
|
|
75038
|
+
</cellMetadata>
|
|
75039
|
+
</metadata>
|
|
75040
|
+
`;
|
|
75041
|
+
files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
|
|
74697
75042
|
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74698
75043
|
type: XLSX_RELATION_TYPE.sharedStrings,
|
|
74699
75044
|
target: "sharedStrings.xml",
|
|
@@ -74702,6 +75047,10 @@ function createWorksheets(data, construct) {
|
|
|
74702
75047
|
type: XLSX_RELATION_TYPE.styles,
|
|
74703
75048
|
target: "styles.xml",
|
|
74704
75049
|
});
|
|
75050
|
+
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
75051
|
+
type: XLSX_RELATION_TYPE.metadata,
|
|
75052
|
+
target: "metadata.xml",
|
|
75053
|
+
});
|
|
74705
75054
|
return files;
|
|
74706
75055
|
}
|
|
74707
75056
|
/**
|
|
@@ -75663,6 +76012,6 @@ exports.tokenColors = tokenColors;
|
|
|
75663
76012
|
exports.tokenize = tokenize;
|
|
75664
76013
|
|
|
75665
76014
|
|
|
75666
|
-
__info__.version = "18.2.
|
|
75667
|
-
__info__.date = "2025-
|
|
75668
|
-
__info__.hash = "
|
|
76015
|
+
__info__.version = "18.2.3";
|
|
76016
|
+
__info__.date = "2025-03-12T15:32:36.274Z";
|
|
76017
|
+
__info__.hash = "81b0e08";
|