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