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