diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js index d832bd8fd40..3ac59582d39 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js @@ -374,6 +374,7 @@ const FILTER_ICON_EDGE_LENGTH = 17; // Menus const MENU_WIDTH = 250; + const MENU_VERTICAL_PADDING = 8; const MENU_ITEM_HEIGHT = 28; const MENU_SEPARATOR_BORDER_WIDTH = 1; const MENU_SEPARATOR_PADDING = 5; @@ -4241,7 +4242,7 @@ return "\n" + str + "\n"; } - const ERROR_TOOLTIP_HEIGHT = 40; + const ERROR_TOOLTIP_MAX_HEIGHT = 80; const ERROR_TOOLTIP_WIDTH = 180; css /* scss */ ` .o-error-tooltip { @@ -4249,11 +4250,13 @@ background-color: white; border-left: 3px solid red; padding: 10px; + width: ${ERROR_TOOLTIP_WIDTH}px; + box-sizing: border-box !important; } `; class ErrorToolTip extends owl.Component { } - ErrorToolTip.size = { width: ERROR_TOOLTIP_WIDTH, height: ERROR_TOOLTIP_HEIGHT }; + ErrorToolTip.maxSize = { maxHeight: ERROR_TOOLTIP_MAX_HEIGHT }; ErrorToolTip.template = "o-spreadsheet-ErrorToolTip"; ErrorToolTip.components = {}; ErrorToolTip.props = { @@ -4627,8 +4630,8 @@ * Return the o-spreadsheet element position relative * to the browser viewport. */ - function useSpreadsheetPosition() { - const position = owl.useState({ x: 0, y: 0 }); + function useSpreadsheetRect() { + const position = owl.useState({ x: 0, y: 0, width: 0, height: 0 }); let spreadsheetElement = document.querySelector(".o-spreadsheet"); updatePosition(); function updatePosition() { @@ -4636,9 +4639,11 @@ spreadsheetElement = document.querySelector(".o-spreadsheet"); } if (spreadsheetElement) { - const { top, left } = spreadsheetElement.getBoundingClientRect(); + const { top, left, width, height } = spreadsheetElement.getBoundingClientRect(); position.x = left; position.y = top; + position.width = width; + position.height = height; } } owl.onMounted(updatePosition); @@ -4669,76 +4674,266 @@ owl.onPatched(updateElPosition); return position; } + /** + * Get the rectangle inside which a popover should stay when being displayed. + * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet" + * element by default. + * + * Coordinates are expressed expressed as absolute DOM position. + */ + function usePopoverContainer() { + const container = owl.useState({ x: 0, y: 0, width: 0, height: 0 }); + const component = owl.useComponent(); + const spreadsheetRect = useSpreadsheetRect(); + function updateRect() { + const env = component.env; + const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect; + container.x = newRect.x; + container.y = newRect.x; + container.width = newRect.width; + container.height = newRect.height; + } + updateRect(); + owl.onMounted(updateRect); + owl.onPatched(updateRect); + return container; + } + /** + * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap + */ + function rectIntersection(rect1, rect2) { + return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2))); + } + function rectToZone(rect) { + return { + left: rect.x, + top: rect.y, + right: rect.x + rect.width, + bottom: rect.y + rect.height, + }; + } + function zoneToRect(zone) { + if (!zone) + return undefined; + return { + x: zone.left, + y: zone.top, + width: zone.right - zone.left, + height: zone.bottom - zone.top, + }; + } + + css /* scss */ ` + .o-popover { + position: absolute; + z-index: ${ComponentsImportance.Popover}; + overflow: auto; + box-shadow: 1px 2px 5px 2px rgb(51 51 51 / 15%); + width: fit-content; + height: fit-content; + } +`; class Popover extends owl.Component { constructor() { super(...arguments); - this.spreadsheetPosition = useSpreadsheetPosition(); + this.popoverRef = owl.useRef("popover"); + this.currentPosition = undefined; + this.currentVisibility = undefined; + this.spreadsheetRect = useSpreadsheetRect(); } - get style() { - // the props's position is expressed relative to the "body" element - // but we teleport the element in ".o-spreadsheet" to keep everything - // within our control and to avoid leaking into external DOM - const horizontalPosition = `left:${this.horizontalPosition() - this.spreadsheetPosition.x}`; - const verticalPosition = `top:${this.verticalPosition() - this.spreadsheetPosition.y}`; - const height = `max-height:${this.viewportDimension.height - BOTTOMBAR_HEIGHT - SCROLLBAR_WIDTH$1}`; - return ` - position: absolute; - z-index: ${ComponentsImportance.Popover}; - ${verticalPosition}px; - ${horizontalPosition}px; - ${height}px; - width:${this.props.childWidth}px; - overflow-y: auto; - overflow-x: hidden; - box-shadow: 1px 2px 5px 2px rgb(51 51 51 / 15%); - `; + setup() { + this.containerRect = usePopoverContainer(); + // useEffect occurs after the DOM is created and the element width/height are computed, but before + // the element in rendered, so we can still set its position + owl.useEffect(() => { + var _a, _b, _c, _d; + if (!this.containerRect) + throw new Error("Popover container is not defined"); + this.resetPopoverElStyle(); + const el = this.popoverRef.el; + const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight }; + const elDims = { + width: el.getBoundingClientRect().width, + height: el.getBoundingClientRect().height, + }; + const spreadsheetRect = this.spreadsheetRect; + const anchor = rectIntersection(this.props.anchorRect, this.containerRect); + const newVisibility = anchor ? "visible" : "hidden"; + if (this.currentVisibility !== "hidden" && newVisibility === "hidden") { + (_b = (_a = this.props).onPopoverHidden) === null || _b === void 0 ? void 0 : _b.call(_a); + } + el.style.visibility = newVisibility; + this.currentVisibility = newVisibility; + if (!anchor) + return; + const popoverPositionHelper = this.props.positioning === "BottomLeft" + ? new BottomLeftPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect) + : new TopRightPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect); + const style = popoverPositionHelper.getCss(elDims, this.props.verticalOffset); + for (const property of Object.keys(style)) { + el.style[property] = style[property]; + } + const newPosition = popoverPositionHelper.getCurrentPosition(elDims); + if (this.currentPosition && newPosition !== this.currentPosition) { + (_d = (_c = this.props).onPopoverMoved) === null || _d === void 0 ? void 0 : _d.call(_c); + } + this.currentPosition = newPosition; + }); } - get viewportDimension() { - return this.env.model.getters.getSheetViewDimensionWithHeaders(); - } - get shouldRenderRight() { - const { x } = this.props.position; - return x + this.props.childWidth < this.viewportDimension.width; - } - get shouldRenderBottom() { - const { y } = this.props.position; - return (y + this.props.childHeight < - this.viewportDimension.height + (this.env.isDashboard() ? 0 : TOPBAR_HEIGHT)); - } - horizontalPosition() { - const { x } = this.props.position; - if (this.shouldRenderRight) { - return x; - } - return x - this.props.childWidth - this.props.flipHorizontalOffset; - } - verticalPosition() { - const { y } = this.props.position; - if (this.shouldRenderBottom) { - return y; - } - return Math.max(y - this.props.childHeight + this.props.flipVerticalOffset, this.props.marginTop); + resetPopoverElStyle() { + const el = this.popoverRef.el; + if (!el) + return; + el.style["visibility"] = "visible"; + el.style["max-height"] = el.style["max-width"] = ""; + el.style["bottom"] = el.style["top"] = el.style["right"] = el.style["left"] = ""; } } Popover.template = "o-spreadsheet-Popover"; Popover.defaultProps = { - flipHorizontalOffset: 0, - flipVerticalOffset: 0, + positioning: "BottomLeft", verticalOffset: 0, - marginTop: 0, onMouseWheel: () => { }, + onPopoverMoved: () => { }, + onPopoverHidden: () => { }, }; Popover.props = { - position: Object, - marginTop: { type: Number, optional: true }, - childWidth: Number, - childHeight: Number, - flipHorizontalOffset: { type: Number, optional: true }, - flipVerticalOffset: { type: Number, optional: true }, + anchorRect: Object, + containerRect: { type: Object, optional: true }, + positioning: { type: String, optional: true }, + maxWidth: { type: Number, optional: true }, + maxHeight: { type: Number, optional: true }, + verticalOffset: { type: Number, optional: true }, onMouseWheel: { type: Function, optional: true }, + onPopoverHidden: { type: Function, optional: true }, + onPopoverMoved: { type: Function, optional: true }, slots: Object, }; + class PopoverPositionContext { + constructor(anchorRect, containerRect, propsMaxSize, spreadsheetOffset) { + this.anchorRect = anchorRect; + this.containerRect = containerRect; + this.propsMaxSize = propsMaxSize; + this.spreadsheetOffset = spreadsheetOffset; + } + /** Check if there is enough space for the popover to be rendered at the bottom of the anchorRect */ + shouldRenderAtBottom(elementHeight) { + return (elementHeight <= this.availableHeightDown || + this.availableHeightDown >= this.availableHeightUp); + } + /** Check if there is enough space for the popover to be rendered at the right of the anchorRect */ + shouldRenderAtRight(elementWidth) { + return (elementWidth <= this.availableWidthRight || + this.availableWidthRight >= this.availableWidthLeft); + } + getMaxHeight(elementHeight) { + const shouldRenderAtBottom = this.shouldRenderAtBottom(elementHeight); + const availableHeight = shouldRenderAtBottom + ? this.availableHeightDown + : this.availableHeightUp; + return this.propsMaxSize.height + ? Math.min(availableHeight, this.propsMaxSize.height) + : availableHeight; + } + getMaxWidth(elementWidth) { + const shouldRenderAtRight = this.shouldRenderAtRight(elementWidth); + const availableWidth = shouldRenderAtRight ? this.availableWidthRight : this.availableWidthLeft; + return this.propsMaxSize.width + ? Math.min(availableWidth, this.propsMaxSize.width) + : availableWidth; + } + getCss(elDims, verticalOffset) { + const maxHeight = this.getMaxHeight(elDims.height); + const maxWidth = this.getMaxWidth(elDims.width); + const actualHeight = Math.min(maxHeight, elDims.height); + const actualWidth = Math.min(maxWidth, elDims.width); + const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height); + const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width); + verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset; + const cssProperties = { + "max-height": maxHeight + "px", + "max-width": maxWidth + "px", + top: this.getTopCoordinate(actualHeight, shouldRenderAtBottom) - + this.spreadsheetOffset.y - + verticalOffset + + "px", + left: this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + "px", + }; + return cssProperties; + } + getCurrentPosition(elDims) { + const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height); + const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width); + if (shouldRenderAtBottom && shouldRenderAtRight) + return "BottomRight"; + if (shouldRenderAtBottom && !shouldRenderAtRight) + return "BottomLeft"; + if (!shouldRenderAtBottom && shouldRenderAtRight) + return "TopRight"; + return "TopLeft"; + } + } + class BottomLeftPopoverContext extends PopoverPositionContext { + get availableHeightUp() { + return this.anchorRect.y - this.containerRect.y; + } + get availableHeightDown() { + return this.containerRect.height - this.availableHeightUp - this.anchorRect.height; + } + get availableWidthRight() { + return this.containerRect.x + this.containerRect.width - this.anchorRect.x; + } + get availableWidthLeft() { + return this.anchorRect.x + this.anchorRect.width - this.containerRect.x; + } + getTopCoordinate(elementHeight, shouldRenderAtBottom) { + if (shouldRenderAtBottom) { + return this.anchorRect.y + this.anchorRect.height; + } + else { + return this.anchorRect.y - elementHeight; + } + } + getLeftCoordinate(elementWidth, shouldRenderAtRight) { + if (shouldRenderAtRight) { + return this.anchorRect.x; + } + else { + return this.anchorRect.x + this.anchorRect.width - elementWidth; + } + } + } + class TopRightPopoverContext extends PopoverPositionContext { + get availableHeightUp() { + return this.anchorRect.y + this.anchorRect.height - this.containerRect.y; + } + get availableHeightDown() { + return this.containerRect.y + this.containerRect.height - this.anchorRect.y; + } + get availableWidthRight() { + return this.containerRect.width - this.anchorRect.width - this.availableWidthLeft; + } + get availableWidthLeft() { + return this.anchorRect.x - this.containerRect.x; + } + getTopCoordinate(elementHeight, shouldRenderAtBottom) { + if (shouldRenderAtBottom) { + return this.anchorRect.y; + } + else { + return this.anchorRect.y + this.anchorRect.height - elementHeight; + } + } + getLeftCoordinate(elementWidth, shouldRenderAtRight) { + if (shouldRenderAtRight) { + return this.anchorRect.x + this.anchorRect.width; + } + else { + return this.anchorRect.x - elementWidth; + } + } + } //------------------------------------------------------------------------------ // Context Menu Component @@ -4746,7 +4941,10 @@ css /* scss */ ` .o-menu { background-color: white; - padding: 5px 0px; + padding: ${MENU_VERTICAL_PADDING}px 0px; + width: ${MENU_WIDTH}px; + box-sizing: border-box !important; + .o-menu-item { display: flex; justify-content: space-between; @@ -4794,7 +4992,6 @@ class Menu extends owl.Component { constructor() { super(...arguments); - this.MENU_WIDTH = MENU_WIDTH; this.subMenu = owl.useState({ isOpen: false, position: null, @@ -4819,20 +5016,25 @@ return position; } get menuHeight() { - return this.menuComponentHeight(this.props.menuItems); - } - get subMenuHeight() { - return this.menuComponentHeight(this.subMenu.menuItems); + const menuItems = this.props.menuItems; + let menuItemsHeight = this.getMenuItemsHeight(menuItems); + // We don't display separator at the end of a menu + if (menuItems[menuItems.length - 1].separator) { + menuItemsHeight -= MENU_SEPARATOR_HEIGHT; + } + return 2 * MENU_VERTICAL_PADDING + menuItemsHeight; } get popover() { const isRoot = this.props.depth === 1; - // TODO: see if we could reformulate this margin - const marginTop = 6 + TOPBAR_HEIGHT + HEADER_HEIGHT; return { - // some margin between the header and the component - marginTop, - flipHorizontalOffset: MENU_WIDTH * (this.props.depth - 1), - flipVerticalOffset: isRoot ? 0 : MENU_ITEM_HEIGHT, + anchorRect: { + x: this.props.position.x - MENU_WIDTH * (this.props.depth - 1), + y: this.props.position.y, + width: isRoot ? 0 : MENU_WIDTH, + height: isRoot ? 0 : MENU_ITEM_HEIGHT, + }, + positioning: "TopRight", + verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING, }; } getColor(menu) { @@ -4852,9 +5054,9 @@ * Return the number of pixels between the top of the menu * and the menu item at a given index. */ - subMenuVerticalPosition(position) { - const menusAbove = this.props.menuItems.slice(0, position); - return this.menuComponentHeight(menusAbove) + this.position.y; + subMenuVerticalPosition(menuIndex) { + const menusAbove = this.props.menuItems.slice(0, menuIndex); + return this.position.y + this.getMenuItemsHeight(menusAbove) + MENU_VERTICAL_PADDING; } onClick(ev) { // Don't close a root menu when clicked to open the submenus. @@ -4872,14 +5074,9 @@ } this.closeSubMenu(); } - /** - * Return the total height (in pixels) needed for some - * menu items - */ - menuComponentHeight(menuItems) { - const separators = menuItems.filter((m) => m.separator); - const others = menuItems; - return MENU_ITEM_HEIGHT * others.length + separators.length * MENU_SEPARATOR_HEIGHT; + getMenuItemsHeight(menuItems) { + const numberOfSeparators = menuItems.filter((m) => m.separator).length; + return MENU_ITEM_HEIGHT * menuItems.length + MENU_SEPARATOR_HEIGHT * numberOfSeparators; } getName(menu) { return getMenuName(menu, this.env); @@ -4903,8 +5100,8 @@ * If the given menu is not disabled, open it's submenu at the * correct position according to available surrounding space. */ - openSubMenu(menu, position) { - const y = this.subMenuVerticalPosition(position); + openSubMenu(menu, menuIndex) { + const y = this.subMenuVerticalPosition(menuIndex); this.subMenu.position = { x: this.position.x + MENU_WIDTH, y: y - (this.subMenu.scrollOffset || 0), @@ -4921,10 +5118,10 @@ this.subMenu.isOpen = false; this.subMenu.parentMenu = undefined; } - onClickMenu(menu, position) { + onClickMenu(menu, menuIndex) { if (this.isEnabled(menu)) { if (this.isRoot(menu)) { - this.openSubMenu(menu, position); + this.openSubMenu(menu, menuIndex); } else { this.activateMenu(menu); @@ -4955,7 +5152,7 @@ onMenuClicked: { type: Function, optional: true }, }; - const LINK_TOOLTIP_HEIGHT = 43; + const LINK_TOOLTIP_HEIGHT = 32; const LINK_TOOLTIP_WIDTH = 220; css /* scss */ ` .o-link-tool { @@ -4966,6 +5163,9 @@ border-radius: 4px; display: flex; justify-content: space-between; + height: ${LINK_TOOLTIP_HEIGHT}px; + width: ${LINK_TOOLTIP_WIDTH}px; + box-sizing: border-box !important; img { margin-right: 3px; @@ -5045,7 +5245,6 @@ } LinkDisplay.components = { Menu }; LinkDisplay.template = "o-spreadsheet-LinkDisplay"; - LinkDisplay.size = { width: LINK_TOOLTIP_WIDTH, height: LINK_TOOLTIP_HEIGHT }; const LinkCellPopoverBuilder = { onHover: (position, getters) => { const cell = getters.getEvaluatedCell(position); @@ -5152,7 +5351,7 @@ const MENU_OFFSET_Y = 100; const PADDING = 12; const LINK_EDITOR_WIDTH = 340; - const LINK_EDITOR_HEIGHT = 180; + const LINK_EDITOR_HEIGHT = 165; css /* scss */ ` .o-link-editor { font-size: 13px; @@ -5162,6 +5361,9 @@ display: flex; flex-direction: column; border-radius: 4px; + height: ${LINK_EDITOR_HEIGHT}px; + width: ${LINK_EDITOR_WIDTH}px; + .o-section { .o-section-title { font-weight: bold; @@ -5314,7 +5516,6 @@ } } LinkEditor.template = "o-spreadsheet-LinkEditor"; - LinkEditor.size = { width: LINK_EDITOR_WIDTH, height: LINK_EDITOR_HEIGHT }; LinkEditor.components = { Menu }; const LinkEditorPopoverBuilder = { onOpen: (position, getters) => { @@ -5943,6 +6144,30 @@ })), }; } + /** + * Aggregates data based on labels + */ + function aggregateDataForLabels(labels, datasets) { + const parseNumber = (value) => (typeof value === "number" ? value : 0); + const labelSet = new Set(labels); + const labelMap = {}; + labelSet.forEach((label) => { + labelMap[label] = new Array(datasets.length).fill(0); + }); + for (const indexOfLabel of range(0, labels.length)) { + const label = labels[indexOfLabel]; + for (const indexOfDataset of range(0, datasets.length)) { + labelMap[label][indexOfDataset] += parseNumber(datasets[indexOfDataset].data[indexOfLabel]); + } + } + return { + labels: Object.keys(labelMap), + dataSetsValues: datasets.map((dataset, indexOfDataset) => ({ + ...dataset, + data: Object.values(labelMap).map((dataOfLabel) => dataOfLabel[indexOfDataset]), + })), + }; + } function truncateLabel(label) { if (!label) { return ""; @@ -6090,6 +6315,7 @@ this.verticalAxisPosition = definition.verticalAxisPosition; this.legendPosition = definition.legendPosition; this.stacked = definition.stacked; + this.aggregated = definition.aggregated; } static transformDefinition(definition, executed) { return transformChartDefinitionWithDataSetsWithZone(definition, executed); @@ -6103,6 +6329,7 @@ dataSets: context.range ? context.range : [], dataSetsHaveTitle: false, stacked: false, + aggregated: false, legendPosition: "top", title: context.title || "", type: "bar", @@ -6146,9 +6373,13 @@ : undefined, title: this.title, stacked: this.stacked, + aggregated: this.aggregated, }; } getDefinitionForExcel() { + // Excel does not support aggregating labels + if (this.aggregated) + return undefined; const dataSets = this.dataSets .map((ds) => toExcelDataset(this.getters, ds)) .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ? @@ -6220,6 +6451,9 @@ let labels = labelValues.formattedValues; let dataSetsValues = getChartDatasetValues(getters, chart.dataSets); ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues)); + if (chart.aggregated) { + ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues)); + } const config = getBarConfiguration(chart, labels); const colors = new ChartColors(); for (let { label, data } of dataSetsValues) { @@ -6379,6 +6613,7 @@ this.legendPosition = definition.legendPosition; this.labelsAsText = definition.labelsAsText; this.stacked = definition.stacked; + this.aggregated = definition.aggregated; } static validateChartDefinition(validator, definition) { return validator.checkValidations(definition, checkDataset, checkLabelRange); @@ -6398,6 +6633,7 @@ verticalAxisPosition: "left", labelRange: context.auxiliaryRange || undefined, stacked: false, + aggregated: false, }; } getDefinition() { @@ -6417,6 +6653,7 @@ title: this.title, labelsAsText: this.labelsAsText, stacked: this.stacked, + aggregated: this.aggregated, }; } getContextCreation() { @@ -6438,6 +6675,9 @@ return new LineChart(definition, this.sheetId, this.getters); } getDefinitionForExcel() { + // Excel does not support aggregating labels + if (this.aggregated) + return undefined; const dataSets = this.dataSets .map((ds) => toExcelDataset(this.getters, ds)) .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ? @@ -6582,6 +6822,9 @@ if (axisType === "time") { ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues)); } + if (chart.aggregated) { + ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues)); + } const config = getLineConfiguration(chart, labels); const labelFormat = getLabelFormat(getters, chart.labelRange); if (axisType === "time") { @@ -6746,6 +6989,7 @@ dataSets, labelsAsText: false, stacked: false, + aggregated: false, labelRange: labelRangeXc, type: "line", background: BACKGROUND_CHART_COLOR, @@ -6761,6 +7005,7 @@ type: "bar", background: BACKGROUND_CHART_COLOR, stacked: false, + aggregated: false, dataSetsHaveTitle, verticalAxisPosition: "left", legendPosition: newLegendPos, @@ -7059,6 +7304,7 @@ this.labelRange = createRange(getters, sheetId, definition.labelRange); this.background = definition.background; this.legendPosition = definition.legendPosition; + this.aggregated = definition.aggregated; } static transformDefinition(definition, executed) { return transformChartDefinitionWithDataSetsWithZone(definition, executed); @@ -7075,6 +7321,7 @@ title: context.title || "", type: "pie", labelRange: context.auxiliaryRange || undefined, + aggregated: false, }; } getDefinition() { @@ -7101,6 +7348,7 @@ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId) : undefined, title: this.title, + aggregated: this.aggregated, }; } copyForSheetId(sheetId) { @@ -7114,6 +7362,9 @@ return new PieChart(definition, sheetId, this.getters); } getDefinitionForExcel() { + // Excel does not support aggregating labels + if (this.aggregated) + return undefined; const dataSets = this.dataSets .map((ds) => toExcelDataset(this.getters, ds)) .filter((ds) => ds.range !== ""); // && range !== INCORRECT_RANGE_STRING ? show incorrect #ref ? @@ -7173,6 +7424,9 @@ let labels = labelValues.formattedValues; let dataSetsValues = getChartDatasetValues(getters, chart.dataSets); ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues)); + if (chart.aggregated) { + ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues)); + } const config = getPieConfiguration(chart, labels); const colors = new ChartColors(); for (let { label, data } of dataSetsValues) { @@ -9303,6 +9557,11 @@ labelRange: this.labelRange, }); } + onUpdateAggregated(ev) { + this.props.updateChart({ + aggregated: ev.target.checked, + }); + } } LineBarPieConfigPanel.template = "o-spreadsheet-LineBarPieConfigPanel"; LineBarPieConfigPanel.components = { SelectionInput }; @@ -9318,6 +9577,11 @@ stacked: ev.target.checked, }); } + onUpdateAggregated(ev) { + this.props.updateChart({ + aggregated: ev.target.checked, + }); + } } BarConfigPanel.template = "o-spreadsheet-BarConfigPanel"; @@ -9787,6 +10051,11 @@ stacked: ev.target.checked, }); } + onUpdateAggregated(ev) { + this.props.updateChart({ + aggregated: ev.target.checked, + }); + } } LineConfigPanel.template = "o-spreadsheet-LineConfigPanel"; @@ -18340,6 +18609,31 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A TEXT: TEXT }); + // ----------------------------------------------------------------------------- + // HYPERLINK + // ----------------------------------------------------------------------------- + const HYPERLINK = { + description: _lt("Creates a hyperlink in a cell."), + args: args(` + url (string) ${_lt("The full URL of the link enclosed in quotation marks.")} + link_label (string, optional) ${_lt("The text to display in the cell, enclosed in quotation marks.")} + `), + returns: ["STRING"], + compute: function (url, linkLabel) { + const processedUrl = toString(url).trim(); + const processedLabel = toString(linkLabel) || processedUrl; + if (processedUrl === "") + return processedLabel; + return markdownLink(processedLabel, processedUrl); + }, + isExported: true, + }; + + var web = /*#__PURE__*/Object.freeze({ + __proto__: null, + HYPERLINK: HYPERLINK + }); + const functions$3 = { database, date, @@ -18353,6 +18647,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A statistical, text, engineering, + web, }; const functionNameRegex = /^[A-Z0-9\_\.]+$/; //------------------------------------------------------------------------------ @@ -21792,13 +22087,14 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A if (!popover.isOpen) { return { isOpen: false }; } - const coordinates = popover.coordinates; + const anchorRect = popover.anchorRect; return { ...popover, // transform from the "canvas coordinate system" to the "body coordinate system" - coordinates: { - x: coordinates.x + this.props.gridPosition.x, - y: coordinates.y + this.props.gridPosition.y, + anchorRect: { + ...anchorRect, + x: anchorRect.x + this.props.gridRect.x, + y: anchorRect.y + this.props.gridRect.y, }, }; } @@ -21806,10 +22102,10 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A GridPopover.template = "o-spreadsheet-GridPopover"; GridPopover.components = { Popover }; GridPopover.props = { - gridPosition: Object, hoveredCell: Object, onClosePopover: Function, onMouseWheel: Function, + gridRect: Object, }; class AbstractResizer extends owl.Component { @@ -22744,7 +23040,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A left: `${this.props.leftOffset + x}px`, bottom: "0px", height: `${SCROLLBAR_WIDTH$1}px`, - right: `${SCROLLBAR_WIDTH$1}px`, + right: `0px`, }; } onScroll(offset) { @@ -22789,7 +23085,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A top: `${this.props.topOffset + y}px`, right: "0px", width: `${SCROLLBAR_WIDTH$1}px`, - bottom: `${SCROLLBAR_WIDTH$1}px`, + bottom: `0px`, }; } onScroll(offset) { @@ -22953,6 +23249,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A this.hiddenInput = owl.useRef("hiddenInput"); this.canvasPosition = useAbsolutePosition(this.gridRef); this.hoveredCell = owl.useState({ col: undefined, row: undefined }); + owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() }); owl.useExternalListener(document.body, "cut", this.copy.bind(this, true)); owl.useExternalListener(document.body, "copy", this.copy.bind(this, false)); owl.useExternalListener(document.body, "paste", this.paste); @@ -23034,6 +23331,9 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A isCellHovered(col, row) { return this.hoveredCell.col === col && this.hoveredCell.row === row; } + getGridRect() { + return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() }; + } // --------------------------------------------------------------------------- // Zone selection with mouse // --------------------------------------------------------------------------- @@ -24627,6 +24927,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A verticalAxisPosition: chartData.verticalAxisPosition, legendPosition: chartData.legendPosition, stacked: chartData.stacked || false, + aggregated: false, labelsAsText: false, }; } @@ -29282,6 +29583,10 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A } ImageFigure.template = "o-spreadsheet-ImageFigure"; ImageFigure.components = { Menu }; + ImageFigure.props = { + figure: Object, + onFigureDeleted: Function, + }; class ImagePlugin extends CorePlugin { constructor() { @@ -31199,6 +31504,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A this.isUpToDate = false; this.evaluatedCells = {}; this.evalContext = config.external; + this.lazyEvaluation = config.lazyEvaluation; } // --------------------------------------------------------------------------- // Command Handling @@ -31299,6 +31605,9 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A this.evaluatedCells[sheetId][col] = {}; } this.evaluatedCells[sheetId][col][row] = evaluatedCell; + if (!this.lazyEvaluation) { + this.evaluatedCells[sheetId][col][row](); + } } *getAllCells() { // use a generator function to avoid re-building a new object @@ -33763,7 +34072,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A ? { isOpen: false } : { ...popover, - ...this.computePopoverProps(this.persistentPopover, popover.cellCorner), + anchorRect: this.computePopoverAnchorRect(this.persistentPopover), }; } if (col === undefined || @@ -33780,7 +34089,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A ? { isOpen: false } : { ...popover, - ...this.computePopoverProps(position, popover.cellCorner), + anchorRect: this.computePopoverAnchorRect(position), }; } getPersistentPopoverTypeAtPosition({ col, row }) { @@ -33791,29 +34100,13 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A } return undefined; } - computePopoverProps({ col, row }, corner) { - const { width, height } = this.getters.getVisibleRect(positionToZone({ col, row })); - return { - coordinates: this.computePopoverPosition({ col, row }, corner), - cellWidth: -width, - cellHeight: -height, - }; - } - computePopoverPosition({ col, row }, corner) { + computePopoverAnchorRect({ col, row }) { const sheetId = this.getters.getActiveSheetId(); const merge = this.getters.getMerge({ sheetId, col, row }); if (merge) { - col = corner === "TopRight" ? merge.right : merge.left; - row = corner === "TopRight" ? merge.top : merge.bottom; - } - // x, y are relative to the canvas - const { x, y, width, height } = this.getters.getVisibleRect(positionToZone({ col, row })); - switch (corner) { - case "BottomLeft": - return { x, y: y + height }; - case "TopRight": - return { x: x + width, y: y }; + return this.getters.getVisibleRect(merge); } + return this.getters.getVisibleRect(positionToZone({ col, row })); } } CellPopoverPlugin.getters = ["getCellPopover", "getPersistentPopoverTypeAtPosition"]; @@ -38085,6 +38378,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A const gridRef = owl.useRef("grid"); this.canvasPosition = useAbsolutePosition(gridRef); this.hoveredCell = owl.useState({ col: undefined, row: undefined }); + owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() }); useGridDrawing("canvas", this.env.model, () => this.env.model.getters.getSheetViewDimension()); this.onMouseWheel = useWheelHandler((deltaX, deltaY) => { this.moveCanvas(deltaX, deltaY); @@ -38185,6 +38479,9 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A offsetY: offsetScrollbarY + deltaY, }); } + getGridRect() { + return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() }; + } } SpreadsheetDashboard.template = "o-spreadsheet-SpreadsheetDashboard"; SpreadsheetDashboard.components = { @@ -42784,6 +43081,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A moveClient: () => { }, snapshotRequested: false, notifyUI: (payload) => this.trigger("notify-ui", payload), + lazyEvaluation: "lazyEvaluation" in config ? config.lazyEvaluation : true, }; } setupCorePluginConfig() { @@ -42805,6 +43103,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A moveClient: this.session.move.bind(this.session), external: this.config.external, uiActions: this.config, + lazyEvaluation: this.config.lazyEvaluation, }; } // --------------------------------------------------------------------------- @@ -42966,6 +43265,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A computeTextWidth, createEmptyWorkbookData, createEmptySheet, + createEmptyExcelSheet, getDefaultChartJsRuntime, chartFontColor, getMenuChildren, @@ -43038,8 +43338,8 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A Object.defineProperty(exports, '__esModule', { value: true }); exports.__info__.version = '2.0.0'; - exports.__info__.date = '2022-12-20T11:01:03.280Z'; - exports.__info__.hash = '7daa2dd'; + exports.__info__.date = '2022-12-22T13:54:58.664Z'; + exports.__info__.hash = '4c5002d'; })(this.o_spreadsheet = this.o_spreadsheet || {}, owl); //# sourceMappingURL=o_spreadsheet.js.map diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml index 2f2cf2ece5e..c611259fd74 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml @@ -291,8 +291,8 @@ /> @@ -633,8 +633,8 @@ @@ -676,11 +676,11 @@ + anchorRect="popover.anchorRect" + positioning="popover.positioning" + verticalOffset="popover.verticalOffset" + onPopoverHidden="() => this.closeSubMenu()" + onPopoverMoved="() => this.closeSubMenu()" + maxHeight="menuHeight">
-
+
@@ -1610,6 +1610,16 @@ onSelectionChanged="(ranges) => this.onLabelRangeChanged(ranges)" onSelectionConfirmed="() => this.onLabelRangeConfirmed()" /> +
@@ -1892,6 +1902,16 @@ onSelectionChanged="(ranges) => this.onLabelRangeChanged(ranges)" onSelectionConfirmed="() => this.onLabelRangeConfirmed()" /> +
@@ -1990,6 +2010,16 @@ onSelectionChanged="(ranges) => this.onLabelRangeChanged(ranges)" onSelectionConfirmed="() => this.onLabelRangeConfirmed()" /> +