@odoo/o-spreadsheet 16.2.0-alpha.3 → 16.2.0-alpha.4

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.
@@ -2029,10 +2029,10 @@ class RangeImpl {
2029
2029
  if (right !== undefined && bottom !== undefined)
2030
2030
  return { left, top, right, bottom };
2031
2031
  else if (bottom === undefined && right !== undefined) {
2032
- return { right, top, left, bottom: this.getSheetSize(this.sheetId).height - 1 };
2032
+ return { right, top, left, bottom: this.getSheetSize(this.sheetId).numberOfRows - 1 };
2033
2033
  }
2034
2034
  else if (right === undefined && bottom !== undefined) {
2035
- return { bottom, left, top, right: this.getSheetSize(this.sheetId).width - 1 };
2035
+ return { bottom, left, top, right: this.getSheetSize(this.sheetId).numberOfCols - 1 };
2036
2036
  }
2037
2037
  throw new Error(_lt("Bad zone format"));
2038
2038
  }
@@ -2655,13 +2655,13 @@ function positionsDifference(positions, toRemove) {
2655
2655
  }
2656
2656
  function zoneToDimension(zone) {
2657
2657
  return {
2658
- height: zone.bottom - zone.top + 1,
2659
- width: zone.right - zone.left + 1,
2658
+ numberOfRows: zone.bottom - zone.top + 1,
2659
+ numberOfCols: zone.right - zone.left + 1,
2660
2660
  };
2661
2661
  }
2662
2662
  function isOneDimensional(zone) {
2663
- const { width, height } = zoneToDimension(zone);
2664
- return width === 1 || height === 1;
2663
+ const { numberOfCols, numberOfRows } = zoneToDimension(zone);
2664
+ return numberOfCols === 1 || numberOfRows === 1;
2665
2665
  }
2666
2666
  /**
2667
2667
  * Array of all positions in the zone.
@@ -3136,7 +3136,7 @@ class ScorecardChart$1 extends owl.Component {
3136
3136
  return this.runtime.fontColor;
3137
3137
  }
3138
3138
  get secondaryFontColor() {
3139
- return relativeLuminance(this.primaryFontColor) <= 0.3 ? "#757575" : "#bbbbbb";
3139
+ return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
3140
3140
  }
3141
3141
  get figure() {
3142
3142
  return this.props.figure;
@@ -3748,11 +3748,11 @@ function toExcelDataset(getters, ds) {
3748
3748
  const labelZone = (_a = ds.labelCell) === null || _a === void 0 ? void 0 : _a.zone;
3749
3749
  let dataZone = ds.dataRange.zone;
3750
3750
  if (labelZone) {
3751
- const { height, width } = zoneToDimension(dataZone);
3752
- if (height === 1) {
3751
+ const { numberOfRows, numberOfCols } = zoneToDimension(dataZone);
3752
+ if (numberOfRows === 1) {
3753
3753
  dataZone = { ...dataZone, left: dataZone.left + 1 };
3754
3754
  }
3755
- else if (width === 1) {
3755
+ else if (numberOfCols === 1) {
3756
3756
  dataZone = { ...dataZone, top: dataZone.top + 1 };
3757
3757
  }
3758
3758
  }
@@ -5465,6 +5465,76 @@ chartComponentRegistry.add("pie", ChartJsComponent);
5465
5465
  chartComponentRegistry.add("gauge", ChartJsComponent);
5466
5466
  chartComponentRegistry.add("scorecard", ScorecardChart$1);
5467
5467
 
5468
+ // -----------------------------------------------------------------------------
5469
+ // STYLE
5470
+ // -----------------------------------------------------------------------------
5471
+ css /* scss */ `
5472
+ .o-chart-container {
5473
+ width: 100%;
5474
+ height: 100%;
5475
+ position: relative;
5476
+ }
5477
+ `;
5478
+ class ChartFigure extends owl.Component {
5479
+ get chartType() {
5480
+ return this.env.model.getters.getChartType(this.props.figure.id);
5481
+ }
5482
+ get chartComponent() {
5483
+ const type = this.chartType;
5484
+ const component = chartComponentRegistry.get(type);
5485
+ if (!component) {
5486
+ throw new Error(`Component is not defined for type ${type}`);
5487
+ }
5488
+ return component;
5489
+ }
5490
+ }
5491
+ ChartFigure.template = "o-spreadsheet-ChartFigure";
5492
+ ChartFigure.components = {};
5493
+ ChartFigure.props = {
5494
+ figure: Object,
5495
+ onFigureDeleted: Function,
5496
+ };
5497
+
5498
+ function interactiveCut(env) {
5499
+ const result = env.model.dispatch("CUT");
5500
+ if (!result.isSuccessful) {
5501
+ if (result.isCancelledBecause(19 /* CommandResult.WrongCutSelection */)) {
5502
+ env.raiseError(_lt("This operation is not allowed with multiple selections."));
5503
+ }
5504
+ }
5505
+ }
5506
+
5507
+ const PasteInteractiveContent = {
5508
+ wrongPasteSelection: _lt("This operation is not allowed with multiple selections."),
5509
+ willRemoveExistingMerge: _lt("This operation is not possible due to a merge. Please remove the merges first than try again."),
5510
+ wrongFigurePasteOption: _lt("Cannot do a special paste of a figure."),
5511
+ frozenPaneOverlap: _lt("Cannot paste merged cells over a frozen pane."),
5512
+ };
5513
+ function handlePasteResult(env, result) {
5514
+ if (!result.isSuccessful) {
5515
+ if (result.reasons.includes(20 /* CommandResult.WrongPasteSelection */)) {
5516
+ env.raiseError(PasteInteractiveContent.wrongPasteSelection);
5517
+ }
5518
+ else if (result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
5519
+ env.raiseError(PasteInteractiveContent.willRemoveExistingMerge);
5520
+ }
5521
+ else if (result.reasons.includes(22 /* CommandResult.WrongFigurePasteOption */)) {
5522
+ env.raiseError(PasteInteractiveContent.wrongFigurePasteOption);
5523
+ }
5524
+ else if (result.reasons.includes(75 /* CommandResult.FrozenPaneOverlap */)) {
5525
+ env.raiseError(PasteInteractiveContent.frozenPaneOverlap);
5526
+ }
5527
+ }
5528
+ }
5529
+ function interactivePaste(env, target, pasteOption) {
5530
+ const result = env.model.dispatch("PASTE", { target, pasteOption });
5531
+ handlePasteResult(env, result);
5532
+ }
5533
+ function interactivePasteFromOS(env, target, text) {
5534
+ const result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", { target, text });
5535
+ handlePasteResult(env, result);
5536
+ }
5537
+
5468
5538
  function createMenu(menuItems) {
5469
5539
  return menuItems.map(createMenuItem).sort((a, b) => a.sequence - b.sequence);
5470
5540
  }
@@ -5545,979 +5615,276 @@ class MenuItemRegistry extends Registry {
5545
5615
  }
5546
5616
 
5547
5617
  /**
5548
- * Return the o-spreadsheet element position relative
5549
- * to the browser viewport.
5618
+ * Create a function used to create a Chart based on the definition
5550
5619
  */
