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