@odoo/o-spreadsheet 19.5.0-alpha.0 → 19.5.0-alpha.2

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.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.5.0-alpha.0
6
- * @date 2026-06-29T09:01:26.331Z
7
- * @hash 2437c0b
5
+ * @version 19.5.0-alpha.2
6
+ * @date 2026-07-13T06:26:20.354Z
7
+ * @hash a4d2f90
8
8
  */
9
9
 
10
10
  (function(exports, _odoo_owl) {
@@ -564,6 +564,7 @@ _odoo_owl = __toESM(_odoo_owl, 1);
564
564
  };
565
565
  const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign;
566
566
  const DEFAULT_WRAPPING_MODE = DEFAULT_STYLE.wrapping;
567
+ const DEFAULT_TEXT_HIGHLIGHT_PERCENT = .25;
567
568
  const DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize;
568
569
  const DEFAULT_FONT = "'Roboto', arial, 'Liberation Sans'";
569
570
  const DEFAULT_BORDER_DESC = {
@@ -2601,7 +2602,7 @@ stores.inject(MyMetaStore, storeInstance);
2601
2602
  *
2602
2603
  * The end goal is to eliminate all compatibility shims.
2603
2604
  */
2604
- const { __ODOO_COMPATIBILITY_LAYER_ADDED__, blockDom: blockDom$1, config, onMounted: onMounted$24, onPatched: onPatched$5, onWillUnmount: onWillUnmount$18, App: OwlApp, Component: OwlComponent, EnvPlugin: OwlEnvPlugin, useChildEnv: OwlUseChildEnv, useChildSubEnv: OwlUseChildSubEnv, useComponent: OwlUseComponent, useEnv: OwlUseEnv, useExternalListener: OwlUseExternalListener, useLayoutEffect: OwlUseLayoutEffect, useSubEnv: OwlUseSubEnv, Plugin, plugin, props: props$166, providePlugins, useScope, xml: xml$4 } = _odoo_owl;
2605
+ const { __ODOO_COMPATIBILITY_LAYER_ADDED__, blockDom: blockDom$1, config, onMounted: onMounted$24, onPatched: onPatched$5, onWillUnmount: onWillUnmount$18, App: OwlApp, Component: OwlComponent, EnvPlugin: OwlEnvPlugin, useChildEnv: OwlUseChildEnv, useChildSubEnv: OwlUseChildSubEnv, useComponent: OwlUseComponent, useEnv: OwlUseEnv, useExternalListener: OwlUseExternalListener, useLayoutEffect: OwlUseLayoutEffect, useSubEnv: OwlUseSubEnv, Plugin, plugin, props: props$167, providePlugins, useScope, xml: xml$4 } = _odoo_owl;
2605
2606
  const isOdooCompatLoaded = __ODOO_COMPATIBILITY_LAYER_ADDED__ === true;
2606
2607
  var _Component = class extends OwlComponent {
2607
2608
  static template = "";
@@ -2731,7 +2732,7 @@ stores.inject(MyMetaStore, storeInstance);
2731
2732
  static template = xml$4`<t t-call-slot="default"/>`;
2732
2733
  constructor(node) {
2733
2734
  super(node);
2734
- this.props = props$166();
2735
+ this.props = props$167();
2735
2736
  this.__owl__ = node;
2736
2737
  }
2737
2738
  setup() {
@@ -8407,534 +8408,6 @@ set(value) {
8407
8408
  return cell;
8408
8409
  }
8409
8410
 
8410
- //#endregion
8411
- //#region src/helpers/text_helper.ts
8412
- function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
8413
- return numberOfLines * (textLineHeight + 4) - 4;
8414
- }
8415
- function getCanvas(width = 100, height = 100) {
8416
- return new OffscreenCanvas(width, height).getContext("2d");
8417
- }
8418
- /**
8419
- * Get the default height of the cell given its style.
8420
- */
8421
- function getDefaultCellHeight(ctx, cell, locale, colSize) {
8422
- if (!cell || !cell.isFormula && !cell.content) return 23;
8423
- let content = "";
8424
- try {
8425
- if (!cell.isFormula) {
8426
- const localeFormat = {
8427
- format: cell.format,
8428
- locale
8429
- };
8430
- content = formatValue(parseLiteral(cell.content, locale), localeFormat);
8431
- }
8432
- } catch {
8433
- content = CellErrorType.GenericError;
8434
- }
8435
- return getCellContentHeight(ctx, content, cell.style, colSize);
8436
- }
8437
- function getCellContentHeight(ctx, content, style, colSize) {
8438
- return computeMultilineTextSize(ctx, splitTextToWidth(ctx, content, style, style?.wrapping === "wrap" ? colSize - 2 * 4 : void 0), style).height + 2 * 3;
8439
- }
8440
- function getDefaultContextFont(fontSize, bold = false, italic = false) {
8441
- return `${italic ? "italic" : ""} ${bold ? "bold" : ""} ${fontSize}px ${DEFAULT_FONT}`;
8442
- }
8443
- function computeMultilineTextSize(context, textLines, style = {}, fontUnit = "pt") {
8444
- if (!textLines.length) return {
8445
- width: 0,
8446
- height: 0
8447
- };
8448
- const font = computeTextFont(style, fontUnit);
8449
- const sizes = textLines.map((line) => computeCachedTextDimension(context, line, font));
8450
- const height = computeTextLinesHeight(sizes[0].height, textLines.length);
8451
- const width = Math.max(...sizes.map((size) => size.width));
8452
- if (!style.rotation) return {
8453
- height,
8454
- width
8455
- };
8456
- const cos = Math.abs(Math.cos(style.rotation));
8457
- const sin = Math.abs(Math.sin(style.rotation));
8458
- return {
8459
- width: width * cos + height * sin,
8460
- height: sin * width + cos * height
8461
- };
8462
- }
8463
- function computeTextWidth(context, text, style = {}, fontUnit = "pt") {
8464
- return computeCachedTextWidth(context, text, computeTextFont(style, fontUnit), style.rotation);
8465
- }
8466
- function computeCachedTextWidth(context, text, font, rotation) {
8467
- const size = computeCachedTextDimension(context, text, font);
8468
- if (!rotation) return size.width;
8469
- const cos = Math.abs(Math.cos(rotation));
8470
- const sin = Math.abs(Math.sin(rotation));
8471
- return size.width * cos + size.height * sin;
8472
- }
8473
- const textDimensionsCache = {};
8474
- function computeTextDimension(context, text, style, fontUnit = "pt") {
8475
- const size = computeCachedTextDimension(context, text, computeTextFont(style, fontUnit));
8476
- if (!style.rotation) return size;
8477
- const cos = Math.abs(Math.cos(style.rotation));
8478
- const sin = Math.abs(Math.sin(style.rotation));
8479
- return {
8480
- width: size.width * cos + size.height * sin,
8481
- height: size.height * cos + size.width * sin
8482
- };
8483
- }
8484
- function computeCachedTextDimension(context, text, font) {
8485
- if (!textDimensionsCache[font]) textDimensionsCache[font] = {};
8486
- if (textDimensionsCache[font][text] === void 0) {
8487
- context.save();
8488
- context.font = font;
8489
- const measure = context.measureText(text);
8490
- context.restore();
8491
- const width = measure.width;
8492
- const height = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
8493
- textDimensionsCache[font][text] = {
8494
- width,
8495
- height
8496
- };
8497
- }
8498
- return textDimensionsCache[font][text];
8499
- }
8500
- function fontSizeInPixels(fontSize) {
8501
- return Math.round(fontSize * 96 / 72);
8502
- }
8503
- function computeTextFont(style, fontUnit = "pt") {
8504
- return `${style.italic ? "italic " : ""}${style.bold ? "bold" : "400"} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
8505
- }
8506
- function computeTextFontSizeInPixels(style) {
8507
- return fontSizeInPixels(style?.fontSize || DEFAULT_FONT_SIZE);
8508
- }
8509
- function splitWordToSpecificWidth(ctx, word, width, style) {
8510
- if (computeTextWidth(ctx, word, style) <= width) return [word];
8511
- const splitWord = [];
8512
- let wordPart = "";
8513
- for (const l of word) if (computeTextWidth(ctx, wordPart + l, style) > width) {
8514
- splitWord.push(wordPart);
8515
- wordPart = l;
8516
- } else wordPart += l;
8517
- splitWord.push(wordPart);
8518
- return splitWord;
8519
- }
8520
- /**
8521
- * Return the given text, split in multiple lines if needed. The text will be split in multiple
8522
- * line if it contains NEWLINE characters, or if it's longer than the given width.
8523
- */
8524
- function splitTextToWidth(ctx, text, style, width) {
8525
- if (!style) style = {};
8526
- if (isMarkdownLink(text)) text = parseMarkdownLink(text).label;
8527
- const brokenText = [];
8528
- const lines = text.includes("\n") ? text.split("\n") : [text];
8529
- for (const line of lines) {
8530
- const words = line.includes(" ") ? line.split(" ") : [line];
8531
- if (!width) {
8532
- brokenText.push(line);
8533
- continue;
8534
- }
8535
- let textLine = "";
8536
- let availableWidth = width;
8537
- for (const word of words) {
8538
- const splitWord = splitWordToSpecificWidth(ctx, word, width, style);
8539
- const lastPart = splitWord.pop();
8540
- const lastPartWidth = computeTextWidth(ctx, lastPart, style);
8541
- if (splitWord.length) {
8542
- if (textLine !== "") {
8543
- brokenText.push(textLine);
8544
- textLine = "";
8545
- availableWidth = width;
8546
- }
8547
- splitWord.forEach((wordPart) => {
8548
- brokenText.push(wordPart);
8549
- });
8550
- textLine = lastPart;
8551
- availableWidth = width - lastPartWidth;
8552
- } else {
8553
- const _word = textLine === "" ? lastPart : " " + lastPart;
8554
- const wordWidth = computeTextWidth(ctx, _word, style);
8555
- if (wordWidth <= availableWidth) {
8556
- textLine += _word;
8557
- availableWidth -= wordWidth;
8558
- } else {
8559
- brokenText.push(textLine);
8560
- textLine = lastPart;
8561
- availableWidth = width - lastPartWidth;
8562
- }
8563
- }
8564
- }
8565
- if (textLine !== "") brokenText.push(textLine);
8566
- }
8567
- return brokenText;
8568
- }
8569
- /**
8570
- * Return the font size that makes the width of a text match the given line width.
8571
- * Minimum font size is 1.
8572
- *
8573
- * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.
8574
- */
8575
- function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = .25) {
8576
- let minFontSize = 1;
8577
- if (getTextWidth(minFontSize) > lineWidth) return minFontSize;
8578
- if (getTextWidth(maxFontSize) < lineWidth) return maxFontSize;
8579
- let fontSize = (minFontSize + maxFontSize) / 2;
8580
- let currentTextWidth = getTextWidth(fontSize);
8581
- let iterations = 0;
8582
- while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {
8583
- if (currentTextWidth >= lineWidth) maxFontSize = (minFontSize + maxFontSize) / 2;
8584
- else minFontSize = (minFontSize + maxFontSize) / 2;
8585
- fontSize = (minFontSize + maxFontSize) / 2;
8586
- currentTextWidth = getTextWidth(fontSize);
8587
- iterations++;
8588
- }
8589
- return fontSize;
8590
- }
8591
- /** Transform a string to lowercase and removes whitespace from both ends of the string*/
8592
- function toTrimmedLowerCase(str) {
8593
- return str ? str.toLowerCase().trim() : "";
8594
- }
8595
- /**
8596
- * Extract the fontSize from a context font string
8597
- * @param font The (context) font string to parse
8598
- * @returns The fontSize in pixels
8599
- */
8600
- const pxRegex = /([0-9\.]*)px/;
8601
- function getContextFontSize(font) {
8602
- return Number(font.match(pxRegex)?.[1]);
8603
- }
8604
- function clipTextWithEllipsis(ctx, text, maxWidth) {
8605
- let width = computeCachedTextWidth(ctx, text, ctx.font);
8606
- if (width <= maxWidth) return text;
8607
- const ellipsis = "…";
8608
- const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis, ctx.font);
8609
- if (width <= ellipsisWidth) return text;
8610
- let len = text.length;
8611
- while (width >= maxWidth - ellipsisWidth && len-- > 0) {
8612
- text = text.substring(0, len);
8613
- width = computeCachedTextWidth(ctx, text, ctx.font);
8614
- }
8615
- return text + ellipsis;
8616
- }
8617
- function drawDecoratedText(context, text, position, underline = false, strikethrough = false, strokeWidth = getContextFontSize(context.font) / 10) {
8618
- context.fillText(text, position.x, position.y);
8619
- if (!underline && !strikethrough) return;
8620
- const measure = context.measureText(text);
8621
- const textWidth = measure.width;
8622
- const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
8623
- const boxHeight = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
8624
- let { x, y } = position;
8625
- let strikeY = y, underlineY = y;
8626
- switch (context.textAlign) {
8627
- case "center":
8628
- x -= textWidth / 2;
8629
- break;
8630
- case "right":
8631
- x -= textWidth;
8632
- break;
8633
- }
8634
- switch (context.textBaseline) {
8635
- case "top":
8636
- underlineY += boxHeight - 2 * strokeWidth;
8637
- strikeY += boxHeight / 2 - strokeWidth;
8638
- break;
8639
- case "middle":
8640
- underlineY += boxHeight / 2 - strokeWidth;
8641
- break;
8642
- case "alphabetic":
8643
- underlineY += 2 * strokeWidth;
8644
- strikeY -= 3 * strokeWidth;
8645
- break;
8646
- case "bottom":
8647
- underlineY = y;
8648
- strikeY -= textHeight / 2 - strokeWidth / 2;
8649
- break;
8650
- }
8651
- if (underline) {
8652
- context.lineWidth = strokeWidth;
8653
- context.strokeStyle = context.fillStyle;
8654
- context.beginPath();
8655
- context.moveTo(x, underlineY);
8656
- context.lineTo(x + textWidth, underlineY);
8657
- context.stroke();
8658
- }
8659
- if (strikethrough) {
8660
- context.lineWidth = strokeWidth;
8661
- context.strokeStyle = context.fillStyle;
8662
- context.beginPath();
8663
- context.moveTo(x, strikeY);
8664
- context.lineTo(x + textWidth, strikeY);
8665
- context.stroke();
8666
- }
8667
- }
8668
- function sliceTextToFitWidth(context, width, text, style, fontUnit = "pt") {
8669
- if (computeTextWidth(context, text, style, fontUnit) <= width) return text;
8670
- const ellipsis = "...";
8671
- const ellipsisWidth = computeTextWidth(context, ellipsis, style, fontUnit);
8672
- if (ellipsisWidth >= width) return "";
8673
- let lowerBoundLen = 1;
8674
- let upperBoundLen = text.length;
8675
- let currentWidth;
8676
- while (lowerBoundLen <= upperBoundLen) {
8677
- const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2);
8678
- currentWidth = computeTextWidth(context, text.slice(0, currentLen), style, fontUnit);
8679
- if (currentWidth + ellipsisWidth > width) upperBoundLen = currentLen - 1;
8680
- else lowerBoundLen = currentLen + 1;
8681
- }
8682
- const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
8683
- return slicedText ? slicedText + ellipsis : "";
8684
- }
8685
- /**
8686
- * Return the position to draw text on a rotated canvas to ensure that the rotated text alignment correspond
8687
- * with to original's text vertical and horizontal alignment.
8688
- */
8689
- function computeRotationPosition(rect, style) {
8690
- if (!style.rotation || style.rotation % (Math.PI * 2) === 0) return rect;
8691
- let { x, y } = rect;
8692
- const cos = Math.cos(-style.rotation);
8693
- const sin = Math.sin(-style.rotation);
8694
- const width = rect.textWidth - 2 * 4;
8695
- const height = rect.textHeight;
8696
- const center = style.align === "center";
8697
- const rotateTowardCellCenter = style.align === "left" === sin < 0;
8698
- const sh = sin * height;
8699
- const sw = Math.abs(sin * width);
8700
- const ch = cos * height;
8701
- if (style.verticalAlign === "top") if (center) {
8702
- y += sw / 2;
8703
- x -= sh / 2;
8704
- } else if (rotateTowardCellCenter) x -= sh;
8705
- else y += sw;
8706
- else if (!style.verticalAlign || style.verticalAlign === "bottom") {
8707
- y += height - ch;
8708
- if (center) {
8709
- y -= sw / 2;
8710
- x -= sh / 2;
8711
- } else if (rotateTowardCellCenter) {
8712
- x -= sh;
8713
- y -= sw;
8714
- }
8715
- } else if (center) {
8716
- x -= sh / 2;
8717
- y -= height / 2;
8718
- if (rotateTowardCellCenter) y += sh;
8719
- else y -= sh;
8720
- } else if (rotateTowardCellCenter) {
8721
- x -= sh;
8722
- y -= sw / 2;
8723
- } else y += sw / 2 + ch / 4;
8724
- return {
8725
- x: cos * x - sin * y,
8726
- y: cos * y + sin * x
8727
- };
8728
- }
8729
-
8730
- //#endregion
8731
- //#region src/components/figures/chart/chartJs/chartjs_colorscale_plugin.ts
8732
- /** This is a chartJS plugin that will draw the heatmap colorscale at the chart legend position */
8733
- const chartColorScalePlugin = {
8734
- id: "chartColorScalePlugin",
8735
- afterDatasetsDraw(chart, args, options) {
8736
- if (!options.position || options.position === "none" || !options.colorScale.length) return;
8737
- const ctx = chart.ctx;
8738
- ctx.save();
8739
- ctx.textAlign = "center";
8740
- ctx.textBaseline = "middle";
8741
- ctx.miterLimit = 1;
8742
- const gradientHeight = (chart.chartArea.bottom - chart.chartArea.top) / 2;
8743
- const gradientWidth = 10;
8744
- const gradientX = options.position === "left" ? 20 : ctx.canvas.width - 70;
8745
- const gradientY = chart.chartArea.top;
8746
- const gradient = ctx.createLinearGradient(0, gradientY + gradientHeight, 0, gradientY);
8747
- const step = 1 / (options.colorScale.length - 1);
8748
- options.colorScale.forEach((color, index) => {
8749
- gradient.addColorStop(index * step, color);
8750
- });
8751
- ctx.fillStyle = gradient;
8752
- ctx.fillRect(gradientX, gradientY, gradientWidth, gradientHeight);
8753
- ctx.fillStyle = options.fontColor ?? "black";
8754
- ctx.font = getDefaultContextFont(12);
8755
- ctx.textAlign = "left";
8756
- let minValue = Math.round(options.minValue * 100) / 100;
8757
- let maxValue = Math.round(options.maxValue * 100) / 100;
8758
- if (options.minValue === options.maxValue) {
8759
- minValue -= 1;
8760
- maxValue += 1;
8761
- }
8762
- const formattedMaxValue = humanizeNumber({
8763
- value: maxValue,
8764
- format: void 0
8765
- }, options.locale);
8766
- const formattedMinValue = humanizeNumber({
8767
- value: minValue,
8768
- format: void 0
8769
- }, options.locale);
8770
- ctx.fillText(formattedMinValue, gradientX + gradientWidth + 5, gradientY + gradientHeight - 6);
8771
- ctx.fillText(formattedMaxValue, gradientX + gradientWidth + 5, gradientY + 6);
8772
- ctx.restore();
8773
- }
8774
- };
8775
-
8776
- //#endregion
8777
- //#region src/components/figures/chart/chartJs/chartjs_funnel_chart.ts
8778
- function getFunnelChartController() {
8779
- if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
8780
- return class FunnelChartController extends globalThis.Chart.BarController {
8781
- static id = "funnel";
8782
- static defaults = {
8783
- ...globalThis.Chart?.BarController.defaults,
8784
- dataElementType: "funnel",
8785
- animation: { duration: (ctx) => {
8786
- if (ctx.type !== "data") return 1e3;
8787
- return 1e3 * (ctx.raw[1] / Math.max(...ctx.dataset.data.map((data) => data[1])));
8788
- } }
8789
- };
8790
- /** Called at each chart render to update the elements of the chart (FunnelChartElement) with the updated data */
8791
- updateElements(rects, start, count, mode) {
8792
- super.updateElements(rects, start, count, mode);
8793
- for (let i = start; i < start + count; i++) {
8794
- const rect = rects[i];
8795
- this.updateElement(rect, i, { nextElement: rects[i + 1] }, mode);
8796
- }
8797
- }
8798
- };
8799
- }
8800
- function getFunnelChartElement() {
8801
- if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
8802
- /**
8803
- * Similar to a bar chart element, but it's a trapezoid rather than a rectangle. The top is of width
8804
- * `width`, and the bottom is of width `nextElementWidth`.
8805
- */
8806
- return class FunnelChartElement extends globalThis.Chart.BarElement {
8807
- static id = "funnel";
8808
- /** Overwrite this to draw a trapezoid rather then a rectangle */
8809
- draw(ctx) {
8810
- ctx.save();
8811
- const { x, y, height, nextElement, base, options } = this.getProps([
8812
- "x",
8813
- "y",
8814
- "width",
8815
- "height",
8816
- "nextElement",
8817
- "base",
8818
- "options"
8819
- ]);
8820
- const width = getElementWidth(this);
8821
- const offset = (width - (nextElement ? getElementWidth(nextElement) : 0)) / 2;
8822
- const startX = Math.min(x, base);
8823
- const startY = y - height / 2;
8824
- ctx.fillStyle = options.backgroundColor;
8825
- ctx.beginPath();
8826
- ctx.moveTo(startX, startY);
8827
- ctx.lineTo(startX + width, startY);
8828
- ctx.lineTo(startX + width - offset, startY + height);
8829
- ctx.lineTo(startX + offset, startY + height);
8830
- ctx.closePath();
8831
- ctx.fill();
8832
- if (options.borderWidth) {
8833
- ctx.strokeStyle = options.borderColor;
8834
- ctx.lineWidth = options.borderWidth;
8835
- ctx.stroke();
8836
- }
8837
- ctx.restore();
8838
- }
8839
- /** Check if the mouse is inside the trapezoid */
8840
- inRange(mouseX, mouseY) {
8841
- const { x, y, height, nextElement, base } = this.getProps([
8842
- "x",
8843
- "y",
8844
- "width",
8845
- "height",
8846
- "nextElement",
8847
- "base",
8848
- "options"
8849
- ]);
8850
- const width = getElementWidth(this);
8851
- const nextElementWidth = nextElement ? getElementWidth(nextElement) : 0;
8852
- const startX = Math.min(x, base);
8853
- const startY = y - height / 2;
8854
- if (mouseY < startY || mouseY > startY + height) return false;
8855
- const offset = (width - nextElementWidth) / 2;
8856
- const left = startX + offset * (mouseY - startY) / height;
8857
- const right = startX + width - offset * (mouseY - startY) / height;
8858
- if (mouseX < left || mouseX > right) return false;
8859
- return true;
8860
- }
8861
- };
8862
- }
8863
- /**
8864
- * Get an element width.
8865
- *
8866
- * The property width is undefined during animations, we need to compute it manually.
8867
- */
8868
- function getElementWidth(element) {
8869
- const { x, base } = element.getProps(["x", "base"]);
8870
- return Math.max(x, base) - Math.min(x, base);
8871
- }
8872
- /**
8873
- * Position the tooltip inside the trapezoid.
8874
- * The default position for tooltips of bar elements is at the end of rectangle, which is not ideal for trapezoids.
8875
- */
8876
- const funnelTooltipPositioner = function(elements) {
8877
- if (!elements.length) return {
8878
- x: 0,
8879
- y: 0
8880
- };
8881
- const { x, y, base, width, height } = elements[0].element.getProps([
8882
- "x",
8883
- "y",
8884
- "width",
8885
- "height",
8886
- "base"
8887
- ]);
8888
- const startX = Math.min(x, base);
8889
- const startY = y - height / 2;
8890
- return {
8891
- x: startX + width * 2 / 3,
8892
- y: startY + height / 2
8893
- };
8894
- };
8895
-
8896
- //#endregion
8897
- //#region src/components/figures/chart/chartJs/chartjs_minor_grid_plugin.ts
8898
- const chartMinorGridPlugin = {
8899
- id: "o-spreadsheet-minor-gridlines",
8900
- beforeDatasetsDraw(chart) {
8901
- const ctx = chart.ctx;
8902
- const chartArea = chart.chartArea;
8903
- if (!chartArea) return;
8904
- for (const scaleId in chart.scales) {
8905
- const scale = chart.scales[scaleId];
8906
- const options = scale.options;
8907
- const minor = options?.grid?.minor;
8908
- if (!minor?.display) continue;
8909
- const showMajorGrid = options?.grid?.display;
8910
- const ticks = scale.ticks;
8911
- if (!ticks || ticks.length < 2) continue;
8912
- ctx.save();
8913
- ctx.lineWidth = 1;
8914
- ctx.strokeStyle = minor.color ?? options?.grid?.color ?? "#e6e6e6";
8915
- for (let i = 0; i < ticks.length - 1; i++) {
8916
- const start = scale.getPixelForTick(i);
8917
- const end = scale.getPixelForTick(i + 1);
8918
- if (!isFinite(start) || !isFinite(end)) continue;
8919
- for (let j = showMajorGrid ? 1 : 0; j < 4; j++) {
8920
- const ratio = j / 4;
8921
- const position = Math.round(start + (end - start) * ratio) + .5;
8922
- ctx.beginPath();
8923
- if (scale.isHorizontal()) {
8924
- ctx.moveTo(position, chartArea.top);
8925
- ctx.lineTo(position, chartArea.bottom);
8926
- } else {
8927
- ctx.moveTo(chartArea.left, position);
8928
- ctx.lineTo(chartArea.right, position);
8929
- }
8930
- ctx.stroke();
8931
- }
8932
- }
8933
- ctx.restore();
8934
- }
8935
- }
8936
- };
8937
-
8938
8411
  //#endregion
8939
8412
  //#region src/helpers/color.ts
8940
8413
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
@@ -9499,142 +8972,675 @@ set(value) {
9499
8972
  function getNthColor(index, palette) {
9500
8973
  return palette[index % palette.length];
9501
8974
  }
9502
- function getColorsPalette(quantity) {
9503
- if (quantity <= 6) return COLORS_SM;
9504
- else if (quantity <= 12) return COLORS_MD;
9505
- else if (quantity <= 24) return COLORS_LG;
9506
- else return COLORS_XL;
8975
+ function getColorsPalette(quantity) {
8976
+ if (quantity <= 6) return COLORS_SM;
8977
+ else if (quantity <= 12) return COLORS_MD;
8978
+ else if (quantity <= 24) return COLORS_LG;
8979
+ else return COLORS_XL;
8980
+ }
8981
+ function getAlternatingColorsPalette(quantity) {
8982
+ if (quantity <= 6) return COLORS_SM;
8983
+ else if (quantity <= 12) return ALTERNATING_COLORS_MD;
8984
+ else if (quantity <= 24) return ALTERNATING_COLORS_LG;
8985
+ else return ALTERNATING_COLORS_XL;
8986
+ }
8987
+ var ColorGenerator = class {
8988
+ preferredColors;
8989
+ currentColorIndex = 0;
8990
+ palette;
8991
+ constructor(paletteSize, preferredColors = []) {
8992
+ this.preferredColors = preferredColors;
8993
+ this.palette = getColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
8994
+ }
8995
+ next() {
8996
+ return this.preferredColors?.[this.currentColorIndex] ? this.preferredColors[this.currentColorIndex++] : getNthColor(this.currentColorIndex++, this.palette);
8997
+ }
8998
+ };
8999
+ var AlternatingColorGenerator = class extends ColorGenerator {
9000
+ constructor(paletteSize, preferredColors = []) {
9001
+ super(paletteSize, preferredColors);
9002
+ this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
9003
+ }
9004
+ };
9005
+ var AlternatingColorMap = class {
9006
+ availableColors;
9007
+ colors = {};
9008
+ constructor(paletteSize = 12) {
9009
+ this.availableColors = new AlternatingColorGenerator(paletteSize);
9010
+ }
9011
+ get(id) {
9012
+ if (!this.colors[id]) this.colors[id] = this.availableColors.next();
9013
+ return this.colors[id];
9014
+ }
9015
+ };
9016
+ const COLORSCHEMES = {
9017
+ greys: [
9018
+ "#ffffff",
9019
+ "#808080",
9020
+ "#000000"
9021
+ ],
9022
+ blues: [
9023
+ "#f7fbff",
9024
+ "#6aaed6",
9025
+ "#08306b"
9026
+ ],
9027
+ reds: [
9028
+ "#fff5f0",
9029
+ "#fb694a",
9030
+ "#67000d"
9031
+ ],
9032
+ greens: [
9033
+ "#f7fcf5",
9034
+ "#73c476",
9035
+ "#00441b"
9036
+ ],
9037
+ oranges: [
9038
+ "#fff5eb",
9039
+ "#fd8c3b",
9040
+ "#7f2704"
9041
+ ],
9042
+ purples: [
9043
+ "#fcfbfd",
9044
+ "#9e9ac8",
9045
+ "#3f007d"
9046
+ ],
9047
+ viridis: [
9048
+ "#440154",
9049
+ "#21918c",
9050
+ "#fde725"
9051
+ ],
9052
+ cividis: [
9053
+ "#00224e",
9054
+ "#7d7c78",
9055
+ "#fee838"
9056
+ ],
9057
+ rainbow: [
9058
+ "#B41DB4",
9059
+ "#FFFF00",
9060
+ "#00FFFF"
9061
+ ]
9062
+ };
9063
+ const COLORSCALES = Object.keys(COLORSCHEMES);
9064
+ /**
9065
+ * Returns a function that maps a value to a color using a color scale defined by the given
9066
+ * color/threshold values pairs.
9067
+ */
9068
+ function getColorScale(colorScalePoints) {
9069
+ if (colorScalePoints.length < 2) throw new Error("Color scale must have at least 2 points");
9070
+ const sortedColorScalePoints = [...colorScalePoints.sort((a, b) => a.value - b.value)];
9071
+ const thresholds = [];
9072
+ for (let i = 1; i < sortedColorScalePoints.length; i++) {
9073
+ const minColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i - 1].color).a;
9074
+ const maxColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i].color).a;
9075
+ const minColor = colorToNumber(sortedColorScalePoints[i - 1].color);
9076
+ const maxColor = colorToNumber(sortedColorScalePoints[i].color);
9077
+ thresholds.push({
9078
+ min: sortedColorScalePoints[i - 1].value,
9079
+ max: sortedColorScalePoints[i].value,
9080
+ minColor,
9081
+ maxColor,
9082
+ minColorAlpha,
9083
+ maxColorAlpha,
9084
+ colorDiff: computeColorDiffUnits(sortedColorScalePoints[i - 1].value, sortedColorScalePoints[i].value, minColor, maxColor)
9085
+ });
9086
+ }
9087
+ return (value) => {
9088
+ if (value < thresholds[0].min) return colorNumberToHex(thresholds[0].minColor, thresholds[0].minColorAlpha);
9089
+ for (const threshold of thresholds) if (value >= threshold.min && value <= threshold.max) return colorNumberToHex(colorCell(value, threshold.min, threshold.minColor, threshold.colorDiff), threshold.maxColorAlpha);
9090
+ return colorNumberToHex(thresholds[thresholds.length - 1].maxColor, thresholds[thresholds.length - 1].maxColorAlpha);
9091
+ };
9092
+ }
9093
+ function computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
9094
+ const deltaValue = maxValue - minValue;
9095
+ const deltaColorR = (minColor >> 16) % 256 - (maxColor >> 16) % 256;
9096
+ const deltaColorG = (minColor >> 8) % 256 - (maxColor >> 8) % 256;
9097
+ const deltaColorB = minColor % 256 - maxColor % 256;
9098
+ return [
9099
+ deltaColorR / deltaValue,
9100
+ deltaColorG / deltaValue,
9101
+ deltaColorB / deltaValue
9102
+ ];
9103
+ }
9104
+ function colorCell(value, minValue, minColor, colorDiffUnit) {
9105
+ const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
9106
+ const r = Math.round((minColor >> 16) % 256 - colorDiffUnitR * (value - minValue));
9107
+ const g = Math.round((minColor >> 8) % 256 - colorDiffUnitG * (value - minValue));
9108
+ const b = Math.round(minColor % 256 - colorDiffUnitB * (value - minValue));
9109
+ return r << 16 | g << 8 | b;
9110
+ }
9111
+
9112
+ //#endregion
9113
+ //#region src/helpers/text_helper.ts
9114
+ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
9115
+ return numberOfLines * (textLineHeight + 4) - 4;
9116
+ }
9117
+ function getCanvas(width = 100, height = 100) {
9118
+ return new OffscreenCanvas(width, height).getContext("2d");
9119
+ }
9120
+ /**
9121
+ * Get the default height of the cell given its style.
9122
+ */
9123
+ function getDefaultCellHeight(ctx, cell, locale, colSize) {
9124
+ if (!cell || !cell.isFormula && !cell.content) return 23;
9125
+ let content = "";
9126
+ try {
9127
+ if (!cell.isFormula) {
9128
+ const localeFormat = {
9129
+ format: cell.format,
9130
+ locale
9131
+ };
9132
+ content = formatValue(parseLiteral(cell.content, locale), localeFormat);
9133
+ }
9134
+ } catch {
9135
+ content = CellErrorType.GenericError;
9136
+ }
9137
+ return getCellContentHeight(ctx, content, cell.style, colSize);
9138
+ }
9139
+ function getCellContentHeight(ctx, content, style, colSize) {
9140
+ return computeMultilineTextSize(ctx, splitTextToWidth(ctx, content, style, style?.wrapping === "wrap" ? colSize - 2 * 4 : void 0), style).height + 2 * 3;
9141
+ }
9142
+ function getDefaultContextFont(fontSize, bold = false, italic = false) {
9143
+ return `${italic ? "italic" : ""} ${bold ? "bold" : ""} ${fontSize}px ${DEFAULT_FONT}`;
9144
+ }
9145
+ function computeMultilineTextSize(context, textLines, style = {}, fontUnit = "pt") {
9146
+ if (!textLines.length) return {
9147
+ width: 0,
9148
+ height: 0
9149
+ };
9150
+ const font = computeTextFont(style, fontUnit);
9151
+ const sizes = textLines.map((line) => computeCachedTextDimension(context, line, font));
9152
+ const height = computeTextLinesHeight(sizes[0].height, textLines.length);
9153
+ const width = Math.max(...sizes.map((size) => size.width));
9154
+ if (!style.rotation) return {
9155
+ height,
9156
+ width
9157
+ };
9158
+ const cos = Math.abs(Math.cos(style.rotation));
9159
+ const sin = Math.abs(Math.sin(style.rotation));
9160
+ return {
9161
+ width: width * cos + height * sin,
9162
+ height: sin * width + cos * height
9163
+ };
9164
+ }
9165
+ function computeTextWidth(context, text, style = {}, fontUnit = "pt") {
9166
+ return computeCachedTextWidth(context, text, computeTextFont(style, fontUnit), style.rotation);
9167
+ }
9168
+ function computeCachedTextWidth(context, text, font, rotation) {
9169
+ const size = computeCachedTextDimension(context, text, font);
9170
+ if (!rotation) return size.width;
9171
+ const cos = Math.abs(Math.cos(rotation));
9172
+ const sin = Math.abs(Math.sin(rotation));
9173
+ return size.width * cos + size.height * sin;
9174
+ }
9175
+ const textDimensionsCache = {};
9176
+ function computeTextDimension(context, text, style, fontUnit = "pt") {
9177
+ const size = computeCachedTextDimension(context, text, computeTextFont(style, fontUnit));
9178
+ if (!style.rotation) return size;
9179
+ const cos = Math.abs(Math.cos(style.rotation));
9180
+ const sin = Math.abs(Math.sin(style.rotation));
9181
+ return {
9182
+ width: size.width * cos + size.height * sin,
9183
+ height: size.height * cos + size.width * sin
9184
+ };
9185
+ }
9186
+ function computeCachedTextDimension(context, text, font) {
9187
+ if (!textDimensionsCache[font]) textDimensionsCache[font] = {};
9188
+ if (textDimensionsCache[font][text] === void 0) {
9189
+ context.save();
9190
+ context.font = font;
9191
+ const measure = context.measureText(text);
9192
+ context.restore();
9193
+ const width = measure.width;
9194
+ const height = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
9195
+ textDimensionsCache[font][text] = {
9196
+ width,
9197
+ height
9198
+ };
9199
+ }
9200
+ return textDimensionsCache[font][text];
9201
+ }
9202
+ function fontSizeInPixels(fontSize) {
9203
+ return Math.round(fontSize * 96 / 72);
9204
+ }
9205
+ function computeTextFont(style, fontUnit = "pt") {
9206
+ return `${style.italic ? "italic " : ""}${style.bold ? "bold" : "400"} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
9207
+ }
9208
+ function computeTextFontSizeInPixels(style) {
9209
+ return fontSizeInPixels(style?.fontSize || DEFAULT_FONT_SIZE);
9210
+ }
9211
+ function splitWordToSpecificWidth(ctx, word, width, style) {
9212
+ if (computeTextWidth(ctx, word, style) <= width) return [word];
9213
+ const splitWord = [];
9214
+ let wordPart = "";
9215
+ for (const l of word) if (computeTextWidth(ctx, wordPart + l, style) > width) {
9216
+ splitWord.push(wordPart);
9217
+ wordPart = l;
9218
+ } else wordPart += l;
9219
+ splitWord.push(wordPart);
9220
+ return splitWord;
9221
+ }
9222
+ /**
9223
+ * Return the given text, split in multiple lines if needed. The text will be split in multiple
9224
+ * line if it contains NEWLINE characters, or if it's longer than the given width.
9225
+ */
9226
+ function splitTextToWidth(ctx, text, style, width) {
9227
+ if (!style) style = {};
9228
+ if (isMarkdownLink(text)) text = parseMarkdownLink(text).label;
9229
+ const brokenText = [];
9230
+ const lines = text.includes("\n") ? text.split("\n") : [text];
9231
+ for (const line of lines) {
9232
+ const words = line.includes(" ") ? line.split(" ") : [line];
9233
+ if (!width) {
9234
+ brokenText.push(line);
9235
+ continue;
9236
+ }
9237
+ let textLine = "";
9238
+ let availableWidth = width;
9239
+ for (const word of words) {
9240
+ const splitWord = splitWordToSpecificWidth(ctx, word, width, style);
9241
+ const lastPart = splitWord.pop();
9242
+ const lastPartWidth = computeTextWidth(ctx, lastPart, style);
9243
+ if (splitWord.length) {
9244
+ if (textLine !== "") {
9245
+ brokenText.push(textLine);
9246
+ textLine = "";
9247
+ availableWidth = width;
9248
+ }
9249
+ splitWord.forEach((wordPart) => {
9250
+ brokenText.push(wordPart);
9251
+ });
9252
+ textLine = lastPart;
9253
+ availableWidth = width - lastPartWidth;
9254
+ } else {
9255
+ const _word = textLine === "" ? lastPart : " " + lastPart;
9256
+ const wordWidth = computeTextWidth(ctx, _word, style);
9257
+ if (wordWidth <= availableWidth) {
9258
+ textLine += _word;
9259
+ availableWidth -= wordWidth;
9260
+ } else {
9261
+ brokenText.push(textLine);
9262
+ textLine = lastPart;
9263
+ availableWidth = width - lastPartWidth;
9264
+ }
9265
+ }
9266
+ }
9267
+ if (textLine !== "") brokenText.push(textLine);
9268
+ }
9269
+ return brokenText;
9270
+ }
9271
+ /**
9272
+ * Return the font size that makes the width of a text match the given line width.
9273
+ * Minimum font size is 1.
9274
+ *
9275
+ * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.
9276
+ */
9277
+ function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = .25) {
9278
+ let minFontSize = 1;
9279
+ if (getTextWidth(minFontSize) > lineWidth) return minFontSize;
9280
+ if (getTextWidth(maxFontSize) < lineWidth) return maxFontSize;
9281
+ let fontSize = (minFontSize + maxFontSize) / 2;
9282
+ let currentTextWidth = getTextWidth(fontSize);
9283
+ let iterations = 0;
9284
+ while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {
9285
+ if (currentTextWidth >= lineWidth) maxFontSize = (minFontSize + maxFontSize) / 2;
9286
+ else minFontSize = (minFontSize + maxFontSize) / 2;
9287
+ fontSize = (minFontSize + maxFontSize) / 2;
9288
+ currentTextWidth = getTextWidth(fontSize);
9289
+ iterations++;
9290
+ }
9291
+ return fontSize;
9292
+ }
9293
+ /** Transform a string to lowercase and removes whitespace from both ends of the string*/
9294
+ function toTrimmedLowerCase(str) {
9295
+ return str ? str.toLowerCase().trim() : "";
9507
9296
  }
9508
- function getAlternatingColorsPalette(quantity) {
9509
- if (quantity <= 6) return COLORS_SM;
9510
- else if (quantity <= 12) return ALTERNATING_COLORS_MD;
9511
- else if (quantity <= 24) return ALTERNATING_COLORS_LG;
9512
- else return ALTERNATING_COLORS_XL;
9297
+ /**
9298
+ * Extract the fontSize from a context font string
9299
+ * @param font The (context) font string to parse
9300
+ * @returns The fontSize in pixels
9301
+ */
9302
+ const pxRegex = /([0-9\.]*)px/;
9303
+ function getContextFontSize(font) {
9304
+ return Number(font.match(pxRegex)?.[1]);
9513
9305
  }
9514
- var ColorGenerator = class {
9515
- preferredColors;
9516
- currentColorIndex = 0;
9517
- palette;
9518
- constructor(paletteSize, preferredColors = []) {
9519
- this.preferredColors = preferredColors;
9520
- this.palette = getColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
9306
+ function clipTextWithEllipsis(ctx, text, maxWidth) {
9307
+ let width = computeCachedTextWidth(ctx, text, ctx.font);
9308
+ if (width <= maxWidth) return text;
9309
+ const ellipsis = "…";
9310
+ const ellipsisWidth = computeCachedTextWidth(ctx, ellipsis, ctx.font);
9311
+ if (width <= ellipsisWidth) return text;
9312
+ let len = text.length;
9313
+ while (width >= maxWidth - ellipsisWidth && len-- > 0) {
9314
+ text = text.substring(0, len);
9315
+ width = computeCachedTextWidth(ctx, text, ctx.font);
9521
9316
  }
9522
- next() {
9523
- return this.preferredColors?.[this.currentColorIndex] ? this.preferredColors[this.currentColorIndex++] : getNthColor(this.currentColorIndex++, this.palette);
9317
+ return text + ellipsis;
9318
+ }
9319
+ function drawDecoratedText(context, text, position, underline = false, strikethrough = false, strokeWidth = getContextFontSize(context.font) / 10, highlightText = false) {
9320
+ if (highlightText) {
9321
+ context.save();
9322
+ context.fillStyle = lightenColor(context.fillStyle, DEFAULT_TEXT_HIGHLIGHT_PERCENT);
9524
9323
  }
9525
- };
9526
- var AlternatingColorGenerator = class extends ColorGenerator {
9527
- constructor(paletteSize, preferredColors = []) {
9528
- super(paletteSize, preferredColors);
9529
- this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
9324
+ context.fillText(text, position.x, position.y);
9325
+ if (!underline && !strikethrough) return;
9326
+ const measure = context.measureText(text);
9327
+ const textWidth = measure.width;
9328
+ const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
9329
+ const boxHeight = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
9330
+ let { x, y } = position;
9331
+ let strikeY = y, underlineY = y;
9332
+ switch (context.textAlign) {
9333
+ case "center":
9334
+ x -= textWidth / 2;
9335
+ break;
9336
+ case "right":
9337
+ x -= textWidth;
9338
+ break;
9530
9339
  }
9531
- };
9532
- var AlternatingColorMap = class {
9533
- availableColors;
9534
- colors = {};
9535
- constructor(paletteSize = 12) {
9536
- this.availableColors = new AlternatingColorGenerator(paletteSize);
9340
+ switch (context.textBaseline) {
9341
+ case "top":
9342
+ underlineY += boxHeight - 2 * strokeWidth;
9343
+ strikeY += boxHeight / 2 - strokeWidth;
9344
+ break;
9345
+ case "middle":
9346
+ underlineY += boxHeight / 2 - strokeWidth;
9347
+ break;
9348
+ case "alphabetic":
9349
+ underlineY += 2 * strokeWidth;
9350
+ strikeY -= 3 * strokeWidth;
9351
+ break;
9352
+ case "bottom":
9353
+ underlineY = y;
9354
+ strikeY -= textHeight / 2 - strokeWidth / 2;
9355
+ break;
9537
9356
  }
9538
- get(id) {
9539
- if (!this.colors[id]) this.colors[id] = this.availableColors.next();
9540
- return this.colors[id];
9357
+ if (underline) {
9358
+ context.lineWidth = strokeWidth;
9359
+ context.strokeStyle = context.fillStyle;
9360
+ context.beginPath();
9361
+ context.moveTo(x, underlineY);
9362
+ context.lineTo(x + textWidth, underlineY);
9363
+ context.stroke();
9541
9364
  }
9542
- };
9543
- const COLORSCHEMES = {
9544
- greys: [
9545
- "#ffffff",
9546
- "#808080",
9547
- "#000000"
9548
- ],
9549
- blues: [
9550
- "#f7fbff",
9551
- "#6aaed6",
9552
- "#08306b"
9553
- ],
9554
- reds: [
9555
- "#fff5f0",
9556
- "#fb694a",
9557
- "#67000d"
9558
- ],
9559
- greens: [
9560
- "#f7fcf5",
9561
- "#73c476",
9562
- "#00441b"
9563
- ],
9564
- oranges: [
9565
- "#fff5eb",
9566
- "#fd8c3b",
9567
- "#7f2704"
9568
- ],
9569
- purples: [
9570
- "#fcfbfd",
9571
- "#9e9ac8",
9572
- "#3f007d"
9573
- ],
9574
- viridis: [
9575
- "#440154",
9576
- "#21918c",
9577
- "#fde725"
9578
- ],
9579
- cividis: [
9580
- "#00224e",
9581
- "#7d7c78",
9582
- "#fee838"
9583
- ],
9584
- rainbow: [
9585
- "#B41DB4",
9586
- "#FFFF00",
9587
- "#00FFFF"
9588
- ]
9589
- };
9590
- const COLORSCALES = Object.keys(COLORSCHEMES);
9365
+ if (strikethrough) {
9366
+ context.lineWidth = strokeWidth;
9367
+ context.strokeStyle = context.fillStyle;
9368
+ context.beginPath();
9369
+ context.moveTo(x, strikeY);
9370
+ context.lineTo(x + textWidth, strikeY);
9371
+ context.stroke();
9372
+ }
9373
+ if (highlightText) context.restore();
9374
+ }
9375
+ function sliceTextToFitWidth(context, width, text, style, fontUnit = "pt") {
9376
+ if (computeTextWidth(context, text, style, fontUnit) <= width) return text;
9377
+ const ellipsis = "...";
9378
+ const ellipsisWidth = computeTextWidth(context, ellipsis, style, fontUnit);
9379
+ if (ellipsisWidth >= width) return "";
9380
+ let lowerBoundLen = 1;
9381
+ let upperBoundLen = text.length;
9382
+ let currentWidth;
9383
+ while (lowerBoundLen <= upperBoundLen) {
9384
+ const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2);
9385
+ currentWidth = computeTextWidth(context, text.slice(0, currentLen), style, fontUnit);
9386
+ if (currentWidth + ellipsisWidth > width) upperBoundLen = currentLen - 1;
9387
+ else lowerBoundLen = currentLen + 1;
9388
+ }
9389
+ const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
9390
+ return slicedText ? slicedText + ellipsis : "";
9391
+ }
9591
9392
  /**
9592
- * Returns a function that maps a value to a color using a color scale defined by the given
9593
- * color/threshold values pairs.
9393
+ * Return the position to draw text on a rotated canvas to ensure that the rotated text alignment correspond
9394
+ * with to original's text vertical and horizontal alignment.
9594
9395
  */
9595
- function getColorScale(colorScalePoints) {
9596
- if (colorScalePoints.length < 2) throw new Error("Color scale must have at least 2 points");
9597
- const sortedColorScalePoints = [...colorScalePoints.sort((a, b) => a.value - b.value)];
9598
- const thresholds = [];
9599
- for (let i = 1; i < sortedColorScalePoints.length; i++) {
9600
- const minColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i - 1].color).a;
9601
- const maxColorAlpha = colorOrNumberToRGBA(sortedColorScalePoints[i].color).a;
9602
- const minColor = colorToNumber(sortedColorScalePoints[i - 1].color);
9603
- const maxColor = colorToNumber(sortedColorScalePoints[i].color);
9604
- thresholds.push({
9605
- min: sortedColorScalePoints[i - 1].value,
9606
- max: sortedColorScalePoints[i].value,
9607
- minColor,
9608
- maxColor,
9609
- minColorAlpha,
9610
- maxColorAlpha,
9611
- colorDiff: computeColorDiffUnits(sortedColorScalePoints[i - 1].value, sortedColorScalePoints[i].value, minColor, maxColor)
9396
+ function computeRotationPosition(rect, style) {
9397
+ if (!style.rotation || style.rotation % (Math.PI * 2) === 0) return rect;
9398
+ let { x, y } = rect;
9399
+ const cos = Math.cos(-style.rotation);
9400
+ const sin = Math.sin(-style.rotation);
9401
+ const width = rect.textWidth - 2 * 4;
9402
+ const height = rect.textHeight;
9403
+ const center = style.align === "center";
9404
+ const rotateTowardCellCenter = style.align === "left" === sin < 0;
9405
+ const sh = sin * height;
9406
+ const sw = Math.abs(sin * width);
9407
+ const ch = cos * height;
9408
+ if (style.verticalAlign === "top") if (center) {
9409
+ y += sw / 2;
9410
+ x -= sh / 2;
9411
+ } else if (rotateTowardCellCenter) x -= sh;
9412
+ else y += sw;
9413
+ else if (!style.verticalAlign || style.verticalAlign === "bottom") {
9414
+ y += height - ch;
9415
+ if (center) {
9416
+ y -= sw / 2;
9417
+ x -= sh / 2;
9418
+ } else if (rotateTowardCellCenter) {
9419
+ x -= sh;
9420
+ y -= sw;
9421
+ }
9422
+ } else if (center) {
9423
+ x -= sh / 2;
9424
+ y -= height / 2;
9425
+ if (rotateTowardCellCenter) y += sh;
9426
+ else y -= sh;
9427
+ } else if (rotateTowardCellCenter) {
9428
+ x -= sh;
9429
+ y -= sw / 2;
9430
+ } else y += sw / 2 + ch / 4;
9431
+ return {
9432
+ x: cos * x - sin * y,
9433
+ y: cos * y + sin * x
9434
+ };
9435
+ }
9436
+
9437
+ //#endregion
9438
+ //#region src/components/figures/chart/chartJs/chartjs_colorscale_plugin.ts
9439
+ /** This is a chartJS plugin that will draw the heatmap colorscale at the chart legend position */
9440
+ const chartColorScalePlugin = {
9441
+ id: "chartColorScalePlugin",
9442
+ afterDatasetsDraw(chart, args, options) {
9443
+ if (!options.position || options.position === "none" || !options.colorScale.length) return;
9444
+ const ctx = chart.ctx;
9445
+ ctx.save();
9446
+ ctx.textAlign = "center";
9447
+ ctx.textBaseline = "middle";
9448
+ ctx.miterLimit = 1;
9449
+ const gradientHeight = (chart.chartArea.bottom - chart.chartArea.top) / 2;
9450
+ const gradientWidth = 10;
9451
+ const gradientX = options.position === "left" ? 20 : ctx.canvas.width - 70;
9452
+ const gradientY = chart.chartArea.top;
9453
+ const gradient = ctx.createLinearGradient(0, gradientY + gradientHeight, 0, gradientY);
9454
+ const step = 1 / (options.colorScale.length - 1);
9455
+ options.colorScale.forEach((color, index) => {
9456
+ gradient.addColorStop(index * step, color);
9612
9457
  });
9458
+ ctx.fillStyle = gradient;
9459
+ ctx.fillRect(gradientX, gradientY, gradientWidth, gradientHeight);
9460
+ ctx.fillStyle = options.fontColor ?? "black";
9461
+ ctx.font = getDefaultContextFont(12);
9462
+ ctx.textAlign = "left";
9463
+ let minValue = Math.round(options.minValue * 100) / 100;
9464
+ let maxValue = Math.round(options.maxValue * 100) / 100;
9465
+ if (options.minValue === options.maxValue) {
9466
+ minValue -= 1;
9467
+ maxValue += 1;
9468
+ }
9469
+ const formattedMaxValue = humanizeNumber({
9470
+ value: maxValue,
9471
+ format: void 0
9472
+ }, options.locale);
9473
+ const formattedMinValue = humanizeNumber({
9474
+ value: minValue,
9475
+ format: void 0
9476
+ }, options.locale);
9477
+ ctx.fillText(formattedMinValue, gradientX + gradientWidth + 5, gradientY + gradientHeight - 6);
9478
+ ctx.fillText(formattedMaxValue, gradientX + gradientWidth + 5, gradientY + 6);
9479
+ ctx.restore();
9613
9480
  }
9614
- return (value) => {
9615
- if (value < thresholds[0].min) return colorNumberToHex(thresholds[0].minColor, thresholds[0].minColorAlpha);
9616
- for (const threshold of thresholds) if (value >= threshold.min && value <= threshold.max) return colorNumberToHex(colorCell(value, threshold.min, threshold.minColor, threshold.colorDiff), threshold.maxColorAlpha);
9617
- return colorNumberToHex(thresholds[thresholds.length - 1].maxColor, thresholds[thresholds.length - 1].maxColorAlpha);
9481
+ };
9482
+
9483
+ //#endregion
9484
+ //#region src/components/figures/chart/chartJs/chartjs_funnel_chart.ts
9485
+ function getFunnelChartController() {
9486
+ if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
9487
+ return class FunnelChartController extends globalThis.Chart.BarController {
9488
+ static id = "funnel";
9489
+ static defaults = {
9490
+ ...globalThis.Chart?.BarController.defaults,
9491
+ dataElementType: "funnel",
9492
+ animation: { duration: (ctx) => {
9493
+ if (ctx.type !== "data") return 1e3;
9494
+ return 1e3 * (ctx.raw[1] / Math.max(...ctx.dataset.data.map((data) => data[1])));
9495
+ } }
9496
+ };
9497
+ /** Called at each chart render to update the elements of the chart (FunnelChartElement) with the updated data */
9498
+ updateElements(rects, start, count, mode) {
9499
+ super.updateElements(rects, start, count, mode);
9500
+ for (let i = start; i < start + count; i++) {
9501
+ const rect = rects[i];
9502
+ this.updateElement(rect, i, { nextElement: rects[i + 1] }, mode);
9503
+ }
9504
+ }
9618
9505
  };
9619
9506
  }
9620
- function computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
9621
- const deltaValue = maxValue - minValue;
9622
- const deltaColorR = (minColor >> 16) % 256 - (maxColor >> 16) % 256;
9623
- const deltaColorG = (minColor >> 8) % 256 - (maxColor >> 8) % 256;
9624
- const deltaColorB = minColor % 256 - maxColor % 256;
9625
- return [
9626
- deltaColorR / deltaValue,
9627
- deltaColorG / deltaValue,
9628
- deltaColorB / deltaValue
9629
- ];
9507
+ function getFunnelChartElement() {
9508
+ if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
9509
+ /**
9510
+ * Similar to a bar chart element, but it's a trapezoid rather than a rectangle. The top is of width
9511
+ * `width`, and the bottom is of width `nextElementWidth`.
9512
+ */
9513
+ return class FunnelChartElement extends globalThis.Chart.BarElement {
9514
+ static id = "funnel";
9515
+ /** Overwrite this to draw a trapezoid rather then a rectangle */
9516
+ draw(ctx) {
9517
+ ctx.save();
9518
+ const { x, y, height, nextElement, base, options } = this.getProps([
9519
+ "x",
9520
+ "y",
9521
+ "width",
9522
+ "height",
9523
+ "nextElement",
9524
+ "base",
9525
+ "options"
9526
+ ]);
9527
+ const width = getElementWidth(this);
9528
+ const offset = (width - (nextElement ? getElementWidth(nextElement) : 0)) / 2;
9529
+ const startX = Math.min(x, base);
9530
+ const startY = y - height / 2;
9531
+ ctx.fillStyle = options.backgroundColor;
9532
+ ctx.beginPath();
9533
+ ctx.moveTo(startX, startY);
9534
+ ctx.lineTo(startX + width, startY);
9535
+ ctx.lineTo(startX + width - offset, startY + height);
9536
+ ctx.lineTo(startX + offset, startY + height);
9537
+ ctx.closePath();
9538
+ ctx.fill();
9539
+ if (options.borderWidth) {
9540
+ ctx.strokeStyle = options.borderColor;
9541
+ ctx.lineWidth = options.borderWidth;
9542
+ ctx.stroke();
9543
+ }
9544
+ ctx.restore();
9545
+ }
9546
+ /** Check if the mouse is inside the trapezoid */
9547
+ inRange(mouseX, mouseY) {
9548
+ const { x, y, height, nextElement, base } = this.getProps([
9549
+ "x",
9550
+ "y",
9551
+ "width",
9552
+ "height",
9553
+ "nextElement",
9554
+ "base",
9555
+ "options"
9556
+ ]);
9557
+ const width = getElementWidth(this);
9558
+ const nextElementWidth = nextElement ? getElementWidth(nextElement) : 0;
9559
+ const startX = Math.min(x, base);
9560
+ const startY = y - height / 2;
9561
+ if (mouseY < startY || mouseY > startY + height) return false;
9562
+ const offset = (width - nextElementWidth) / 2;
9563
+ const left = startX + offset * (mouseY - startY) / height;
9564
+ const right = startX + width - offset * (mouseY - startY) / height;
9565
+ if (mouseX < left || mouseX > right) return false;
9566
+ return true;
9567
+ }
9568
+ };
9630
9569
  }
9631
- function colorCell(value, minValue, minColor, colorDiffUnit) {
9632
- const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
9633
- const r = Math.round((minColor >> 16) % 256 - colorDiffUnitR * (value - minValue));
9634
- const g = Math.round((minColor >> 8) % 256 - colorDiffUnitG * (value - minValue));
9635
- const b = Math.round(minColor % 256 - colorDiffUnitB * (value - minValue));
9636
- return r << 16 | g << 8 | b;
9570
+ /**
9571
+ * Get an element width.
9572
+ *
9573
+ * The property width is undefined during animations, we need to compute it manually.
9574
+ */
9575
+ function getElementWidth(element) {
9576
+ const { x, base } = element.getProps(["x", "base"]);
9577
+ return Math.max(x, base) - Math.min(x, base);
9637
9578
  }
9579
+ /**
9580
+ * Position the tooltip inside the trapezoid.
9581
+ * The default position for tooltips of bar elements is at the end of rectangle, which is not ideal for trapezoids.
9582
+ */
9583
+ const funnelTooltipPositioner = function(elements) {
9584
+ if (!elements.length) return {
9585
+ x: 0,
9586
+ y: 0
9587
+ };
9588
+ const { x, y, base, width, height } = elements[0].element.getProps([
9589
+ "x",
9590
+ "y",
9591
+ "width",
9592
+ "height",
9593
+ "base"
9594
+ ]);
9595
+ const startX = Math.min(x, base);
9596
+ const startY = y - height / 2;
9597
+ return {
9598
+ x: startX + width * 2 / 3,
9599
+ y: startY + height / 2
9600
+ };
9601
+ };
9602
+
9603
+ //#endregion
9604
+ //#region src/components/figures/chart/chartJs/chartjs_minor_grid_plugin.ts
9605
+ const chartMinorGridPlugin = {
9606
+ id: "o-spreadsheet-minor-gridlines",
9607
+ beforeDatasetsDraw(chart) {
9608
+ const ctx = chart.ctx;
9609
+ const chartArea = chart.chartArea;
9610
+ if (!chartArea) return;
9611
+ for (const scaleId in chart.scales) {
9612
+ const scale = chart.scales[scaleId];
9613
+ const options = scale.options;
9614
+ const minor = options?.grid?.minor;
9615
+ if (!minor?.display) continue;
9616
+ const showMajorGrid = options?.grid?.display;
9617
+ const ticks = scale.ticks;
9618
+ if (!ticks || ticks.length < 2) continue;
9619
+ ctx.save();
9620
+ ctx.lineWidth = 1;
9621
+ ctx.strokeStyle = minor.color ?? options?.grid?.color ?? "#e6e6e6";
9622
+ for (let i = 0; i < ticks.length - 1; i++) {
9623
+ const start = scale.getPixelForTick(i);
9624
+ const end = scale.getPixelForTick(i + 1);
9625
+ if (!isFinite(start) || !isFinite(end)) continue;
9626
+ for (let j = showMajorGrid ? 1 : 0; j < 4; j++) {
9627
+ const ratio = j / 4;
9628
+ const position = Math.round(start + (end - start) * ratio) + .5;
9629
+ ctx.beginPath();
9630
+ if (scale.isHorizontal()) {
9631
+ ctx.moveTo(position, chartArea.top);
9632
+ ctx.lineTo(position, chartArea.bottom);
9633
+ } else {
9634
+ ctx.moveTo(chartArea.left, position);
9635
+ ctx.lineTo(chartArea.right, position);
9636
+ }
9637
+ ctx.stroke();
9638
+ }
9639
+ }
9640
+ ctx.restore();
9641
+ }
9642
+ }
9643
+ };
9638
9644
 
9639
9645
  //#endregion
9640
9646
  //#region src/xlsx/constants.ts
@@ -12074,11 +12080,11 @@ set(value) {
12074
12080
  if (structure.baseline) {
12075
12081
  ctx.font = structure.baseline.style.font;
12076
12082
  ctx.fillStyle = structure.baseline.style.color;
12077
- drawDecoratedText(ctx, structure.baseline.text, structure.baseline.position, structure.baseline.style.underline, structure.baseline.style.strikethrough);
12083
+ drawDecoratedText(ctx, structure.baseline.text, structure.baseline.position, structure.baseline.style.underline, structure.baseline.style.strikethrough, void 0, structure.baseline.style.highlightText);
12078
12084
  }
12079
12085
  if (structure.baselineArrow && structure.baselineArrow.style.size > 0 && Path2DConstructor) {
12080
12086
  ctx.save();
12081
- ctx.fillStyle = structure.baselineArrow.style.color;
12087
+ ctx.fillStyle = structure.baselineArrow.style.highlight ? lightenColor(structure.baselineArrow.style.color, DEFAULT_TEXT_HIGHLIGHT_PERCENT) : structure.baselineArrow.style.color;
12082
12088
  ctx.translate(structure.baselineArrow.position.x, structure.baselineArrow.position.y);
12083
12089
  const ratio = structure.baselineArrow.style.size / 10;
12084
12090
  ctx.scale(ratio, ratio);
@@ -12096,18 +12102,18 @@ set(value) {
12096
12102
  const descr = structure.baselineDescr;
12097
12103
  ctx.font = descr.style.font;
12098
12104
  ctx.fillStyle = descr.style.color;
12099
- ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
12105
+ drawDecoratedText(ctx, clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position, void 0, void 0, void 0, structure.baseline?.style.highlightText);
12100
12106
  }
12101
12107
  if (structure.key) {
12102
12108
  ctx.font = structure.key.style.font;
12103
12109
  ctx.fillStyle = structure.key.style.color;
12104
- drawDecoratedText(ctx, clipTextWithEllipsis(ctx, structure.key.text, availableWidth - structure.key.position.x), structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
12110
+ drawDecoratedText(ctx, clipTextWithEllipsis(ctx, structure.key.text, availableWidth - structure.key.position.x), structure.key.position, structure.key.style.underline, structure.key.style.strikethrough, void 0, structure.key.style.highlightText);
12105
12111
  }
12106
12112
  if (structure.keyDescr) {
12107
12113
  const descr = structure.keyDescr;
12108
12114
  ctx.font = structure.keyDescr?.style.font ?? descr.style.font;
12109
12115
  ctx.fillStyle = descr.style.color;
12110
- ctx.fillText(clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position.x, descr.position.y);
12116
+ drawDecoratedText(ctx, clipTextWithEllipsis(ctx, descr.text, availableWidth - descr.position.x), descr.position, void 0, void 0, void 0, structure.key?.style.highlightText);
12111
12117
  }
12112
12118
  if (structure.progressBar) {
12113
12119
  ctx.fillStyle = structure.progressBar.style.backgroundColor;
@@ -12331,29 +12337,34 @@ set(value) {
12331
12337
  color: this.runtime.keyValueStyle?.textColor || this.runtime.fontColor,
12332
12338
  font: getDefaultContextFont(keyValueFontSize, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
12333
12339
  strikethrough: this.runtime.keyValueStyle?.strikethrough,
12334
- underline: this.runtime.keyValueStyle?.underline
12340
+ underline: this.runtime.keyValueStyle?.underline,
12341
+ highlightText: this.runtime.keyHighlight
12335
12342
  },
12336
12343
  keyDescr: {
12337
12344
  color: this.runtime.keyValueDescrStyle?.textColor || this.runtime.fontColor,
12338
12345
  font: getDefaultContextFont(keyValueDescrFontSize, this.runtime.keyValueDescrStyle?.bold, this.runtime.keyValueDescrStyle?.italic),
12339
12346
  strikethrough: this.runtime.keyValueDescrStyle?.strikethrough,
12340
- underline: this.runtime.keyValueDescrStyle?.underline
12347
+ underline: this.runtime.keyValueDescrStyle?.underline,
12348
+ highlightText: this.runtime.keyHighlight
12341
12349
  },
12342
12350
  baselineValue: {
12343
12351
  font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
12344
12352
  strikethrough: this.runtime.baselineStyle?.strikethrough,
12345
12353
  underline: this.runtime.baselineStyle?.underline,
12346
- color: this.runtime.baselineColor || this.runtime.baselineStyle?.textColor || this.secondaryFontColor
12354
+ color: this.runtime.baselineColor || this.runtime.baselineStyle?.textColor || this.secondaryFontColor,
12355
+ highlightText: this.runtime.baselineHighlight
12347
12356
  },
12348
12357
  baselineDescr: {
12349
12358
  font: getDefaultContextFont(baselineDescrFontSize, this.runtime.baselineDescrStyle?.bold, this.runtime.baselineDescrStyle?.italic),
12350
12359
  strikethrough: this.runtime.baselineDescrStyle?.strikethrough,
12351
12360
  underline: this.runtime.baselineDescrStyle?.underline,
12352
- color: this.runtime.baselineDescrStyle?.textColor ?? this.secondaryFontColor
12361
+ color: this.runtime.baselineDescrStyle?.textColor ?? this.secondaryFontColor,
12362
+ highlightText: this.runtime.baselineHighlight
12353
12363
  },
12354
12364
  baselineArrow: this.baselineArrow === "neutral" || this.runtime.progressBar ? void 0 : {
12355
12365
  size: this.keyValue ? .8 * baselineValueFontSize : 0,
12356
- color: this.runtime.baselineColor || this.runtime.baselineStyle?.textColor || this.secondaryFontColor
12366
+ color: this.runtime.baselineColor || this.runtime.baselineStyle?.textColor || this.secondaryFontColor,
12367
+ highlight: this.runtime.baselineHighlight
12357
12368
  }
12358
12369
  };
12359
12370
  }
@@ -12449,11 +12460,14 @@ set(value) {
12449
12460
  });
12450
12461
  (0, _odoo_owl.onWillUnmount)(() => resizeObserver.disconnect());
12451
12462
  }
12463
+ config(canvasRect, zoom) {
12464
+ return getScorecardConfiguration(getZoomedRect(1 / zoom, canvasRect), this.runtime);
12465
+ }
12452
12466
  createChart() {
12453
12467
  const canvas = this.canvas();
12454
12468
  if (!canvas) return;
12455
12469
  const zoom = this.env.model.getters.getViewportZoomLevel();
12456
- drawScoreChart(getScorecardConfiguration(getZoomedRect(1 / zoom, canvas.getBoundingClientRect()), this.runtime), canvas, zoom);
12470
+ drawScoreChart(this.config(canvas.getBoundingClientRect(), zoom), canvas, zoom);
12457
12471
  }
12458
12472
  };
12459
12473
 
@@ -21793,10 +21807,10 @@ set(value) {
21793
21807
  type: dimension.type
21794
21808
  }));
21795
21809
  if (groupValueString.toLowerCase() === "false") return false;
21796
- return pivotNormalizationValueRegistry.get(dimension.type)(groupValueString, dimension.granularity);
21810
+ return pivotNormalizationValueRegistry.get(dimension.type)(groupValueString, dimension);
21797
21811
  }
21798
- function normalizeDateTime(value, granularity) {
21799
- return pivotTimeAdapter(granularity ?? "month").normalizeFunctionValue(value);
21812
+ function normalizeDateTime(value, dimension) {
21813
+ return pivotTimeAdapter(dimension.granularity ?? "month").normalizeFunctionValue(value);
21800
21814
  }
21801
21815
  function toFunctionPivotValue(value, dimension) {
21802
21816
  if (value === null) return `"null"`;
@@ -28057,7 +28071,7 @@ set(value) {
28057
28071
  */
28058
28072
  function filterInvalidHierarchicalPoints(values, hierarchy) {
28059
28073
  const numberOfDataPoints = Math.max(values.length, ...hierarchy.map((dataset) => dataset.data?.length || 0));
28060
- const isEmpty = (value) => value === null || value === "";
28074
+ const isEmpty = (value) => value === void 0 || value === null || value === "";
28061
28075
  const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
28062
28076
  const groups = hierarchy.map((dataset) => dataset.data?.[dataPointIndex]);
28063
28077
  if (isEmpty(groups[0]?.value)) return false;
@@ -34538,6 +34552,35 @@ set(value) {
34538
34552
  }
34539
34553
  };
34540
34554
 
34555
+ //#endregion
34556
+ //#region src/components/side_panel/criterion_form/calendar_button/calendar_button.ts
34557
+ var CalendarButton = class extends Component {
34558
+ static template = "o-spreadsheet-CalendarButton";
34559
+ props = (0, _odoo_owl.props)({
34560
+ value: types.string().optional(""),
34561
+ onChange: types.function()
34562
+ });
34563
+ datePickerRef = _odoo_owl.signal.ref(HTMLInputElement);
34564
+ openCalendar() {
34565
+ this.datePickerRef()?.showPicker();
34566
+ }
34567
+ formatDateForInput(value) {
34568
+ const dateValue = parseDateTime(value, DEFAULT_LOCALE);
34569
+ return dateValue ? formatValue(dateValue.value, {
34570
+ format: "yyyy-mm-dd",
34571
+ locale: DEFAULT_LOCALE
34572
+ }) : "";
34573
+ }
34574
+ onDateInputValueChanged(value) {
34575
+ const dateValue = parseDateTime(value, DEFAULT_LOCALE);
34576
+ const formattedValue = dateValue ? formatValue(dateValue.value, {
34577
+ format: DEFAULT_LOCALE.dateFormat,
34578
+ locale: DEFAULT_LOCALE
34579
+ }) : "";
34580
+ this.props.onChange(formattedValue);
34581
+ }
34582
+ };
34583
+
34541
34584
  //#endregion
34542
34585
  //#region src/components/side_panel/criterion_form/criterion_form.ts
34543
34586
  var CriterionForm = class extends Component {
@@ -35226,7 +35269,8 @@ set(value) {
35226
35269
  static template = "o-spreadsheet-DataValidationDateCriterion";
35227
35270
  static components = {
35228
35271
  CriterionInput,
35229
- Select
35272
+ Select,
35273
+ CalendarButton
35230
35274
  };
35231
35275
  get currentDateValue() {
35232
35276
  return this.props.criterion.dateValue || "exactDate";
@@ -35252,7 +35296,10 @@ set(value) {
35252
35296
  //#region src/components/side_panel/criterion_form/double_input_criterion/double_input_criterion.ts
35253
35297
  var DoubleInputCriterionForm = class extends CriterionForm {
35254
35298
  static template = "o-spreadsheet-DoubleInputCriterionForm";
35255
- static components = { CriterionInput };
35299
+ static components = {
35300
+ CriterionInput,
35301
+ CalendarButton
35302
+ };
35256
35303
  onFirstValueChanged(value) {
35257
35304
  const values = this.props.criterion.values;
35258
35305
  this.updateCriterion({ values: [value, values[1] || ""] });
@@ -35261,6 +35308,9 @@ set(value) {
35261
35308
  const values = this.props.criterion.values;
35262
35309
  this.updateCriterion({ values: [values[0] || "", value] });
35263
35310
  }
35311
+ get isDateType() {
35312
+ return ["dateIsBetween", "dateIsNotBetween"].includes(this.props.criterion.type);
35313
+ }
35264
35314
  };
35265
35315
 
35266
35316
  //#endregion
@@ -46607,7 +46657,7 @@ set(value) {
46607
46657
  horizontalSnap: void 0,
46608
46658
  verticalSnap: void 0,
46609
46659
  cancelDnd: void 0,
46610
- overlappingCarousel: void 0
46660
+ overlappingChartOrCarousel: void 0
46611
46661
  });
46612
46662
  setup() {
46613
46663
  (0, _odoo_owl.onMounted)(() => {
@@ -46623,7 +46673,7 @@ set(value) {
46623
46673
  this.dnd.selectedRect = void 0;
46624
46674
  this.dnd.horizontalSnap = void 0;
46625
46675
  this.dnd.verticalSnap = void 0;
46626
- this.dnd.overlappingCarousel = void 0;
46676
+ this.dnd.overlappingChartOrCarousel = void 0;
46627
46677
  this.dnd.cancelDnd = void 0;
46628
46678
  }
46629
46679
  });
@@ -46761,11 +46811,11 @@ set(value) {
46761
46811
  hasStartedDnd = true;
46762
46812
  const selectedFigures = dragFigureForMove(currentMousePosition, initialMousePosition, initialFigures, maxDimensions, initialScrollPosition, getters.getActiveSheetScrollInfo());
46763
46813
  const draggedFigure = selectedFigures.find((f) => f.id === draggedFigureId);
46764
- let overlappingCarousel = void 0;
46814
+ let overlappingChartOrCarousel = void 0;
46765
46815
  const otherFigures = this.getOtherFigures(selectedFigures.map((f) => f.id));
46766
- if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingCarousel = this.getCarouselOverlappingChart(draggedFigure, otherFigures);
46767
- this.dnd.overlappingCarousel = overlappingCarousel;
46768
- if (!overlappingCarousel) {
46816
+ if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = this.getOverlappingFigure(draggedFigure, otherFigures);
46817
+ this.dnd.overlappingChartOrCarousel = overlappingChartOrCarousel;
46818
+ if (!overlappingChartOrCarousel) {
46769
46819
  const snapReturn = snapForMove(getters, selectedFigures, otherFigures);
46770
46820
  this.dnd.selectedFigures = snapReturn.snappedFigures;
46771
46821
  this.dnd.selectedRect = this.getDndFigureRect();
@@ -46786,7 +46836,7 @@ set(value) {
46786
46836
  else this.env.model.dispatch("SELECT_FIGURE", { figureId: figureUI.id });
46787
46837
  return;
46788
46838
  }
46789
- if (!this.dnd.overlappingCarousel) {
46839
+ if (!this.dnd.overlappingChartOrCarousel) {
46790
46840
  const payloads = this.dnd.selectedFigures?.map((f) => {
46791
46841
  return {
46792
46842
  sheetId,
@@ -46796,20 +46846,25 @@ set(value) {
46796
46846
  }) || [];
46797
46847
  this.env.model.dispatch("MOVE_FIGURES", { figures: payloads });
46798
46848
  } else {
46799
- const carouselFigureId = this.dnd.overlappingCarousel.id;
46849
+ const overlappingFigureId = this.dnd.overlappingChartOrCarousel.id;
46800
46850
  const chartFigureIds = this.dnd.selectedFigures?.map((f) => f.id) || [];
46801
- this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
46851
+ if (this.dnd.overlappingChartOrCarousel.tag === "carousel") this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
46802
46852
  sheetId,
46803
- carouselFigureId,
46853
+ carouselFigureId: overlappingFigureId,
46804
46854
  chartFigureIds
46805
46855
  });
46856
+ else if (this.dnd.overlappingChartOrCarousel.tag === "chart") this.env.model.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
46857
+ sheetId,
46858
+ baseFigureId: overlappingFigureId,
46859
+ chartFigureIds: [overlappingFigureId, ...chartFigureIds]
46860
+ });
46806
46861
  }
46807
46862
  this.dnd.draggedFigure = void 0;
46808
46863
  this.dnd.selectedFigures = void 0;
46809
46864
  this.dnd.selectedRect = void 0;
46810
46865
  this.dnd.horizontalSnap = void 0;
46811
46866
  this.dnd.verticalSnap = void 0;
46812
- this.dnd.overlappingCarousel = void 0;
46867
+ this.dnd.overlappingChartOrCarousel = void 0;
46813
46868
  };
46814
46869
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
46815
46870
  }
@@ -46865,7 +46920,7 @@ set(value) {
46865
46920
  this.dnd.selectedRect = void 0;
46866
46921
  this.dnd.horizontalSnap = void 0;
46867
46922
  this.dnd.verticalSnap = void 0;
46868
- this.dnd.overlappingCarousel = void 0;
46923
+ this.dnd.overlappingChartOrCarousel = void 0;
46869
46924
  };
46870
46925
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
46871
46926
  }
@@ -46875,12 +46930,12 @@ set(value) {
46875
46930
  getFigureStyle(figureUI) {
46876
46931
  if (figureUI.id !== this.dnd.draggedFigure?.id) return "";
46877
46932
  return cssPropertiesToCss({
46878
- opacity: this.dnd.overlappingCarousel?.id ? "0.6" : "0.9",
46933
+ opacity: this.dnd.overlappingChartOrCarousel?.id ? "0.6" : "0.9",
46879
46934
  cursor: "grabbing"
46880
46935
  });
46881
46936
  }
46882
46937
  getFigureClass(figureUI) {
46883
- if (figureUI.id !== this.dnd.overlappingCarousel?.id) return "";
46938
+ if (figureUI.id !== this.dnd.overlappingChartOrCarousel?.id) return "";
46884
46939
  return "o-add-to-carousel";
46885
46940
  }
46886
46941
  getSnap(snapLine) {
@@ -46923,18 +46978,18 @@ set(value) {
46923
46978
  height: `100%`
46924
46979
  });
46925
46980
  }
46926
- getCarouselOverlappingChart(figureUI, otherFigures) {
46981
+ getOverlappingFigure(figureUI, otherFigures) {
46927
46982
  if (figureUI.tag !== "chart") return;
46928
46983
  const figureCenterX = figureUI.x + figureUI.width / 2;
46929
46984
  const figureCenterY = figureUI.y + figureUI.height / 2;
46930
46985
  let bestMatch;
46931
46986
  let smallestDistance = Infinity;
46932
46987
  for (const figure of otherFigures) {
46933
- if (figure.tag !== "carousel") continue;
46934
- const carouselCenterX = figure.x + figure.width / 2;
46935
- const carouselCenterY = figure.y + figure.height / 2;
46936
- const distanceX = Math.abs(figureCenterX - carouselCenterX);
46937
- const distanceY = Math.abs(figureCenterY - carouselCenterY);
46988
+ if (figure.tag !== "chart" && figure.tag !== "carousel") continue;
46989
+ const targetCenterX = figure.x + figure.width / 2;
46990
+ const targetCenterY = figure.y + figure.height / 2;
46991
+ const distanceX = Math.abs(figureCenterX - targetCenterX);
46992
+ const distanceY = Math.abs(figureCenterY - targetCenterY);
46938
46993
  const squaredDistance = distanceX ** 2 + distanceY ** 2;
46939
46994
  if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
46940
46995
  smallestDistance = squaredDistance;
@@ -58721,13 +58776,13 @@ set(value) {
58721
58776
  const minValue = this.parsePoint(sheetId, range, rule.minimum, "min");
58722
58777
  const midValue = rule.midpoint ? this.parsePoint(sheetId, range, rule.midpoint) : null;
58723
58778
  const maxValue = this.parsePoint(sheetId, range, rule.maximum, "max");
58724
- if (minValue === null || maxValue === null || minValue >= maxValue || midValue && (minValue >= midValue || midValue >= maxValue)) return;
58779
+ if (minValue === null || maxValue === null || minValue >= maxValue || midValue !== null && (minValue >= midValue || midValue >= maxValue)) return;
58725
58780
  const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
58726
58781
  const colorThresholds = [{
58727
58782
  value: minValue,
58728
58783
  color: rule.minimum.color
58729
58784
  }];
58730
- if (rule.midpoint && midValue) colorThresholds.push({
58785
+ if (rule.midpoint && midValue !== null) colorThresholds.push({
58731
58786
  value: midValue,
58732
58787
  color: rule.midpoint.color
58733
58788
  });
@@ -66186,7 +66241,9 @@ set(value) {
66186
66241
  ctx.lineWidth = 1.5 * thinLineWidth;
66187
66242
  const isDarkMode = this.getters.isDarkMode();
66188
66243
  for (const zone of zones) {
66244
+ if (!viewports.isZoneVisibleInViewport(sheetId, zone)) continue;
66189
66245
  const { x, y, width, height } = viewports.getVisibleRect(sheetId, zone);
66246
+ const currentLineWidth = ctx.lineWidth;
66190
66247
  if (!isDarkMode) ctx.globalCompositeOperation = "multiply";
66191
66248
  if (height === 0 || width === 0) ctx.lineWidth = 3 * thinLineWidth;
66192
66249
  if (height === 0 && width === 0) {
@@ -66200,6 +66257,7 @@ set(value) {
66200
66257
  ctx.globalCompositeOperation = "source-over";
66201
66258
  ctx.strokeRect(x, y, width, height);
66202
66259
  }
66260
+ ctx.lineWidth = currentLineWidth;
66203
66261
  }
66204
66262
  ctx.globalCompositeOperation = "source-over";
66205
66263
  const position = renderingContext.activePosition;
@@ -66346,6 +66404,9 @@ set(value) {
66346
66404
  this.adjustViewportZoneX();
66347
66405
  this.adjustViewportZoneY();
66348
66406
  }
66407
+ isZoneVisible(zone) {
66408
+ return intersection(zone, this) !== void 0;
66409
+ }
66349
66410
  /**
66350
66411
  *
66351
66412
  * Computes the visible coordinates & dimensions of a given zone inside the viewport
@@ -66626,6 +66687,9 @@ set(value) {
66626
66687
  isVisibleInViewport({ sheetId, col, row }) {
66627
66688
  return this.getSubViewports(sheetId).some((pane) => pane.isVisible(col, row));
66628
66689
  }
66690
+ isZoneVisibleInViewport(sheetId, zone) {
66691
+ return this.getSubViewports(sheetId).some((pane) => pane.isZoneVisible(zone));
66692
+ }
66629
66693
  getScrollBarWidth() {
66630
66694
  return 15 / this.zoomLevel;
66631
66695
  }
@@ -70699,18 +70763,28 @@ set(value) {
70699
70763
  return;
70700
70764
  }
70701
70765
  const activeSheetId = this.env.model.getters.getActiveSheetId();
70702
- if (activeSheetId !== sheetId) this.env.model.dispatch("ACTIVATE_SHEET", {
70703
- sheetIdFrom: activeSheetId,
70704
- sheetIdTo: sheetId
70705
- });
70706
- this.env.model.selection.selectCell(zone.right, zone.bottom);
70766
+ if (activeSheetId !== sheetId) {
70767
+ if (!this.env.model.getters.getSheet(sheetId).isVisible) {
70768
+ this.env.notifyUser({
70769
+ text: _t("The sheet on which the range is defined is hidden."),
70770
+ type: "info",
70771
+ sticky: false
70772
+ });
70773
+ return;
70774
+ }
70775
+ this.env.model.dispatch("ACTIVATE_SHEET", {
70776
+ sheetIdFrom: activeSheetId,
70777
+ sheetIdTo: sheetId
70778
+ });
70779
+ }
70780
+ this.env.model.selection.selectCell(zone.right, zone.bottom, { allowsHiddenSelection: true });
70707
70781
  this.env.model.selection.selectZone({
70708
70782
  cell: {
70709
70783
  col: zone.left,
70710
70784
  row: zone.top
70711
70785
  },
70712
70786
  zone
70713
- });
70787
+ }, { allowsHiddenSelection: true });
70714
70788
  }
70715
70789
  get selectionKey() {
70716
70790
  return `${this.env.model.getters.getActiveSheetId()}-${zoneToXc(this.selectedZone)}`;
@@ -87335,7 +87409,9 @@ set(value) {
87335
87409
  collapseHierarchicalDisplayName,
87336
87410
  getCanonicalSymbolName,
87337
87411
  fuzzyLookup,
87338
- replaceSymbolInFormula
87412
+ replaceSymbolInFormula,
87413
+ isSingleCellReference,
87414
+ computeCachedTextDimension
87339
87415
  };
87340
87416
  const links = {
87341
87417
  isMarkdownLink,
@@ -87405,7 +87481,8 @@ set(value) {
87405
87481
  FullScreenFigure,
87406
87482
  NumberInput,
87407
87483
  TopBar,
87408
- Composer
87484
+ Composer,
87485
+ CalendarButton
87409
87486
  };
87410
87487
  const hooks = {
87411
87488
  useDragAndDropListItems,
@@ -87559,8 +87636,8 @@ exports.stores = stores;
87559
87636
  exports.tokenColors = tokenColors;
87560
87637
  exports.tokenize = tokenize;
87561
87638
 
87562
- __info__.version = "19.5.0-alpha.0";
87563
- __info__.date = "2026-06-29T09:01:26.331Z";
87564
- __info__.hash = "2437c0b";
87639
+ __info__.version = "19.5.0-alpha.2";
87640
+ __info__.date = "2026-07-13T06:26:20.354Z";
87641
+ __info__.hash = "a4d2f90";
87565
87642
 
87566
87643
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);