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