5551
- function useSpreadsheetRect() {
5552
- const position = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
5553
- let spreadsheetElement = document.querySelector(".o-spreadsheet");
5554
- updatePosition();
5555
- function updatePosition() {
5556
- if (!spreadsheetElement) {
5557
- spreadsheetElement = document.querySelector(".o-spreadsheet");
5558
- }
5559
- if (spreadsheetElement) {
5560
- const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();
5561
- position.x = left;
5562
- position.y = top;
5563
- position.width = width;
5564
- position.height = height;
5620
+ function chartFactory(getters) {
5621
+ const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
5622
+ function createChart(id, definition, sheetId) {
5623
+ const builder = builders.find((builder) => builder.match(definition.type));
5624
+ if (!builder) {
5625
+ throw new Error(`No builder for this chart: ${definition.type}`);
5565
5626
  }
5627
+ return builder.createChart(definition, sheetId, getters);
5566
5628
  }
5567
- owl.onMounted(updatePosition);
5568
- owl.onPatched(updatePosition);
5569
- return position;
5629
+ return createChart;
5570
5630
  }
5571
5631
  /**
5572
- * Return the component (or ref's component) BoundingRect, relative
5573
- * to the upper left corner of the screen (<body> element).
5574
- *
5575
- * Note: when used with a <Portal/> component, it will
5576
- * return the portal position, not the teleported position.
5632
+ * Create a function used to create a Chart Runtime based on the chart class
5633
+ * instance
5577
5634
  */
5578
- function useAbsoluteBoundingRect(ref) {
5579
- const rect = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
5580
- function updateElRect() {
5581
- const el = ref.el;
5582
- if (el === null) {
5583
- return;
5635
+ function chartRuntimeFactory(getters) {
5636
+ const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
5637
+ function createRuntimeChart(chart) {
5638
+ const builder = builders.find((builder) => builder.match(chart.type));
5639
+ if (!builder) {
5640
+ throw new Error("No runtime builder for this chart.");
5584
5641
  }
5585
- const { top, left, width, height } = el.getBoundingClientRect();
5586
- rect.x = left;
5587
- rect.y = top;
5588
- rect.width = width;
5589
- rect.height = height;
5642
+ return builder.getChartRuntime(chart, getters);
5590
5643
  }
5591
- owl.onMounted(updateElRect);
5592
- owl.onPatched(updateElRect);
5593
- return rect;
5644
+ return createRuntimeChart;
5594
5645
  }
5595
5646
  /**
5596
- * Get the rectangle inside which a popover should stay when being displayed.
5597
- * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
5598
- * element by default.
5599
- *
5600
- * Coordinates are expressed expressed as absolute DOM position.
5647
+ * Validate the chart definition given in arguments
5601
5648
  */
5602
- function usePopoverContainer() {
5603
- const container = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
5604
- const component = owl.useComponent();
5605
- const spreadsheetRect = useSpreadsheetRect();
5606
- function updateRect() {
5607
- const env = component.env;
5608
- const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
5609
- container.x = newRect.x;
5610
- container.y = newRect.y;
5611
- container.width = newRect.width;
5612
- container.height = newRect.height;
5649
+ function validateChartDefinition(validator, definition) {
5650
+ const validators = chartRegistry.getAll().find((validator) => validator.match(definition.type));
5651
+ if (!validators) {
5652
+ throw new Error("Unknown chart type.");
5613
5653
  }
5614
- updateRect();
5615
- owl.onMounted(updateRect);
5616
- owl.onPatched(updateRect);
5617
- return container;
5618
- }
5619
-
5654
+ return validators.validateChartDefinition(validator, definition);
5655
+ }
5620
5656
  /**
5621
- * Return true if the event was triggered from
5622
- * a child element.
5657
+ * Get a new chart definition transformed with the executed command. This
5658
+ * functions will be called during operational transform process
5623
5659
  */
5624
- function isChildEvent(parent, ev) {
5625
- return !!ev.target && parent.contains(ev.target);
5626
- }
5627
- function gridOverlayPosition() {
5628
- const spreadsheetElement = document.querySelector(".o-grid-overlay");
5629
- if (spreadsheetElement) {
5630
- const { top, left } = spreadsheetElement === null || spreadsheetElement === void 0 ? void 0 : spreadsheetElement.getBoundingClientRect();
5631
- return { top, left };
5660
+ function transformDefinition(definition, executed) {
5661
+ const transformation = chartRegistry.getAll().find((factory) => factory.match(definition.type));
5662
+ if (!transformation) {
5663
+ throw new Error("Unknown chart type.");
5632
5664
  }
5633
- throw new Error("Can't find spreadsheet position");
5634
- }
5635
- function getBoundingRectAsPOJO(el) {
5636
- const rect = el.getBoundingClientRect();
5637
- return {
5638
- x: rect.x,
5639
- y: rect.y,
5640
- width: rect.width,
5641
- height: rect.height,
5642
- };
5665
+ return transformation.transformDefinition(definition, executed);
5643
5666
  }
5644
5667
  /**
5645
- * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
5668
+ * Get an empty definition based on the given context and the given type
5646
5669
  */
5647
- function* iterateChildren(el) {
5648
- yield el;
5649
- if (el.hasChildNodes()) {
5650
- for (let child of el.childNodes) {
5651
- yield* iterateChildren(child);
5652
- }
5653
- }
5654
- }
5655
- function getElementScrollTop(el) {
5656
- return (el === null || el === void 0 ? void 0 : el.scrollTop) || 0;
5670
+ function getChartDefinitionFromContextCreation(context, type) {
5671
+ const chartClass = chartRegistry.get(type);
5672
+ return chartClass.getChartDefinitionFromContextCreation(context);
5657
5673
  }
5658
- function setElementScrollTop(el, scroll) {
5659
- if (!el)
5660
- return;
5661
- el.scrollTop = scroll;
5674
+ function getChartTypes() {
5675
+ const result = {};
5676
+ for (const key of chartRegistry.getKeys()) {
5677
+ result[key] = chartRegistry.get(key).name;
5678
+ }
5679
+ return result;
5662
5680
  }
5663
- function getOpenedMenus() {
5664
- return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
5665
- }
5666
-
5667
5681
  /**
5668
- * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap
5682
+ * Return a "smart" chart definition in the given zone. The definition is "smart" because it will
5683
+ * use the best type of chart to display the data of the zone.
5684
+ *
5685
+ * It will also try to find labels and datasets in the range, and try to find title for the datasets.
5686
+ *
5687
+ * The type of chart will be :
5688
+ * - If the zone is a single non-empty cell, returns a scorecard
5689
+ * - If the all the labels are numbers/date, returns a line chart
5690
+ * - Else returns a bar chart
5669
5691
  */
5670
- function rectIntersection(rect1, rect2) {
5671
- return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2)));
5672
- }
5673
- function rectToZone(rect) {
5692
+ function getSmartChartDefinition(zone, getters) {
5693
+ var _a;
5694
+ let dataSetZone = zone;
5695
+ if (zone.left !== zone.right) {
5696
+ dataSetZone = { ...zone, left: zone.left + 1 };
5697
+ }
5698
+ const dataSets = [zoneToXc(dataSetZone)];
5699
+ const sheetId = getters.getActiveSheetId();
5700
+ const topLeftCell = getters.getCell({ sheetId, col: zone.left, row: zone.top });
5701
+ if (getZoneArea(zone) === 1 && (topLeftCell === null || topLeftCell === void 0 ? void 0 : topLeftCell.content)) {
5702
+ return {
5703
+ type: "scorecard",
5704
+ title: "",
5705
+ background: ((_a = topLeftCell.style) === null || _a === void 0 ? void 0 : _a.fillColor) || undefined,
5706
+ keyValue: zoneToXc(zone),
5707
+ baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
5708
+ baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
5709
+ baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
5710
+ };
5711
+ }
5712
+ let title = "";
5713
+ const cellsInFirstRow = getters.getEvaluatedCellsInZone(sheetId, {
5714
+ ...dataSetZone,
5715
+ bottom: dataSetZone.top,
5716
+ });
5717
+ const dataSetsHaveTitle = !!cellsInFirstRow.find((cell) => cell.type !== CellValueType.empty && cell.type !== CellValueType.number);
5718
+ if (dataSetsHaveTitle) {
5719
+ const texts = cellsInFirstRow
5720
+ .filter((cell) => cell.type !== CellValueType.error && cell.type !== CellValueType.empty)
5721
+ .map((cell) => cell.formattedValue);
5722
+ const lastElement = texts.splice(-1)[0];
5723
+ title = texts.join(", ");
5724
+ if (lastElement) {
5725
+ title += (title ? " " + _t("and") + " " : "") + lastElement;
5726
+ }
5727
+ }
5728
+ let labelRangeXc;
5729
+ if (zone.left !== zone.right) {
5730
+ labelRangeXc = zoneToXc({
5731
+ ...zone,
5732
+ right: zone.left,
5733
+ top: dataSetsHaveTitle ? zone.top + 1 : zone.top,
5734
+ });
5735
+ }
5736
+ // Only display legend for several datasets.
5737
+ const newLegendPos = dataSetZone.right === dataSetZone.left ? "none" : "top";
5738
+ const labelRange = labelRangeXc ? getters.getRangeFromSheetXC(sheetId, labelRangeXc) : undefined;
5739
+ if (canChartParseLabels(labelRange, getters)) {
5740
+ return {
5741
+ title,
5742
+ dataSets,
5743
+ labelsAsText: false,
5744
+ stacked: false,
5745
+ aggregated: false,
5746
+ labelRange: labelRangeXc,
5747
+ type: "line",
5748
+ dataSetsHaveTitle,
5749
+ verticalAxisPosition: "left",
5750
+ legendPosition: newLegendPos,
5751
+ };
5752
+ }
5674
5753
  return {
5675
- left: rect.x,
5676
- top: rect.y,
5677
- right: rect.x + rect.width,
5678
- bottom: rect.y + rect.height,
5754
+ title,
5755
+ dataSets,
5756
+ labelRange: labelRangeXc,
5757
+ type: "bar",
5758
+ stacked: false,
5759
+ aggregated: false,
5760
+ dataSetsHaveTitle,
5761
+ verticalAxisPosition: "left",
5762
+ legendPosition: newLegendPos,
5679
5763
  };
5764
+ }
5765
+
5766
+ function centerFigurePosition(getters, size) {
5767
+ const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();
5768
+ const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
5769
+ const dim = getters.getSheetViewDimension();
5770
+ const rect = getters.getVisibleRect(getters.getActiveMainViewport());
5771
+ const scrollableViewportWidth = Math.min(rect.width, dim.width - offsetCorrectionX);
5772
+ const scrollableViewportHeight = Math.min(rect.height, dim.height - offsetCorrectionY);
5773
+ const position = {
5774
+ x: offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2),
5775
+ y: offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2),
5776
+ }; // Position at the center of the scrollable viewport
5777
+ return position;
5680
5778
  }
5681
- function zoneToRect(zone) {
5682
- if (!zone)
5683
- return undefined;
5684
- return {
5685
- x: zone.left,
5686
- y: zone.top,
5687
- width: zone.right - zone.left,
5688
- height: zone.bottom - zone.top,
5689
- };
5779
+ function getMaxFigureSize(getters, figureSize) {
5780
+ const size = deepCopy(figureSize);
5781
+ const dim = getters.getSheetViewDimension();
5782
+ const maxWidth = dim.width;
5783
+ const maxHeight = dim.height;
5784
+ if (size.width > maxWidth) {
5785
+ const ratio = maxWidth / size.width;
5786
+ size.width = maxWidth;
5787
+ size.height = size.height * ratio;
5788
+ }
5789
+ if (size.height > maxHeight) {
5790
+ const ratio = maxHeight / size.height;
5791
+ size.height = maxHeight;
5792
+ size.width = size.width * ratio;
5793
+ }
5794
+ return size;
5690
5795
  }
5691
5796
 
5692
- css /* scss */ `
5693
- .o-popover {
5694
- position: absolute;
5695
- z-index: ${ComponentsImportance.Popover};
5696
- overflow: auto;
5697
- box-shadow: 1px 2px 5px 2px rgb(51 51 51 / 15%);
5698
- width: fit-content;
5699
- height: fit-content;
5700
- }
5701
- `;
5702
- class Popover extends owl.Component {
5703
- constructor() {
5704
- super(...arguments);
5705
- this.popoverRef = owl.useRef("popover");
5706
- this.currentPosition = undefined;
5707
- this.currentDisplayValue = undefined;
5708
- this.spreadsheetRect = useSpreadsheetRect();
5797
+ const SORT_TYPES = [
5798
+ CellValueType.number,
5799
+ CellValueType.error,
5800
+ CellValueType.text,
5801
+ CellValueType.boolean,
5802
+ ];
5803
+ function sortCells(cells, sortDirection, emptyCellAsZero) {
5804
+ const cellsWithIndex = cells.map((cell, index) => ({
5805
+ index,
5806
+ type: cell.type,
5807
+ value: cell.value,
5808
+ }));
5809
+ let emptyCells = cellsWithIndex.filter((x) => x.type === CellValueType.empty);
5810
+ let nonEmptyCells = cellsWithIndex.filter((x) => x.type !== CellValueType.empty);
5811
+ if (emptyCellAsZero) {
5812
+ nonEmptyCells.push(...emptyCells.map((emptyCell) => ({ ...emptyCell, type: CellValueType.number, value: 0 })));
5813
+ emptyCells = [];
5709
5814
  }
5710
- setup() {
5711
- this.containerRect = usePopoverContainer();
5712
- // useEffect occurs after the DOM is created and the element width/height are computed, but before
5713
- // the element in rendered, so we can still set its position
5714
- owl.useEffect(() => {
5715
- var _a, _b, _c, _d;
5716
- if (!this.containerRect)
5717
- throw new Error("Popover container is not defined");
5718
- const el = this.popoverRef.el;
5719
- const anchor = rectIntersection(this.props.anchorRect, this.containerRect);
5720
- const newDisplay = anchor ? "block" : "none";
5721
- if (this.currentDisplayValue !== "none" && newDisplay === "none") {
5722
- (_b = (_a = this.props).onPopoverHidden) === null || _b === void 0 ? void 0 : _b.call(_a);
5815
+ const inverse = sortDirection === "descending" ? -1 : 1;
5816
+ return nonEmptyCells
5817
+ .sort((left, right) => {
5818
+ let typeOrder = SORT_TYPES.indexOf(left.type) - SORT_TYPES.indexOf(right.type);
5819
+ if (typeOrder === 0) {
5820
+ if (left.type === CellValueType.text || left.type === CellValueType.error) {
5821
+ typeOrder = left.value.localeCompare(right.value);
5723
5822
  }
5724
- el.style.display = newDisplay;
5725
- this.currentDisplayValue = newDisplay;
5726
- if (!anchor)
5727
- return;
5728
- const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
5729
- const elDims = {
5730
- width: el.getBoundingClientRect().width,
5731
- height: el.getBoundingClientRect().height,
5732
- };
5733
- const spreadsheetRect = this.spreadsheetRect;
5734
- const popoverPositionHelper = this.props.positioning === "BottomLeft"
5735
- ? new BottomLeftPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect)
5736
- : new TopRightPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect);
5737
- const style = popoverPositionHelper.getCss(elDims, this.props.verticalOffset);
5738
- for (const property of Object.keys(style)) {
5739
- el.style[property] = style[property];
5823
+ else
5824
+ typeOrder = left.value - right.value;
5825
+ }
5826
+ return inverse * typeOrder;
5827
+ })
5828
+ .concat(emptyCells);
5829
+ }
5830
+ function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
5831
+ let result = DispatchResult.Success;
5832
+ //several columns => bypass the contiguity check
5833
+ let multiColumns = zone.right > zone.left;
5834
+ if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
5835
+ multiColumns = false;
5836
+ let table;
5837
+ for (let row = zone.top; row <= zone.bottom; row++) {
5838
+ table = [];
5839
+ for (let col = zone.left; col <= zone.right; col++) {
5840
+ let merge = env.model.getters.getMerge({ sheetId, col, row });
5841
+ if (merge && !table.includes(merge.id.toString())) {
5842
+ table.push(merge.id.toString());
5843
+ }
5740
5844
  }
5741
- const newPosition = popoverPositionHelper.getCurrentPosition(elDims);
5742
- if (this.currentPosition && newPosition !== this.currentPosition) {
5743
- (_d = (_c = this.props).onPopoverMoved) === null || _d === void 0 ? void 0 : _d.call(_c);
5845
+ if (table.length >= 2) {
5846
+ multiColumns = true;
5847
+ break;
5744
5848
  }
5745
- this.currentPosition = newPosition;
5746
- });
5747
- }
5748
- }
5749
- Popover.template = "o-spreadsheet-Popover";
5750
- Popover.defaultProps = {
5751
- positioning: "BottomLeft",
5752
- verticalOffset: 0,
5753
- onMouseWheel: () => { },
5754
- onPopoverMoved: () => { },
5755
- onPopoverHidden: () => { },
5756
- };
5757
- Popover.props = {
5758
- anchorRect: Object,
5759
- containerRect: { type: Object, optional: true },
5760
- positioning: { type: String, optional: true },
5761
- maxWidth: { type: Number, optional: true },
5762
- maxHeight: { type: Number, optional: true },
5763
- verticalOffset: { type: Number, optional: true },
5764
- onMouseWheel: { type: Function, optional: true },
5765
- onPopoverHidden: { type: Function, optional: true },
5766
- onPopoverMoved: { type: Function, optional: true },
5767
- slots: Object,
5768
- };
5769
- class PopoverPositionContext {
5770
- constructor(anchorRect, containerRect, propsMaxSize, spreadsheetOffset) {
5771
- this.anchorRect = anchorRect;
5772
- this.containerRect = containerRect;
5773
- this.propsMaxSize = propsMaxSize;
5774
- this.spreadsheetOffset = spreadsheetOffset;
5849
+ }
5775
5850
  }
5776
- /** Check if there is enough space for the popover to be rendered at the bottom of the anchorRect */
5777
- shouldRenderAtBottom(elementHeight) {
5778
- return (elementHeight <= this.availableHeightDown ||
5779
- this.availableHeightDown >= this.availableHeightUp);
5851
+ const { col, row } = anchor;
5852
+ if (multiColumns) {
5853
+ result = env.model.dispatch("SORT_CELLS", { sheetId, col, row, zone, sortDirection });
5780
5854
  }
5781
- /** Check if there is enough space for the popover to be rendered at the right of the anchorRect */
5782
- shouldRenderAtRight(elementWidth) {
5783
- return (elementWidth <= this.availableWidthRight ||
5784
- this.availableWidthRight >= this.availableWidthLeft);
5785
- }
5786
- getMaxHeight(elementHeight) {
5787
- const shouldRenderAtBottom = this.shouldRenderAtBottom(elementHeight);
5788
- const availableHeight = shouldRenderAtBottom
5789
- ? this.availableHeightDown
5790
- : this.availableHeightUp;
5791
- return this.propsMaxSize.height
5792
- ? Math.min(availableHeight, this.propsMaxSize.height)
5793
- : availableHeight;
5794
- }
5795
- getMaxWidth(elementWidth) {
5796
- const shouldRenderAtRight = this.shouldRenderAtRight(elementWidth);
5797
- const availableWidth = shouldRenderAtRight ? this.availableWidthRight : this.availableWidthLeft;
5798
- return this.propsMaxSize.width
5799
- ? Math.min(availableWidth, this.propsMaxSize.width)
5800
- : availableWidth;
5801
- }
5802
- getCss(elDims, verticalOffset) {
5803
- const maxHeight = this.getMaxHeight(elDims.height);
5804
- const maxWidth = this.getMaxWidth(elDims.width);
5805
- const actualHeight = Math.min(maxHeight, elDims.height);
5806
- const actualWidth = Math.min(maxWidth, elDims.width);
5807
- const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
5808
- const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
5809
- verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset;
5810
- const cssProperties = {
5811
- "max-height": maxHeight + "px",
5812
- "max-width": maxWidth + "px",
5813
- top: this.getTopCoordinate(actualHeight, shouldRenderAtBottom) -
5814
- this.spreadsheetOffset.y -
5815
- verticalOffset +
5816
- "px",
5817
- left: this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + "px",
5818
- };
5819
- return cssProperties;
5820
- }
5821
- getCurrentPosition(elDims) {
5822
- const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
5823
- const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
5824
- if (shouldRenderAtBottom && shouldRenderAtRight)
5825
- return "BottomRight";
5826
- if (shouldRenderAtBottom && !shouldRenderAtRight)
5827
- return "BottomLeft";
5828
- if (!shouldRenderAtBottom && shouldRenderAtRight)
5829
- return "TopRight";
5830
- return "TopLeft";
5831
- }
5832
- }
5833
- class BottomLeftPopoverContext extends PopoverPositionContext {
5834
- get availableHeightUp() {
5835
- return this.anchorRect.y - this.containerRect.y;
5836
- }
5837
- get availableHeightDown() {
5838
- return this.containerRect.height - this.availableHeightUp - this.anchorRect.height;
5839
- }
5840
- get availableWidthRight() {
5841
- return this.containerRect.x + this.containerRect.width - this.anchorRect.x;
5842
- }
5843
- get availableWidthLeft() {
5844
- return this.anchorRect.x + this.anchorRect.width - this.containerRect.x;
5845
- }
5846
- getTopCoordinate(elementHeight, shouldRenderAtBottom) {
5847
- if (shouldRenderAtBottom) {
5848
- return this.anchorRect.y + this.anchorRect.height;
5849
- }
5850
- else {
5851
- return this.anchorRect.y - elementHeight;
5852
- }
5853
- }
5854
- getLeftCoordinate(elementWidth, shouldRenderAtRight) {
5855
- if (shouldRenderAtRight) {
5856
- return this.anchorRect.x;
5857
- }
5858
- else {
5859
- return this.anchorRect.x + this.anchorRect.width - elementWidth;
5860
- }
5861
- }
5862
- }
5863
- class TopRightPopoverContext extends PopoverPositionContext {
5864
- get availableHeightUp() {
5865
- return this.anchorRect.y + this.anchorRect.height - this.containerRect.y;
5866
- }
5867
- get availableHeightDown() {
5868
- return this.containerRect.y + this.containerRect.height - this.anchorRect.y;
5869
- }
5870
- get availableWidthRight() {
5871
- return this.containerRect.width - this.anchorRect.width - this.availableWidthLeft;
5872
- }
5873
- get availableWidthLeft() {
5874
- return this.anchorRect.x - this.containerRect.x;
5875
- }
5876
- getTopCoordinate(elementHeight, shouldRenderAtBottom) {
5877
- if (shouldRenderAtBottom) {
5878
- return this.anchorRect.y;
5879
- }
5880
- else {
5881
- return this.anchorRect.y + this.anchorRect.height - elementHeight;
5882
- }
5883
- }
5884
- getLeftCoordinate(elementWidth, shouldRenderAtRight) {
5885
- if (shouldRenderAtRight) {
5886
- return this.anchorRect.x + this.anchorRect.width;
5887
- }
5888
- else {
5889
- return this.anchorRect.x - elementWidth;
5890
- }
5891
- }
5892
- }
5893
-
5894
- //------------------------------------------------------------------------------
5895
- // Context Menu Component
5896
- //------------------------------------------------------------------------------
5897
- css /* scss */ `
5898
- .o-menu {
5899
- background-color: white;
5900
- padding: ${MENU_VERTICAL_PADDING}px 0px;
5901
- width: ${MENU_WIDTH}px;
5902
- box-sizing: border-box !important;
5903
-
5904
- .o-menu-item {
5905
- display: flex;
5906
- justify-content: space-between;
5907
- align-items: center;
5908
- box-sizing: border-box;
5909
- height: ${MENU_ITEM_HEIGHT}px;
5910
- padding: 4px 16px;
5911
- cursor: pointer;
5912
- user-select: none;
5913
-
5914
- .o-menu-item-name {
5915
- overflow: hidden;
5916
- white-space: nowrap;
5917
- text-overflow: ellipsis;
5918
- }
5919
-
5920
- &.o-menu-root {
5921
- display: flex;
5922
- justify-content: space-between;
5923
- }
5924
- .o-menu-item-icon {
5925
- margin-top: auto;
5926
- margin-bottom: auto;
5927
- }
5928
- .o-icon {
5929
- width: 10px;
5930
- }
5931
-
5932
- &:not(.disabled) {
5933
- &:hover,
5934
- &.o-menu-item-active {
5935
- background-color: #ebebeb;
5936
- }
5937
- .o-menu-item-description {
5938
- color: grey;
5939
- }
5940
- }
5941
- &.disabled {
5942
- color: ${MENU_ITEM_DISABLED_COLOR};
5943
- cursor: not-allowed;
5944
- }
5945
- }
5946
- }
5947
- `;
5948
- class Menu extends owl.Component {
5949
- constructor() {
5950
- super(...arguments);
5951
- this.subMenu = owl.useState({
5952
- isOpen: false,
5953
- position: null,
5954
- scrollOffset: 0,
5955
- menuItems: [],
5956
- });
5957
- this.menuRef = owl.useRef("menu");
5958
- this.position = useAbsoluteBoundingRect(this.menuRef);
5959
- }
5960
- setup() {
5961
- owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
5962
- owl.useExternalListener(window, "contextmenu", this.onExternalClick, { capture: true });
5963
- owl.onWillUpdateProps((nextProps) => {
5964
- if (nextProps.menuItems !== this.props.menuItems) {
5965
- this.closeSubMenu();
5966
- }
5967
- });
5968
- }
5969
- get visibleMenuItems() {
5970
- return this.props.menuItems.filter((x) => x.isVisible(this.env));
5971
- }
5972
- get subMenuPosition() {
5973
- const position = Object.assign({}, this.subMenu.position);
5974
- position.y -= this.subMenu.scrollOffset || 0;
5975
- return position;
5976
- }
5977
- get menuHeight() {
5978
- const menuItems = this.visibleMenuItems;
5979
- let menuItemsHeight = this.getMenuItemsHeight(menuItems);
5980
- // We don't display separator at the end of a menu
5981
- if (menuItems[menuItems.length - 1].separator) {
5982
- menuItemsHeight -= MENU_SEPARATOR_HEIGHT;
5983
- }
5984
- const menuHeight = 2 * MENU_VERTICAL_PADDING + menuItemsHeight;
5985
- return this.props.maxHeight ? Math.min(menuHeight, this.props.maxHeight) : menuHeight;
5986
- }
5987
- get popoverProps() {
5988
- const isRoot = this.props.depth === 1;
5989
- return {
5990
- anchorRect: {
5991
- x: this.props.position.x - MENU_WIDTH * (this.props.depth - 1),
5992
- y: this.props.position.y,
5993
- width: isRoot ? 0 : MENU_WIDTH,
5994
- height: isRoot ? 0 : MENU_ITEM_HEIGHT,
5995
- },
5996
- positioning: "TopRight",
5997
- verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
5998
- onPopoverHidden: () => this.closeSubMenu(),
5999
- onPopoverMoved: () => this.closeSubMenu(),
6000
- maxHeight: this.menuHeight,
6001
- };
6002
- }
6003
- getColor(menu) {
6004
- return menu.textColor ? `color: ${menu.textColor}` : undefined;
6005
- }
6006
- async activateMenu(menu) {
6007
- var _a, _b, _c;
6008
- const result = await ((_a = menu.action) === null || _a === void 0 ? void 0 : _a.call(menu, this.env));
6009
- this.close();
6010
- (_c = (_b = this.props).onMenuClicked) === null || _c === void 0 ? void 0 : _c.call(_b, { detail: result });
6011
- }
6012
- close() {
6013
- this.closeSubMenu();
6014
- this.props.onClose();
6015
- }
6016
- /**
6017
- * Return the number of pixels between the top of the menu
6018
- * and the menu item at a given index.
6019
- */
6020
- subMenuVerticalPosition(menuIndex) {
6021
- const menusAbove = this.visibleMenuItems.slice(0, menuIndex);
6022
- return this.position.y + this.getMenuItemsHeight(menusAbove) + MENU_VERTICAL_PADDING;
6023
- }
6024
- onExternalClick(ev) {
6025
- // Don't close a root menu when clicked to open the submenus.
6026
- const el = this.menuRef.el;
6027
- if (el && getOpenedMenus().some((el) => isChildEvent(el, ev))) {
6028
- return;
6029
- }
6030
- ev.closedMenuId = this.props.menuId;
6031
- this.close();
6032
- }
6033
- getMenuItemsHeight(menuItems) {
6034
- const numberOfSeparators = menuItems.filter((m) => m.separator).length;
6035
- return MENU_ITEM_HEIGHT * menuItems.length + MENU_SEPARATOR_HEIGHT * numberOfSeparators;
6036
- }
6037
- getName(menu) {
6038
- return menu.name(this.env);
6039
- }
6040
- isRoot(menu) {
6041
- return !menu.action;
6042
- }
6043
- isEnabled(menu) {
6044
- if (menu.isEnabled(this.env)) {
6045
- return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
6046
- }
6047
- return false;
6048
- }
6049
- onScroll(ev) {
6050
- this.subMenu.scrollOffset = ev.target.scrollTop;
6051
- }
6052
- /**
6053
- * If the given menu is not disabled, open it's submenu at the
6054
- * correct position according to available surrounding space.
6055
- */
6056
- openSubMenu(menu, menuIndex) {
6057
- const y = this.subMenuVerticalPosition(menuIndex);
6058
- this.subMenu.position = {
6059
- x: this.position.x + this.props.depth * MENU_WIDTH,
6060
- y: y - (this.subMenu.scrollOffset || 0),
6061
- };
6062
- this.subMenu.menuItems = menu.children(this.env);
6063
- this.subMenu.isOpen = true;
6064
- this.subMenu.parentMenu = menu;
6065
- }
6066
- isParentMenu(subMenu, menuItem) {
6067
- var _a;
6068
- return ((_a = subMenu.parentMenu) === null || _a === void 0 ? void 0 : _a.id) === menuItem.id;
6069
- }
6070
- closeSubMenu() {
6071
- this.subMenu.isOpen = false;
6072
- this.subMenu.parentMenu = undefined;
6073
- }
6074
- onClickMenu(menu, menuIndex) {
6075
- if (this.isEnabled(menu)) {
6076
- if (this.isRoot(menu)) {
6077
- this.openSubMenu(menu, menuIndex);
6078
- }
6079
- else {
6080
- this.activateMenu(menu);
6081
- }
6082
- }
6083
- }
6084
- onMouseOver(menu, position) {
6085
- if (menu.isEnabled(this.env)) {
6086
- if (this.isRoot(menu)) {
6087
- this.openSubMenu(menu, position);
6088
- }
6089
- else {
6090
- this.closeSubMenu();
6091
- }
6092
- }
6093
- }
6094
- }
6095
- Menu.template = "o-spreadsheet-Menu";
6096
- Menu.components = { Menu, Popover };
6097
- Menu.defaultProps = {
6098
- depth: 1,
6099
- };
6100
- Menu.props = {
6101
- position: Object,
6102
- menuItems: Array,
6103
- depth: { type: Number, optional: true },
6104
- maxHeight: { type: Number, optional: true },
6105
- onClose: Function,
6106
- onMenuClicked: { type: Function, optional: true },
6107
- menuId: { type: String, optional: true },
6108
- };
6109
-
6110
- // -----------------------------------------------------------------------------
6111
- // STYLE
6112
- // -----------------------------------------------------------------------------
6113
- css /* scss */ `
6114
- .o-chart-container {
6115
- width: 100%;
6116
- height: 100%;
6117
- position: relative;
6118
- }
6119
- `;
6120
- class ChartFigure extends owl.Component {
6121
- constructor() {
6122
- super(...arguments);
6123
- this.menuState = owl.useState({ isOpen: false, position: null, menuItems: [] });
6124
- this.chartContainerRef = owl.useRef("chartContainer");
6125
- this.menuButtonRef = owl.useRef("menuButton");
6126
- this.menuButtonRect = useAbsoluteBoundingRect(this.menuButtonRef);
6127
- this.position = useAbsoluteBoundingRect(this.chartContainerRef);
6128
- }
6129
- getMenuItemRegistry() {
6130
- const registry = new MenuItemRegistry();
6131
- registry.add("edit", {
6132
- name: _lt("Edit"),
6133
- sequence: 1,
6134
- action: () => {
6135
- this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
6136
- this.env.openSidePanel("ChartPanel");
6137
- },
6138
- });
6139
- registry.add("copy", {
6140
- name: _lt("Copy"),
6141
- sequence: 2,
6142
- action: async () => {
6143
- this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
6144
- this.env.model.dispatch("COPY");
6145
- await this.env.clipboard.write(this.env.model.getters.getClipboardContent());
6146
- },
6147
- });
6148
- registry.add("cut", {
6149
- name: _lt("Cut"),
6150
- sequence: 3,
6151
- action: async () => {
6152
- this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
6153
- this.env.model.dispatch("CUT");
6154
- await this.env.clipboard.write(this.env.model.getters.getClipboardContent());
6155
- },
6156
- });
6157
- registry.add("delete", {
6158
- name: _lt("Delete"),
6159
- sequence: 10,
6160
- action: () => {
6161
- this.env.model.dispatch("DELETE_FIGURE", {
6162
- sheetId: this.env.model.getters.getActiveSheetId(),
6163
- id: this.props.figure.id,
6164
- });
6165
- this.props.onFigureDeleted();
6166
- },
6167
- });
6168
- return registry;
6169
- }
6170
- get chartType() {
6171
- return this.env.model.getters.getChartType(this.props.figure.id);
6172
- }
6173
- onContextMenu(ev) {
6174
- const position = {
6175
- x: this.position.x + ev.offsetX,
6176
- y: this.position.y + ev.offsetY,
6177
- };
6178
- this.openContextMenu(position);
6179
- }
6180
- showMenu() {
6181
- const { x, y, width } = this.menuButtonRect;
6182
- const menuPosition = {
6183
- x: x >= MENU_WIDTH ? x - MENU_WIDTH : x + width,
6184
- y: y,
6185
- };
6186
- this.openContextMenu(menuPosition);
6187
- }
6188
- openContextMenu(position) {
6189
- const registry = this.getMenuItemRegistry();
6190
- this.menuState.isOpen = true;
6191
- this.menuState.menuItems = registry.getMenuItems();
6192
- this.menuState.position = position;
6193
- }
6194
- get chartComponent() {
6195
- const type = this.chartType;
6196
- const component = chartComponentRegistry.get(type);
6197
- if (!component) {
6198
- throw new Error(`Component is not defined for type ${type}`);
6199
- }
6200
- return component;
6201
- }
6202
- }
6203
- ChartFigure.template = "o-spreadsheet-ChartFigure";
6204
- ChartFigure.components = { Menu };
6205
- ChartFigure.props = {
6206
- figure: Object,
6207
- onFigureDeleted: Function,
6208
- };
6209
-
6210
- function interactiveCut(env) {
6211
- const result = env.model.dispatch("CUT");
6212
- if (!result.isSuccessful) {
6213
- if (result.isCancelledBecause(19 /* CommandResult.WrongCutSelection */)) {
6214
- env.raiseError(_lt("This operation is not allowed with multiple selections."));
6215
- }
6216
- }
6217
- }
6218
-
6219
- const PasteInteractiveContent = {
6220
- wrongPasteSelection: _lt("This operation is not allowed with multiple selections."),
6221
- willRemoveExistingMerge: _lt("This operation is not possible due to a merge. Please remove the merges first than try again."),
6222
- wrongFigurePasteOption: _lt("Cannot do a special paste of a figure."),
6223
- frozenPaneOverlap: _lt("Cannot paste merged cells over a frozen pane."),
6224
- };
6225
- function handlePasteResult(env, result) {
6226
- if (!result.isSuccessful) {
6227
- if (result.reasons.includes(20 /* CommandResult.WrongPasteSelection */)) {
6228
- env.raiseError(PasteInteractiveContent.wrongPasteSelection);
6229
- }
6230
- else if (result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
6231
- env.raiseError(PasteInteractiveContent.willRemoveExistingMerge);
6232
- }
6233
- else if (result.reasons.includes(22 /* CommandResult.WrongFigurePasteOption */)) {
6234
- env.raiseError(PasteInteractiveContent.wrongFigurePasteOption);
6235
- }
6236
- else if (result.reasons.includes(75 /* CommandResult.FrozenPaneOverlap */)) {
6237
- env.raiseError(PasteInteractiveContent.frozenPaneOverlap);
6238
- }
6239
- }
6240
- }
6241
- function interactivePaste(env, target, pasteOption) {
6242
- const result = env.model.dispatch("PASTE", { target, pasteOption });
6243
- handlePasteResult(env, result);
6244
- }
6245
- function interactivePasteFromOS(env, target, text) {
6246
- const result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", { target, text });
6247
- handlePasteResult(env, result);
6248
- }
6249
-
6250
- /**
6251
- * Create a function used to create a Chart based on the definition
6252
- */
6253
- function chartFactory(getters) {
6254
- const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
6255
- function createChart(id, definition, sheetId) {
6256
- const builder = builders.find((builder) => builder.match(definition.type));
6257
- if (!builder) {
6258
- throw new Error(`No builder for this chart: ${definition.type}`);
6259
- }
6260
- return builder.createChart(definition, sheetId, getters);
6261
- }
6262
- return createChart;
6263
- }
6264
- /**
6265
- * Create a function used to create a Chart Runtime based on the chart class
6266
- * instance
6267
- */
6268
- function chartRuntimeFactory(getters) {
6269
- const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
6270
- function createRuntimeChart(chart) {
6271
- const builder = builders.find((builder) => builder.match(chart.type));
6272
- if (!builder) {
6273
- throw new Error("No runtime builder for this chart.");
6274
- }
6275
- return builder.getChartRuntime(chart, getters);
6276
- }
6277
- return createRuntimeChart;
6278
- }
6279
- /**
6280
- * Validate the chart definition given in arguments
6281
- */
6282
- function validateChartDefinition(validator, definition) {
6283
- const validators = chartRegistry.getAll().find((validator) => validator.match(definition.type));
6284
- if (!validators) {
6285
- throw new Error("Unknown chart type.");
6286
- }
6287
- return validators.validateChartDefinition(validator, definition);
6288
- }
6289
- /**
6290
- * Get a new chart definition transformed with the executed command. This
6291
- * functions will be called during operational transform process
6292
- */
6293
- function transformDefinition(definition, executed) {
6294
- const transformation = chartRegistry.getAll().find((factory) => factory.match(definition.type));
6295
- if (!transformation) {
6296
- throw new Error("Unknown chart type.");
6297
- }
6298
- return transformation.transformDefinition(definition, executed);
6299
- }
6300
- /**
6301
- * Get an empty definition based on the given context and the given type
6302
- */
6303
- function getChartDefinitionFromContextCreation(context, type) {
6304
- const chartClass = chartRegistry.get(type);
6305
- return chartClass.getChartDefinitionFromContextCreation(context);
6306
- }
6307
- function getChartTypes() {
6308
- const result = {};
6309
- for (const key of chartRegistry.getKeys()) {
6310
- result[key] = chartRegistry.get(key).name;
6311
- }
6312
- return result;
6313
- }
6314
- /**
6315
- * Return a "smart" chart definition in the given zone. The definition is "smart" because it will
6316
- * use the best type of chart to display the data of the zone.
6317
- *
6318
- * It will also try to find labels and datasets in the range, and try to find title for the datasets.
6319
- *
6320
- * The type of chart will be :
6321
- * - If the zone is a single non-empty cell, returns a scorecard
6322
- * - If the all the labels are numbers/date, returns a line chart
6323
- * - Else returns a bar chart
6324
- */
6325
- function getSmartChartDefinition(zone, getters) {
6326
- var _a;
6327
- let dataSetZone = zone;
6328
- if (zone.left !== zone.right) {
6329
- dataSetZone = { ...zone, left: zone.left + 1 };
6330
- }
6331
- const dataSets = [zoneToXc(dataSetZone)];
6332
- const sheetId = getters.getActiveSheetId();
6333
- const topLeftCell = getters.getCell({ sheetId, col: zone.left, row: zone.top });
6334
- if (getZoneArea(zone) === 1 && (topLeftCell === null || topLeftCell === void 0 ? void 0 : topLeftCell.content)) {
6335
- return {
6336
- type: "scorecard",
6337
- title: "",
6338
- background: ((_a = topLeftCell.style) === null || _a === void 0 ? void 0 : _a.fillColor) || undefined,
6339
- keyValue: zoneToXc(zone),
6340
- baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
6341
- baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
6342
- baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
6343
- };
6344
- }
6345
- let title = "";
6346
- const cellsInFirstRow = getters.getEvaluatedCellsInZone(sheetId, {
6347
- ...dataSetZone,
6348
- bottom: dataSetZone.top,
6349
- });
6350
- const dataSetsHaveTitle = !!cellsInFirstRow.find((cell) => cell.type !== CellValueType.empty && cell.type !== CellValueType.number);
6351
- if (dataSetsHaveTitle) {
6352
- const texts = cellsInFirstRow
6353
- .filter((cell) => cell.type !== CellValueType.error && cell.type !== CellValueType.empty)
6354
- .map((cell) => cell.formattedValue);
6355
- const lastElement = texts.splice(-1)[0];
6356
- title = texts.join(", ");
6357
- if (lastElement) {
6358
- title += (title ? " " + _t("and") + " " : "") + lastElement;
6359
- }
6360
- }
6361
- let labelRangeXc;
6362
- if (zone.left !== zone.right) {
6363
- labelRangeXc = zoneToXc({
6364
- ...zone,
6365
- right: zone.left,
6366
- top: dataSetsHaveTitle ? zone.top + 1 : zone.top,
6367
- });
6368
- }
6369
- // Only display legend for several datasets.
6370
- const newLegendPos = dataSetZone.right === dataSetZone.left ? "none" : "top";
6371
- const labelRange = labelRangeXc ? getters.getRangeFromSheetXC(sheetId, labelRangeXc) : undefined;
6372
- if (canChartParseLabels(labelRange, getters)) {
6373
- return {
6374
- title,
6375
- dataSets,
6376
- labelsAsText: false,
6377
- stacked: false,
6378
- aggregated: false,
6379
- labelRange: labelRangeXc,
6380
- type: "line",
6381
- dataSetsHaveTitle,
6382
- verticalAxisPosition: "left",
6383
- legendPosition: newLegendPos,
6384
- };
6385
- }
6386
- return {
6387
- title,
6388
- dataSets,
6389
- labelRange: labelRangeXc,
6390
- type: "bar",
6391
- stacked: false,
6392
- aggregated: false,
6393
- dataSetsHaveTitle,
6394
- verticalAxisPosition: "left",
6395
- legendPosition: newLegendPos,
6396
- };
6397
- }
6398
-
6399
- function centerFigurePosition(getters, size) {
6400
- const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();
6401
- const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
6402
- const dim = getters.getSheetViewDimension();
6403
- const rect = getters.getVisibleRect(getters.getActiveMainViewport());
6404
- const scrollableViewportWidth = Math.min(rect.width, dim.width - offsetCorrectionX);
6405
- const scrollableViewportHeight = Math.min(rect.height, dim.height - offsetCorrectionY);
6406
- const position = {
6407
- x: offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2),
6408
- y: offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2),
6409
- }; // Position at the center of the scrollable viewport
6410
- return position;
6411
- }
6412
- function getMaxFigureSize(getters, figureSize) {
6413
- const size = deepCopy(figureSize);
6414
- const dim = getters.getSheetViewDimension();
6415
- const maxWidth = dim.width;
6416
- const maxHeight = dim.height;
6417
- if (size.width > maxWidth) {
6418
- const ratio = maxWidth / size.width;
6419
- size.width = maxWidth;
6420
- size.height = size.height * ratio;
6421
- }
6422
- if (size.height > maxHeight) {
6423
- const ratio = maxHeight / size.height;
6424
- size.height = maxHeight;
6425
- size.width = size.width * ratio;
6426
- }
6427
- return size;
6428
- }
6429
-
6430
- const SORT_TYPES = [
6431
- CellValueType.number,
6432
- CellValueType.error,
6433
- CellValueType.text,
6434
- CellValueType.boolean,
6435
- ];
6436
- function sortCells(cells, sortDirection, emptyCellAsZero) {
6437
- const cellsWithIndex = cells.map((cell, index) => ({
6438
- index,
6439
- type: cell.type,
6440
- value: cell.value,
6441
- }));
6442
- let emptyCells = cellsWithIndex.filter((x) => x.type === CellValueType.empty);
6443
- let nonEmptyCells = cellsWithIndex.filter((x) => x.type !== CellValueType.empty);
6444
- if (emptyCellAsZero) {
6445
- nonEmptyCells.push(...emptyCells.map((emptyCell) => ({ ...emptyCell, type: CellValueType.number, value: 0 })));
6446
- emptyCells = [];
6447
- }
6448
- const inverse = sortDirection === "descending" ? -1 : 1;
6449
- return nonEmptyCells
6450
- .sort((left, right) => {
6451
- let typeOrder = SORT_TYPES.indexOf(left.type) - SORT_TYPES.indexOf(right.type);
6452
- if (typeOrder === 0) {
6453
- if (left.type === CellValueType.text || left.type === CellValueType.error) {
6454
- typeOrder = left.value.localeCompare(right.value);
6455
- }
6456
- else
6457
- typeOrder = left.value - right.value;
6458
- }
6459
- return inverse * typeOrder;
6460
- })
6461
- .concat(emptyCells);
6462
- }
6463
- function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
6464
- let result = DispatchResult.Success;
6465
- //several columns => bypass the contiguity check
6466
- let multiColumns = zone.right > zone.left;
6467
- if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
6468
- multiColumns = false;
6469
- let table;
6470
- for (let row = zone.top; row <= zone.bottom; row++) {
6471
- table = [];
6472
- for (let col = zone.left; col <= zone.right; col++) {
6473
- let merge = env.model.getters.getMerge({ sheetId, col, row });
6474
- if (merge && !table.includes(merge.id.toString())) {
6475
- table.push(merge.id.toString());
6476
- }
6477
- }
6478
- if (table.length >= 2) {
6479
- multiColumns = true;
6480
- break;
6481
- }
6482
- }
6483
- }
6484
- const { col, row } = anchor;
6485
- if (multiColumns) {
6486
- result = env.model.dispatch("SORT_CELLS", { sheetId, col, row, zone, sortDirection });
6487
- }
6488
- else {
6489
- // check contiguity
6490
- const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
6491
- if (isEqual(contiguousZone, zone)) {
6492
- // merge as it is
6493
- result = env.model.dispatch("SORT_CELLS", {
6494
- sheetId,
6495
- col,
6496
- row,
6497
- zone,
6498
- sortDirection,
6499
- });
6500
- }
6501
- else {
6502
- env.askConfirmation(_lt("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => {
6503
- zone = contiguousZone;
6504
- result = env.model.dispatch("SORT_CELLS", {
6505
- sheetId,
6506
- col,
6507
- row,
6508
- zone,
6509
- sortDirection,
6510
- });
6511
- }, () => {
6512
- result = env.model.dispatch("SORT_CELLS", {
6513
- sheetId,
6514
- col,
6515
- row,
6516
- zone,
6517
- sortDirection,
6518
- });
6519
- });
6520
- }
5855
+ else {
5856
+ // check contiguity
5857
+ const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
5858
+ if (isEqual(contiguousZone, zone)) {
5859
+ // merge as it is
5860
+ result = env.model.dispatch("SORT_CELLS", {
5861
+ sheetId,
5862
+ col,
5863
+ row,
5864
+ zone,
5865
+ sortDirection,
5866
+ });
5867
+ }
5868
+ else {
5869
+ env.askConfirmation(_lt("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => {
5870
+ zone = contiguousZone;
5871
+ result = env.model.dispatch("SORT_CELLS", {
5872
+ sheetId,
5873
+ col,
5874
+ row,
5875
+ zone,
5876
+ sortDirection,
5877
+ });
5878
+ }, () => {
5879
+ result = env.model.dispatch("SORT_CELLS", {
5880
+ sheetId,
5881
+ col,
5882
+ row,
5883
+ zone,
5884
+ sortDirection,
5885
+ });
5886
+ });
5887
+ }
6521
5888
  }
6522
5889
  if (result.isCancelledBecause(63 /* CommandResult.InvalidSortZone */)) {
6523
5890
  const { col, row } = anchor;
@@ -7444,6 +6811,53 @@ rowMenuRegistry
7444
6811
  action: OPEN_CF_SIDEPANEL_ACTION,
7445
6812
  });
7446
6813
 
6814
+ /**
6815
+ * Return true if the event was triggered from
6816
+ * a child element.
6817
+ */
6818
+ function isChildEvent(parent, ev) {
6819
+ return !!ev.target && parent.contains(ev.target);
6820
+ }
6821
+ function gridOverlayPosition() {
6822
+ const spreadsheetElement = document.querySelector(".o-grid-overlay");
6823
+ if (spreadsheetElement) {
6824
+ const { top, left } = spreadsheetElement === null || spreadsheetElement === void 0 ? void 0 : spreadsheetElement.getBoundingClientRect();
6825
+ return { top, left };
6826
+ }
6827
+ throw new Error("Can't find spreadsheet position");
6828
+ }
6829
+ function getBoundingRectAsPOJO(el) {
6830
+ const rect = el.getBoundingClientRect();
6831
+ return {
6832
+ x: rect.x,
6833
+ y: rect.y,
6834
+ width: rect.width,
6835
+ height: rect.height,
6836
+ };
6837
+ }
6838
+ /**
6839
+ * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
6840
+ */
6841
+ function* iterateChildren(el) {
6842
+ yield el;
6843
+ if (el.hasChildNodes()) {
6844
+ for (let child of el.childNodes) {
6845
+ yield* iterateChildren(child);
6846
+ }
6847
+ }
6848
+ }
6849
+ function getElementScrollTop(el) {
6850
+ return (el === null || el === void 0 ? void 0 : el.scrollTop) || 0;
6851
+ }
6852
+ function setElementScrollTop(el, scroll) {
6853
+ if (!el)
6854
+ return;
6855
+ el.scrollTop = scroll;
6856
+ }
6857
+ function getOpenedMenus() {
6858
+ return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
6859
+ }
6860
+
7447
6861
  function startDnd(onMouseMove, onMouseUp, onMouseDown = () => { }) {
7448
6862
  const _onMouseDown = (ev) => {
7449
6863
  ev.preventDefault();
@@ -15723,8 +15137,7 @@ class Composer extends owl.Component {
15723
15137
  return false;
15724
15138
  const range = this.env.model.getters.getRangeFromSheetXC(refSheet, xc);
15725
15139
  let zone = range.zone;
15726
- const { height, width } = zoneToDimension(zone);
15727
- zone = height * width === 1 ? this.env.model.getters.expandZone(refSheet, zone) : zone;
15140
+ zone = getZoneArea(zone) === 1 ? this.env.model.getters.expandZone(refSheet, zone) : zone;
15728
15141
  return isEqual(zone, highlight.zone);
15729
15142
  });
15730
15143
  return highlight && highlight.color ? highlight.color : undefined;
@@ -16341,484 +15754,1000 @@ autofillModifiersRegistry
16341
15754
  y = 0;
16342
15755
  break;
16343
15756
  }
16344
- const cell = data.cell;
16345
- if (!cell || !cell.isFormula) {
16346
- return { cellData: {} };
15757
+ const cell = data.cell;
15758
+ if (!cell || !cell.isFormula) {
15759
+ return { cellData: {} };
15760
+ }
15761
+ const sheetId = data.sheetId;
15762
+ const ranges = getters.createAdaptedRanges(cell.dependencies, x, y, sheetId);
15763
+ const content = getters.buildFormulaContent(sheetId, cell, ranges);
15764
+ return {
15765
+ cellData: {
15766
+ border: data.border,
15767
+ style: cell.style,
15768
+ format: cell.format,
15769
+ content,
15770
+ },
15771
+ tooltip: content ? { props: { content } } : undefined,
15772
+ };
15773
+ },
15774
+ });
15775
+
15776
+ const autofillRulesRegistry = new Registry();
15777
+ const numberPostfixRegExp = /(\d+)$/;
15778
+ const stringPrefixRegExp = /^(.*\D+)/;
15779
+ const alphaNumericValueRegExp = /^(.*\D+)(\d+)$/;
15780
+ /**
15781
+ * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
15782
+ * Return the one which contains the given cell
15783
+ */
15784
+ function getGroup(cell, cells, filter) {
15785
+ let group = [];
15786
+ let found = false;
15787
+ for (let x of cells) {
15788
+ if (x === cell) {
15789
+ found = true;
15790
+ }
15791
+ const cellValue = evaluateLiteral(x === null || x === void 0 ? void 0 : x.content);
15792
+ if (filter(cellValue)) {
15793
+ group.push(cellValue);
15794
+ }
15795
+ else {
15796
+ if (found) {
15797
+ return group;
15798
+ }
15799
+ group = [];
15800
+ }
15801
+ }
15802
+ return group;
15803
+ }
15804
+ /**
15805
+ * Get the average steps between numbers
15806
+ */
15807
+ function getAverageIncrement(group) {
15808
+ const averages = [];
15809
+ let last = group[0];
15810
+ for (let i = 1; i < group.length; i++) {
15811
+ const current = group[i];
15812
+ averages.push(current - last);
15813
+ last = current;
15814
+ }
15815
+ return averages.reduce((a, b) => a + b, 0) / averages.length;
15816
+ }
15817
+ /**
15818
+ * Get the step for a group
15819
+ */
15820
+ function calculateIncrementBasedOnGroup(group) {
15821
+ let increment = 1;
15822
+ if (group.length >= 2) {
15823
+ increment = getAverageIncrement(group) * group.length;
15824
+ }
15825
+ return increment;
15826
+ }
15827
+ autofillRulesRegistry
15828
+ .add("simple_value_copy", {
15829
+ condition: (cell, cells) => {
15830
+ var _a;
15831
+ return cells.length === 1 && !cell.isFormula && !((_a = cell.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_FORMAT));
15832
+ },
15833
+ generateRule: () => {
15834
+ return { type: "COPY_MODIFIER" };
15835
+ },
15836
+ sequence: 10,
15837
+ })
15838
+ .add("increment_alphanumeric_value", {
15839
+ condition: (cell) => !cell.isFormula &&
15840
+ evaluateLiteral(cell.content).type === CellValueType.text &&
15841
+ alphaNumericValueRegExp.test(cell.content),
15842
+ generateRule: (cell, cells) => {
15843
+ const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
15844
+ const prefix = cell.content.match(stringPrefixRegExp)[0];
15845
+ const numberPostfixLength = cell.content.length - prefix.length;
15846
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
15847
+ alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
15848
+ .filter((cell) => prefix === cell.value.toString().match(stringPrefixRegExp)[0])
15849
+ .map((cell) => parseInt(cell.value.toString().match(numberPostfixRegExp)[0]));
15850
+ const increment = calculateIncrementBasedOnGroup(group);
15851
+ return {
15852
+ type: "ALPHANUMERIC_INCREMENT_MODIFIER",
15853
+ prefix,
15854
+ current: numberPostfix,
15855
+ increment,
15856
+ numberPostfixLength,
15857
+ };
15858
+ },
15859
+ sequence: 15,
15860
+ })
15861
+ .add("copy_text", {
15862
+ condition: (cell) => !cell.isFormula && evaluateLiteral(cell.content).type === CellValueType.text,
15863
+ generateRule: () => {
15864
+ return { type: "COPY_MODIFIER" };
15865
+ },
15866
+ sequence: 20,
15867
+ })
15868
+ .add("update_formula", {
15869
+ condition: (cell) => cell.isFormula,
15870
+ generateRule: (_, cells) => {
15871
+ return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
15872
+ },
15873
+ sequence: 30,
15874
+ })
15875
+ .add("increment_number", {
15876
+ condition: (cell) => !cell.isFormula && evaluateLiteral(cell.content).type === CellValueType.number,
15877
+ generateRule: (cell, cells) => {
15878
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
15879
+ const increment = calculateIncrementBasedOnGroup(group);
15880
+ const evaluation = evaluateLiteral(cell.content);
15881
+ return {
15882
+ type: "INCREMENT_MODIFIER",
15883
+ increment,
15884
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
15885
+ };
15886
+ },
15887
+ sequence: 40,
15888
+ });
15889
+
15890
+ const ERROR_TOOLTIP_MAX_HEIGHT = 80;
15891
+ const ERROR_TOOLTIP_WIDTH = 180;
15892
+ css /* scss */ `
15893
+ .o-error-tooltip {
15894
+ font-size: 13px;
15895
+ background-color: white;
15896
+ border-left: 3px solid red;
15897
+ padding: 10px;
15898
+ width: ${ERROR_TOOLTIP_WIDTH}px;
15899
+ box-sizing: border-box !important;
15900
+ }
15901
+ `;
15902
+ class ErrorToolTip extends owl.Component {
15903
+ }
15904
+ ErrorToolTip.maxSize = { maxHeight: ERROR_TOOLTIP_MAX_HEIGHT };
15905
+ ErrorToolTip.template = "o-spreadsheet-ErrorToolTip";
15906
+ ErrorToolTip.components = {};
15907
+ ErrorToolTip.props = {
15908
+ text: String,
15909
+ onClosed: { type: Function, optional: true },
15910
+ };
15911
+ const ErrorToolTipPopoverBuilder = {
15912
+ onHover: (position, getters) => {
15913
+ const cell = getters.getEvaluatedCell(position);
15914
+ if (cell.type === CellValueType.error && cell.error.logLevel > CellErrorLevel.silent) {
15915
+ return {
15916
+ isOpen: true,
15917
+ props: { text: cell.error.message },
15918
+ Component: ErrorToolTip,
15919
+ cellCorner: "TopRight",
15920
+ };
15921
+ }
15922
+ return { isOpen: false };
15923
+ },
15924
+ };
15925
+
15926
+ css /*SCSS*/ `
15927
+ .o-filter-menu-value {
15928
+ padding: 4px;
15929
+ line-height: 20px;
15930
+ height: 28px;
15931
+ .o-filter-menu-value-checked {
15932
+ width: 20px;
15933
+ }
15934
+ }
15935
+ `;
15936
+ class FilterMenuValueItem extends owl.Component {
15937
+ constructor() {
15938
+ super(...arguments);
15939
+ this.itemRef = owl.useRef("menuValueItem");
15940
+ }
15941
+ setup() {
15942
+ owl.onWillPatch(() => {
15943
+ if (this.props.scrolledTo) {
15944
+ this.scrollListToSelectedValue();
15945
+ }
15946
+ });
15947
+ }
15948
+ scrollListToSelectedValue() {
15949
+ var _a, _b;
15950
+ if (!this.itemRef.el) {
15951
+ return;
15952
+ }
15953
+ (_b = (_a = this.itemRef.el).scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, {
15954
+ block: this.props.scrolledTo === "bottom" ? "end" : "start",
15955
+ });
15956
+ }
15957
+ }
15958
+ FilterMenuValueItem.template = "o-spreadsheet-FilterMenuValueItem";
15959
+ FilterMenuValueItem.props = {
15960
+ value: String,
15961
+ isChecked: Boolean,
15962
+ isSelected: Boolean,
15963
+ onMouseMove: Function,
15964
+ onClick: Function,
15965
+ scrolledTo: { type: String, optional: true },
15966
+ };
15967
+
15968
+ const FILTER_MENU_HEIGHT = 295;
15969
+ const CSS = css /* scss */ `
15970
+ .o-filter-menu {
15971
+ box-sizing: border-box;
15972
+ padding: 8px 16px;
15973
+ height: ${FILTER_MENU_HEIGHT}px;
15974
+ line-height: 1;
15975
+
15976
+ .o-filter-menu-item {
15977
+ display: flex;
15978
+ box-sizing: border-box;
15979
+ height: ${MENU_ITEM_HEIGHT}px;
15980
+ padding: 4px 4px 4px 0px;
15981
+ cursor: pointer;
15982
+ user-select: none;
15983
+
15984
+ &.selected {
15985
+ background-color: rgba(0, 0, 0, 0.08);
15986
+ }
15987
+ }
15988
+
15989
+ input {
15990
+ box-sizing: border-box;
15991
+ margin-bottom: 5px;
15992
+ border: 1px solid #949494;
15993
+ height: 24px;
15994
+ padding-right: 28px;
15995
+ }
15996
+
15997
+ .o-search-icon {
15998
+ right: 5px;
15999
+ top: 4px;
16000
+
16001
+ svg {
16002
+ height: 16px;
16003
+ width: 16px;
16004
+ vertical-align: middle;
16005
+ }
16006
+ }
16007
+
16008
+ .o-filter-menu-actions {
16009
+ display: flex;
16010
+ flex-direction: row;
16011
+ margin-bottom: 4px;
16012
+
16013
+ .o-filter-menu-action-text {
16014
+ cursor: pointer;
16015
+ margin-right: 10px;
16016
+ color: blue;
16017
+ text-decoration: underline;
16018
+ }
16019
+ }
16020
+
16021
+ .o-filter-menu-list {
16022
+ flex: auto;
16023
+ overflow-y: auto;
16024
+ border: 1px solid #949494;
16025
+
16026
+ .o-filter-menu-no-values {
16027
+ color: #949494;
16028
+ font-style: italic;
16029
+ }
16030
+ }
16031
+
16032
+ .o-filter-menu-buttons {
16033
+ margin-top: 9px;
16034
+
16035
+ .o-filter-menu-button {
16036
+ border: 1px solid lightgrey;
16037
+ padding: 6px 10px;
16038
+ cursor: pointer;
16039
+ border-radius: 4px;
16040
+ font-weight: 500;
16041
+ line-height: 16px;
16042
+ }
16043
+
16044
+ .o-filter-menu-button-cancel {
16045
+ background: white;
16046
+ &:hover {
16047
+ background-color: rgba(0, 0, 0, 0.08);
16048
+ }
16049
+ }
16050
+
16051
+ .o-filter-menu-button-primary {
16052
+ background-color: #188038;
16053
+ &:hover {
16054
+ background-color: #1d9641;
16055
+ }
16056
+ color: white;
16057
+ font-weight: bold;
16058
+ margin-left: 10px;
16059
+ }
16060
+ }
16061
+ }
16062
+ `;
16063
+ class FilterMenu extends owl.Component {
16064
+ constructor() {
16065
+ super(...arguments);
16066
+ this.state = owl.useState({
16067
+ values: [],
16068
+ textFilter: "",
16069
+ selectedValue: undefined,
16070
+ });
16071
+ this.searchBar = owl.useRef("filterMenuSearchBar");
16072
+ }
16073
+ setup() {
16074
+ owl.onWillUpdateProps((nextProps) => {
16075
+ if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
16076
+ this.state.values = this.getFilterValues(nextProps.filterPosition);
16077
+ }
16078
+ });
16079
+ this.state.values = this.getFilterValues(this.props.filterPosition);
16080
+ }
16081
+ getFilterValues(position) {
16082
+ const sheetId = this.env.model.getters.getActiveSheetId();
16083
+ const filter = this.env.model.getters.getFilter({ sheetId, ...position });
16084
+ if (!filter) {
16085
+ return [];
16086
+ }
16087
+ const cellValues = (filter.filteredZone ? positions(filter.filteredZone) : [])
16088
+ .filter(({ row }) => !this.env.model.getters.isRowHidden(sheetId, row))
16089
+ .map(({ col, row }) => this.env.model.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue);
16090
+ const filterValues = this.env.model.getters.getFilterValues({ sheetId, ...position });
16091
+ const strValues = [...cellValues, ...filterValues];
16092
+ const normalizedFilteredValues = filterValues.map(toLowerCase);
16093
+ // Set with lowercase values to avoid duplicates
16094
+ const normalizedValues = [...new Set(strValues.map(toLowerCase))];
16095
+ const sortedValues = normalizedValues.sort((val1, val2) => val1.localeCompare(val2, undefined, { numeric: true, sensitivity: "base" }));
16096
+ return sortedValues.map((normalizedValue) => {
16097
+ const checked = normalizedFilteredValues.findIndex((filteredValue) => filteredValue === normalizedValue) ===
16098
+ -1;
16099
+ return {
16100
+ checked,
16101
+ string: strValues.find((val) => toLowerCase(val) === normalizedValue) || "",
16102
+ };
16103
+ });
16104
+ }
16105
+ checkValue(value) {
16106
+ var _a;
16107
+ this.state.selectedValue = value.string;
16108
+ value.checked = !value.checked;
16109
+ (_a = this.searchBar.el) === null || _a === void 0 ? void 0 : _a.focus();
16110
+ }
16111
+ onMouseMove(value) {
16112
+ this.state.selectedValue = value.string;
16113
+ }
16114
+ selectAll() {
16115
+ this.state.values.forEach((value) => (value.checked = true));
16116
+ }
16117
+ clearAll() {
16118
+ this.state.values.forEach((value) => (value.checked = false));
16119
+ }
16120
+ get filterTable() {
16121
+ const sheetId = this.env.model.getters.getActiveSheetId();
16122
+ const position = this.props.filterPosition;
16123
+ return this.env.model.getters.getFilterTable({ sheetId, ...position });
16124
+ }
16125
+ get displayedValues() {
16126
+ if (!this.state.textFilter) {
16127
+ return this.state.values;
16128
+ }
16129
+ return fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
16130
+ }
16131
+ confirm() {
16132
+ var _a, _b;
16133
+ const position = this.props.filterPosition;
16134
+ this.env.model.dispatch("UPDATE_FILTER", {
16135
+ ...position,
16136
+ sheetId: this.env.model.getters.getActiveSheetId(),
16137
+ values: this.state.values.filter((val) => !val.checked).map((val) => val.string),
16138
+ });
16139
+ (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
16140
+ }
16141
+ cancel() {
16142
+ var _a, _b;
16143
+ (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
16144
+ }
16145
+ onKeyDown(ev) {
16146
+ const displayedValues = this.displayedValues;
16147
+ if (displayedValues.length === 0)
16148
+ return;
16149
+ let selectedIndex = undefined;
16150
+ if (this.state.selectedValue !== undefined) {
16151
+ const index = displayedValues.findIndex((val) => val.string === this.state.selectedValue);
16152
+ selectedIndex = index === -1 ? undefined : index;
16153
+ }
16154
+ switch (ev.key) {
16155
+ case "ArrowDown":
16156
+ if (selectedIndex === undefined) {
16157
+ selectedIndex = 0;
16158
+ }
16159
+ else {
16160
+ selectedIndex = Math.min(selectedIndex + 1, displayedValues.length - 1);
16161
+ }
16162
+ ev.preventDefault();
16163
+ ev.stopPropagation();
16164
+ break;
16165
+ case "ArrowUp":
16166
+ if (selectedIndex === undefined) {
16167
+ selectedIndex = displayedValues.length - 1;
16168
+ }
16169
+ else {
16170
+ selectedIndex = Math.max(selectedIndex - 1, 0);
16171
+ }
16172
+ ev.preventDefault();
16173
+ ev.stopPropagation();
16174
+ break;
16175
+ case "Enter":
16176
+ if (selectedIndex !== undefined) {
16177
+ this.checkValue(displayedValues[selectedIndex]);
16178
+ }
16179
+ ev.stopPropagation();
16180
+ ev.preventDefault();
16181
+ break;
16182
+ }
16183
+ this.state.selectedValue =
16184
+ selectedIndex !== undefined ? displayedValues[selectedIndex].string : undefined;
16185
+ if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
16186
+ this.scrollListToSelectedValue(ev.key);
16187
+ }
16188
+ }
16189
+ clearScrolledToValue() {
16190
+ this.state.values.forEach((val) => (val.scrolledTo = undefined));
16191
+ }
16192
+ scrollListToSelectedValue(arrow) {
16193
+ this.clearScrolledToValue();
16194
+ const selectedValue = this.state.values.find((val) => val.string === this.state.selectedValue);
16195
+ if (selectedValue) {
16196
+ selectedValue.scrolledTo = arrow === "ArrowUp" ? "top" : "bottom";
16347
16197
  }
16348
- const sheetId = data.sheetId;
16349
- const ranges = getters.createAdaptedRanges(cell.dependencies, x, y, sheetId);
16350
- const content = getters.buildFormulaContent(sheetId, cell, ranges);
16198
+ }
16199
+ sortFilterZone(sortDirection) {
16200
+ var _a, _b;
16201
+ const filterPosition = this.props.filterPosition;
16202
+ const filterTable = this.filterTable;
16203
+ if (!filterPosition || !filterTable || !filterTable.contentZone) {
16204
+ return;
16205
+ }
16206
+ const sheetId = this.env.model.getters.getActiveSheetId();
16207
+ this.env.model.dispatch("SORT_CELLS", {
16208
+ sheetId,
16209
+ col: filterPosition.col,
16210
+ row: filterTable.contentZone.top,
16211
+ zone: filterTable.contentZone,
16212
+ sortDirection,
16213
+ sortOptions: { emptyCellAsZero: true, sortHeaders: true },
16214
+ });
16215
+ (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
16216
+ }
16217
+ }
16218
+ FilterMenu.size = { width: MENU_WIDTH, height: FILTER_MENU_HEIGHT };
16219
+ FilterMenu.template = "o-spreadsheet-FilterMenu";
16220
+ FilterMenu.style = CSS;
16221
+ FilterMenu.components = { FilterMenuValueItem };
16222
+ FilterMenu.props = {
16223
+ filterPosition: Object,
16224
+ onClosed: { type: Function, optional: true },
16225
+ };
16226
+ const FilterMenuPopoverBuilder = {
16227
+ onOpen: (position, getters) => {
16351
16228
  return {
16352
- cellData: {
16353
- border: data.border,
16354
- style: cell.style,
16355
- format: cell.format,
16356
- content,
16357
- },
16358
- tooltip: content ? { props: { content } } : undefined,
16229
+ isOpen: true,
16230
+ props: { filterPosition: position },
16231
+ Component: FilterMenu,
16232
+ cellCorner: "BottomLeft",
16359
16233
  };
16360
16234
  },
16361
- });
16235
+ };
16362
16236
 
16363
- const autofillRulesRegistry = new Registry();
16364
- const numberPostfixRegExp = /(\d+)$/;
16365
- const stringPrefixRegExp = /^(.*\D+)/;
16366
- const alphaNumericValueRegExp = /^(.*\D+)(\d+)$/;
16367
16237
  /**
16368
- * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
16369
- * Return the one which contains the given cell
16238
+ * Return the o-spreadsheet element position relative
16239
+ * to the browser viewport.
16370
16240
  */
16371
- function getGroup(cell, cells, filter) {
16372
- let group = [];
16373
- let found = false;
16374
- for (let x of cells) {
16375
- if (x === cell) {
16376
- found = true;
16377
- }
16378
- const cellValue = evaluateLiteral(x === null || x === void 0 ? void 0 : x.content);
16379
- if (filter(cellValue)) {
16380
- group.push(cellValue);
16241
+ function useSpreadsheetRect() {
16242
+ const position = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
16243
+ let spreadsheetElement = document.querySelector(".o-spreadsheet");
16244
+ updatePosition();
16245
+ function updatePosition() {
16246
+ if (!spreadsheetElement) {
16247
+ spreadsheetElement = document.querySelector(".o-spreadsheet");
16381
16248
  }
16382
- else {
16383
- if (found) {
16384
- return group;
16385
- }
16386
- group = [];
16249
+ if (spreadsheetElement) {
16250
+ const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();
16251
+ position.x = left;
16252
+ position.y = top;
16253
+ position.width = width;
16254
+ position.height = height;
16387
16255
  }
16388
16256
  }
16389
- return group;
16257
+ owl.onMounted(updatePosition);
16258
+ owl.onPatched(updatePosition);
16259
+ return position;
16390
16260
  }
16391
16261
  /**
16392
- * Get the average steps between numbers
16262
+ * Return the component (or ref's component) BoundingRect, relative
16263
+ * to the upper left corner of the screen (<body> element).
16264
+ *
16265
+ * Note: when used with a <Portal/> component, it will
16266
+ * return the portal position, not the teleported position.
16393
16267
  */
16394
- function getAverageIncrement(group) {
16395
- const averages = [];
16396
- let last = group[0];
16397
- for (let i = 1; i < group.length; i++) {
16398
- const current = group[i];
16399
- averages.push(current - last);
16400
- last = current;
16268
+ function useAbsoluteBoundingRect(ref) {
16269
+ const rect = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
16270
+ function updateElRect() {
16271
+ const el = ref.el;
16272
+ if (el === null) {
16273
+ return;
16274
+ }
16275
+ const { top, left, width, height } = el.getBoundingClientRect();
16276
+ rect.x = left;
16277
+ rect.y = top;
16278
+ rect.width = width;
16279
+ rect.height = height;
16401
16280
  }
16402
- return averages.reduce((a, b) => a + b, 0) / averages.length;
16281
+ owl.onMounted(updateElRect);
16282
+ owl.onPatched(updateElRect);
16283
+ return rect;
16403
16284
  }
16404
16285
  /**
16405
- * Get the step for a group
16286
+ * Get the rectangle inside which a popover should stay when being displayed.
16287
+ * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
16288
+ * element by default.
16289
+ *
16290
+ * Coordinates are expressed expressed as absolute DOM position.
16406
16291
  */
16407
- function calculateIncrementBasedOnGroup(group) {
16408
- let increment = 1;
16409
- if (group.length >= 2) {
16410
- increment = getAverageIncrement(group) * group.length;
16292
+ function usePopoverContainer() {
16293
+ const container = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
16294
+ const component = owl.useComponent();
16295
+ const spreadsheetRect = useSpreadsheetRect();
16296
+ function updateRect() {
16297
+ const env = component.env;
16298
+ const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
16299
+ container.x = newRect.x;
16300
+ container.y = newRect.y;
16301
+ container.width = newRect.width;
16302
+ container.height = newRect.height;
16411
16303
  }
16412
- return increment;
16413
- }
16414
- autofillRulesRegistry
16415
- .add("simple_value_copy", {
16416
- condition: (cell, cells) => {
16417
- var _a;
16418
- return cells.length === 1 && !cell.isFormula && !((_a = cell.format) === null || _a === void 0 ? void 0 : _a.match(DATETIME_FORMAT));
16419
- },
16420
- generateRule: () => {
16421
- return { type: "COPY_MODIFIER" };
16422
- },
16423
- sequence: 10,
16424
- })
16425
- .add("increment_alphanumeric_value", {
16426
- condition: (cell) => !cell.isFormula &&
16427
- evaluateLiteral(cell.content).type === CellValueType.text &&
16428
- alphaNumericValueRegExp.test(cell.content),
16429
- generateRule: (cell, cells) => {
16430
- const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
16431
- const prefix = cell.content.match(stringPrefixRegExp)[0];
16432
- const numberPostfixLength = cell.content.length - prefix.length;
16433
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
16434
- alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
16435
- .filter((cell) => prefix === cell.value.toString().match(stringPrefixRegExp)[0])
16436
- .map((cell) => parseInt(cell.value.toString().match(numberPostfixRegExp)[0]));
16437
- const increment = calculateIncrementBasedOnGroup(group);
16438
- return {
16439
- type: "ALPHANUMERIC_INCREMENT_MODIFIER",
16440
- prefix,
16441
- current: numberPostfix,
16442
- increment,
16443
- numberPostfixLength,
16444
- };
16445
- },
16446
- sequence: 15,
16447
- })
16448
- .add("copy_text", {
16449
- condition: (cell) => !cell.isFormula && evaluateLiteral(cell.content).type === CellValueType.text,
16450
- generateRule: () => {
16451
- return { type: "COPY_MODIFIER" };
16452
- },
16453
- sequence: 20,
16454
- })
16455
- .add("update_formula", {
16456
- condition: (cell) => cell.isFormula,
16457
- generateRule: (_, cells) => {
16458
- return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
16459
- },
16460
- sequence: 30,
16461
- })
16462
- .add("increment_number", {
16463
- condition: (cell) => !cell.isFormula && evaluateLiteral(cell.content).type === CellValueType.number,
16464
- generateRule: (cell, cells) => {
16465
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
16466
- const increment = calculateIncrementBasedOnGroup(group);
16467
- const evaluation = evaluateLiteral(cell.content);
16468
- return {
16469
- type: "INCREMENT_MODIFIER",
16470
- increment,
16471
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
16472
- };
16473
- },
16474
- sequence: 40,
16475
- });
16304
+ updateRect();
16305
+ owl.onMounted(updateRect);
16306
+ owl.onPatched(updateRect);
16307
+ return container;
16308
+ }
16476
16309
 
16477
- const ERROR_TOOLTIP_MAX_HEIGHT = 80;
16478
- const ERROR_TOOLTIP_WIDTH = 180;
16479
- css /* scss */ `
16480
- .o-error-tooltip {
16481
- font-size: 13px;
16482
- background-color: white;
16483
- border-left: 3px solid red;
16484
- padding: 10px;
16485
- width: ${ERROR_TOOLTIP_WIDTH}px;
16486
- box-sizing: border-box !important;
16487
- }
16488
- `;
16489
- class ErrorToolTip extends owl.Component {
16310
+ /**
16311
+ * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap
16312
+ */
16313
+ function rectIntersection(rect1, rect2) {
16314
+ return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2)));
16490
16315
  }
16491
- ErrorToolTip.maxSize = { maxHeight: ERROR_TOOLTIP_MAX_HEIGHT };
16492
- ErrorToolTip.template = "o-spreadsheet-ErrorToolTip";
16493
- ErrorToolTip.components = {};
16494
- ErrorToolTip.props = {
16495
- text: String,
16496
- onClosed: { type: Function, optional: true },
16497
- };
16498
- const ErrorToolTipPopoverBuilder = {
16499
- onHover: (position, getters) => {
16500
- const cell = getters.getEvaluatedCell(position);
16501
- if (cell.type === CellValueType.error && cell.error.logLevel > CellErrorLevel.silent) {
16502
- return {
16503
- isOpen: true,
16504
- props: { text: cell.error.message },
16505
- Component: ErrorToolTip,
16506
- cellCorner: "TopRight",
16507
- };
16508
- }
16509
- return { isOpen: false };
16510
- },
16511
- };
16316
+ function rectToZone(rect) {
16317
+ return {
16318
+ left: rect.x,
16319
+ top: rect.y,
16320
+ right: rect.x + rect.width,
16321
+ bottom: rect.y + rect.height,
16322
+ };
16323
+ }
16324
+ function zoneToRect(zone) {
16325
+ if (!zone)
16326
+ return undefined;
16327
+ return {
16328
+ x: zone.left,
16329
+ y: zone.top,
16330
+ width: zone.right - zone.left,
16331
+ height: zone.bottom - zone.top,
16332
+ };
16333
+ }
16512
16334
 
16513
- css /*SCSS*/ `
16514
- .o-filter-menu-value {
16515
- padding: 4px;
16516
- line-height: 20px;
16517
- height: 28px;
16518
- .o-filter-menu-value-checked {
16519
- width: 20px;
16520
- }
16335
+ css /* scss */ `
16336
+ .o-popover {
16337
+ position: absolute;
16338
+ z-index: ${ComponentsImportance.Popover};
16339
+ overflow: auto;
16340
+ box-shadow: 1px 2px 5px 2px rgb(51 51 51 / 15%);
16341
+ width: fit-content;
16342
+ height: fit-content;
16521
16343
  }
16522
16344
  `;
16523
- class FilterMenuValueItem extends owl.Component {
16345
+ class Popover extends owl.Component {
16524
16346
  constructor() {
16525
16347
  super(...arguments);
16526
- this.itemRef = owl.useRef("menuValueItem");
16348
+ this.popoverRef = owl.useRef("popover");
16349
+ this.currentPosition = undefined;
16350
+ this.currentDisplayValue = undefined;
16351
+ this.spreadsheetRect = useSpreadsheetRect();
16527
16352
  }
16528
16353
  setup() {
16529
- owl.onWillPatch(() => {
16530
- if (this.props.scrolledTo) {
16531
- this.scrollListToSelectedValue();
16354
+ this.containerRect = usePopoverContainer();
16355
+ // useEffect occurs after the DOM is created and the element width/height are computed, but before
16356
+ // the element in rendered, so we can still set its position
16357
+ owl.useEffect(() => {
16358
+ var _a, _b, _c, _d;
16359
+ if (!this.containerRect)
16360
+ throw new Error("Popover container is not defined");
16361
+ const el = this.popoverRef.el;
16362
+ const anchor = rectIntersection(this.props.anchorRect, this.containerRect);
16363
+ const newDisplay = anchor ? "block" : "none";
16364
+ if (this.currentDisplayValue !== "none" && newDisplay === "none") {
16365
+ (_b = (_a = this.props).onPopoverHidden) === null || _b === void 0 ? void 0 : _b.call(_a);
16366
+ }
16367
+ el.style.display = newDisplay;
16368
+ this.currentDisplayValue = newDisplay;
16369
+ if (!anchor)
16370
+ return;
16371
+ const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
16372
+ const elDims = {
16373
+ width: el.getBoundingClientRect().width,
16374
+ height: el.getBoundingClientRect().height,
16375
+ };
16376
+ const spreadsheetRect = this.spreadsheetRect;
16377
+ const popoverPositionHelper = this.props.positioning === "BottomLeft"
16378
+ ? new BottomLeftPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect)
16379
+ : new TopRightPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect);
16380
+ const style = popoverPositionHelper.getCss(elDims, this.props.verticalOffset);
16381
+ for (const property of Object.keys(style)) {
16382
+ el.style[property] = style[property];
16383
+ }
16384
+ const newPosition = popoverPositionHelper.getCurrentPosition(elDims);
16385
+ if (this.currentPosition && newPosition !== this.currentPosition) {
16386
+ (_d = (_c = this.props).onPopoverMoved) === null || _d === void 0 ? void 0 : _d.call(_c);
16532
16387
  }
16388
+ this.currentPosition = newPosition;
16533
16389
  });
16534
16390
  }
16535
- scrollListToSelectedValue() {
16536
- var _a, _b;
16537
- if (!this.itemRef.el) {
16538
- return;
16391
+ }
16392
+ Popover.template = "o-spreadsheet-Popover";
16393
+ Popover.defaultProps = {
16394
+ positioning: "BottomLeft",
16395
+ verticalOffset: 0,
16396
+ onMouseWheel: () => { },
16397
+ onPopoverMoved: () => { },
16398
+ onPopoverHidden: () => { },
16399
+ };
16400
+ Popover.props = {
16401
+ anchorRect: Object,
16402
+ containerRect: { type: Object, optional: true },
16403
+ positioning: { type: String, optional: true },
16404
+ maxWidth: { type: Number, optional: true },
16405
+ maxHeight: { type: Number, optional: true },
16406
+ verticalOffset: { type: Number, optional: true },
16407
+ onMouseWheel: { type: Function, optional: true },
16408
+ onPopoverHidden: { type: Function, optional: true },
16409
+ onPopoverMoved: { type: Function, optional: true },
16410
+ slots: Object,
16411
+ };
16412
+ class PopoverPositionContext {
16413
+ constructor(anchorRect, containerRect, propsMaxSize, spreadsheetOffset) {
16414
+ this.anchorRect = anchorRect;
16415
+ this.containerRect = containerRect;
16416
+ this.propsMaxSize = propsMaxSize;
16417
+ this.spreadsheetOffset = spreadsheetOffset;
16418
+ }
16419
+ /** Check if there is enough space for the popover to be rendered at the bottom of the anchorRect */
16420
+ shouldRenderAtBottom(elementHeight) {
16421
+ return (elementHeight <= this.availableHeightDown ||
16422
+ this.availableHeightDown >= this.availableHeightUp);
16423
+ }
16424
+ /** Check if there is enough space for the popover to be rendered at the right of the anchorRect */
16425
+ shouldRenderAtRight(elementWidth) {
16426
+ return (elementWidth <= this.availableWidthRight ||
16427
+ this.availableWidthRight >= this.availableWidthLeft);
16428
+ }
16429
+ getMaxHeight(elementHeight) {
16430
+ const shouldRenderAtBottom = this.shouldRenderAtBottom(elementHeight);
16431
+ const availableHeight = shouldRenderAtBottom
16432
+ ? this.availableHeightDown
16433
+ : this.availableHeightUp;
16434
+ return this.propsMaxSize.height
16435
+ ? Math.min(availableHeight, this.propsMaxSize.height)
16436
+ : availableHeight;
16437
+ }
16438
+ getMaxWidth(elementWidth) {
16439
+ const shouldRenderAtRight = this.shouldRenderAtRight(elementWidth);
16440
+ const availableWidth = shouldRenderAtRight ? this.availableWidthRight : this.availableWidthLeft;
16441
+ return this.propsMaxSize.width
16442
+ ? Math.min(availableWidth, this.propsMaxSize.width)
16443
+ : availableWidth;
16444
+ }
16445
+ getCss(elDims, verticalOffset) {
16446
+ const maxHeight = this.getMaxHeight(elDims.height);
16447
+ const maxWidth = this.getMaxWidth(elDims.width);
16448
+ const actualHeight = Math.min(maxHeight, elDims.height);
16449
+ const actualWidth = Math.min(maxWidth, elDims.width);
16450
+ const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
16451
+ const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
16452
+ verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset;
16453
+ const cssProperties = {
16454
+ "max-height": maxHeight + "px",
16455
+ "max-width": maxWidth + "px",
16456
+ top: this.getTopCoordinate(actualHeight, shouldRenderAtBottom) -
16457
+ this.spreadsheetOffset.y -
16458
+ verticalOffset +
16459
+ "px",
16460
+ left: this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + "px",
16461
+ };
16462
+ return cssProperties;
16463
+ }
16464
+ getCurrentPosition(elDims) {
16465
+ const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
16466
+ const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
16467
+ if (shouldRenderAtBottom && shouldRenderAtRight)
16468
+ return "BottomRight";
16469
+ if (shouldRenderAtBottom && !shouldRenderAtRight)
16470
+ return "BottomLeft";
16471
+ if (!shouldRenderAtBottom && shouldRenderAtRight)
16472
+ return "TopRight";
16473
+ return "TopLeft";
16474
+ }
16475
+ }
16476
+ class BottomLeftPopoverContext extends PopoverPositionContext {
16477
+ get availableHeightUp() {
16478
+ return this.anchorRect.y - this.containerRect.y;
16479
+ }
16480
+ get availableHeightDown() {
16481
+ return this.containerRect.height - this.availableHeightUp - this.anchorRect.height;
16482
+ }
16483
+ get availableWidthRight() {
16484
+ return this.containerRect.x + this.containerRect.width - this.anchorRect.x;
16485
+ }
16486
+ get availableWidthLeft() {
16487
+ return this.anchorRect.x + this.anchorRect.width - this.containerRect.x;
16488
+ }
16489
+ getTopCoordinate(elementHeight, shouldRenderAtBottom) {
16490
+ if (shouldRenderAtBottom) {
16491
+ return this.anchorRect.y + this.anchorRect.height;
16492
+ }
16493
+ else {
16494
+ return this.anchorRect.y - elementHeight;
16495
+ }
16496
+ }
16497
+ getLeftCoordinate(elementWidth, shouldRenderAtRight) {
16498
+ if (shouldRenderAtRight) {
16499
+ return this.anchorRect.x;
16500
+ }
16501
+ else {
16502
+ return this.anchorRect.x + this.anchorRect.width - elementWidth;
16539
16503
  }
16540
- (_b = (_a = this.itemRef.el).scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, {
16541
- block: this.props.scrolledTo === "bottom" ? "end" : "start",
16542
- });
16543
16504
  }
16544
16505
  }
16545
- FilterMenuValueItem.template = "o-spreadsheet-FilterMenuValueItem";
16546
- FilterMenuValueItem.props = {
16547
- value: String,
16548
- isChecked: Boolean,
16549
- isSelected: Boolean,
16550
- onMouseMove: Function,
16551
- onClick: Function,
16552
- scrolledTo: { type: String, optional: true },
16553
- };
16506
+ class TopRightPopoverContext extends PopoverPositionContext {
16507
+ get availableHeightUp() {
16508
+ return this.anchorRect.y + this.anchorRect.height - this.containerRect.y;
16509
+ }
16510
+ get availableHeightDown() {
16511
+ return this.containerRect.y + this.containerRect.height - this.anchorRect.y;
16512
+ }
16513
+ get availableWidthRight() {
16514
+ return this.containerRect.width - this.anchorRect.width - this.availableWidthLeft;
16515
+ }
16516
+ get availableWidthLeft() {
16517
+ return this.anchorRect.x - this.containerRect.x;
16518
+ }
16519
+ getTopCoordinate(elementHeight, shouldRenderAtBottom) {
16520
+ if (shouldRenderAtBottom) {
16521
+ return this.anchorRect.y;
16522
+ }
16523
+ else {
16524
+ return this.anchorRect.y + this.anchorRect.height - elementHeight;
16525
+ }
16526
+ }
16527
+ getLeftCoordinate(elementWidth, shouldRenderAtRight) {
16528
+ if (shouldRenderAtRight) {
16529
+ return this.anchorRect.x + this.anchorRect.width;
16530
+ }
16531
+ else {
16532
+ return this.anchorRect.x - elementWidth;
16533
+ }
16534
+ }
16535
+ }
16554
16536
 
16555
- const FILTER_MENU_HEIGHT = 295;
16556
- const CSS = css /* scss */ `
16557
- .o-filter-menu {
16558
- box-sizing: border-box;
16559
- padding: 8px 16px;
16560
- height: ${FILTER_MENU_HEIGHT}px;
16561
- line-height: 1;
16537
+ //------------------------------------------------------------------------------
16538
+ // Context Menu Component
16539
+ //------------------------------------------------------------------------------
16540
+ css /* scss */ `
16541
+ .o-menu {
16542
+ background-color: white;
16543
+ padding: ${MENU_VERTICAL_PADDING}px 0px;
16544
+ width: ${MENU_WIDTH}px;
16545
+ box-sizing: border-box !important;
16562
16546
 
16563
- .o-filter-menu-item {
16547
+ .o-menu-item {
16564
16548
  display: flex;
16549
+ justify-content: space-between;
16550
+ align-items: center;
16565
16551
  box-sizing: border-box;
16566
16552
  height: ${MENU_ITEM_HEIGHT}px;
16567
- padding: 4px 4px 4px 0px;
16553
+ padding: 4px 16px;
16568
16554
  cursor: pointer;
16569
16555
  user-select: none;
16570
16556
 
16571
- &.selected {
16572
- background-color: rgba(0, 0, 0, 0.08);
16573
- }
16574
- }
16575
-
16576
- input {
16577
- box-sizing: border-box;
16578
- margin-bottom: 5px;
16579
- border: 1px solid #949494;
16580
- height: 24px;
16581
- padding-right: 28px;
16582
- }
16583
-
16584
- .o-search-icon {
16585
- right: 5px;
16586
- top: 4px;
16587
-
16588
- svg {
16589
- height: 16px;
16590
- width: 16px;
16591
- vertical-align: middle;
16557
+ .o-menu-item-name {
16558
+ overflow: hidden;
16559
+ white-space: nowrap;
16560
+ text-overflow: ellipsis;
16592
16561
  }
16593
- }
16594
-
16595
- .o-filter-menu-actions {
16596
- display: flex;
16597
- flex-direction: row;
16598
- margin-bottom: 4px;
16599
16562
 
16600
- .o-filter-menu-action-text {
16601
- cursor: pointer;
16602
- margin-right: 10px;
16603
- color: blue;
16604
- text-decoration: underline;
16563
+ &.o-menu-root {
16564
+ display: flex;
16565
+ justify-content: space-between;
16605
16566
  }
16606
- }
16607
-
16608
- .o-filter-menu-list {
16609
- flex: auto;
16610
- overflow-y: auto;
16611
- border: 1px solid #949494;
16612
-
16613
- .o-filter-menu-no-values {
16614
- color: #949494;
16615
- font-style: italic;
16567
+ .o-menu-item-icon {
16568
+ margin-top: auto;
16569
+ margin-bottom: auto;
16616
16570
  }
16617
- }
16618
-
16619
- .o-filter-menu-buttons {
16620
- margin-top: 9px;
16621
-
16622
- .o-filter-menu-button {
16623
- border: 1px solid lightgrey;
16624
- padding: 6px 10px;
16625
- cursor: pointer;
16626
- border-radius: 4px;
16627
- font-weight: 500;
16628
- line-height: 16px;
16571
+ .o-icon {
16572
+ width: 10px;
16629
16573
  }
16630
16574
 
16631
- .o-filter-menu-button-cancel {
16632
- background: white;
16633
- &:hover {
16634
- background-color: rgba(0, 0, 0, 0.08);
16575
+ &:not(.disabled) {
16576
+ &:hover,
16577
+ &.o-menu-item-active {
16578
+ background-color: #ebebeb;
16635
16579
  }
16636
- }
16637
-
16638
- .o-filter-menu-button-primary {
16639
- background-color: #188038;
16640
- &:hover {
16641
- background-color: #1d9641;
16580
+ .o-menu-item-description {
16581
+ color: grey;
16642
16582
  }
16643
- color: white;
16644
- font-weight: bold;
16645
- margin-left: 10px;
16583
+ }
16584
+ &.disabled {
16585
+ color: ${MENU_ITEM_DISABLED_COLOR};
16586
+ cursor: not-allowed;
16646
16587
  }
16647
16588
  }
16648
16589
  }
16649
16590
  `;
16650
- class FilterMenu extends owl.Component {
16591
+ class Menu extends owl.Component {
16651
16592
  constructor() {
16652
16593
  super(...arguments);
16653
- this.state = owl.useState({
16654
- values: [],
16655
- textFilter: "",
16656
- selectedValue: undefined,
16594
+ this.subMenu = owl.useState({
16595
+ isOpen: false,
16596
+ position: null,
16597
+ scrollOffset: 0,
16598
+ menuItems: [],
16657
16599
  });
16658
- this.searchBar = owl.useRef("filterMenuSearchBar");
16600
+ this.menuRef = owl.useRef("menu");
16601
+ this.position = useAbsoluteBoundingRect(this.menuRef);
16659
16602
  }
16660
16603
  setup() {
16604
+ owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
16605
+ owl.useExternalListener(window, "contextmenu", this.onExternalClick, { capture: true });
16661
16606
  owl.onWillUpdateProps((nextProps) => {
16662
- if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
16663
- this.state.values = this.getFilterValues(nextProps.filterPosition);
16607
+ if (nextProps.menuItems !== this.props.menuItems) {
16608
+ this.closeSubMenu();
16664
16609
  }
16665
16610
  });
16666
- this.state.values = this.getFilterValues(this.props.filterPosition);
16667
16611
  }
16668
- getFilterValues(position) {
16669
- const sheetId = this.env.model.getters.getActiveSheetId();
16670
- const filter = this.env.model.getters.getFilter({ sheetId, ...position });
16671
- if (!filter) {
16672
- return [];
16612
+ get visibleMenuItems() {
16613
+ return this.props.menuItems.filter((x) => x.isVisible(this.env));
16614
+ }
16615
+ get subMenuPosition() {
16616
+ const position = Object.assign({}, this.subMenu.position);
16617
+ position.y -= this.subMenu.scrollOffset || 0;
16618
+ return position;
16619
+ }
16620
+ get menuHeight() {
16621
+ const menuItems = this.visibleMenuItems;
16622
+ let menuItemsHeight = this.getMenuItemsHeight(menuItems);
16623
+ // We don't display separator at the end of a menu
16624
+ if (menuItems[menuItems.length - 1].separator) {
16625
+ menuItemsHeight -= MENU_SEPARATOR_HEIGHT;
16673
16626
  }
16674
- const cellValues = (filter.filteredZone ? positions(filter.filteredZone) : [])
16675
- .filter(({ row }) => !this.env.model.getters.isRowHidden(sheetId, row))
16676
- .map(({ col, row }) => this.env.model.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue);
16677
- const filterValues = this.env.model.getters.getFilterValues({ sheetId, ...position });
16678
- const strValues = [...cellValues, ...filterValues];
16679
- const normalizedFilteredValues = filterValues.map(toLowerCase);
16680
- // Set with lowercase values to avoid duplicates
16681
- const normalizedValues = [...new Set(strValues.map(toLowerCase))];
16682
- const sortedValues = normalizedValues.sort((val1, val2) => val1.localeCompare(val2, undefined, { numeric: true, sensitivity: "base" }));
16683
- return sortedValues.map((normalizedValue) => {
16684
- const checked = normalizedFilteredValues.findIndex((filteredValue) => filteredValue === normalizedValue) ===
16685
- -1;
16686
- return {
16687
- checked,
16688
- string: strValues.find((val) => toLowerCase(val) === normalizedValue) || "",
16689
- };
16690
- });
16627
+ const menuHeight = 2 * MENU_VERTICAL_PADDING + menuItemsHeight;
16628
+ return this.props.maxHeight ? Math.min(menuHeight, this.props.maxHeight) : menuHeight;
16691
16629
  }
16692
- checkValue(value) {
16693
- var _a;
16694
- this.state.selectedValue = value.string;
16695
- value.checked = !value.checked;
16696
- (_a = this.searchBar.el) === null || _a === void 0 ? void 0 : _a.focus();
16630
+ get popoverProps() {
16631
+ const isRoot = this.props.depth === 1;
16632
+ return {
16633
+ anchorRect: {
16634
+ x: this.props.position.x - MENU_WIDTH * (this.props.depth - 1),
16635
+ y: this.props.position.y,
16636
+ width: isRoot ? 0 : MENU_WIDTH,
16637
+ height: isRoot ? 0 : MENU_ITEM_HEIGHT,
16638
+ },
16639
+ positioning: "TopRight",
16640
+ verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
16641
+ onPopoverHidden: () => this.closeSubMenu(),
16642
+ onPopoverMoved: () => this.closeSubMenu(),
16643
+ maxHeight: this.menuHeight,
16644
+ };
16697
16645
  }
16698
- onMouseMove(value) {
16699
- this.state.selectedValue = value.string;
16646
+ getColor(menu) {
16647
+ return menu.textColor ? `color: ${menu.textColor}` : undefined;
16700
16648
  }
16701
- selectAll() {
16702
- this.state.values.forEach((value) => (value.checked = true));
16649
+ async activateMenu(menu) {
16650
+ var _a, _b, _c;
16651
+ const result = await ((_a = menu.action) === null || _a === void 0 ? void 0 : _a.call(menu, this.env));
16652
+ this.close();
16653
+ (_c = (_b = this.props).onMenuClicked) === null || _c === void 0 ? void 0 : _c.call(_b, { detail: result });
16703
16654
  }
16704
- clearAll() {
16705
- this.state.values.forEach((value) => (value.checked = false));
16655
+ close() {
16656
+ this.closeSubMenu();
16657
+ this.props.onClose();
16706
16658
  }
16707
- get filterTable() {
16708
- const sheetId = this.env.model.getters.getActiveSheetId();
16709
- const position = this.props.filterPosition;
16710
- return this.env.model.getters.getFilterTable({ sheetId, ...position });
16659
+ /**
16660
+ * Return the number of pixels between the top of the menu
16661
+ * and the menu item at a given index.
16662
+ */
16663
+ subMenuVerticalPosition(menuIndex) {
16664
+ const menusAbove = this.visibleMenuItems.slice(0, menuIndex);
16665
+ return this.position.y + this.getMenuItemsHeight(menusAbove) + MENU_VERTICAL_PADDING;
16711
16666
  }
16712
- get displayedValues() {
16713
- if (!this.state.textFilter) {
16714
- return this.state.values;
16667
+ onExternalClick(ev) {
16668
+ // Don't close a root menu when clicked to open the submenus.
16669
+ const el = this.menuRef.el;
16670
+ if (el && getOpenedMenus().some((el) => isChildEvent(el, ev))) {
16671
+ return;
16715
16672
  }
16716
- return fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
16673
+ ev.closedMenuId = this.props.menuId;
16674
+ this.close();
16717
16675
  }
16718
- confirm() {
16719
- var _a, _b;
16720
- const position = this.props.filterPosition;
16721
- this.env.model.dispatch("UPDATE_FILTER", {
16722
- ...position,
16723
- sheetId: this.env.model.getters.getActiveSheetId(),
16724
- values: this.state.values.filter((val) => !val.checked).map((val) => val.string),
16725
- });
16726
- (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
16676
+ getMenuItemsHeight(menuItems) {
16677
+ const numberOfSeparators = menuItems.filter((m) => m.separator).length;
16678
+ return MENU_ITEM_HEIGHT * menuItems.length + MENU_SEPARATOR_HEIGHT * numberOfSeparators;
16727
16679
  }
16728
- cancel() {
16729
- var _a, _b;
16730
- (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
16680
+ getName(menu) {
16681
+ return menu.name(this.env);
16731
16682
  }
16732
- onKeyDown(ev) {
16733
- const displayedValues = this.displayedValues;
16734
- if (displayedValues.length === 0)
16735
- return;
16736
- let selectedIndex = undefined;
16737
- if (this.state.selectedValue !== undefined) {
16738
- const index = displayedValues.findIndex((val) => val.string === this.state.selectedValue);
16739
- selectedIndex = index === -1 ? undefined : index;
16740
- }
16741
- switch (ev.key) {
16742
- case "ArrowDown":
16743
- if (selectedIndex === undefined) {
16744
- selectedIndex = 0;
16745
- }
16746
- else {
16747
- selectedIndex = Math.min(selectedIndex + 1, displayedValues.length - 1);
16748
- }
16749
- ev.preventDefault();
16750
- ev.stopPropagation();
16751
- break;
16752
- case "ArrowUp":
16753
- if (selectedIndex === undefined) {
16754
- selectedIndex = displayedValues.length - 1;
16755
- }
16756
- else {
16757
- selectedIndex = Math.max(selectedIndex - 1, 0);
16758
- }
16759
- ev.preventDefault();
16760
- ev.stopPropagation();
16761
- break;
16762
- case "Enter":
16763
- if (selectedIndex !== undefined) {
16764
- this.checkValue(displayedValues[selectedIndex]);
16765
- }
16766
- ev.stopPropagation();
16767
- ev.preventDefault();
16768
- break;
16769
- }
16770
- this.state.selectedValue =
16771
- selectedIndex !== undefined ? displayedValues[selectedIndex].string : undefined;
16772
- if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
16773
- this.scrollListToSelectedValue(ev.key);
16683
+ isRoot(menu) {
16684
+ return !menu.action;
16685
+ }
16686
+ isEnabled(menu) {
16687
+ if (menu.isEnabled(this.env)) {
16688
+ return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
16774
16689
  }
16690
+ return false;
16775
16691
  }
16776
- clearScrolledToValue() {
16777
- this.state.values.forEach((val) => (val.scrolledTo = undefined));
16692
+ onScroll(ev) {
16693
+ this.subMenu.scrollOffset = ev.target.scrollTop;
16778
16694
  }
16779
- scrollListToSelectedValue(arrow) {
16780
- this.clearScrolledToValue();
16781
- const selectedValue = this.state.values.find((val) => val.string === this.state.selectedValue);
16782
- if (selectedValue) {
16783
- selectedValue.scrolledTo = arrow === "ArrowUp" ? "top" : "bottom";
16695
+ /**
16696
+ * If the given menu is not disabled, open it's submenu at the
16697
+ * correct position according to available surrounding space.
16698
+ */
16699
+ openSubMenu(menu, menuIndex) {
16700
+ const y = this.subMenuVerticalPosition(menuIndex);
16701
+ this.subMenu.position = {
16702
+ x: this.position.x + this.props.depth * MENU_WIDTH,
16703
+ y: y - (this.subMenu.scrollOffset || 0),
16704
+ };
16705
+ this.subMenu.menuItems = menu.children(this.env);
16706
+ this.subMenu.isOpen = true;
16707
+ this.subMenu.parentMenu = menu;
16708
+ }
16709
+ isParentMenu(subMenu, menuItem) {
16710
+ var _a;
16711
+ return ((_a = subMenu.parentMenu) === null || _a === void 0 ? void 0 : _a.id) === menuItem.id;
16712
+ }
16713
+ closeSubMenu() {
16714
+ this.subMenu.isOpen = false;
16715
+ this.subMenu.parentMenu = undefined;
16716
+ }
16717
+ onClickMenu(menu, menuIndex) {
16718
+ if (this.isEnabled(menu)) {
16719
+ if (this.isRoot(menu)) {
16720
+ this.openSubMenu(menu, menuIndex);
16721
+ }
16722
+ else {
16723
+ this.activateMenu(menu);
16724
+ }
16784
16725
  }
16785
16726
  }
16786
- sortFilterZone(sortDirection) {
16787
- var _a, _b;
16788
- const filterPosition = this.props.filterPosition;
16789
- const filterTable = this.filterTable;
16790
- if (!filterPosition || !filterTable || !filterTable.contentZone) {
16791
- return;
16727
+ onMouseOver(menu, position) {
16728
+ if (menu.isEnabled(this.env)) {
16729
+ if (this.isRoot(menu)) {
16730
+ this.openSubMenu(menu, position);
16731
+ }
16732
+ else {
16733
+ this.closeSubMenu();
16734
+ }
16792
16735
  }
16793
- const sheetId = this.env.model.getters.getActiveSheetId();
16794
- this.env.model.dispatch("SORT_CELLS", {
16795
- sheetId,
16796
- col: filterPosition.col,
16797
- row: filterTable.contentZone.top,
16798
- zone: filterTable.contentZone,
16799
- sortDirection,
16800
- sortOptions: { emptyCellAsZero: true, sortHeaders: true },
16801
- });
16802
- (_b = (_a = this.props).onClosed) === null || _b === void 0 ? void 0 : _b.call(_a);
16803
16736
  }
16804
16737
  }
16805
- FilterMenu.size = { width: MENU_WIDTH, height: FILTER_MENU_HEIGHT };
16806
- FilterMenu.template = "o-spreadsheet-FilterMenu";
16807
- FilterMenu.style = CSS;
16808
- FilterMenu.components = { FilterMenuValueItem };
16809
- FilterMenu.props = {
16810
- filterPosition: Object,
16811
- onClosed: { type: Function, optional: true },
16738
+ Menu.template = "o-spreadsheet-Menu";
16739
+ Menu.components = { Menu, Popover };
16740
+ Menu.defaultProps = {
16741
+ depth: 1,
16812
16742
  };
16813
- const FilterMenuPopoverBuilder = {
16814
- onOpen: (position, getters) => {
16815
- return {
16816
- isOpen: true,
16817
- props: { filterPosition: position },
16818
- Component: FilterMenu,
16819
- cellCorner: "BottomLeft",
16820
- };
16821
- },
16743
+ Menu.props = {
16744
+ position: Object,
16745
+ menuItems: Array,
16746
+ depth: { type: Number, optional: true },
16747
+ maxHeight: { type: Number, optional: true },
16748
+ onClose: Function,
16749
+ onMenuClicked: { type: Function, optional: true },
16750
+ menuId: { type: String, optional: true },
16822
16751
  };
16823
16752
 
16824
16753
  const LINK_TOOLTIP_HEIGHT = 32;
@@ -17152,83 +17081,6 @@ cellPopoverRegistry
17152
17081
  const currenciesRegistry = new Registry();
17153
17082
 
17154
17083
  class ImageFigure extends owl.Component {
17155
- constructor() {
17156
- super(...arguments);
17157
- this.menuState = owl.useState({
17158
- isOpen: false,
17159
- position: null,
17160
- });
17161
- this.imageContainerRef = owl.useRef("o-image");
17162
- this.menuButtonRef = owl.useRef("menuButton");
17163
- this.menuButtonRect = useAbsoluteBoundingRect(this.menuButtonRef);
17164
- this.position = useAbsoluteBoundingRect(this.imageContainerRef);
17165
- this.menuItems = createMenu([
17166
- {
17167
- id: "copy",
17168
- name: _lt("Copy"),
17169
- description: "Ctrl+C",
17170
- action: async () => {
17171
- this.env.model.dispatch("SELECT_FIGURE", { id: this.figureId });
17172
- this.env.model.dispatch("COPY");
17173
- await this.env.clipboard.write(this.env.model.getters.getClipboardContent());
17174
- },
17175
- },
17176
- {
17177
- id: "cut",
17178
- name: _lt("Cut"),
17179
- description: "Ctrl+X",
17180
- action: async () => {
17181
- this.env.model.dispatch("SELECT_FIGURE", { id: this.figureId });
17182
- this.env.model.dispatch("CUT");
17183
- await this.env.clipboard.write(this.env.model.getters.getClipboardContent());
17184
- },
17185
- },
17186
- {
17187
- id: "reset_size",
17188
- name: _lt("Reset size"),
17189
- action: () => {
17190
- const size = this.env.model.getters.getImageSize(this.figureId);
17191
- const { height, width } = getMaxFigureSize(this.env.model.getters, size);
17192
- this.env.model.dispatch("UPDATE_FIGURE", {
17193
- sheetId: this.env.model.getters.getActiveSheetId(),
17194
- id: this.figureId,
17195
- height,
17196
- width,
17197
- });
17198
- },
17199
- },
17200
- {
17201
- id: "delete",
17202
- name: _lt("Delete image"),
17203
- description: "delete",
17204
- action: () => {
17205
- this.env.model.dispatch("DELETE_FIGURE", {
17206
- sheetId: this.env.model.getters.getActiveSheetId(),
17207
- id: this.figureId,
17208
- });
17209
- },
17210
- },
17211
- ]);
17212
- }
17213
- onContextMenu(ev) {
17214
- const position = {
17215
- x: this.position.x + ev.offsetX,
17216
- y: this.position.y + ev.offsetY,
17217
- };
17218
- this.openContextMenu(position);
17219
- }
17220
- showMenu() {
17221
- const { x, y, width } = this.menuButtonRect;
17222
- const menuPosition = {
17223
- x: x >= MENU_WIDTH ? x - MENU_WIDTH : x + width,
17224
- y: y,
17225
- };
17226
- this.openContextMenu(menuPosition);
17227
- }
17228
- openContextMenu(position) {
17229
- this.menuState.isOpen = true;
17230
- this.menuState.position = position;
17231
- }
17232
17084
  // ---------------------------------------------------------------------------
17233
17085
  // Getters
17234
17086
  // ---------------------------------------------------------------------------
@@ -17240,20 +17092,105 @@ class ImageFigure extends owl.Component {
17240
17092
  }
17241
17093
  }
17242
17094
  ImageFigure.template = "o-spreadsheet-ImageFigure";
17243
- ImageFigure.components = { Menu };
17095
+ ImageFigure.components = {};
17244
17096
  ImageFigure.props = {
17245
17097
  figure: Object,
17246
17098
  onFigureDeleted: Function,
17247
17099
  };
17248
17100
 
17249
17101
  const figureRegistry = new Registry();
17250
- figureRegistry.add("chart", { Component: ChartFigure, SidePanelComponent: "ChartPanel" });
17102
+ figureRegistry.add("chart", {
17103
+ Component: ChartFigure,
17104
+ SidePanelComponent: "ChartPanel",
17105
+ menuBuilder: getChartMenu,
17106
+ });
17251
17107
  figureRegistry.add("image", {
17252
17108
  Component: ImageFigure,
17253
17109
  keepRatio: true,
17254
17110
  minFigSize: 20,
17255
17111
  borderWidth: 0,
17256
- });
17112
+ menuBuilder: getImageMenuRegistry,
17113
+ });
17114
+ function getChartMenu(figureId, onFigureDeleted, env) {
17115
+ const menuItemSpecs = [
17116
+ {
17117
+ id: "edit",
17118
+ name: _lt("Edit"),
17119
+ sequence: 1,
17120
+ action: () => {
17121
+ env.model.dispatch("SELECT_FIGURE", { id: figureId });
17122
+ env.openSidePanel("ChartPanel");
17123
+ },
17124
+ },
17125
+ getCopyMenuItem(figureId, env),
17126
+ getCutMenuItem(figureId, env),
17127
+ getDeleteMenuItem(figureId, onFigureDeleted, env),
17128
+ ];
17129
+ return createMenu(menuItemSpecs);
17130
+ }
17131
+ function getImageMenuRegistry(figureId, onFigureDeleted, env) {
17132
+ const menuItemSpecs = [
17133
+ getCopyMenuItem(figureId, env),
17134
+ getCutMenuItem(figureId, env),
17135
+ {
17136
+ id: "reset_size",
17137
+ name: _lt("Reset size"),
17138
+ sequence: 4,
17139
+ action: () => {
17140
+ const size = env.model.getters.getImageSize(figureId);
17141
+ const { height, width } = getMaxFigureSize(env.model.getters, size);
17142
+ env.model.dispatch("UPDATE_FIGURE", {
17143
+ sheetId: env.model.getters.getActiveSheetId(),
17144
+ id: figureId,
17145
+ height,
17146
+ width,
17147
+ });
17148
+ },
17149
+ },
17150
+ getDeleteMenuItem(figureId, onFigureDeleted, env),
17151
+ ];
17152
+ return createMenu(menuItemSpecs);
17153
+ }
17154
+ function getCopyMenuItem(figureId, env) {
17155
+ return {
17156
+ id: "copy",
17157
+ name: _lt("Copy"),
17158
+ sequence: 2,
17159
+ description: "Ctrl+C",
17160
+ action: async () => {
17161
+ env.model.dispatch("SELECT_FIGURE", { id: figureId });
17162
+ env.model.dispatch("COPY");
17163
+ await env.clipboard.write(env.model.getters.getClipboardContent());
17164
+ },
17165
+ };
17166
+ }
17167
+ function getCutMenuItem(figureId, env) {
17168
+ return {
17169
+ id: "cut",
17170
+ name: _lt("Cut"),
17171
+ sequence: 3,
17172
+ description: "Ctrl+X",
17173
+ action: async () => {
17174
+ env.model.dispatch("SELECT_FIGURE", { id: figureId });
17175
+ env.model.dispatch("CUT");
17176
+ await env.clipboard.write(env.model.getters.getClipboardContent());
17177
+ },
17178
+ };
17179
+ }
17180
+ function getDeleteMenuItem(figureId, onFigureDeleted, env) {
17181
+ return {
17182
+ id: "delete",
17183
+ name: _lt("Delete"),
17184
+ sequence: 10,
17185
+ action: () => {
17186
+ env.model.dispatch("DELETE_FIGURE", {
17187
+ sheetId: env.model.getters.getActiveSheetId(),
17188
+ id: figureId,
17189
+ });
17190
+ onFigureDeleted();
17191
+ },
17192
+ };
17193
+ }
17257
17194
 
17258
17195
  const inverseCommandRegistry = new Registry()
17259
17196
  .add("ADD_COLUMNS_ROWS", inverseAddColumnsRows)
@@ -18428,7 +18365,7 @@ class ColorPicker extends owl.Component {
18428
18365
  }
18429
18366
  onColorClick(color) {
18430
18367
  if (color) {
18431
- this.props.onColorPicked(color);
18368
+ this.props.onColorPicked(toHex(color));
18432
18369
  }
18433
18370
  }
18434
18371
  getCheckMarkColor() {
@@ -18443,8 +18380,10 @@ class ColorPicker extends owl.Component {
18443
18380
  this.state.isCurrentColorInvalid = true;
18444
18381
  return;
18445
18382
  }
18383
+ const color = toHex(this.state.currentColor);
18446
18384
  this.state.isCurrentColorInvalid = false;
18447
- this.props.onColorPicked(this.state.currentColor);
18385
+ this.props.onColorPicked(color);
18386
+ this.state.currentColor = color;
18448
18387
  }
18449
18388
  toggleColorPicker() {
18450
18389
  this.state.showGradient = !this.state.showGradient;
@@ -20181,9 +20120,8 @@ css /*SCSS*/ `
20181
20120
 
20182
20121
  .o-figure-menu {
20183
20122
  right: 0px;
20123
+ top: 0px;
20184
20124
  display: none;
20185
- position: absolute;
20186
- padding: 5px;
20187
20125
  }
20188
20126
 
20189
20127
  .o-figure-menu-item {
@@ -20201,7 +20139,10 @@ css /*SCSS*/ `
20201
20139
  class FigureComponent extends owl.Component {
20202
20140
  constructor() {
20203
20141
  super(...arguments);
20142
+ this.menuState = owl.useState({ isOpen: false, position: null, menuItems: [] });
20204
20143
  this.figureRef = owl.useRef("figure");
20144
+ this.menuButtonRef = owl.useRef("menuButton");
20145
+ this.menuButtonRect = useAbsoluteBoundingRect(this.menuButtonRef);
20205
20146
  }
20206
20147
  get isSelected() {
20207
20148
  return this.env.model.getters.getSelectedFigureId() === this.props.figure.id;
@@ -20309,9 +20250,33 @@ class FigureComponent extends owl.Component {
20309
20250
  break;
20310
20251
  }
20311
20252
  }
20253
+ onContextMenu(ev) {
20254
+ if (this.env.isDashboard())
20255
+ return;
20256
+ const position = {
20257
+ x: ev.clientX,
20258
+ y: ev.clientY,
20259
+ };
20260
+ this.openContextMenu(position);
20261
+ }
20262
+ showMenu() {
20263
+ const { x, y, width } = this.menuButtonRect;
20264
+ const menuPosition = {
20265
+ x: x >= MENU_WIDTH ? x - MENU_WIDTH : x + width,
20266
+ y: y,
20267
+ };
20268
+ this.openContextMenu(menuPosition);
20269
+ }
20270
+ openContextMenu(position) {
20271
+ this.menuState.isOpen = true;
20272
+ this.menuState.position = position;
20273
+ this.menuState.menuItems = figureRegistry
20274
+ .get(this.props.figure.tag)
20275
+ .menuBuilder(this.props.figure.id, this.props.onFigureDeleted, this.env);
20276
+ }
20312
20277
  }
20313
20278
  FigureComponent.template = "o-spreadsheet-FigureComponent";
20314
- FigureComponent.components = {};
20279
+ FigureComponent.components = { Menu };
20315
20280
  FigureComponent.defaultProps = {
20316
20281
  onFigureDeleted: () => { },
20317
20282
  onMouseDown: () => { },
@@ -22396,6 +22361,7 @@ const XLSX_RELATION_TYPE = {
22396
22361
  theme: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
22397
22362
  table: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table",
22398
22363
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22364
+ image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22399
22365
  };
22400
22366
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22401
22367
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
@@ -23419,12 +23385,8 @@ function getCellType(value) {
23419
23385
  return "n";
23420
23386
  }
23421
23387
  }
23422
- /**
23423
- * For some reason, Excel will only take the devicePixelRatio (i.e. interface scale on Windows desktop)
23424
- * into account for the height.
23425
- */
23426
23388
  function convertHeightToExcel(height) {
23427
- return Math.round(HEIGHT_FACTOR * height * window.devicePixelRatio * 100) / 100;
23389
+ return Math.round(HEIGHT_FACTOR * height * 100) / 100;
23428
23390
  }
23429
23391
  function convertWidthToExcel(width) {
23430
23392
  return Math.round(WIDTH_FACTOR * width * 100) / 100;
@@ -23566,6 +23528,19 @@ function convertChartId(chartId) {
23566
23528
  }
23567
23529
  return xlsxId + 1;
23568
23530
  }
23531
+ const imageIds = [];
23532
+ /**
23533
+ * Convert a image o-spreadsheet id to a xlsx id which
23534
+ * are unsigned integers (starting from 1).
23535
+ */
23536
+ function convertImageId(imageId) {
23537
+ const xlsxId = imageIds.findIndex((id) => id === imageId);
23538
+ if (xlsxId === -1) {
23539
+ imageIds.push(imageId);
23540
+ return imageIds.length;
23541
+ }
23542
+ return xlsxId + 1;
23543
+ }
23569
23544
  /**
23570
23545
  * Convert a value expressed in dot to EMU.
23571
23546
  * EMU = English Metrical Unit
@@ -26327,6 +26302,7 @@ function createEmptyExcelSheet(sheetId, name) {
26327
26302
  return {
26328
26303
  ...createEmptySheet(sheetId, name),
26329
26304
  charts: [],
26305
+ images: [],
26330
26306
  };
26331
26307
  }
26332
26308
  function createEmptyExcelWorkbookData() {
@@ -28962,7 +28938,7 @@ class FiltersPlugin extends CorePlugin {
28962
28938
  filters.push(new Filter(filter.id, filterZone));
28963
28939
  }
28964
28940
  // Add filters for new columns
28965
- if (filters.length < zoneToDimension(zone).width) {
28941
+ if (filters.length < zoneToDimension(zone).numberOfCols) {
28966
28942
  for (let col = zone.left; col <= zone.right; col++) {
28967
28943
  if (!filters.find((filter) => filter.col === col)) {
28968
28944
  filters.push(new Filter(this.uuidGenerator.uuidv4(), { ...zone, left: col, right: col }));
@@ -29613,6 +29589,24 @@ class ImagePlugin extends CorePlugin {
29613
29589
  }
29614
29590
  }
29615
29591
  }
29592
+ exportForExcel(data) {
29593
+ for (const sheet of data.sheets) {
29594
+ const figures = this.getters.getFigures(sheet.id);
29595
+ const images = [];
29596
+ for (const figure of figures) {
29597
+ if ((figure === null || figure === void 0 ? void 0 : figure.tag) === "image") {
29598
+ const image = this.getImage(figure.id);
29599
+ if (image) {
29600
+ images.push({
29601
+ ...figure,
29602
+ data: deepCopy(image),
29603
+ });
29604
+ }
29605
+ }
29606
+ }
29607
+ sheet.images = images;
29608
+ }
29609
+ }
29616
29610
  getAllImages() {
29617
29611
  const images = [];
29618
29612
  for (const sheetId in this.images) {
@@ -29810,8 +29804,8 @@ class MergePlugin extends CorePlugin {
29810
29804
  if (merge) {
29811
29805
  return isEqual(zone, merge);
29812
29806
  }
29813
- const { width, height } = zoneToDimension(zone);
29814
- return width === 1 && height === 1;
29807
+ const { numberOfCols, numberOfRows } = zoneToDimension(zone);
29808
+ return numberOfCols === 1 && numberOfRows === 1;
29815
29809
  }
29816
29810
  // ---------------------------------------------------------------------------
29817
29811
  // Merges
@@ -29979,8 +29973,8 @@ class MergePlugin extends CorePlugin {
29979
29973
  this.removeMerge(sheetId, currentZone);
29980
29974
  break;
29981
29975
  default:
29982
- const { width, height } = zoneToDimension(result.range.zone);
29983
- if (width === 1 && height === 1) {
29976
+ const { numberOfCols, numberOfRows } = zoneToDimension(result.range.zone);
29977
+ if (numberOfCols === 1 && numberOfRows === 1) {
29984
29978
  this.removeMerge(sheetId, currentZone);
29985
29979
  }
29986
29980
  else {
@@ -30739,8 +30733,8 @@ class SheetPlugin extends CorePlugin {
30739
30733
  }
30740
30734
  getSheetSize(sheetId) {
30741
30735
  return {
30742
- height: this.getNumberRows(sheetId),
30743
- width: this.getNumberCols(sheetId),
30736
+ numberOfRows: this.getNumberRows(sheetId),
30737
+ numberOfCols: this.getNumberCols(sheetId),
30744
30738
  };
30745
30739
  }
30746
30740
  getSheetZone(sheetId) {
@@ -32256,6 +32250,9 @@ class FilterEvaluationPlugin extends UIPlugin {
32256
32250
  case "CREATE_SHEET":
32257
32251
  this.filterValues[cmd.sheetId] = {};
32258
32252
  break;
32253
+ case "HIDE_COLUMNS_ROWS":
32254
+ this.updateHiddenRows();
32255
+ break;
32259
32256
  case "UPDATE_FILTER":
32260
32257
  this.updateFilter(cmd);
32261
32258
  this.updateHiddenRows();
@@ -32364,9 +32361,16 @@ class FilterEvaluationPlugin extends UIPlugin {
32364
32361
  updateHiddenRows() {
32365
32362
  var _a, _b;
32366
32363
  const sheetId = this.getters.getActiveSheetId();
32367
- const filters = this.getters.getFilters(sheetId);
32364
+ const filters = this.getters
32365
+ .getFilters(sheetId)
32366
+ .sort((filter1, filter2) => filter1.zoneWithHeaders.top - filter2.zoneWithHeaders.top);
32368
32367
  const hiddenRows = new Set();
32369
32368
  for (let filter of filters) {
32369
+ // Disable filters whose header are hidden
32370
+ if (this.getters.isRowHiddenByUser(sheetId, filter.zoneWithHeaders.top))
32371
+ continue;
32372
+ if (hiddenRows.has(filter.zoneWithHeaders.top))
32373
+ continue;
32370
32374
  const filteredValues = (_b = (_a = this.filterValues[sheetId]) === null || _a === void 0 ? void 0 : _a[filter.id]) === null || _b === void 0 ? void 0 : _b.map(toLowerCase);
32371
32375
  if (!filteredValues || !filter.filteredZone)
32372
32376
  continue;
@@ -32389,7 +32393,7 @@ class FilterEvaluationPlugin extends UIPlugin {
32389
32393
  const tableZone = toZone(tableData.range);
32390
32394
  const filters = [];
32391
32395
  const headerNames = [];
32392
- for (const i of range(0, zoneToDimension(tableZone).width)) {
32396
+ for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {
32393
32397
  const position = {
32394
32398
  sheetId: sheetData.id,
32395
32399
  col: tableZone.left + i,
@@ -33900,7 +33904,7 @@ class AutomaticSumPlugin extends UIPlugin {
33900
33904
  dimensionsToSum(sheetId, zone) {
33901
33905
  const dimensions = new Set();
33902
33906
  if (isOneDimensional(zone)) {
33903
- dimensions.add(zoneToDimension(zone).width === 1 ? "COL" : "ROW");
33907
+ dimensions.add(zoneToDimension(zone).numberOfCols === 1 ? "COL" : "ROW");
33904
33908
  return dimensions;
33905
33909
  }
33906
33910
  if (this.lastColIsEmpty(sheetId, zone)) {
@@ -34488,8 +34492,8 @@ class HighlightPlugin extends UIPlugin {
34488
34492
  x.zone.bottom < this.getters.getNumberRows(x.sheetId) &&
34489
34493
  x.zone.right < this.getters.getNumberCols(x.sheetId))
34490
34494
  .map((highlight) => {
34491
- const { height, width } = zoneToDimension(highlight.zone);
34492
- const zone = height * width === 1
34495
+ const { numberOfRows, numberOfCols } = zoneToDimension(highlight.zone);
34496
+ const zone = numberOfRows * numberOfCols === 1
34493
34497
  ? this.getters.expandZone(highlight.sheetId, highlight.zone)
34494
34498
  : highlight.zone;
34495
34499
  return {
@@ -36396,7 +36400,7 @@ class SortPlugin extends UIPlugin {
36396
36400
  const merges = this.getters.getMerges(sheetId).filter((merge) => overlap(merge, zone));
36397
36401
  /*Test the presence of merges of different sizes*/
36398
36402
  const mergeDimension = zoneToDimension(merges[0]);
36399
- let [widthFirst, heightFirst] = [mergeDimension.width, mergeDimension.height];
36403
+ let [widthFirst, heightFirst] = [mergeDimension.numberOfCols, mergeDimension.numberOfRows];
36400
36404
  if (!merges.every((merge) => {
36401
36405
  let [widthCurrent, heightCurrent] = [
36402
36406
  merge.right - merge.left + 1,
@@ -37529,9 +37533,9 @@ class ClipboardOsState extends ClipboardCellsAbstractState {
37529
37533
  const values = this.values;
37530
37534
  const pasteZone = this.getPasteZone(target);
37531
37535
  const { left: activeCol, top: activeRow } = pasteZone;
37532
- const { width, height } = zoneToDimension(pasteZone);
37536
+ const { numberOfCols, numberOfRows } = zoneToDimension(pasteZone);
37533
37537
  const sheetId = this.getters.getActiveSheetId();
37534
- this.addMissingDimensions(width, height, activeCol, activeRow);
37538
+ this.addMissingDimensions(numberOfCols, numberOfRows, activeCol, activeRow);
37535
37539
  for (let i = 0; i < values.length; i++) {
37536
37540
  for (let j = 0; j < values[i].length; j++) {
37537
37541
  this.dispatch("UPDATE_CELL", {
@@ -37545,8 +37549,8 @@ class ClipboardOsState extends ClipboardCellsAbstractState {
37545
37549
  const zone = {
37546
37550
  left: activeCol,
37547
37551
  top: activeRow,
37548
- right: activeCol + width - 1,
37549
- bottom: activeRow + height - 1,
37552
+ right: activeCol + numberOfCols - 1,
37553
+ bottom: activeRow + numberOfRows - 1,
37550
37554
  };
37551
37555
  this.selection.selectZone({ cell: { col: activeCol, row: activeRow }, zone });
37552
37556
  }
@@ -43645,7 +43649,7 @@ function getExcelThresholdType(type, position) {
43645
43649
  }
43646
43650
  }
43647
43651
 
43648
- function createDrawing(chartRelIds, sheet, figures) {
43652
+ function createDrawing(drawingRelIds, sheet, figures) {
43649
43653
  const namespaces = [
43650
43654
  ["xmlns:xdr", NAMESPACE.drawing],
43651
43655
  ["xmlns:r", RELATIONSHIP_NSR],
@@ -43654,46 +43658,14 @@ function createDrawing(chartRelIds, sheet, figures) {
43654
43658
  ];
43655
43659
  const figuresNodes = [];
43656
43660
  for (const [figureIndex, figure] of Object.entries(figures)) {
43657
- // position
43658
- const { from, to } = convertFigureData(figure, sheet);
43659
- const chartId = convertChartId(figure.id);
43660
- const cNvPrAttrs = [
43661
- ["id", chartId],
43662
- ["name", `Chart ${chartId}`],
43663
- ["title", "Chart"],
43664
- ];
43665
- figuresNodes.push(escapeXml /*xml*/ `
43666
- <xdr:twoCellAnchor>
43667
- <xdr:from>
43668
- <xdr:col>${from.col}</xdr:col>
43669
- <xdr:colOff>${from.colOff}</xdr:colOff>
43670
- <xdr:row>${from.row}</xdr:row>
43671
- <xdr:rowOff>${from.rowOff}</xdr:rowOff>
43672
- </xdr:from>
43673
- <xdr:to>
43674
- <xdr:col>${to.col}</xdr:col>
43675
- <xdr:colOff>${to.colOff}</xdr:colOff>
43676
- <xdr:row>${to.row}</xdr:row>
43677
- <xdr:rowOff>${to.rowOff}</xdr:rowOff>
43678
- </xdr:to>
43679
- <xdr:graphicFrame>
43680
- <xdr:nvGraphicFramePr>
43681
- <xdr:cNvPr ${formatAttributes(cNvPrAttrs)} />
43682
- <xdr:cNvGraphicFramePr />
43683
- </xdr:nvGraphicFramePr>
43684
- <xdr:xfrm>
43685
- <a:off x="0" y="0"/>
43686
- <a:ext cx="0" cy="0"/>
43687
- </xdr:xfrm>
43688
- <a:graphic>
43689
- <a:graphicData uri="${DRAWING_NS_C}">
43690
- <c:chart r:id="${chartRelIds[figureIndex]}" />
43691
- </a:graphicData>
43692
- </a:graphic>
43693
- </xdr:graphicFrame>
43694
- <xdr:clientData fLocksWithSheet="0"/>
43695
- </xdr:twoCellAnchor>
43696
- `);
43661
+ switch (figure === null || figure === void 0 ? void 0 : figure.tag) {
43662
+ case "chart":
43663
+ figuresNodes.push(createChartDrawing(figure, sheet, drawingRelIds[figureIndex]));
43664
+ break;
43665
+ case "image":
43666
+ figuresNodes.push(createImageDrawing(figure, sheet, drawingRelIds[figureIndex]));
43667
+ break;
43668
+ }
43697
43669
  }
43698
43670
  const xml = escapeXml /*xml*/ `
43699
43671
  <xdr:wsDr ${formatAttributes(namespaces)}>
@@ -43748,6 +43720,93 @@ function figureCoordinates(headers, position) {
43748
43720
  index: headers.length - 1,
43749
43721
  offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_SIZE),
43750
43722
  };
43723
+ }
43724
+ function createChartDrawing(figure, sheet, chartRelId) {
43725
+ // position
43726
+ const { from, to } = convertFigureData(figure, sheet);
43727
+ const chartId = convertChartId(figure.id);
43728
+ const cNvPrAttrs = [
43729
+ ["id", chartId],
43730
+ ["name", `Chart ${chartId}`],
43731
+ ["title", "Chart"],
43732
+ ];
43733
+ return escapeXml /*xml*/ `
43734
+ <xdr:twoCellAnchor>
43735
+ <xdr:from>
43736
+ <xdr:col>${from.col}</xdr:col>
43737
+ <xdr:colOff>${from.colOff}</xdr:colOff>
43738
+ <xdr:row>${from.row}</xdr:row>
43739
+ <xdr:rowOff>${from.rowOff}</xdr:rowOff>
43740
+ </xdr:from>
43741
+ <xdr:to>
43742
+ <xdr:col>${to.col}</xdr:col>
43743
+ <xdr:colOff>${to.colOff}</xdr:colOff>
43744
+ <xdr:row>${to.row}</xdr:row>
43745
+ <xdr:rowOff>${to.rowOff}</xdr:rowOff>
43746
+ </xdr:to>
43747
+ <xdr:graphicFrame>
43748
+ <xdr:nvGraphicFramePr>
43749
+ <xdr:cNvPr ${formatAttributes(cNvPrAttrs)} />
43750
+ <xdr:cNvGraphicFramePr />
43751
+ </xdr:nvGraphicFramePr>
43752
+ <xdr:xfrm>
43753
+ <a:off x="0" y="0"/>
43754
+ <a:ext cx="0" cy="0"/>
43755
+ </xdr:xfrm>
43756
+ <a:graphic>
43757
+ <a:graphicData uri="${DRAWING_NS_C}">
43758
+ <c:chart r:id="${chartRelId}" />
43759
+ </a:graphicData>
43760
+ </a:graphic>
43761
+ </xdr:graphicFrame>
43762
+ <xdr:clientData fLocksWithSheet="0"/>
43763
+ </xdr:twoCellAnchor>
43764
+ `;
43765
+ }
43766
+ function createImageDrawing(figure, sheet, imageRelId) {
43767
+ // position
43768
+ const { from, to } = convertFigureData(figure, sheet);
43769
+ const imageId = convertImageId(figure.id);
43770
+ const cNvPrAttrs = [
43771
+ ["id", imageId],
43772
+ ["name", `Image ${imageId}`],
43773
+ ["title", "Image"],
43774
+ ];
43775
+ return escapeXml /*xml*/ `
43776
+ <xdr:twoCellAnchor editAs="oneCell">
43777
+ <xdr:from>
43778
+ <xdr:col>${from.col}</xdr:col>
43779
+ <xdr:colOff>${from.colOff}</xdr:colOff>
43780
+ <xdr:row>${from.row}</xdr:row>
43781
+ <xdr:rowOff>${from.rowOff}</xdr:rowOff>
43782
+ </xdr:from>
43783
+ <xdr:to>
43784
+ <xdr:col>${to.col}</xdr:col>
43785
+ <xdr:colOff>${to.colOff}</xdr:colOff>
43786
+ <xdr:row>${to.row}</xdr:row>
43787
+ <xdr:rowOff>${to.rowOff}</xdr:rowOff>
43788
+ </xdr:to>
43789
+ <xdr:pic>
43790
+ <xdr:nvPicPr>
43791
+ <xdr:cNvPr ${formatAttributes(cNvPrAttrs)}/>
43792
+ <xdr:cNvPicPr preferRelativeResize="0"/>
43793
+ </xdr:nvPicPr>
43794
+ <xdr:blipFill>
43795
+ <a:blip cstate="print" r:embed="${imageRelId}"/>
43796
+ <a:stretch>
43797
+ <a:fillRect/>
43798
+ </a:stretch>
43799
+ </xdr:blipFill>
43800
+ <xdr:spPr>
43801
+ <a:prstGeom prst="rect">
43802
+ <a:avLst/>
43803
+ </a:prstGeom>
43804
+ <a:noFill/>
43805
+ </xdr:spPr>
43806
+ </xdr:pic>
43807
+ <xdr:clientData fLocksWithSheet="0"/>
43808
+ </xdr:twoCellAnchor>
43809
+ `;
43751
43810
  }
43752
43811
 
43753
43812
  function addNumberFormats(numFmts) {
@@ -43943,7 +44002,7 @@ function addAutoFilter(table) {
43943
44002
  function addFilterColumns(table) {
43944
44003
  const tableZone = toZone(table.range);
43945
44004
  const columns = [];
43946
- for (const i of range(0, zoneToDimension(tableZone).width)) {
44005
+ for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {
43947
44006
  const filter = table.filters[i];
43948
44007
  if (!filter || !filter.filteredValues.length) {
43949
44008
  continue;
@@ -43969,7 +44028,7 @@ function addTableColumns(table, sheetData) {
43969
44028
  var _a;
43970
44029
  const tableZone = toZone(table.range);
43971
44030
  const columns = [];
43972
- for (const i of range(0, zoneToDimension(tableZone).width)) {
44031
+ for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {
43973
44032
  const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
43974
44033
  const colName = ((_a = sheetData.cells[colHeaderXc]) === null || _a === void 0 ? void 0 : _a.content) || `col${i}`;
43975
44034
  const colAttributes = [
@@ -44217,23 +44276,37 @@ function createWorksheets(data, construct) {
44217
44276
  currentTableIndex += sheet.filterTables.length;
44218
44277
  // Figures and Charts
44219
44278
  let drawingNode = escapeXml ``;
44279
+ const drawingRelIds = [];
44220
44280
  const charts = sheet.charts;
44221
- if (charts.length) {
44222
- const chartRelIds = [];
44223
- for (const chart of charts) {
44224
- const xlsxChartId = convertChartId(chart.id);
44225
- const chartRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {
44226
- target: `../charts/chart${xlsxChartId}.xml`,
44227
- type: XLSX_RELATION_TYPE.chart,
44228
- });
44229
- chartRelIds.push(chartRelId);
44230
- files.push(createXMLFile(createChart(chart, sheetIndex, data), `xl/charts/chart${xlsxChartId}.xml`, "chart"));
44231
- }
44281
+ for (const chart of charts) {
44282
+ const xlsxChartId = convertChartId(chart.id);
44283
+ const chartRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {
44284
+ target: `../charts/chart${xlsxChartId}.xml`,
44285
+ type: XLSX_RELATION_TYPE.chart,
44286
+ });
44287
+ drawingRelIds.push(chartRelId);
44288
+ files.push(createXMLFile(createChart(chart, sheetIndex, data), `xl/charts/chart${xlsxChartId}.xml`, "chart"));
44289
+ }
44290
+ const images = sheet.images;
44291
+ for (const image of images) {
44292
+ const xlsxImageId = convertImageId(image.id);
44293
+ const imageRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {
44294
+ target: `../media/image${xlsxImageId}`,
44295
+ type: XLSX_RELATION_TYPE.image,
44296
+ });
44297
+ drawingRelIds.push(imageRelId);
44298
+ files.push({
44299
+ path: `xl/media/image${xlsxImageId}`,
44300
+ imagePath: image.data.path,
44301
+ });
44302
+ }
44303
+ const drawings = [...charts, ...images];
44304
+ if (drawings.length) {
44232
44305
  const drawingRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {
44233
44306
  target: `../drawings/drawing${sheetIndex}.xml`,
44234
44307
  type: XLSX_RELATION_TYPE.drawing,
44235
44308
  });
44236
- files.push(createXMLFile(createDrawing(chartRelIds, sheet, charts), `xl/drawings/drawing${sheetIndex}.xml`, "drawing"));
44309
+ files.push(createXMLFile(createDrawing(drawingRelIds, sheet, drawings), `xl/drawings/drawing${sheetIndex}.xml`, "drawing"));
44237
44310
  drawingNode = escapeXml /*xml*/ `<drawing r:id="${drawingRelId}" />`;
44238
44311
  }
44239
44312
  const sheetXml = escapeXml /*xml*/ `
@@ -44347,7 +44420,7 @@ function createRelsFiles(relsFiles) {
44347
44420
  function createContentTypes(files) {
44348
44421
  const overrideNodes = [];
44349
44422
  for (const file of files) {
44350
- if (file.contentType) {
44423
+ if ("contentType" in file && file.contentType) {
44351
44424
  overrideNodes.push(createOverride("/" + file.path, CONTENT_TYPES[file.contentType]));
44352
44425
  }
44353
44426
  }
@@ -44903,6 +44976,6 @@ exports.setTranslationMethod = setTranslationMethod;
44903
44976
  exports.tokenize = tokenize;
44904
44977
 
44905
44978
 
44906
- __info__.version = '16.2.0-alpha.3';
44907
- __info__.date = '2023-03-06T07:18:09.258Z';
44908
- __info__.hash = 'ec749cf';
44979
+ __info__.version = '16.2.0-alpha.4';
44980
+ __info__.date = '2023-03-14T09:24:38.219Z';
44981
+ __info__.hash = '2c6fe88';