diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js index 20c8d91d420..c9e1e32853a 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js @@ -2629,19 +2629,19 @@ let x = 0; let y = 0; switch (direction) { - case 0 /* UP */: + case 0 /* DIRECTION.UP */: x = 0; y = -rule.current; break; - case 1 /* DOWN */: + case 1 /* DIRECTION.DOWN */: x = 0; y = rule.current; break; - case 2 /* LEFT */: + case 2 /* DIRECTION.LEFT */: x = -rule.current; y = 0; break; - case 3 /* RIGHT */: + case 3 /* DIRECTION.RIGHT */: x = rule.current; y = 0; break; @@ -2789,7 +2789,7 @@ results = [results]; } results = [...new Set(results)]; - this.reasons = results.filter((result) => result !== 0 /* Success */); + this.reasons = results.filter((result) => result !== 0 /* CommandResult.Success */); } /** * Static helper which returns a successful DispatchResult @@ -2875,15 +2875,15 @@ CommandResult[CommandResult["MergeOverlap"] = 62] = "MergeOverlap"; CommandResult[CommandResult["TooManyHiddenElements"] = 63] = "TooManyHiddenElements"; CommandResult[CommandResult["Readonly"] = 64] = "Readonly"; - CommandResult[CommandResult["InvalidOffset"] = 65] = "InvalidOffset"; - CommandResult[CommandResult["InvalidViewportSize"] = 66] = "InvalidViewportSize"; - CommandResult[CommandResult["InvalidScrollingDirection"] = 67] = "InvalidScrollingDirection"; - CommandResult[CommandResult["FigureDoesNotExist"] = 68] = "FigureDoesNotExist"; - CommandResult[CommandResult["InvalidConditionalFormatId"] = 69] = "InvalidConditionalFormatId"; - CommandResult[CommandResult["InvalidCellPopover"] = 70] = "InvalidCellPopover"; - CommandResult[CommandResult["EmptyTarget"] = 71] = "EmptyTarget"; - CommandResult[CommandResult["InvalidFreezeQuantity"] = 72] = "InvalidFreezeQuantity"; - CommandResult[CommandResult["FrozenPaneOverlap"] = 73] = "FrozenPaneOverlap"; + CommandResult[CommandResult["InvalidViewportSize"] = 65] = "InvalidViewportSize"; + CommandResult[CommandResult["InvalidScrollingDirection"] = 66] = "InvalidScrollingDirection"; + CommandResult[CommandResult["FigureDoesNotExist"] = 67] = "FigureDoesNotExist"; + CommandResult[CommandResult["InvalidConditionalFormatId"] = 68] = "InvalidConditionalFormatId"; + CommandResult[CommandResult["InvalidCellPopover"] = 69] = "InvalidCellPopover"; + CommandResult[CommandResult["EmptyTarget"] = 70] = "EmptyTarget"; + CommandResult[CommandResult["InvalidFreezeQuantity"] = 71] = "InvalidFreezeQuantity"; + CommandResult[CommandResult["FrozenPaneOverlap"] = 72] = "FrozenPaneOverlap"; + CommandResult[CommandResult["ValuesNotChanged"] = 73] = "ValuesNotChanged"; })(exports.CommandResult || (exports.CommandResult = {})); var DIRECTION; @@ -3068,6 +3068,41 @@ sheet.setAttribute("component", id); document.head.appendChild(sheet); } + function getTextDecoration({ strikethrough, underline, }) { + if (!strikethrough && !underline) { + return "none"; + } + return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`; + } + /** + * Convert the cell text style to CSS properties. + */ + function cellTextStyleToCss(style) { + const attributes = {}; + if (!style) + return attributes; + if (style.bold) { + attributes["font-weight"] = "bold"; + } + if (style.italic) { + attributes["font-style"] = "italic"; + } + if (style.strikethrough || style.underline) { + let decoration = style.strikethrough ? "line-through" : ""; + decoration = style.underline ? decoration + " underline" : decoration; + attributes["text-decoration"] = decoration; + } + if (style.textColor) { + attributes["color"] = style.textColor; + } + return attributes; + } + function cssPropertiesToCss(attributes) { + const str = Object.entries(attributes) + .map(([attName, attValue]) => `${attName}: ${attValue};`) + .join("\n"); + return "\n" + str + "\n"; + } const ERROR_TOOLTIP_HEIGHT = 40; const ERROR_TOOLTIP_WIDTH = 180; @@ -3129,41 +3164,6 @@ function isChildEvent(parent, ev) { return !!ev.target && parent.contains(ev.target); } - function getTextDecoration({ strikethrough, underline, }) { - if (!strikethrough && !underline) { - return "none"; - } - return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`; - } - /** - * Convert the cell text style to CSS properties. - */ - function cellTextStyleToCss(style) { - const attributes = {}; - if (!style) - return attributes; - if (style.bold) { - attributes["font-weight"] = "bold"; - } - if (style.italic) { - attributes["font-style"] = "italic"; - } - if (style.strikethrough || style.underline) { - let decoration = style.strikethrough ? "line-through" : ""; - decoration = style.underline ? decoration + " underline" : decoration; - attributes["text-decoration"] = decoration; - } - if (style.textColor) { - attributes["color"] = style.textColor; - } - return attributes; - } - function cssPropertiesToCss(attributes) { - const str = Object.entries(attributes) - .map(([attName, attValue]) => `${attName}: ${attValue};`) - .join("\n"); - return "\n" + str + "\n"; - } function gridOverlayPosition() { const spreadsheetElement = document.querySelector(".o-grid-overlay"); if (spreadsheetElement) { @@ -4070,7 +4070,7 @@ }); } } - if (result.isCancelledBecause(60 /* InvalidSortZone */)) { + if (result.isCancelledBecause(60 /* CommandResult.InvalidSortZone */)) { const { col, row } = anchor; env.model.selection.selectZone({ cell: { col, row }, zone }); env.raiseError(_lt("Cannot sort. To sort, select only cells or only merges that have the same size.")); @@ -4080,7 +4080,7 @@ function interactiveCut(env) { const result = env.model.dispatch("CUT"); if (!result.isSuccessful) { - if (result.isCancelledBecause(17 /* WrongCutSelection */)) { + if (result.isCancelledBecause(17 /* CommandResult.WrongCutSelection */)) { env.raiseError(_lt("This operation is not allowed with multiple selections.")); } } @@ -4094,16 +4094,16 @@ }; function handlePasteResult(env, result) { if (!result.isSuccessful) { - if (result.reasons.includes(18 /* WrongPasteSelection */)) { + if (result.reasons.includes(18 /* CommandResult.WrongPasteSelection */)) { env.raiseError(PasteInteractiveContent.wrongPasteSelection); } - else if (result.reasons.includes(2 /* WillRemoveExistingMerge */)) { + else if (result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) { env.raiseError(PasteInteractiveContent.willRemoveExistingMerge); } - else if (result.reasons.includes(20 /* WrongFigurePasteOption */)) { + else if (result.reasons.includes(20 /* CommandResult.WrongFigurePasteOption */)) { env.raiseError(PasteInteractiveContent.wrongFigurePasteOption); } - else if (result.reasons.includes(73 /* FrozenPaneOverlap */)) { + else if (result.reasons.includes(72 /* CommandResult.FrozenPaneOverlap */)) { env.raiseError(PasteInteractiveContent.frozenPaneOverlap); } } @@ -4679,7 +4679,8 @@ legendPosition: newLegendPos, }, }); - env.openSidePanel("ChartPanel", { figureId: id }); + env.model.dispatch("SELECT_FIGURE", { id }); + env.openSidePanel("ChartPanel"); }; //------------------------------------------------------------------------------ // Style/Format @@ -4942,8 +4943,6 @@ action: OPEN_CF_SIDEPANEL_ACTION, }); - const dashboardMenuRegistry = new MenuItemRegistry(); - const rowMenuRegistry = new MenuItemRegistry(); rowMenuRegistry .add("cut", { @@ -5044,10 +5043,10 @@ } const result = env.model.dispatch("RENAME_SHEET", { sheetId, name }); if (!result.isSuccessful) { - if (result.reasons.includes(10 /* DuplicatedSheetName */)) { + if (result.reasons.includes(10 /* CommandResult.DuplicatedSheetName */)) { interactiveRenameSheet(env, sheetId, _lt("A sheet with the name %s already exists. Please select another name.", name)); } - if (result.reasons.includes(11 /* ForbiddenCharactersInSheetName */)) { + if (result.reasons.includes(11 /* CommandResult.ForbiddenCharactersInSheetName */)) { interactiveRenameSheet(env, sheetId, _lt("Some used characters are not allowed in a sheet name (Forbidden characters are %s).", FORBIDDEN_SHEET_CHARS.join(" "))); } } @@ -5122,24 +5121,24 @@ const CfTerms = { Errors: { - [23 /* InvalidRange */]: _lt("The range is invalid"), - [48 /* FirstArgMissing */]: _lt("The argument is missing. Please provide a value"), - [49 /* SecondArgMissing */]: _lt("The second argument is missing. Please provide a value"), - [50 /* MinNaN */]: _lt("The minpoint must be a number"), - [51 /* MidNaN */]: _lt("The midpoint must be a number"), - [52 /* MaxNaN */]: _lt("The maxpoint must be a number"), - [53 /* ValueUpperInflectionNaN */]: _lt("The first value must be a number"), - [54 /* ValueLowerInflectionNaN */]: _lt("The second value must be a number"), - [44 /* MinBiggerThanMax */]: _lt("Minimum must be smaller then Maximum"), - [47 /* MinBiggerThanMid */]: _lt("Minimum must be smaller then Midpoint"), - [46 /* MidBiggerThanMax */]: _lt("Midpoint must be smaller then Maximum"), - [45 /* LowerBiggerThanUpper */]: _lt("Lower inflection point must be smaller than upper inflection point"), - [55 /* MinInvalidFormula */]: _lt("Invalid Minpoint formula"), - [57 /* MaxInvalidFormula */]: _lt("Invalid Maxpoint formula"), - [56 /* MidInvalidFormula */]: _lt("Invalid Midpoint formula"), - [58 /* ValueUpperInvalidFormula */]: _lt("Invalid upper inflection point formula"), - [59 /* ValueLowerInvalidFormula */]: _lt("Invalid lower inflection point formula"), - [22 /* EmptyRange */]: _lt("A range needs to be defined"), + [23 /* CommandResult.InvalidRange */]: _lt("The range is invalid"), + [48 /* CommandResult.FirstArgMissing */]: _lt("The argument is missing. Please provide a value"), + [49 /* CommandResult.SecondArgMissing */]: _lt("The second argument is missing. Please provide a value"), + [50 /* CommandResult.MinNaN */]: _lt("The minpoint must be a number"), + [51 /* CommandResult.MidNaN */]: _lt("The midpoint must be a number"), + [52 /* CommandResult.MaxNaN */]: _lt("The maxpoint must be a number"), + [53 /* CommandResult.ValueUpperInflectionNaN */]: _lt("The first value must be a number"), + [54 /* CommandResult.ValueLowerInflectionNaN */]: _lt("The second value must be a number"), + [44 /* CommandResult.MinBiggerThanMax */]: _lt("Minimum must be smaller then Maximum"), + [47 /* CommandResult.MinBiggerThanMid */]: _lt("Minimum must be smaller then Midpoint"), + [46 /* CommandResult.MidBiggerThanMax */]: _lt("Midpoint must be smaller then Maximum"), + [45 /* CommandResult.LowerBiggerThanUpper */]: _lt("Lower inflection point must be smaller than upper inflection point"), + [55 /* CommandResult.MinInvalidFormula */]: _lt("Invalid Minpoint formula"), + [57 /* CommandResult.MaxInvalidFormula */]: _lt("Invalid Maxpoint formula"), + [56 /* CommandResult.MidInvalidFormula */]: _lt("Invalid Midpoint formula"), + [58 /* CommandResult.ValueUpperInvalidFormula */]: _lt("Invalid upper inflection point formula"), + [59 /* CommandResult.ValueLowerInvalidFormula */]: _lt("Invalid lower inflection point formula"), + [22 /* CommandResult.EmptyRange */]: _lt("A range needs to be defined"), Unexpected: _lt("The rule is invalid for an unknown reason"), }, ColorScale: _lt("Color scale"), @@ -5166,20 +5165,20 @@ Errors: { Unexpected: _lt("The chart definition is invalid for an unknown reason"), // BASIC CHART ERRORS (LINE | BAR | PIE) - [29 /* InvalidDataSet */]: _lt("The dataset is invalid"), - [30 /* InvalidLabelRange */]: _lt("Labels are invalid"), + [29 /* CommandResult.InvalidDataSet */]: _lt("The dataset is invalid"), + [30 /* CommandResult.InvalidLabelRange */]: _lt("Labels are invalid"), // SCORECARD CHART ERRORS - [31 /* InvalidScorecardKeyValue */]: _lt("The key value is invalid"), - [32 /* InvalidScorecardBaseline */]: _lt("The baseline value is invalid"), + [31 /* CommandResult.InvalidScorecardKeyValue */]: _lt("The key value is invalid"), + [32 /* CommandResult.InvalidScorecardBaseline */]: _lt("The baseline value is invalid"), // GAUGE CHART ERRORS - [33 /* InvalidGaugeDataRange */]: _lt("The data range is invalid"), - [34 /* EmptyGaugeRangeMin */]: _lt("A minimum range limit value is needed"), - [35 /* GaugeRangeMinNaN */]: _lt("The minimum range limit value must be a number"), - [36 /* EmptyGaugeRangeMax */]: _lt("A maximum range limit value is needed"), - [37 /* GaugeRangeMaxNaN */]: _lt("The maximum range limit value must be a number"), - [38 /* GaugeRangeMinBiggerThanRangeMax */]: _lt("Minimum range limit must be smaller than maximum range limit"), - [39 /* GaugeLowerInflectionPointNaN */]: _lt("The lower inflection point value must be a number"), - [40 /* GaugeUpperInflectionPointNaN */]: _lt("The upper inflection point value must be a number"), + [33 /* CommandResult.InvalidGaugeDataRange */]: _lt("The data range is invalid"), + [34 /* CommandResult.EmptyGaugeRangeMin */]: _lt("A minimum range limit value is needed"), + [35 /* CommandResult.GaugeRangeMinNaN */]: _lt("The minimum range limit value must be a number"), + [36 /* CommandResult.EmptyGaugeRangeMax */]: _lt("A maximum range limit value is needed"), + [37 /* CommandResult.GaugeRangeMaxNaN */]: _lt("The maximum range limit value must be a number"), + [38 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */]: _lt("Minimum range limit must be smaller than maximum range limit"), + [39 /* CommandResult.GaugeLowerInflectionPointNaN */]: _lt("The lower inflection point value must be a number"), + [40 /* CommandResult.GaugeUpperInflectionPointNaN */]: _lt("The upper inflection point value must be a number"), }, }; const NumberFormatTerms = { @@ -5203,7 +5202,7 @@ const sheetId = env.model.getters.getActiveSheetId(); const cmd = dimension === "COL" ? "FREEZE_COLUMNS" : "FREEZE_ROWS"; const result = env.model.dispatch(cmd, { sheetId, quantity: base }); - if (result.isCancelledBecause(62 /* MergeOverlap */)) { + if (result.isCancelledBecause(62 /* CommandResult.MergeOverlap */)) { env.raiseError(MergeErrorMessage); } } @@ -5824,11 +5823,11 @@ } get isDatasetInvalid() { var _a; - return !!((_a = this.state.datasetDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(29 /* InvalidDataSet */)); + return !!((_a = this.state.datasetDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(29 /* CommandResult.InvalidDataSet */)); } get isLabelInvalid() { var _a; - return !!((_a = this.state.labelsDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(30 /* InvalidLabelRange */)); + return !!((_a = this.state.labelsDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(30 /* CommandResult.InvalidLabelRange */)); } onUpdateDataSetsHaveTitle(ev) { this.props.updateChart({ @@ -6176,23 +6175,23 @@ if (definition.dataSets) { const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range)) !== undefined; if (invalidRanges) { - return 29 /* InvalidDataSet */; + return 29 /* CommandResult.InvalidDataSet */; } const zones = definition.dataSets.map(toUnboundedZone); if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) { - return 29 /* InvalidDataSet */; + return 29 /* CommandResult.InvalidDataSet */; } } - return 0 /* Success */; + return 0 /* CommandResult.Success */; } function checkLabelRange(definition) { if (definition.labelRange) { const invalidLabels = !rangeReference.test(definition.labelRange || ""); if (invalidLabels) { - return 30 /* InvalidLabelRange */; + return 30 /* CommandResult.InvalidLabelRange */; } } - return 0 /* Success */; + return 0 /* CommandResult.Success */; } // --------------------------------------------------------------------------- // Scorecard @@ -6647,20 +6646,20 @@ }); function isDataRangeValid(definition) { return definition.dataRange && !rangeReference.test(definition.dataRange) - ? 33 /* InvalidGaugeDataRange */ - : 0 /* Success */; + ? 33 /* CommandResult.InvalidGaugeDataRange */ + : 0 /* CommandResult.Success */; } function checkRangeLimits(check, batchValidations) { return batchValidations((definition) => { if (definition.sectionRule) { return check(definition.sectionRule.rangeMin, "rangeMin"); } - return 0 /* Success */; + return 0 /* CommandResult.Success */; }, (definition) => { if (definition.sectionRule) { return check(definition.sectionRule.rangeMax, "rangeMax"); } - return 0 /* Success */; + return 0 /* CommandResult.Success */; }); } function checkInflectionPointsValue(check, batchValidations) { @@ -6668,47 +6667,47 @@ if (definition.sectionRule) { return check(definition.sectionRule.lowerInflectionPoint.value, "lowerInflectionPointValue"); } - return 0 /* Success */; + return 0 /* CommandResult.Success */; }, (definition) => { if (definition.sectionRule) { return check(definition.sectionRule.upperInflectionPoint.value, "upperInflectionPointValue"); } - return 0 /* Success */; + return 0 /* CommandResult.Success */; }); } function checkRangeMinBiggerThanRangeMax(definition) { if (definition.sectionRule) { if (Number(definition.sectionRule.rangeMin) >= Number(definition.sectionRule.rangeMax)) { - return 38 /* GaugeRangeMinBiggerThanRangeMax */; + return 38 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */; } } - return 0 /* Success */; + return 0 /* CommandResult.Success */; } function checkEmpty(value, valueName) { if (value === "") { switch (valueName) { case "rangeMin": - return 34 /* EmptyGaugeRangeMin */; + return 34 /* CommandResult.EmptyGaugeRangeMin */; case "rangeMax": - return 36 /* EmptyGaugeRangeMax */; + return 36 /* CommandResult.EmptyGaugeRangeMax */; } } - return 0 /* Success */; + return 0 /* CommandResult.Success */; } function checkNaN(value, valueName) { if (isNaN(value)) { switch (valueName) { case "rangeMin": - return 35 /* GaugeRangeMinNaN */; + return 35 /* CommandResult.GaugeRangeMinNaN */; case "rangeMax": - return 37 /* GaugeRangeMaxNaN */; + return 37 /* CommandResult.GaugeRangeMaxNaN */; case "lowerInflectionPointValue": - return 39 /* GaugeLowerInflectionPointNaN */; + return 39 /* CommandResult.GaugeLowerInflectionPointNaN */; case "upperInflectionPointValue": - return 40 /* GaugeUpperInflectionPointNaN */; + return 40 /* CommandResult.GaugeUpperInflectionPointNaN */; } } - return 0 /* Success */; + return 0 /* CommandResult.Success */; } class GaugeChart extends AbstractChart { constructor(definition, sheetId, getters) { @@ -7445,13 +7444,13 @@ }); function checkKeyValue(definition) { return definition.keyValue && !rangeReference.test(definition.keyValue) - ? 31 /* InvalidScorecardKeyValue */ - : 0 /* Success */; + ? 31 /* CommandResult.InvalidScorecardKeyValue */ + : 0 /* CommandResult.Success */; } function checkBaseline(definition) { return definition.baseline && !rangeReference.test(definition.baseline) - ? 32 /* InvalidScorecardBaseline */ - : 0 /* Success */; + ? 32 /* CommandResult.InvalidScorecardBaseline */ + : 0 /* CommandResult.Success */; } class ScorecardChart$1 extends AbstractChart { constructor(definition, sheetId, getters) { @@ -7849,7 +7848,7 @@ } get isDataRangeInvalid() { var _a; - return !!((_a = this.state.dataRangeDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(33 /* InvalidGaugeDataRange */)); + return !!((_a = this.state.dataRangeDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(33 /* CommandResult.InvalidGaugeDataRange */)); } onDataRangeChanged(ranges) { this.dataRange = ranges[0]; @@ -7930,28 +7929,28 @@ } isRangeMinInvalid() { var _a, _b, _c; - return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(34 /* EmptyGaugeRangeMin */)) || - ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(35 /* GaugeRangeMinNaN */)) || - ((_c = this.state.sectionRuleDispatchResult) === null || _c === void 0 ? void 0 : _c.isCancelledBecause(38 /* GaugeRangeMinBiggerThanRangeMax */))); + return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(34 /* CommandResult.EmptyGaugeRangeMin */)) || + ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(35 /* CommandResult.GaugeRangeMinNaN */)) || + ((_c = this.state.sectionRuleDispatchResult) === null || _c === void 0 ? void 0 : _c.isCancelledBecause(38 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */))); } isRangeMaxInvalid() { var _a, _b, _c; - return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(36 /* EmptyGaugeRangeMax */)) || - ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(37 /* GaugeRangeMaxNaN */)) || - ((_c = this.state.sectionRuleDispatchResult) === null || _c === void 0 ? void 0 : _c.isCancelledBecause(38 /* GaugeRangeMinBiggerThanRangeMax */))); + return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(36 /* CommandResult.EmptyGaugeRangeMax */)) || + ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(37 /* CommandResult.GaugeRangeMaxNaN */)) || + ((_c = this.state.sectionRuleDispatchResult) === null || _c === void 0 ? void 0 : _c.isCancelledBecause(38 /* CommandResult.GaugeRangeMinBiggerThanRangeMax */))); } // --------------------------------------------------------------------------- // COLOR_SECTION_TEMPLATE // --------------------------------------------------------------------------- get isLowerInflectionPointInvalid() { var _a, _b; - return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(39 /* GaugeLowerInflectionPointNaN */)) || - ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(41 /* GaugeLowerBiggerThanUpper */))); + return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(39 /* CommandResult.GaugeLowerInflectionPointNaN */)) || + ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(41 /* CommandResult.GaugeLowerBiggerThanUpper */))); } get isUpperInflectionPointInvalid() { var _a, _b; - return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(40 /* GaugeUpperInflectionPointNaN */)) || - ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(41 /* GaugeLowerBiggerThanUpper */))); + return !!(((_a = this.state.sectionRuleDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(40 /* CommandResult.GaugeUpperInflectionPointNaN */)) || + ((_b = this.state.sectionRuleDispatchResult) === null || _b === void 0 ? void 0 : _b.isCancelledBecause(41 /* CommandResult.GaugeLowerBiggerThanUpper */))); } updateInflectionPointValue(attr, ev) { const sectionRule = deepCopy(this.props.definition.sectionRule); @@ -8049,11 +8048,11 @@ } get isKeyValueInvalid() { var _a; - return !!((_a = this.state.keyValueDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(31 /* InvalidScorecardKeyValue */)); + return !!((_a = this.state.keyValueDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(31 /* CommandResult.InvalidScorecardKeyValue */)); } get isBaselineInvalid() { var _a; - return !!((_a = this.state.keyValueDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(32 /* InvalidScorecardBaseline */)); + return !!((_a = this.state.keyValueDispatchResult) === null || _a === void 0 ? void 0 : _a.isCancelledBecause(32 /* CommandResult.InvalidScorecardBaseline */)); } onKeyValueRangeChanged(ranges) { this.keyValue = ranges[0]; @@ -8170,14 +8169,23 @@ `; class ChartPanel extends owl.Component { get figureId() { - return this.props.figureId; + return this.state.figureId; } setup() { + const selectedFigureId = this.env.model.getters.getSelectedFigureId(); + if (!selectedFigureId) { + throw new Error(_lt("Cannot open the chart side panel while no chart are selected")); + } this.state = owl.useState({ panel: "configuration", + figureId: selectedFigureId, }); - owl.onWillUpdateProps((nextProps) => { - if (!this.env.model.getters.isChartDefined(nextProps.figureId)) { + owl.onWillUpdateProps(() => { + const selectedFigureId = this.env.model.getters.getSelectedFigureId(); + if (selectedFigureId && selectedFigureId !== this.state.figureId) { + this.state.figureId = selectedFigureId; + } + if (!this.env.model.getters.isChartDefined(this.figureId)) { this.props.onCloseSidePanel(); return; } @@ -8657,7 +8665,7 @@ return this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId()); } get isRangeValid() { - return this.state.errors.includes(22 /* EmptyRange */); + return this.state.errors.includes(22 /* CommandResult.EmptyRange */); } errorMessage(error) { return CfTerms.Errors[error] || CfTerms.Errors.Unexpected; @@ -8719,7 +8727,7 @@ if (this.state.currentCF) { const invalidRanges = this.state.currentCF.ranges.some((xc) => !xc.match(rangeReference)); if (invalidRanges) { - this.state.errors = [23 /* InvalidRange */]; + this.state.errors = [23 /* CommandResult.InvalidRange */]; return; } const sheetId = this.env.model.getters.getActiveSheetId(); @@ -8875,11 +8883,11 @@ ****************************************************************************/ get isValue1Invalid() { var _a; - return !!((_a = this.state.errors) === null || _a === void 0 ? void 0 : _a.includes(48 /* FirstArgMissing */)); + return !!((_a = this.state.errors) === null || _a === void 0 ? void 0 : _a.includes(48 /* CommandResult.FirstArgMissing */)); } get isValue2Invalid() { var _a; - return !!((_a = this.state.errors) === null || _a === void 0 ? void 0 : _a.includes(49 /* SecondArgMissing */)); + return !!((_a = this.state.errors) === null || _a === void 0 ? void 0 : _a.includes(49 /* CommandResult.SecondArgMissing */)); } toggleStyle(tool) { const style = this.state.rules.cellIs.style; @@ -8896,17 +8904,17 @@ isValueInvalid(threshold) { switch (threshold) { case "minimum": - return (this.state.errors.includes(55 /* MinInvalidFormula */) || - this.state.errors.includes(47 /* MinBiggerThanMid */) || - this.state.errors.includes(44 /* MinBiggerThanMax */) || - this.state.errors.includes(50 /* MinNaN */)); + return (this.state.errors.includes(55 /* CommandResult.MinInvalidFormula */) || + this.state.errors.includes(47 /* CommandResult.MinBiggerThanMid */) || + this.state.errors.includes(44 /* CommandResult.MinBiggerThanMax */) || + this.state.errors.includes(50 /* CommandResult.MinNaN */)); case "midpoint": - return (this.state.errors.includes(56 /* MidInvalidFormula */) || - this.state.errors.includes(51 /* MidNaN */) || - this.state.errors.includes(46 /* MidBiggerThanMax */)); + return (this.state.errors.includes(56 /* CommandResult.MidInvalidFormula */) || + this.state.errors.includes(51 /* CommandResult.MidNaN */) || + this.state.errors.includes(46 /* CommandResult.MidBiggerThanMax */)); case "maximum": - return (this.state.errors.includes(57 /* MaxInvalidFormula */) || - this.state.errors.includes(52 /* MaxNaN */)); + return (this.state.errors.includes(57 /* CommandResult.MaxInvalidFormula */) || + this.state.errors.includes(52 /* CommandResult.MaxNaN */)); default: return false; } @@ -8955,13 +8963,13 @@ isInflectionPointInvalid(inflectionPoint) { switch (inflectionPoint) { case "lowerInflectionPoint": - return (this.state.errors.includes(54 /* ValueLowerInflectionNaN */) || - this.state.errors.includes(59 /* ValueLowerInvalidFormula */) || - this.state.errors.includes(45 /* LowerBiggerThanUpper */)); + return (this.state.errors.includes(54 /* CommandResult.ValueLowerInflectionNaN */) || + this.state.errors.includes(59 /* CommandResult.ValueLowerInvalidFormula */) || + this.state.errors.includes(45 /* CommandResult.LowerBiggerThanUpper */)); case "upperInflectionPoint": - return (this.state.errors.includes(53 /* ValueUpperInflectionNaN */) || - this.state.errors.includes(58 /* ValueUpperInvalidFormula */) || - this.state.errors.includes(45 /* LowerBiggerThanUpper */)); + return (this.state.errors.includes(53 /* CommandResult.ValueUpperInflectionNaN */) || + this.state.errors.includes(58 /* CommandResult.ValueUpperInvalidFormula */) || + this.state.errors.includes(45 /* CommandResult.LowerBiggerThanUpper */)); default: return true; } @@ -9555,7 +9563,10 @@ registry.add("edit", { name: _lt("Edit"), sequence: 1, - action: () => this.env.openSidePanel("ChartPanel", { figureId: this.props.figure.id }), + action: () => { + this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id }); + this.env.openSidePanel("ChartPanel"); + }, }); registry.add("copy", { name: _lt("Copy"), @@ -9584,7 +9595,7 @@ id: this.props.figure.id, }); if (this.props.sidePanelIsOpen) { - this.env.toggleSidePanel("ChartPanel", { figureId: this.props.figure.id }); + this.env.toggleSidePanel("ChartPanel"); } this.props.onFigureDeleted(); }, @@ -9676,7 +9687,6 @@ if (timeOutId) { return; } - const { maxOffsetX, maxOffsetY } = getters.getMaximumSheetOffset(); const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates(); let { top, left, bottom, right } = getters.getActiveMainViewport(); let { offsetScrollbarX: offsetX, offsetScrollbarY: offsetY } = getters.getActiveSheetScrollInfo(); @@ -9706,7 +9716,7 @@ newTarget = left - 1; break; } - offsetX = Math.min(maxOffsetX, getters.getColDimensions(sheetId, newTarget).start - offsetCorrectionX); + offsetX = getters.getColDimensions(sheetId, newTarget).start - offsetCorrectionX; } } const y = currentEv.clientY - position.top; @@ -9732,7 +9742,7 @@ newTarget = top + edgeScrollInfoY.direction; break; } - offsetY = Math.min(maxOffsetY, env.model.getters.getRowDimensions(sheetId, newTarget).start - offsetCorrectionY); + offsetY = env.model.getters.getRowDimensions(sheetId, newTarget).start - offsetCorrectionY; } } cbMouseMove(colIndex, rowIndex, currentEv); @@ -10062,6 +10072,7 @@ } // HELPERS + const SORT_TYPES_ORDER = ["number", "string", "boolean", "undefined"]; function assert(condition, message) { if (!condition()) { throw new Error(message); @@ -10108,6 +10119,20 @@ return ""; } } + /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */ + function normalizeString(str) { + return str + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + } + /** + * Normalize a value. + * If the cell value is a string, this will set it to lowercase and replacing accent letters with plain letters + */ + function normalizeValue(value) { + return typeof value === "string" ? normalizeString(value) : value; + } const expectBooleanValueError = (value) => _lt("The function [[FUNCTION_NAME]] expects a boolean value, but '%s' is a text, and cannot be coerced to a number.", value); function toBoolean(value) { switch (typeof value) { @@ -10429,62 +10454,81 @@ // ----------------------------------------------------------------------------- // COMMON FUNCTIONS // ----------------------------------------------------------------------------- + function getNormalizedValueFromColumnRange(range, index) { + return normalizeValue(range[0][index]); + } + function getNormalizedValueFromRowRange(range, index) { + return normalizeValue(range[index][0]); + } /** - * Perform a dichotomic search and return the index of the nearest match less than - * or equal to the target. If all values in the range are greater than the target, - * -1 is returned. - * If the range is not in sorted order, an incorrect value might be returned. + * Perform a dichotomic search on an array and return the index of the nearest match. * - * Example: - * - [3, 6, 10], 3 => 0 - * - [3, 6, 10], 6 => 1 - * - [3, 6, 10], 9 => 1 - * - [3, 6, 10], 42 => 2 - * - [3, 6, 10], 2 => -1 - * - [3, undefined, 6, undefined, 10], 9 => 2 - * - [3, 6, undefined, undefined, undefined, 10], 2 => -1 + * The array should be sorted, if not an incorrect value might be returned. In the case where multiple + * element of the array match the target, the method will return the first match if the array is sorted + * in descending order, and the last match if the array is in ascending order. + * + * + * @param data the array in which to search. + * @param target the value to search. + * @param mode "nextGreater/nextSmaller" : return next greater/smaller value if no exact match is found. + * @param sortOrder whether the array is sorted in ascending or descending order. + * @param rangeLength the number of elements to consider in the search array. + * @param getValueInData function returning the element at index i in the search array. */ - function dichotomicPredecessorSearch(range, target) { - if (target === null) { + function dichotomicSearch(data, target, mode, sortOrder, rangeLength, getValueInData) { + if (target === null || target === undefined) { return -1; } const targetType = typeof target; - let valMin = undefined; - let valMinIndex = undefined; + let matchVal = undefined; + let matchValIndex = undefined; let indexLeft = 0; - let indexRight = range.length - 1; - if (typeof range[indexLeft] === targetType && target < range[indexLeft]) { - return -1; - } - if (typeof range[indexRight] === targetType && range[indexRight] <= target) { - return indexRight; - } + let indexRight = rangeLength - 1; let indexMedian; let currentIndex; let currentVal; let currentType; while (indexRight - indexLeft >= 0) { - indexMedian = Math.ceil((indexLeft + indexRight) / 2); + indexMedian = Math.floor((indexLeft + indexRight) / 2); currentIndex = indexMedian; - currentVal = range[currentIndex]; + currentVal = getValueInData(data, currentIndex); currentType = typeof currentVal; // 1 - linear search to find value with the same type while (indexLeft <= currentIndex && targetType !== currentType) { currentIndex--; - currentVal = range[currentIndex]; + currentVal = getValueInData(data, currentIndex); currentType = typeof currentVal; } + if (currentType !== targetType || currentVal === undefined) { + indexLeft = indexMedian + 1; + continue; + } // 2 - check if value match - if (currentType === targetType && currentVal <= target) { - if (valMin === undefined || - valMin < currentVal || - (valMin === currentVal && valMinIndex < currentIndex)) { - valMin = currentVal; - valMinIndex = currentIndex; + if (mode === "strict" && currentVal === target) { + matchVal = currentVal; + matchValIndex = currentIndex; + } + else if (mode === "nextSmaller" && currentVal <= target) { + if (matchVal === undefined || + matchVal < currentVal || + (matchVal === currentVal && sortOrder === "asc" && matchValIndex < currentIndex) || + (matchVal === currentVal && sortOrder === "desc" && matchValIndex > currentIndex)) { + matchVal = currentVal; + matchValIndex = currentIndex; } } - // 3 - give new indexs for the Binary search - if (currentType === targetType && currentVal > target) { + else if (mode === "nextGreater" && currentVal >= target) { + if (matchVal === undefined || + matchVal > currentVal || + (matchVal === currentVal && sortOrder === "asc" && matchValIndex < currentIndex) || + (matchVal === currentVal && sortOrder === "desc" && matchValIndex > currentIndex)) { + matchVal = currentVal; + matchValIndex = currentIndex; + } + } + // 3 - give new indexes for the Binary search + if ((sortOrder === "asc" && currentVal > target) || + (sortOrder === "desc" && currentVal <= target)) { indexRight = currentIndex - 1; } else { @@ -10492,69 +10536,71 @@ } } // note that valMinIndex could be 0 - return valMinIndex !== undefined ? valMinIndex : -1; + return matchValIndex !== undefined ? matchValIndex : -1; } /** - * Perform a dichotomic search and return the index of the nearest match more than - * or equal to the target. If all values in the range are smaller than the target, - * -1 is returned. - * If the range is not in sorted order, an incorrect value might be returned. + * Perform a linear search and return the index of the match. + * -1 is returned if no value is found. * * Example: - * - [10, 6, 3], 3 => 2 - * - [10, 6, 3], 6 => 1 - * - [10, 6, 3], 9 => 0 - * - [10, 6, 3], 42 => -1 - * - [10, 6, 3], 2 => 2 - * - [10, undefined, 6, undefined, 3], 9 => 0 - * - [10, 6, undefined, undefined, undefined, 3], 2 => 5 + * - [3, 6, 10], 3 => 0 + * - [3, 6, 10], 6 => 1 + * - [3, 6, 10], 9 => -1 + * - [3, 6, 10], 2 => -1 + * + * @param data the array to search in. + * @param target the value to search in the array. + * @param mode if "strict" return exact match index. "nextGreater" returns the next greater + * element from the target and "nextSmaller" the next smaller + * @param numberOfValues the number of elements to consider in the search array. + * @param getValueInData function returning the element at index i in the search array. + * @param reverseSearch if true, search in the array starting from the end. + */ - function dichotomicSuccessorSearch(range, target) { - const targetType = typeof target; - let valMax; - let valMaxIndex = undefined; - let indexLeft = 0; - let indexRight = range.length - 1; - if (typeof range[indexLeft] === targetType && target > range[indexLeft]) { + function linearSearch(data, target, mode, numberOfValues, getValueInData, reverseSearch = false) { + if (target === null || target === undefined) return -1; - } - if (typeof range[indexRight] === targetType && range[indexRight] > target) { - return indexRight; - } - let indexMedian; - let currentIndex; - let currentVal; - let currentType; - while (indexRight - indexLeft >= 0) { - indexMedian = Math.ceil((indexLeft + indexRight) / 2); - currentIndex = indexMedian; - currentVal = range[currentIndex]; - currentType = typeof currentVal; - // 1 - linear search to find value with the same type - while (indexLeft <= currentIndex && targetType !== currentType) { - currentIndex--; - currentVal = range[currentIndex]; - currentType = typeof currentVal; + const getValue = reverseSearch + ? (data, i) => getValueInData(data, numberOfValues - i - 1) + : getValueInData; + let closestMatch = undefined; + let closestMatchIndex = -1; + for (let i = 0; i < numberOfValues; i++) { + const value = getValue(data, i); + if (value === target) { + return reverseSearch ? numberOfValues - i - 1 : i; } - // 2 - check if value match - if (currentType === targetType && currentVal >= target) { - if (valMax === undefined || - valMax > currentVal || - (valMax === currentVal && valMaxIndex > currentIndex)) { - valMax = currentVal; - valMaxIndex = currentIndex; + if (mode === "nextSmaller") { + if ((!closestMatch && compareCellValues(target, value) >= 0) || + (compareCellValues(target, value) >= 0 && compareCellValues(value, closestMatch) > 0)) { + closestMatch = value; + closestMatchIndex = i; } } - // 3 - give new indexs for the Binary search - if (currentType === targetType && currentVal <= target) { - indexRight = currentIndex - 1; - } - else { - indexLeft = indexMedian + 1; + else if (mode === "nextGreater") { + if ((!closestMatch && compareCellValues(target, value) <= 0) || + (compareCellValues(target, value) <= 0 && compareCellValues(value, closestMatch) < 0)) { + closestMatch = value; + closestMatchIndex = i; + } } } - // note that valMaxIndex could be 0 - return valMaxIndex !== undefined ? valMaxIndex : -1; + return reverseSearch ? numberOfValues - closestMatchIndex - 1 : closestMatchIndex; + } + function compareCellValues(left, right) { + let typeOrder = SORT_TYPES_ORDER.indexOf(typeof left) - SORT_TYPES_ORDER.indexOf(typeof right); + if (typeOrder === 0) { + if (typeof left === "string" && typeof right === "string") { + typeOrder = left.localeCompare(right); + } + else if (typeof left === "number" && typeof right === "number") { + typeOrder = left - right; + } + else if (typeof left === "boolean" && typeof right === "boolean") { + typeOrder = Number(left) - Number(right); + } + } + return typeOrder; } // ----------------------------------------------------------------------------- @@ -11767,7 +11813,7 @@ let count = 0; visitAny(data, (d) => { if (typeof d === "number") { - index = dichotomicPredecessorSearch(sortedArray, d); + index = dichotomicSearch(sortedArray, d, "nextSmaller", "asc", sortedArray.length, (array, i) => array[i]); sortedArray.splice(index + 1, 0, d); count++; } @@ -12084,7 +12130,7 @@ let count = 0; visitAny([data], (d) => { if (typeof d === "number") { - index = dichotomicPredecessorSearch(largests, d); + index = dichotomicSearch(largests, d, "nextSmaller", "asc", largests.length, (array, i) => array[i]); largests.splice(index + 1, 0, d); count++; if (count > _n) { @@ -12394,7 +12440,7 @@ let count = 0; visitAny([data], (d) => { if (typeof d === "number") { - index = dichotomicPredecessorSearch(largests, d); + index = dichotomicSearch(largests, d, "nextSmaller", "asc", largests.length, (array, i) => array[i]); largests.splice(index + 1, 0, d); count++; if (count > _n) { @@ -14373,6 +14419,24 @@ isExported: true, }; // ----------------------------------------------------------------------------- + // ISBLANK + // ----------------------------------------------------------------------------- + const ISBLANK = { + description: _lt("Whether the referenced cell is empty"), + args: args(`value (any, lazy) ${_lt("Reference to the cell that will be checked for emptiness.")}`), + returns: ["BOOLEAN"], + compute: function (value) { + try { + const val = value(); + return val === null; + } + catch (e) { + return false; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- // NA // ----------------------------------------------------------------------------- const NA = { @@ -14394,6 +14458,7 @@ ISNONTEXT: ISNONTEXT, ISNUMBER: ISNUMBER, ISTEXT: ISTEXT, + ISBLANK: ISBLANK, NA: NA }); @@ -14469,6 +14534,33 @@ isExported: true, }; // ----------------------------------------------------------------------------- + // IFNA + // ----------------------------------------------------------------------------- + const IFNA = { + description: _lt("Value if it is not an #N/A error, otherwise 2nd argument."), + args: args(` + value (any, lazy) ${_lt("The value to return if value itself is not #N/A an error.")} + value_if_error (any, lazy, default=${_lt("An empty value")}) ${_lt("The value the function returns if value is an #N/A error.")} + `), + returns: ["ANY"], + compute: function (value, valueIfError = () => "") { + let result; + try { + result = value(); + } + catch (e) { + if (e.errorType === CellErrorType.NotAvailable) { + result = valueIfError(); + } + else { + result = value(); + } + } + return result === null || result === undefined ? "" : result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- // IFS // ----------------------------------------------------------------------------- const IFS = { @@ -14557,6 +14649,7 @@ AND: AND, IF: IF, IFERROR: IFERROR, + IFNA: IFNA, IFS: IFS, NOT: NOT, OR: OR, @@ -14564,25 +14657,8 @@ }); const DEFAULT_IS_SORTED = true; - /** - * Perform a linear search and return the index of the perfect match. - * -1 is returned if no value is found. - * - * Example: - * - [3, 6, 10], 3 => 0 - * - [3, 6, 10], 6 => 1 - * - [3, 6, 10], 9 => -1 - * - [3, 6, 10], 2 => -1 - */ - function linearSearch(range, target) { - for (let i = 0; i < range.length; i++) { - if (range[i] === target) { - return i; - } - } - // no value is found, -1 is returned - return -1; - } + const DEFAULT_MATCH_MODE = 0; + const DEFAULT_SEARCH_MODE = 1; // ----------------------------------------------------------------------------- // COLUMN // ----------------------------------------------------------------------------- @@ -14627,15 +14703,15 @@ returns: ["ANY"], compute: function (searchKey, range, index, isSorted = DEFAULT_IS_SORTED) { const _index = Math.trunc(toNumber(index)); + const _searchKey = normalizeValue(searchKey); assert(() => 1 <= _index && _index <= range[0].length, _lt("[[FUNCTION_NAME]] evaluates to an out of bounds range.")); const _isSorted = toBoolean(isSorted); - const firstRow = range.map((col) => col[0]); let colIndex; if (_isSorted) { - colIndex = dichotomicPredecessorSearch(firstRow, searchKey); + colIndex = dichotomicSearch(range, _searchKey, "nextSmaller", "asc", range.length, getNormalizedValueFromRowRange); } else { - colIndex = linearSearch(firstRow, searchKey); + colIndex = linearSearch(range, _searchKey, "strict", range.length, getNormalizedValueFromRowRange); } assert(() => colIndex > -1, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey))); return range[colIndex][_index - 1]; @@ -14656,9 +14732,13 @@ compute: function (searchKey, searchArray, resultRange) { let nbCol = searchArray.length; let nbRow = searchArray[0].length; + const _searchKey = normalizeValue(searchKey); const verticalSearch = nbRow >= nbCol; - const searchRange = verticalSearch ? searchArray[0] : searchArray.map((c) => c[0]); - const index = dichotomicPredecessorSearch(searchRange, searchKey); + const getElement = verticalSearch + ? getNormalizedValueFromColumnRange + : getNormalizedValueFromRowRange; + const rangeLength = verticalSearch ? nbRow : nbCol; + const index = dichotomicSearch(searchArray, _searchKey, "nextSmaller", "asc", rangeLength, getElement); assert(() => index >= 0, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey))); if (resultRange === undefined) { return (verticalSearch ? searchArray[nbCol - 1][index] : searchArray[index][nbRow - 1]); @@ -14689,21 +14769,23 @@ returns: ["NUMBER"], compute: function (searchKey, range, searchType = DEFAULT_SEARCH_TYPE) { let _searchType = toNumber(searchType); + const _searchKey = normalizeValue(searchKey); const nbCol = range.length; const nbRow = range[0].length; assert(() => nbCol === 1 || nbRow === 1, _lt("The range must be a single row or a single column.")); let index = -1; - const _range = range.flat(); + const getElement = nbCol === 1 ? getNormalizedValueFromColumnRange : getNormalizedValueFromRowRange; + const rangeLen = nbCol === 1 ? range[0].length : range.length; _searchType = Math.sign(_searchType); switch (_searchType) { case 1: - index = dichotomicPredecessorSearch(_range, searchKey); + index = dichotomicSearch(range, _searchKey, "nextSmaller", "asc", rangeLen, getElement); break; case 0: - index = linearSearch(_range, searchKey); + index = linearSearch(range, _searchKey, "strict", rangeLen, getElement); break; case -1: - index = dichotomicSuccessorSearch(_range, searchKey); + index = dichotomicSearch(range, _searchKey, "nextGreater", "desc", rangeLen, getElement); break; } assert(() => index >= 0, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey))); @@ -14754,21 +14836,71 @@ returns: ["ANY"], compute: function (searchKey, range, index, isSorted = DEFAULT_IS_SORTED) { const _index = Math.trunc(toNumber(index)); + const _searchKey = normalizeValue(searchKey); assert(() => 1 <= _index && _index <= range.length, _lt("[[FUNCTION_NAME]] evaluates to an out of bounds range.")); const _isSorted = toBoolean(isSorted); - const firstCol = range[0]; let rowIndex; if (_isSorted) { - rowIndex = dichotomicPredecessorSearch(firstCol, searchKey); + rowIndex = dichotomicSearch(range, _searchKey, "nextSmaller", "asc", range[0].length, getNormalizedValueFromColumnRange); } else { - rowIndex = linearSearch(firstCol, searchKey); + rowIndex = linearSearch(range, _searchKey, "strict", range[0].length, getNormalizedValueFromColumnRange); } assert(() => rowIndex > -1, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey))); return range[_index - 1][rowIndex]; }, isExported: true, }; + // ----------------------------------------------------------------------------- + // XLOOKUP + // ----------------------------------------------------------------------------- + const XLOOKUP = { + description: _lt(`Search a range for a match and return the corresponding item from a second range.`), + args: args(` + search_key (any) ${_lt("The value to search for.")} + lookup_range (any, range) ${_lt("The range to consider for the search. Should be a single column or a single row.")} + return_range (any, range) ${_lt("The range containing the return value. Should have the same dimensions as lookup_range.")} + if_not_found (any, lazy, optional) ${_lt("If a valid match is not found, return this value.")} + match_mode (any, default=${DEFAULT_MATCH_MODE}) ${_lt("(0) Exact match. (-1) Return next smaller item if no match. (1) Return next greater item if no match.")} + search_mode (any, default=${DEFAULT_SEARCH_MODE}) ${_lt("(1) Search starting at first item. \ + (-1) Search starting at last item. \ + (2) Perform a binary search that relies on lookup_array being sorted in ascending order. If not sorted, invalid results will be returned. \ + (-2) Perform a binary search that relies on lookup_array being sorted in descending order. If not sorted, invalid results will be returned.\ + ")} + + `), + returns: ["ANY"], + compute: function (searchKey, lookupRange, returnRange, defaultValue, matchMode = DEFAULT_MATCH_MODE, searchMode = DEFAULT_SEARCH_MODE) { + const _matchMode = Math.trunc(toNumber(matchMode)); + const _searchMode = Math.trunc(toNumber(searchMode)); + const _searchKey = normalizeValue(searchKey); + assert(() => lookupRange.length === 1 || lookupRange[0].length === 1, _lt("lookup_range should be either a single row or single column.")); + assert(() => returnRange.length === 1 || returnRange[0].length === 1, _lt("return_range should be either a single row or single column.")); + assert(() => returnRange.length === lookupRange.length && + returnRange[0].length === lookupRange[0].length, _lt("return_range should have the same dimensions as lookup_range.")); + assert(() => [-1, 1, -2, 2].includes(_searchMode), _lt("searchMode should be a value in [-1, 1, -2, 2].")); + assert(() => [-1, 0, 1].includes(_matchMode), _lt("matchMode should be a value in [-1, 0, 1].")); + const getElement = lookupRange.length === 1 ? getNormalizedValueFromColumnRange : getNormalizedValueFromRowRange; + const rangeLen = lookupRange.length === 1 ? lookupRange[0].length : lookupRange.length; + const mode = _matchMode === 0 ? "strict" : _matchMode === 1 ? "nextGreater" : "nextSmaller"; + const reverseSearch = _searchMode === -1; + let index; + if (_searchMode === 2 || _searchMode === -2) { + const sortOrder = _searchMode === 2 ? "asc" : "desc"; + index = dichotomicSearch(lookupRange, _searchKey, mode, sortOrder, rangeLen, getElement); + } + else { + index = linearSearch(lookupRange, _searchKey, mode, rangeLen, getElement, reverseSearch); + } + if (index !== -1) { + return (lookupRange.length === 1 ? returnRange[0][index] : returnRange[index][0]); + } + const _defaultValue = defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue(); + assert(() => !!_defaultValue, _lt("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey))); + return _defaultValue; + }, + isExported: true, + }; var lookup = /*#__PURE__*/Object.freeze({ __proto__: null, @@ -14779,7 +14911,8 @@ MATCH: MATCH, ROW: ROW, ROWS: ROWS, - VLOOKUP: VLOOKUP + VLOOKUP: VLOOKUP, + XLOOKUP: XLOOKUP }); // ----------------------------------------------------------------------------- @@ -15054,6 +15187,8 @@ }); const DEFAULT_STARTING_AT = 1; + /** Regex matching all the words in a string */ + const wordRegex = /[A-Za-zÀ-ÖØ-öø-ÿ]+/g; // ----------------------------------------------------------------------------- // CHAR // ----------------------------------------------------------------------------- @@ -15071,6 +15206,27 @@ isExported: true, }; // ----------------------------------------------------------------------------- + // CLEAN + // ----------------------------------------------------------------------------- + const CLEAN = { + description: _lt("Remove non-printable characters from a piece of text."), + args: args(` + text (string) ${_lt("The text whose non-printable characters are to be removed.")} + `), + returns: ["STRING"], + compute: function (text) { + const _text = toString(text); + let cleanedStr = ""; + for (const char of _text) { + if (char && char.charCodeAt(0) > 31) { + cleanedStr += char; + } + } + return cleanedStr; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- // CONCATENATE // ----------------------------------------------------------------------------- const CONCATENATE = { @@ -15185,6 +15341,44 @@ isExported: true, }; // ----------------------------------------------------------------------------- + // MID + // ----------------------------------------------------------------------------- + const MID = { + description: _lt("A segment of a string."), + args: args(` + text (string) ${_lt("The string to extract a segment from.")} + starting_at (number) ${_lt("The index from the left of string from which to begin extracting. The first character in string has the index 1.")} + extract_length (number) ${_lt("The length of the segment to extract.")} + `), + returns: ["STRING"], + compute: function (text, starting_at, extract_length) { + const _text = toString(text); + const _starting_at = toNumber(starting_at); + const _extract_length = toNumber(extract_length); + assert(() => _starting_at >= 1, _lt("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString())); + assert(() => _extract_length >= 0, _lt("The extract_length argument (%s) must be positive or null.", _extract_length.toString())); + return _text.slice(_starting_at - 1, _starting_at + _extract_length - 1); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PROPER + // ----------------------------------------------------------------------------- + const PROPER = { + description: _lt("Capitalizes each word in a specified string."), + args: args(` + text_to_capitalize (string) ${_lt("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase.")} + `), + returns: ["STRING"], + compute: function (text) { + const _text = toString(text); + return _text.replace(wordRegex, (word) => { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- // REPLACE // ----------------------------------------------------------------------------- const REPLACE = { @@ -15346,6 +15540,7 @@ var text = /*#__PURE__*/Object.freeze({ __proto__: null, CHAR: CHAR, + CLEAN: CLEAN, CONCATENATE: CONCATENATE, EXACT: EXACT, FIND: FIND, @@ -15353,6 +15548,8 @@ LEFT: LEFT, LEN: LEN, LOWER: LOWER, + MID: MID, + PROPER: PROPER, REPLACE: REPLACE, RIGHT: RIGHT, SEARCH: SEARCH, @@ -16287,7 +16484,9 @@ * the compiled formula does not depend on their actual value. * Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function. * - * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|, |N0|, |S0|)` + * Spaces are also ignored to compute the cache key. + * + * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)` */ function compilationCacheKey(tokens, dependencies, constantValues) { return concat(tokens.map((token) => { @@ -16300,6 +16499,8 @@ case "REFERENCE": case "INVALID_REFERENCE": return `|${dependencies.indexOf(token.value)}|`; + case "SPACE": + return ""; default: return token.value; } @@ -16555,7 +16756,7 @@ * There should not be any side effects in this method. */ allowDispatch(command) { - return 0 /* Success */; + return 0 /* CommandResult.Success */; } /** * This method is useful when a plugin need to perform some action before a @@ -16593,12 +16794,12 @@ if (!Array.isArray(results)) { results = [results]; } - const cancelledReasons = results.filter((result) => result !== 0 /* Success */); + const cancelledReasons = results.filter((result) => result !== 0 /* CommandResult.Success */); if (cancelledReasons.length) { return cancelledReasons; } } - return 0 /* Success */; + return 0 /* CommandResult.Success */; }; } checkValidations(command, ...validations) { @@ -16656,7 +16857,7 @@ return this.validateSelection(cmd.content.length, cmd.selection.start, cmd.selection.end); } else { - return 0 /* Success */; + return 0 /* CommandResult.Success */; } case "START_EDITION": if (cmd.selection) { @@ -16665,10 +16866,10 @@ return this.validateSelection(content.length, cmd.selection.start, cmd.selection.end); } else { - return 0 /* Success */; + return 0 /* CommandResult.Success */; } default: - return 0 /* Success */; + return 0 /* CommandResult.Success */; } } handleEvent(event) { @@ -16865,8 +17066,8 @@ } validateSelection(length, start, end) { return start >= 0 && start <= length && end >= 0 && end <= length - ? 0 /* Success */ - : 43 /* WrongComposerSelection */; + ? 0 /* CommandResult.Success */ + : 43 /* CommandResult.WrongComposerSelection */; } onColumnsRemoved(cmd) { if (cmd.elements.includes(this.col) && this.mode !== "inactive") { @@ -18090,16 +18291,12 @@ const ACTIVE_BORDER_WIDTH = 2; const MIN_FIG_SIZE = 80; css /*SCSS*/ ` - .o-figure-wrapper { - position: absolute; - width: 100%; - height: 100%; - overflow: hidden; - } - div.o-figure { box-sizing: content-box; position: absolute; + width: 100%; + height: 100%; + bottom: 0px; right: 0px; border: solid ${FIGURE_BORDER_COLOR}; @@ -18116,10 +18313,16 @@ } } - .o-figure-container { + .o-figure-wrapper { position: absolute; box-sizing: content-box; + .o-figure-overflow-wrapper { + position: absolute; + overflow: hidden; + width: 100%; + height: 100%; + } .o-anchor { z-index: ${ComponentsImportance.ChartAnchor}; position: absolute; @@ -18155,64 +18358,47 @@ } } `; - class FiguresContainer extends owl.Component { + class FigureComponent extends owl.Component { constructor() { super(...arguments); this.figureRegistry = figureRegistry; + this.figureRef = owl.useRef("figure"); this.dnd = owl.useState({ - figureId: "", + isActive: false, x: 0, y: 0, width: 0, height: 0, }); } - getVisibleFigures() { - const selectedId = this.env.model.getters.getSelectedFigureId(); - return this.env.model.getters.getVisibleFigures().map((f) => { - let figure = f; - // Returns current state of drag&drop figure instead of its stored state - if (this.dnd.figureId === f.id) { - figure = { - ...f, - x: this.dnd.x, - y: this.dnd.y, - width: this.dnd.width, - height: this.dnd.height, - }; - } - return { - id: f.id, - isSelected: f.id === selectedId, - figure: figure, - }; - }); + get displayedFigure() { + return this.dnd.isActive ? { ...this.props.figure, ...this.dnd } : this.props.figure; + } + get isSelected() { + return this.env.model.getters.getSelectedFigureId() === this.props.figure.id; } /** Get the current figure size, which is either the stored figure size of the DnD figure size */ - getFigureSize(info) { - const { figure, isSelected } = info; - const target = figure.id === (isSelected && this.dnd.figureId) ? this.dnd : figure; - const { width, height } = target; + getFigureSize() { + const { width, height } = this.displayedFigure; return { width, height }; } - getFigureSizeWithBorders(info) { - const { width, height } = this.getFigureSize(info); - const borders = this.getBorderWidth(info) * 2; + getFigureSizeWithBorders() { + const { width, height } = this.getFigureSize(); + const borders = this.getBorderWidth() * 2; return { width: width + borders, height: height + borders }; } - getBorderWidth(info) { - return info.isSelected ? ACTIVE_BORDER_WIDTH : this.env.isDashboard() ? 0 : BORDER_WIDTH; + getBorderWidth() { + return this.isSelected ? ACTIVE_BORDER_WIDTH : this.env.isDashboard() ? 0 : BORDER_WIDTH; } - getFigureStyle(info) { - const { width, height } = info.figure; - return `width:${width}px;height:${height}px;border-width: ${this.getBorderWidth(info)}px;`; + getFigureStyle() { + const { width, height } = this.displayedFigure; + return `width:${width}px;height:${height}px;border-width: ${this.getBorderWidth()}px;`; } - getContainerStyle(info) { - const { figure, isSelected } = info; - const target = figure.id === (isSelected && this.dnd.figureId) ? this.dnd : figure; + getContainerStyle() { + const target = this.displayedFigure; const { x: offsetCorrectionX, y: offsetCorrectionY } = this.env.model.getters.getMainViewportCoordinates(); const { offsetX, offsetY } = this.env.model.getters.getActiveSheetScrollInfo(); - let { width, height } = this.getFigureSizeWithBorders(info); + let { width, height } = this.getFigureSizeWithBorders(); let x, y; // Visually, the content of the container is slightly shifted as it includes borders and/or corners. // If we want to make assertions on the position of the content, we need to take this shift into account @@ -18240,19 +18426,18 @@ if (width < 0 || height < 0) { return `display:none;`; } - const borderOffset = BORDER_WIDTH - this.getBorderWidth(info); + const borderOffset = BORDER_WIDTH - this.getBorderWidth(); // TODO : remove the +1 once 2951210 is fixed return (`top:${y + borderOffset + 1}px;` + `left:${x + borderOffset}px;` + `width:${width}px;` + `height:${height}px;` + - `z-index: ${ComponentsImportance.Figure + (info.isSelected ? 1 : 0)}`); + `z-index: ${ComponentsImportance.Figure + (this.isSelected ? 1 : 0)}`); } - getAnchorPosition(anchor, info) { - let { width, height } = this.getFigureSizeWithBorders(info); + getAnchorPosition(anchor) { + let { width, height } = this.getFigureSizeWithBorders(); const anchorCenteringOffset = (ANCHOR_SIZE - ACTIVE_BORDER_WIDTH) / 2; - const { figure, isSelected } = info; - const target = figure.id === (isSelected && this.dnd.figureId) ? this.dnd : figure; + const target = this.displayedFigure; let x = 0; let y = 0; const { x: offsetCorrectionX, y: offsetCorrectionY } = this.env.model.getters.getMainViewportCoordinates(); @@ -18301,22 +18486,18 @@ return `visibility:${visibility};top:${y}px; left:${x}px;`; } setup() { - owl.onMounted(() => { - // horrible, but necessary - // the following line ensures that we render the figures with the correct - // viewport. The reason is that whenever we initialize the grid - // component, we do not know yet the actual size of the viewport, so the - // first owl rendering is done with an empty viewport. Only then we can - // compute which figures should be displayed, so we have to force a - // new rendering - this.render(); - }); + owl.useEffect((selectedFigureId, thisFigureId, el) => { + if (selectedFigureId === thisFigureId) { + el === null || el === void 0 ? void 0 : el.focus(); + } + }, () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el]); } - resize(figure, dirX, dirY, ev) { + resize(dirX, dirY, ev) { + const figure = this.props.figure; ev.stopPropagation(); const initialX = ev.clientX; const initialY = ev.clientY; - this.dnd.figureId = figure.id; + this.dnd.isActive = true; this.dnd.x = figure.x; this.dnd.y = figure.y; this.dnd.width = figure.width; @@ -18334,7 +18515,7 @@ } }; const onMouseUp = (ev) => { - this.dnd.figureId = ""; + this.dnd.isActive = false; const update = { x: this.dnd.x, y: this.dnd.y, @@ -18353,7 +18534,8 @@ }; startDnd(onMouseMove, onMouseUp); } - onMouseDown(figure, ev) { + onMouseDown(ev) { + const figure = this.props.figure; if (ev.button > 0 || this.env.model.getters.isReadonly()) { // not main button, probably a context menu return; @@ -18363,14 +18545,14 @@ return; } if (this.props.sidePanelIsOpen) { - this.env.openSidePanel("ChartPanel", { figureId: figure.id }); + this.env.openSidePanel("ChartPanel"); } const position = gridOverlayPosition(); const { x: offsetCorrectionX, y: offsetCorrectionY } = this.env.model.getters.getMainViewportCoordinates(); const { offsetX, offsetY } = this.env.model.getters.getActiveSheetScrollInfo(); const initialX = ev.clientX - position.left; const initialY = ev.clientY - position.top; - this.dnd.figureId = figure.id; + this.dnd.isActive = true; this.dnd.x = figure.x; this.dnd.y = figure.y; this.dnd.width = figure.width; @@ -18396,7 +18578,7 @@ this.dnd.y = Math.max(figure.y + deltaY, 0); }; const onMouseUp = (ev) => { - this.dnd.figureId = ""; + this.dnd.isActive = false; this.env.model.dispatch("UPDATE_FIGURE", { sheetId: this.env.model.getters.getActiveSheetId(), id: figure.id, @@ -18406,7 +18588,8 @@ }; startDnd(onMouseMove, onMouseUp); } - onKeyDown(figure, ev) { + onKeyDown(ev) { + const figure = this.props.figure; switch (ev.key) { case "Delete": this.env.model.dispatch("DELETE_FIGURE", { @@ -18438,10 +18621,229 @@ } } } + FigureComponent.template = "o-spreadsheet-FigureComponent"; + FigureComponent.components = {}; + + class FiguresContainer extends owl.Component { + getVisibleFigures() { + return this.env.model.getters.getVisibleFigures(); + } + setup() { + owl.onMounted(() => { + // horrible, but necessary + // the following line ensures that we render the figures with the correct + // viewport. The reason is that whenever we initialize the grid + // component, we do not know yet the actual size of the viewport, so the + // first owl rendering is done with an empty viewport. Only then we can + // compute which figures should be displayed, so we have to force a + // new rendering + this.render(); + }); + } + } FiguresContainer.template = "o-spreadsheet-FiguresContainer"; - FiguresContainer.components = {}; + FiguresContainer.components = { FigureComponent }; figureRegistry.add("chart", { Component: ChartFigure, SidePanelComponent: "ChartPanel" }); + /** + * Repeatedly calls a callback function with a time delay between calls. + */ + function useInterval(callback, delay) { + let intervalId; + const { setInterval, clearInterval } = window; + owl.useEffect(() => { + intervalId = setInterval(callback, delay); + return () => clearInterval(intervalId); + }, () => [delay]); + return { + pause: () => { + clearInterval(intervalId); + intervalId = undefined; + }, + resume: () => { + if (intervalId === undefined) { + intervalId = setInterval(callback, delay); + } + }, + }; + } + + function useCellHovered(env, gridRef, callback) { + let hoveredPosition = { + col: undefined, + row: undefined, + }; + const { Date } = window; + let x = 0; + let y = 0; + let lastMoved = 0; + function getPosition() { + const col = env.model.getters.getColIndex(x); + const row = env.model.getters.getRowIndex(y); + return { col, row }; + } + const { pause, resume } = useInterval(checkTiming, 200); + function checkTiming() { + const { col, row } = getPosition(); + const delta = Date.now() - lastMoved; + if (delta > 300 && (col !== hoveredPosition.col || row !== hoveredPosition.row)) { + setPosition(undefined, undefined); + } + if (delta > 300) { + if (col < 0 || row < 0) { + return; + } + setPosition(col, row); + } + } + function updateMousePosition(e) { + x = e.offsetX; + y = e.offsetY; + lastMoved = Date.now(); + } + function recompute() { + const { col, row } = getPosition(); + if (col !== hoveredPosition.col || row !== hoveredPosition.row) { + setPosition(undefined, undefined); + } + } + owl.onMounted(() => { + const grid = gridRef.el; + grid.addEventListener("mousemove", updateMousePosition); + grid.addEventListener("mouseleave", pause); + grid.addEventListener("mouseenter", resume); + grid.addEventListener("mousedown", recompute); + }); + owl.onWillUnmount(() => { + const grid = gridRef.el; + grid.removeEventListener("mousemove", updateMousePosition); + grid.removeEventListener("mouseleave", pause); + grid.removeEventListener("mouseenter", resume); + grid.removeEventListener("mousedown", recompute); + }); + function setPosition(col, row) { + if (col !== hoveredPosition.col || row !== hoveredPosition.row) { + hoveredPosition.col = col; + hoveredPosition.row = row; + callback({ col, row }); + } + } + return hoveredPosition; + } + function useTouchMove(gridRef, handler, canMoveUp) { + let x = null; + let y = null; + function onTouchStart(ev) { + if (ev.touches.length !== 1) + return; + x = ev.touches[0].clientX; + y = ev.touches[0].clientY; + } + function onTouchEnd() { + x = null; + y = null; + } + function onTouchMove(ev) { + if (ev.touches.length !== 1) + return; + // On mobile browsers, swiping down is often associated with "pull to refresh". + // We only want this behavior if the grid is already at the top. + // Otherwise we only want to move the canvas up, without triggering any refresh. + if (canMoveUp()) { + ev.preventDefault(); + ev.stopPropagation(); + } + const currentX = ev.touches[0].clientX; + const currentY = ev.touches[0].clientY; + handler(x - currentX, y - currentY); + x = currentX; + y = currentY; + } + owl.onMounted(() => { + gridRef.el.addEventListener("touchstart", onTouchStart); + gridRef.el.addEventListener("touchend", onTouchEnd); + gridRef.el.addEventListener("touchmove", onTouchMove); + }); + owl.onWillUnmount(() => { + gridRef.el.removeEventListener("touchstart", onTouchStart); + gridRef.el.removeEventListener("touchend", onTouchEnd); + gridRef.el.removeEventListener("touchmove", onTouchMove); + }); + } + class GridOverlay extends owl.Component { + setup() { + this.gridOverlay = owl.useRef("gridOverlay"); + useCellHovered(this.env, this.gridOverlay, this.props.onCellHovered); + owl.useEffect(() => this.props.onGridResized({ + height: this.gridOverlayEl.clientHeight, + width: this.gridOverlayEl.clientWidth, + }), () => [this.gridOverlayEl.clientHeight, this.gridOverlayEl.clientWidth]); + useTouchMove(this.gridOverlay, this.props.onGridMoved, () => { + const { offsetScrollbarY } = this.env.model.getters.getActiveSheetScrollInfo(); + return offsetScrollbarY > 0; + }); + } + get gridOverlayEl() { + if (!this.gridOverlay.el) { + throw new Error("GridOverlay el is not defined."); + } + return this.gridOverlay.el; + } + onMouseDown(ev) { + if (ev.button > 0) { + // not main button, probably a context menu + return; + } + const [col, row] = this.getCartesianCoordinates(ev); + this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey }); + } + onDoubleClick(ev) { + const [col, row] = this.getCartesianCoordinates(ev); + this.props.onCellDoubleClicked(col, row); + } + onContextMenu(ev) { + ev.preventDefault(); + const [col, row] = this.getCartesianCoordinates(ev); + this.props.onCellRightClicked(col, row, { x: ev.clientX, y: ev.clientY }); + } + getCartesianCoordinates(ev) { + const colIndex = this.env.model.getters.getColIndex(ev.offsetX); + const rowIndex = this.env.model.getters.getRowIndex(ev.offsetY); + return [colIndex, rowIndex]; + } + } + GridOverlay.template = "o-spreadsheet-GridOverlay"; + GridOverlay.components = { FiguresContainer }; + GridOverlay.defaultProps = { + onCellHovered: () => { }, + onCellDoubleClicked: () => { }, + onCellClicked: () => { }, + onCellRightClicked: () => { }, + onGridResized: () => { }, + onFigureDeleted: () => { }, + sidePanelIsOpen: false, + }; + + class GridPopover extends owl.Component { + get cellPopover() { + const popover = this.env.model.getters.getCellPopover(this.props.hoveredCell); + if (!popover.isOpen) { + return { isOpen: false }; + } + const coordinates = popover.coordinates; + 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, + }, + }; + } + } + GridPopover.template = "o-spreadsheet-GridPopover"; + GridPopover.components = { Popover }; + class AbstractResizer extends owl.Component { constructor() { super(...arguments); @@ -18759,7 +19161,7 @@ base: this.state.base, elements, }); - if (!result.isSuccessful && result.reasons.includes(2 /* WillRemoveExistingMerge */)) { + if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) { this.env.raiseError(MergeErrorMessage); } } @@ -18928,7 +19330,7 @@ base: this.state.base, elements, }); - if (!result.isSuccessful && result.reasons.includes(2 /* WillRemoveExistingMerge */)) { + if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) { this.env.raiseError(MergeErrorMessage); } } @@ -18993,27 +19395,47 @@ HeadersOverlay.template = "o-spreadsheet-HeadersOverlay"; HeadersOverlay.components = { ColResizer, RowResizer }; - /** - * Repeatedly calls a callback function with a time delay between calls. - */ - function useInterval(callback, delay) { - let intervalId; - const { setInterval, clearInterval } = window; - owl.useEffect(() => { - intervalId = setInterval(callback, delay); - return () => clearInterval(intervalId); - }, () => [delay]); - return { - pause: () => { - clearInterval(intervalId); - intervalId = undefined; - }, - resume: () => { - if (intervalId === undefined) { - intervalId = setInterval(callback, delay); - } - }, + function useGridDrawing(refName, model, canvasSize) { + const canvasRef = owl.useRef(refName); + owl.useEffect(() => drawGrid()); + function drawGrid() { + const canvas = canvasRef.el; + const dpr = window.devicePixelRatio || 1; + const ctx = canvas.getContext("2d", { alpha: false }); + const thinLineWidth = 0.4 * dpr; + const renderingContext = { + ctx, + dpr, + thinLineWidth, + }; + const { width, height } = canvasSize(); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + canvas.width = width * dpr; + canvas.height = height * dpr; + canvas.setAttribute("style", `width:${width}px;height:${height}px;`); + // Imagine each pixel as a large square. The whole-number coordinates (0, 1, 2…) + // are the edges of the squares. If you draw a one-unit-wide line between whole-number + // coordinates, it will overlap opposite sides of the pixel square, and the resulting + // line will be drawn two pixels wide. To draw a line that is only one pixel wide, + // you need to shift the coordinates by 0.5 perpendicular to the line's direction. + // http://diveintohtml5.info/canvas.html#pixel-madness + ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT); + ctx.scale(dpr, dpr); + model.drawGrid(renderingContext); + } + } + + function useWheelHandler(handler) { + function normalize(val, deltaMode) { + return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT); + } + const onMouseWheel = (ev) => { + const deltaX = normalize(ev.shiftKey ? ev.deltaY : ev.deltaX, ev.deltaMode); + const deltaY = normalize(ev.shiftKey ? ev.deltaX : ev.deltaY, ev.deltaMode); + handler(deltaX, deltaY); }; + return onMouseWheel; } css /* scss */ ` @@ -19222,7 +19644,7 @@ Border, }; - class ScrollBar { + class ScrollBar$1 { constructor(el, direction) { this.el = el; this.direction = direction; @@ -19240,186 +19662,177 @@ } } + css /* scss */ ` + .o-scrollbar { + position: absolute; + overflow: auto; + z-index: ${ComponentsImportance.ScrollBar}; + background-color: ${BACKGROUND_GRAY_COLOR}; + + // &.vertical { + // right: 0; + // bottom: ${SCROLLBAR_WIDTH$1}px; + // width: ${SCROLLBAR_WIDTH$1}px; + // overflow-x: hidden; + // } + // &.horizontal { + // bottom: 0; + // height: ${SCROLLBAR_WIDTH$1}px; + // right: ${SCROLLBAR_WIDTH$1}px; + // overflow-y: hidden; + // } + &.corner { + right: 0px; + bottom: 0px; + height: ${SCROLLBAR_WIDTH$1}px; + width: ${SCROLLBAR_WIDTH$1}px; + border-top: 1px solid #e2e3e3; + border-left: 1px solid #e2e3e3; + } + } +`; + class ScrollBar extends owl.Component { + setup() { + this.scrollbarRef = owl.useRef("scrollbar"); + this.scrollbar = new ScrollBar$1(this.scrollbarRef.el, this.props.direction); + owl.onMounted(() => { + this.scrollbar.el = this.scrollbarRef.el; + }); + // TODO improve useEffect dependencies typing in owl + owl.useEffect(() => { + if (this.scrollbar.scroll !== this.props.offset) { + this.scrollbar.scroll = this.props.offset; + } + }, () => [this.scrollbar.scroll, this.props.offset]); + } + get sizeCss() { + return cssPropertiesToCss({ + width: `${this.props.width}px`, + height: `${this.props.height}px`, + }); + } + get positionCss() { + return cssPropertiesToCss(this.props.position); + } + onScroll(ev) { + if (this.props.offset !== this.scrollbar.scroll) { + this.props.onScroll(this.scrollbar.scroll); + } + } + } + ScrollBar.template = owl.xml /*xml*/ ` +