From b6f2087bf8309afdb5e0768b53aa916f9bc83852 Mon Sep 17 00:00:00 2001 From: "Rmi Rahir (rar)" Date: Wed, 26 Jul 2023 13:08:51 +0000 Subject: [PATCH] [IMP] spreadsheet: update o_spreadsheet to latest version ### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/91607311 [REL] 16.5.0-alpha.1 https://github.com/odoo/o-spreadsheet/commit/b9db2a6f [FIX] find_and_replace: update search undo/redo & add/delete header https://github.com/odoo/o-spreadsheet/commit/c701edb0 [FIX] find_and_replace: disabled button css and shorten the debounce Task: 3432224 https://github.com/odoo/o-spreadsheet/commit/52e98921 [IMP] Demo: Add an uncaught error catcher Task: 3392372 https://github.com/odoo/o-spreadsheet/commit/5aef9dd4 [FIX] find_and_replace: show formula checkbox not updating properly https://github.com/odoo/o-spreadsheet/commit/6ace3df7 [FIX] find_and_replace: updating the cell will update the search https://github.com/odoo/o-spreadsheet/commit/ee0506d2 [FIX] find_and_replace: hidden cell should not be considered https://github.com/odoo/o-spreadsheet/commit/e1f68c2b [IMP] dates: detect mm/yyyy strings as dates Task: 3413243 https://github.com/odoo/o-spreadsheet/commit/3ba2c3c0 [REL] Bump master version to 16.5.0-alpha.0 https://github.com/odoo/o-spreadsheet/commit/39a37866 [IMP] jest: update jest to 29.5.0 Task: 3047642 https://github.com/odoo/o-spreadsheet/commit/500997ef [FIX] SortPlugin: fix crash whith empty-ish cells https://github.com/odoo/o-spreadsheet/commit/69051ea5 [FIX] *: unify to strict equality operator https://github.com/odoo/o-spreadsheet/commit/8a9914a2 [FIX] ActionButton: update SVG icon to display current style in top bar https://github.com/odoo/o-spreadsheet/commit/9ba05e3a [FIX] dashboard: Fixed filter icon position https://github.com/odoo/o-spreadsheet/commit/80aec0bc [IMP] topbar: allow double clicking on paint format https://github.com/odoo/o-spreadsheet/commit/49554975 [FIX] currency: display localized custom format preview Task: 3389619 https://github.com/odoo/o-spreadsheet/commit/87108395 [IMP] format: config default currency https://github.com/odoo/o-spreadsheet/commit/01823df7 [IMP] menus: localize number format preview Task: 3389619 https://github.com/odoo/o-spreadsheet/commit/599003a3 [IMP] header_size: resize row based on wrapped text Task: 3240138 https://github.com/odoo/o-spreadsheet/commit/fabc8449 [REF] ui_sheet: extract getters into helpers Task: 3240138 https://github.com/odoo/o-spreadsheet/commit/f044ec98 [MOV] helpers: move text helpers to a separate file Task: 3240138 https://github.com/odoo/o-spreadsheet/commit/71cc9589 [FIX] menus: dynamically add all functions https://github.com/odoo/o-spreadsheet/commit/bd7271e9 [IMP] pr_template: match template with commit format https://github.com/odoo/o-spreadsheet/commit/dc81d48c [IMP] Figure: add icons to figures context menu Task: 3391903 Part-of: odoo/odoo#129782 --- .../static/src/o_spreadsheet/o_spreadsheet.js | 1205 ++++++++++------- .../src/o_spreadsheet/o_spreadsheet.xml | 38 +- 2 files changed, 740 insertions(+), 503 deletions(-) diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js index cdf30f9e057..553943ef435 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js @@ -109,8 +109,8 @@ const HEADER_BORDER_COLOR = "#C0C0C0"; const CELL_BORDER_COLOR = "#E2E3E3"; const BACKGROUND_CHART_COLOR = "#FFFFFF"; - const MENU_ITEM_DISABLED_COLOR = "#CACACA"; const BG_HOVER_COLOR = "#EBEBEB"; + const DISABLED_TEXT_COLOR = "#CACACA"; const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8; const LINK_COLOR = "#01666b"; const FILTERS_COLOR = "#188038"; @@ -236,6 +236,7 @@ const HEADER_FONT_SIZE = 11; const DEFAULT_FONT = "'Roboto', arial"; const DEFAULT_VERTICAL_ALIGN = "bottom"; + const DEFAULT_WRAPPING_MODE = "overflow"; // Borders const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" }; const DEFAULT_FILTER_BORDER_DESC = { style: "thin", color: FILTERS_COLOR }; @@ -393,82 +394,6 @@ function clip(val, min, max) { return val < min ? min : val > max ? max : val; } - function computeTextLinesHeight(textLineHeight, numberOfLines = 1) { - return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN; - } - /** - * Get the default height of the cell given its style. - */ - function getDefaultCellHeight(cell) { - if (!cell || !cell.content) { - return DEFAULT_CELL_HEIGHT; - } - const fontSize = computeTextFontSizeInPixels(cell.style); - // the number of lines should be computed from the formula result, but it's not evaluated at this point - const numberOfLines = cell.isFormula ? 1 : cell.content.split(NEWLINE).length; - return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL; - } - function computeTextWidth(context, text, style) { - const font = computeTextFont(style); - if (!textWidthCache[font]) { - textWidthCache[font] = {}; - } - if (textWidthCache[font][text] === undefined) { - context.save(); - context.font = font; - const textWidth = context.measureText(text).width; - context.restore(); - textWidthCache[font][text] = textWidth; - } - return textWidthCache[font][text]; - } - const textWidthCache = {}; - function fontSizeInPixels(fontSize) { - return Math.round((fontSize * 96) / 72); - } - function computeTextFont(style) { - const italic = style.italic ? "italic " : ""; - const weight = style.bold ? "bold" : DEFAULT_FONT_WEIGHT; - const size = computeTextFontSizeInPixels(style); - return `${italic}${weight} ${size}px ${DEFAULT_FONT}`; - } - function computeTextFontSizeInPixels(style) { - const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE; - return fontSizeInPixels(sizeInPt); - } - /** - * Return the font size that makes the width of a text match the given line width. - * Minimum font size is 1. - * - * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size. - */ - function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) { - let minFontSize = 1; - if (getTextWidth(minFontSize) > lineWidth) - return minFontSize; - if (getTextWidth(maxFontSize) < lineWidth) - return maxFontSize; - // Dichotomic search - let fontSize = (minFontSize + maxFontSize) / 2; - let currentTextWidth = getTextWidth(fontSize); - // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise - let iterations = 0; - while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) { - if (currentTextWidth >= lineWidth) { - maxFontSize = (minFontSize + maxFontSize) / 2; - } - else { - minFontSize = (minFontSize + maxFontSize) / 2; - } - fontSize = (minFontSize + maxFontSize) / 2; - currentTextWidth = getTextWidth(fontSize); - iterations++; - } - return fontSize; - } - function computeIconWidth(style) { - return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN; - } /** * Create a range from start (included) to end (excluded). * range(10, 13) => [10, 11, 12] @@ -723,10 +648,6 @@ Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]); return cleanObject; } - /** Transform a string to lower case. If the string is undefined, return an empty string */ - function toLowerCase(str) { - return str ? str.toLowerCase() : ""; - } function transpose2dArray(matrix, callback = (val) => val) { if (!matrix.length) return []; @@ -816,6 +737,14 @@ }, }[funcName]; } + function removeIndexesFromArray(array, indexes) { + return array.filter((_, index) => !indexes.includes(index)); + } + function insertItemsAtIndex(array, items, index) { + const newArray = [...array]; + newArray.splice(index, 0, ...items); + return newArray; + } const RBA_REGEX = /rgba?\(|\s+|\)/gi; const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/; @@ -958,13 +887,13 @@ let g = rgba.g.toString(16); let b = rgba.b.toString(16); let a = Math.round(rgba.a * 255).toString(16); - if (r.length == 1) + if (r.length === 1) r = "0" + r; - if (g.length == 1) + if (g.length === 1) g = "0" + g; - if (b.length == 1) + if (b.length === 1) b = "0" + b; - if (a.length == 1) + if (a.length === 1) a = "0" + a; if (a === "ff") a = ""; @@ -1067,13 +996,13 @@ let l = 0; // Calculate hue // No difference - if (delta == 0) + if (delta === 0) h = 0; // Red is max - else if (cMax == r) + else if (cMax === r) h = ((g - b) / delta) % 6; // Green is max - else if (cMax == g) + else if (cMax === g) h = (b - r) / delta + 2; // Blue is max else @@ -1084,7 +1013,7 @@ h += 360; l = (cMax + cMin) / 2; // Calculate saturation - s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); // Multiply l and s by 100 s = +(s * 100).toFixed(1); l = +(l * 100).toFixed(1); @@ -1256,7 +1185,7 @@ const mdyDateRegexp = /^\d{1,2}(\/|-|\s)\d{1,2}((\/|-|\s)\d{1,4})?$/; const ymdDateRegexp = /^\d{3,4}(\/|-|\s)\d{1,2}(\/|-|\s)\d{1,2}$/; const dateSeparatorsRegex = /\/|-|\s/; - const dateRegexp = /^(\d{1,4})[\/-\s](\d{1,2})([\/-\s](\d{1,4}))?$/; + const dateRegexp = /^(\d{1,4})[\/-\s](\d{1,4})([\/-\s](\d{1,4}))?$/; const timeRegexp = /((\d+(:\d+)?(:\d+)?\s*(AM|PM))|(\d+:\d+(:\d+)?))$/; function isDateTime(str, locale) { return parseDateTime(str, locale) !== null; @@ -1317,6 +1246,10 @@ } const localeDateType = getLocaleDateFormatType(locale); if (!part3) { + if (part2.length > 2) { + // e.g. 11/2023 + return { month: part1, year: part2, day: undefined, dateString, type: localeDateType }; + } if (localeDateType === "dmy") { return { day: part1, month: part2, year: part3, dateString, type: "dmy" }; } @@ -1521,7 +1454,7 @@ } function isLeapYear(year) { const _year = Math.trunc(year); - return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 == 0; + return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 === 0; } function getYearFrac(startDate, endDate, _dayCountConvention) { if (startDate === endDate) { @@ -2182,6 +2115,39 @@ } return undefined; } + function createCurrencyFormat(currency) { + const decimalPlaces = currency.decimalPlaces ?? 2; + const position = currency.position ?? "before"; + const code = currency.code ?? ""; + const symbol = currency.symbol ?? ""; + const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : ""; + const numberFormat = "#,##0" + decimalRepresentation; + let textExpression = `${code} ${symbol}`.trim(); + if (position === "after" && code) { + textExpression = " " + textExpression; + } + return insertTextInFormat(textExpression, position, numberFormat); + } + function insertTextInFormat(text, position, format) { + const textExpression = `[$${text}]`; + return position === "before" ? textExpression + format : format + textExpression; + } + function roundFormat(format) { + const internalFormat = parseFormat(format); + const roundedFormat = internalFormat.map((formatPart) => { + if (formatPart.type === "NUMBER") { + return { + type: formatPart.type, + format: { + ...formatPart.format, + decimalPart: undefined, + }, + }; + } + return formatPart; + }); + return convertInternalFormatToFormat(roundedFormat); + } function createLargeNumberFormat(format, magnitude, postFix, locale) { const internalFormat = parseFormat(format || "#,##0"); const largeNumberFormat = internalFormat @@ -3275,6 +3241,169 @@ return rows; } + function computeTextLinesHeight(textLineHeight, numberOfLines = 1) { + return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN; + } + /** + * Get the default height of the cell given its style. + */ + function getDefaultCellHeight(ctx, cell, colSize) { + if (!cell || !cell.content) + return DEFAULT_CELL_HEIGHT; + const maxWidth = cell.style?.wrapping ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined; + const numberOfLines = cell.isFormula + ? 1 + : splitTextToWidth(ctx, cell.content, cell.style, maxWidth).length; + const fontSize = computeTextFontSizeInPixels(cell.style); + return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL; + } + const textWidthCache = {}; + function computeTextWidth(context, text, style) { + const font = computeTextFont(style); + if (!textWidthCache[font]) { + textWidthCache[font] = {}; + } + if (textWidthCache[font][text] === undefined) { + context.save(); + context.font = font; + const textWidth = context.measureText(text).width; + context.restore(); + textWidthCache[font][text] = textWidth; + } + return textWidthCache[font][text]; + } + function fontSizeInPixels(fontSize) { + return Math.round((fontSize * 96) / 72); + } + function computeTextFont(style) { + const italic = style.italic ? "italic " : ""; + const weight = style.bold ? "bold" : DEFAULT_FONT_WEIGHT; + const size = computeTextFontSizeInPixels(style); + return `${italic}${weight} ${size}px ${DEFAULT_FONT}`; + } + function computeTextFontSizeInPixels(style) { + const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE; + return fontSizeInPixels(sizeInPt); + } + function splitWordToSpecificWidth(ctx, word, width, style) { + const wordWidth = computeTextWidth(ctx, word, style); + if (wordWidth <= width) { + return [word]; + } + const splitWord = []; + let wordPart = ""; + for (let l of word) { + const wordPartWidth = computeTextWidth(ctx, wordPart + l, style); + if (wordPartWidth > width) { + splitWord.push(wordPart); + wordPart = l; + } + else { + wordPart += l; + } + } + splitWord.push(wordPart); + return splitWord; + } + /** + * Return the given text, split in multiple lines if needed. The text will be split in multiple + * line if it contains NEWLINE characters, or if it's longer than the given width. + */ + function splitTextToWidth(ctx, text, style, width) { + if (!style) + style = {}; + const brokenText = []; + // Checking if text contains NEWLINE before split makes it very slightly slower if text contains it, + // but 5-10x faster if it doesn't + const lines = text.includes(NEWLINE) ? text.split(NEWLINE) : [text]; + for (const line of lines) { + const words = line.includes(" ") ? line.split(" ") : [line]; + if (!width) { + brokenText.push(line); + continue; + } + let textLine = ""; + let availableWidth = width; + for (let word of words) { + const splitWord = splitWordToSpecificWidth(ctx, word, width, style); + const lastPart = splitWord.pop(); + const lastPartWidth = computeTextWidth(ctx, lastPart, style); + // At this step: "splitWord" is an array composed of parts of word whose + // length is at most equal to "width". + // Last part contains the end of the word. + // Note that: When word length is less than width, then lastPart is equal + // to word and splitWord is empty + if (splitWord.length) { + if (textLine !== "") { + brokenText.push(textLine); + textLine = ""; + availableWidth = width; + } + splitWord.forEach((wordPart) => { + brokenText.push(wordPart); + }); + textLine = lastPart; + availableWidth = width - lastPartWidth; + } + else { + // here "lastPart" is equal to "word" and the "word" size is smaller than "width" + const _word = textLine === "" ? lastPart : " " + lastPart; + const wordWidth = computeTextWidth(ctx, _word, style); + if (wordWidth <= availableWidth) { + textLine += _word; + availableWidth -= wordWidth; + } + else { + brokenText.push(textLine); + textLine = lastPart; + availableWidth = width - lastPartWidth; + } + } + } + if (textLine !== "") { + brokenText.push(textLine); + } + } + return brokenText; + } + /** + * Return the font size that makes the width of a text match the given line width. + * Minimum font size is 1. + * + * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size. + */ + function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) { + let minFontSize = 1; + if (getTextWidth(minFontSize) > lineWidth) + return minFontSize; + if (getTextWidth(maxFontSize) < lineWidth) + return maxFontSize; + // Dichotomic search + let fontSize = (minFontSize + maxFontSize) / 2; + let currentTextWidth = getTextWidth(fontSize); + // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise + let iterations = 0; + while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) { + if (currentTextWidth >= lineWidth) { + maxFontSize = (minFontSize + maxFontSize) / 2; + } + else { + minFontSize = (minFontSize + maxFontSize) / 2; + } + fontSize = (minFontSize + maxFontSize) / 2; + currentTextWidth = getTextWidth(fontSize); + iterations++; + } + return fontSize; + } + function computeIconWidth(style) { + return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN; + } + /** Transform a string to lower case. If the string is undefined, return an empty string */ + function toLowerCase(str) { + return str ? str.toLowerCase() : ""; + } + /* * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript * */ @@ -3297,7 +3426,7 @@ else { // mainly for jest and other browsers that do not have the crypto functionality return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8; + var r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); } @@ -3927,7 +4056,6 @@ "SET_VIEWPORT_OFFSET", "SELECT_SEARCH_NEXT_MATCH", "SELECT_SEARCH_PREVIOUS_MATCH", - "REFRESH_SEARCH", "UPDATE_SEARCH", "CLEAR_SEARCH", "EVALUATE_CELLS", @@ -4125,6 +4253,7 @@ CommandResult[CommandResult["NoSplitSeparatorInSelection"] = 92] = "NoSplitSeparatorInSelection"; CommandResult[CommandResult["NoActiveSheet"] = 93] = "NoActiveSheet"; CommandResult[CommandResult["InvalidLocale"] = 94] = "InvalidLocale"; + CommandResult[CommandResult["AlreadyInPaintingFormatMode"] = 95] = "AlreadyInPaintingFormatMode"; })(exports.CommandResult || (exports.CommandResult = {})); const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"]; @@ -5865,7 +5994,7 @@ } } &.disabled { - color: ${MENU_ITEM_DISABLED_COLOR}; + color: ${DISABLED_TEXT_COLOR}; cursor: not-allowed; } } @@ -5940,8 +6069,8 @@ return this.props.menuItems.some((menuItem) => !!menuItem.icon || !!menuItem.isActive); } getIconName(menu) { - if (menu.icon) { - return menu.icon; + if (menu.icon(this.env)) { + return menu.icon(this.env); } if (menu.isActive?.(this.env)) { return "o-spreadsheet-Icon.CHECK"; @@ -6375,7 +6504,7 @@ for (let pivot = 0; pivot < dim; pivot++) { let diagonalElement = C[pivot][pivot]; // if we have a 0 on the diagonal we'll need to swap with a lower row - if (diagonalElement == 0) { + if (diagonalElement === 0) { //look through every row below the i'th row for (let row = pivot + 1; row < dim; row++) { //if the ii'th row has a non-0 in the i'th col, swap it with that row @@ -6402,7 +6531,7 @@ // the other rows so that there will be 0's in this column in the // rows above and below this one for (let row = 0; row < dim; row++) { - if (row == pivot) { + if (row === pivot) { continue; } // We want to change this element to 0 @@ -12467,7 +12596,7 @@ return false; } catch (e) { - return e?.errorType == CellErrorType.NotAvailable; + return e?.errorType === CellErrorType.NotAvailable; } }, isExported: true, @@ -15183,6 +15312,8 @@ function createAction(item) { const name = item.name; const children = item.children; + const description = item.description; + const icon = item.icon; return { id: item.id || uuidGenerator$2.uuidv4(), name: typeof name === "function" ? name : () => name, @@ -15200,8 +15331,8 @@ : () => [], isReadonlyAllowed: item.isReadonlyAllowed || false, separator: item.separator || false, - icon: item.icon || "", - description: item.description || "", + icon: typeof icon === "function" ? icon : () => icon || "", + description: typeof description === "function" ? description : () => description || "", textColor: item.textColor, sequence: item.sequence || 0, }; @@ -16667,7 +16798,7 @@ function convertDateFormatForMoment(format) { format = format.replace(/y/g, "Y"); format = format.replace(/d/g, "D"); - // "m" before "h" == month, "m" after "h" == minute + // "m" before "h" === month, "m" after "h" === minute const indexH = format.indexOf("h"); if (indexH >= 0) { format = format.slice(0, indexH).replace(/m/g, "M") + format.slice(indexH); @@ -17521,6 +17652,7 @@ env.model.dispatch("SELECT_FIGURE", { id: figureId }); env.openSidePanel("ChartPanel"); }, + icon: "o-spreadsheet-Icon.EDIT", }, getCopyMenuItem(figureId, env), getCutMenuItem(figureId, env), @@ -17552,6 +17684,7 @@ width, }); }, + icon: "o-spreadsheet-Icon.REFRESH", }, getDeleteMenuItem(figureId, onFigureDeleted, env), ]; @@ -17568,6 +17701,7 @@ env.model.dispatch("COPY"); await env.clipboard.write(env.model.getters.getClipboardContent()); }, + icon: "o-spreadsheet-Icon.COPY", }; } function getCutMenuItem(figureId, env) { @@ -17581,6 +17715,7 @@ env.model.dispatch("CUT"); await env.clipboard.write(env.model.getters.getClipboardContent()); }, + icon: "o-spreadsheet-Icon.CUT", }; } function getDeleteMenuItem(figureId, onFigureDeleted, env) { @@ -17595,6 +17730,7 @@ }); onFigureDeleted(); }, + icon: "o-spreadsheet-Icon.DELETE", }; } @@ -18662,7 +18798,7 @@ }; const categorieFunctionAll = { name: _lt("All"), - children: allFunctionListMenuBuilder(), + children: [allFunctionListMenuBuilder], }; function allFunctionListMenuBuilder() { const fnNames = functionRegistry.getKeys(); @@ -18994,7 +19130,10 @@ }; const formatNumberNumber = { name: _lt("Number"), - description: "1,000.12", + description: (env) => formatValue(1000.12, { + format: "#,##0.00", + locale: env.model.getters.getLocale(), + }), execute: (env) => setFormatter(env, "#,##0.00"), isActive: (env) => isFormatSelected(env, "#,##0.00"), }; @@ -19005,21 +19144,34 @@ }; const formatNumberPercent = { name: _lt("Percent"), - description: "10.12%", + description: (env) => formatValue(0.1012, { + format: "0.00%", + locale: env.model.getters.getLocale(), + }), execute: FORMAT_PERCENT_ACTION, isActive: (env) => isFormatSelected(env, "0.00%"), }; const formatNumberCurrency = { name: _lt("Currency"), - description: "$1,000.12", - execute: (env) => setFormatter(env, "[$$]#,##0.00"), - isActive: (env) => isFormatSelected(env, "[$$]#,##0.00"), + description: (env) => formatValue(1000.12, { + format: env.model.config.defaultCurrencyFormat, + locale: env.model.getters.getLocale(), + }), + execute: (env) => setFormatter(env, env.model.config.defaultCurrencyFormat), + isActive: (env) => isFormatSelected(env, env.model.config.defaultCurrencyFormat), }; const formatNumberCurrencyRounded = { name: _lt("Currency rounded"), - description: "$1,000", - execute: (env) => setFormatter(env, "[$$]#,##0"), - isActive: (env) => isFormatSelected(env, "[$$]#,##0"), + description: (env) => formatValue(1000, { + format: roundFormat(env.model.config.defaultCurrencyFormat), + locale: env.model.getters.getLocale(), + }), + execute: (env) => setFormatter(env, roundFormat(env.model.config.defaultCurrencyFormat)), + isActive: (env) => isFormatSelected(env, roundFormat(env.model.config.defaultCurrencyFormat)), + isVisible: (env) => { + const currencyFormat = env.model.config.defaultCurrencyFormat; + return currencyFormat !== roundFormat(currencyFormat); + }, }; const formatCustomCurrency = { name: _lt("Custom currency"), @@ -19028,19 +19180,37 @@ }; const formatNumberDate = { name: _lt("Date"), - description: "9/26/2008", + description: (env) => { + const locale = env.model.getters.getLocale(); + return formatValue(parseLiteral("9/26/2023", DEFAULT_LOCALE), { + format: locale.dateFormat, + locale, + }); + }, execute: (env) => setFormatter(env, env.model.getters.getLocale().dateFormat), isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().dateFormat), }; const formatNumberTime = { name: _lt("Time"), - description: "10:43:00 PM", + description: (env) => { + const locale = env.model.getters.getLocale(); + return formatValue(parseLiteral("9/26/2023 10:43:00 PM", DEFAULT_LOCALE), { + format: locale.timeFormat, + locale, + }); + }, execute: (env) => setFormatter(env, env.model.getters.getLocale().timeFormat), isActive: (env) => isFormatSelected(env, env.model.getters.getLocale().timeFormat), }; const formatNumberDateTime = { name: _lt("Date time"), - description: "9/26/2008 22:43:00", + description: (env) => { + const locale = env.model.getters.getLocale(); + return formatValue(parseLiteral("9/26/2023 22:43:00", DEFAULT_LOCALE), { + format: locale.dateFormat + " " + locale.timeFormat, + locale, + }); + }, execute: (env) => { const locale = env.model.getters.getLocale(); setFormatter(env, locale.dateFormat + " " + locale.timeFormat); @@ -19112,7 +19282,7 @@ }; const formatAlignmentHorizontal = { name: _lt("Horizontal align"), - icon: "o-spreadsheet-Icon.ALIGN_LEFT", + icon: (env) => getHorizontalAlignmentIcon(env), }; const formatAlignmentLeft = { name: _lt("Left"), @@ -19137,46 +19307,50 @@ }; const formatAlignmentVertical = { name: _lt("Vertical align"), - icon: "o-spreadsheet-Icon.ALIGN_MIDDLE", + icon: (env) => getVerticalAlignmentIcon(env), }; const formatAlignmentTop = { name: _lt("Top"), execute: (env) => setStyle(env, { verticalAlign: "top" }), - isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "top", + isActive: (env) => getVerticalAlign(env) === "top", icon: "o-spreadsheet-Icon.ALIGN_TOP", }; const formatAlignmentMiddle = { name: _lt("Middle"), execute: (env) => setStyle(env, { verticalAlign: "middle" }), - isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "middle", + isActive: (env) => getVerticalAlign(env) === "middle", icon: "o-spreadsheet-Icon.ALIGN_MIDDLE", }; const formatAlignmentBottom = { name: _lt("Bottom"), execute: (env) => setStyle(env, { verticalAlign: "bottom" }), - isActive: (env) => (env.model.getters.getCurrentStyle().verticalAlign || DEFAULT_VERTICAL_ALIGN) === "bottom", + isActive: (env) => getVerticalAlign(env) === "bottom", icon: "o-spreadsheet-Icon.ALIGN_BOTTOM", }; + const formatWrappingIcon = { + name: _lt("Wrapping"), + icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW", + }; const formatWrapping = { name: _lt("Wrapping"), - icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW", + icon: (env) => getWrapModeIcon(env), }; const formatWrappingOverflow = { name: _lt("Overflow"), execute: (env) => setStyle(env, { wrapping: "overflow" }), - isActive: (env) => (env.model.getters.getCurrentStyle().wrapping || "overflow") === "overflow", + isActive: (env) => getWrappingMode(env) === "overflow", icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW", }; const formatWrappingWrap = { name: _lt("Wrap"), execute: (env) => setStyle(env, { wrapping: "wrap" }), - isActive: (env) => env.model.getters.getCurrentStyle().wrapping === "wrap", + isActive: (env) => getWrappingMode(env) === "wrap", icon: "o-spreadsheet-Icon.WRAPPING_WRAP", }; const formatWrappingClip = { name: _lt("Clip"), execute: (env) => setStyle(env, { wrapping: "clip" }), - isActive: (env) => env.model.getters.getCurrentStyle().wrapping === "clip", + isActive: (env) => getWrappingMode(env) === "clip", icon: "o-spreadsheet-Icon.WRAPPING_CLIP", }; const textColor = { @@ -19192,14 +19366,6 @@ execute: OPEN_CF_SIDEPANEL_ACTION, icon: "o-spreadsheet-Icon.CONDITIONAL_FORMAT", }; - const paintFormat = { - name: _lt("Paint Format"), - execute: (env) => env.model.dispatch("ACTIVATE_PAINT_FORMAT", { - target: env.model.getters.getSelectedZones(), - }), - icon: "o-spreadsheet-Icon.PAINT_FORMAT", - isActive: (env) => env.model.getters.isPaintingFormat(), - }; const clearFormat = { name: _lt("Clear formatting"), description: "Ctrl+<", @@ -19240,6 +19406,53 @@ const cell = env.model.getters.getActiveCell(); return cell.defaultAlign; } + function getVerticalAlign(env) { + const style = env.model.getters.getCurrentStyle(); + if (style.verticalAlign) { + return style.verticalAlign; + } + return DEFAULT_VERTICAL_ALIGN; + } + function getWrappingMode(env) { + const style = env.model.getters.getCurrentStyle(); + if (style.wrapping) { + return style.wrapping; + } + return DEFAULT_WRAPPING_MODE; + } + function getHorizontalAlignmentIcon(env) { + const horizontalAlign = getHorizontalAlign(env); + switch (horizontalAlign) { + case "right": + return "o-spreadsheet-Icon.ALIGN_RIGHT"; + case "center": + return "o-spreadsheet-Icon.ALIGN_CENTER"; + default: + return "o-spreadsheet-Icon.ALIGN_LEFT"; + } + } + function getVerticalAlignmentIcon(env) { + const verticalAlign = getVerticalAlign(env); + switch (verticalAlign) { + case "top": + return "o-spreadsheet-Icon.ALIGN_TOP"; + case "middle": + return "o-spreadsheet-Icon.ALIGN_MIDDLE"; + default: + return "o-spreadsheet-Icon.ALIGN_BOTTOM"; + } + } + function getWrapModeIcon(env) { + const wrapMode = getWrappingMode(env); + switch (wrapMode) { + case "wrap": + return "o-spreadsheet-Icon.WRAPPING_WRAP"; + case "clip": + return "o-spreadsheet-Icon.WRAPPING_CLIP"; + default: + return "o-spreadsheet-Icon.WRAPPING_OVERFLOW"; + } + } var ACTION_FORMAT = /*#__PURE__*/Object.freeze({ __proto__: null, @@ -19270,6 +19483,7 @@ formatAlignmentTop: formatAlignmentTop, formatAlignmentMiddle: formatAlignmentMiddle, formatAlignmentBottom: formatAlignmentBottom, + formatWrappingIcon: formatWrappingIcon, formatWrapping: formatWrapping, formatWrappingOverflow: formatWrappingOverflow, formatWrappingWrap: formatWrappingWrap, @@ -19277,7 +19491,6 @@ textColor: textColor, fillColor: fillColor, formatCF: formatCF, - paintFormat: paintFormat, clearFormat: clearFormat }); @@ -20040,7 +20253,7 @@ separator: true, }) .addChild("format_wrapping", ["format"], { - ...formatWrapping, + ...formatWrappingIcon, sequence: 80, separator: true, }) @@ -22330,17 +22543,45 @@ } get formatProposals() { const currency = this.availableCurrencies[this.state.selectedCurrencyIndex]; - const proposalBases = this.initProposalBases(currency.decimalPlaces); - const firstPosition = currency.position; - const secondPosition = currency.position === "before" ? "after" : "before"; + const position = currency.position; + const opposite = currency.position === "before" ? "after" : "before"; const symbol = this.state.currencySymbol.trim() ? this.state.currencySymbol : ""; const code = this.state.currencyCode.trim() ? this.state.currencyCode : ""; - return code || symbol - ? [ - ...this.createFormatProposals(proposalBases, symbol, code, firstPosition), - ...this.createFormatProposals(proposalBases, symbol, code, secondPosition), - ] - : []; + const decimalPlaces = currency.decimalPlaces; + if (!symbol && !code) { + return []; + } + const simple = symbol ? createCurrencyFormat({ symbol, position, decimalPlaces }) : ""; + const rounded = simple ? roundFormat(simple) : ""; + const simpleWithCode = createCurrencyFormat({ symbol, position, decimalPlaces, code }); + const roundedWithCode = roundFormat(simpleWithCode); + const simpleOpposite = symbol + ? createCurrencyFormat({ symbol, position: opposite, decimalPlaces }) + : ""; + const roundedOpposite = simpleOpposite ? roundFormat(simpleOpposite) : ""; + const simpleOppositeWithCode = createCurrencyFormat({ + symbol, + position: opposite, + decimalPlaces, + code, + }); + const roundedOppositeWithCode = roundFormat(simpleOppositeWithCode); + const formats = new Set([ + rounded, + simple, + roundedWithCode, + simpleWithCode, + roundedOpposite, + simpleOpposite, + roundedOppositeWithCode, + simpleOppositeWithCode, + ]); + return [...formats] + .filter((format) => format !== "") + .map((format) => ({ + format, + example: formatValue(1000.0, { format, locale: this.env.model.getters.getLocale() }), + })); } get isSameFormat() { const selectedFormat = this.formatProposals[this.state.selectedFormatIndex]; @@ -22397,41 +22638,6 @@ initAvailableCurrencies() { this.state.selectedCurrencyIndex = 0; } - initProposalBases(decimalPlaces) { - const result = [{ format: "#,##0", example: "1,000" }]; - const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : ""; - if (decimalRepresentation) { - result.push({ - format: "#,##0" + decimalRepresentation, - example: "1,000" + decimalRepresentation, - }); - } - return result; - } - createFormatProposals(proposalBases, symbol, code, position) { - let formatProposals = []; - // 1 - add proposal with symbol and without code - if (symbol) { - for (let base of proposalBases) { - formatProposals.push(this.createFormatProposal(position, base.example, base.format, symbol)); - } - } - // 2 - if code exist --> add more proposal with symbol and with code - if (code) { - for (let base of proposalBases) { - const expression = (position === "after" ? " " : "") + code + " " + symbol; - formatProposals.push(this.createFormatProposal(position, base.example, base.format, expression)); - } - } - return formatProposals; - } - createFormatProposal(position, baseExample, formatBase, expression) { - const formatExpression = "[$" + expression + "]"; - return { - example: position === "before" ? expression + baseExample : baseExample + expression, - format: position === "before" ? formatExpression + formatBase : formatBase + formatExpression, - }; - } getCommonFormat() { const selectedZones = this.env.model.getters.getSelectedZones(); const sheetId = this.env.model.getters.getActiveSheetId(); @@ -22488,6 +22694,7 @@ state = owl.useState(this.initialState()); debounceTimeoutId; showFormulaState = false; + debouncedUpdateSearch; findAndReplaceRef = owl.useRef("findAndReplace"); get hasSearchResult() { return this.env.model.getters.getCurrentSelectedMatchIndex() !== null; @@ -22497,11 +22704,16 @@ } setup() { this.showFormulaState = this.env.model.getters.shouldShowFormulas(); + this.debouncedUpdateSearch = debounce(this.updateSearch.bind(this), 200); owl.onMounted(() => this.focusInput()); owl.onWillUnmount(() => { this.env.model.dispatch("CLEAR_SEARCH"); this.env.model.dispatch("SET_FORMULA_VISIBILITY", { show: this.showFormulaState }); }); + owl.useEffect(() => { + this.state.searchOptions.searchFormulas = this.env.model.getters.shouldShowFormulas(); + this.searchFormulas(); + }, () => [this.env.model.getters.shouldShowFormulas()]); } onInput(ev) { this.state.toSearch = ev.target.value; @@ -22521,10 +22733,6 @@ this.replace(); } } - onFocusSidePanel() { - this.state.searchOptions.searchFormulas = this.env.model.getters.shouldShowFormulas(); - this.env.model.dispatch("REFRESH_SEARCH"); - } searchFormulas() { this.env.model.dispatch("SET_FORMULA_VISIBILITY", { show: this.state.searchOptions.searchFormulas, @@ -22543,13 +22751,6 @@ searchOptions: this.state.searchOptions, }); } - debouncedUpdateSearch() { - clearTimeout(this.debounceTimeoutId); - this.debounceTimeoutId = setTimeout(() => { - this.updateSearch(); - this.debounceTimeoutId = undefined; - }, 400); - } replace() { this.env.model.dispatch("REPLACE_SEARCH", { replaceWith: this.state.replaceWith, @@ -23234,7 +23435,7 @@ selection.removeAllRanges(); selection.addRange(range); } - if (start == end && start === 0) { + if (start === end && start === 0) { range.setStart(this.el, 0); range.setEnd(this.el, 0); } @@ -23487,7 +23688,7 @@ }, 0); } } - if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent == "") { + if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") { usedCharacters++; } return usedCharacters; @@ -25034,6 +25235,14 @@ }; } + const CURSOR_SVG = /*xml*/ ` + +`; + css /* scss */ ` + .o-paint-format-cursor { + cursor: url("data:image/svg+xml,${encodeURIComponent(CURSOR_SVG)}"), auto; + } +`; function useCellHovered(env, gridRef, callback) { let hoveredPosition = { col: undefined, @@ -25157,6 +25366,12 @@ } return this.gridOverlay.el; } + get style() { + return this.props.gridOverlayDimensions; + } + get isPaintingFormat() { + return this.env.model.getters.isPaintingFormat(); + } onMouseDown(ev) { if (ev.button > 0) { // not main button, probably a context menu @@ -26334,6 +26549,9 @@ else if (this.menuState.isOpen) { this.closeMenu(); } + else if (this.env.model.getters.isPaintingFormat()) { + this.env.model.dispatch("CANCEL_PAINT_FORMAT"); + } else { this.env.model.dispatch("CLEAN_CLIPBOARD_HIGHLIGHT"); } @@ -27536,8 +27754,8 @@ throw new Error("Multiple escaped blocks in format"); } convertedFormat = convertedFormat.replace(/"(.*)"/g, "[$$$1]"); // replace '"..."' by '[$...]' - convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ == ignore width of next char for align purposes. Not supported ATM - convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * == repeat next character enough to fill the line. Not supported ATM + convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ === ignore width of next char for align purposes. Not supported ATM + convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * === repeat next character enough to fill the line. Not supported ATM convertedFormat = convertedFormat.replace(/\\ /g, " "); // unescape spaces convertedFormat = convertedFormat.replace(/\\./g, (match) => match[1]); // unescape other characters if (isXlsxDateFormat(convertedFormat)) { @@ -28829,29 +29047,26 @@ return new XMLString(concat(str)); } /** - * Removes the namespace of all the xml tags in the string. + * Removes the escaped namespace of all the xml tags in the string. * - * Eg. : "ns:test a" => "test a" + * Eg. : "NAMESPACEnsNAMESPACEtest a" => "test a" */ - function removeNamespaces(query) { - return query.replace(/[a-z0-9]+:(?=[a-z0-9]+)/gi, ""); + function removeTagEscapedNamespaces(tag) { + return tag.replace(/NAMESPACE.*NAMESPACE(.*)/, "$1"); } /** - * Escape the namespace's colons of all the xml tags in the string. + * Encase the namespaces in the element's tags with NAMESPACE string * - * Eg. : "ns:test a" => "ns\\:test a" + * e.g. becomes + * + * That's useful because namespaces aren't supported by the HTML specification, so it's arbitrary whether a HTML parser/querySelector + * implementation will support namespaces in the tags or not. */ - function escapeNamespaces(query) { - return query.replace(/([a-z0-9]+):(?=[a-z0-9]+)/gi, "$1\\:"); + function escapeTagNamespaces(str) { + return str.replaceAll(/(<\/?)([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "$1" + "NAMESPACE" + "$2" + "NAMESPACE" + "$3"); } - /** - * Return true if the querySelector ignores the namespaces when searching for a tag in the DOM. - * - * Should return true if it's running on a browser, and false if it's running on jest (jsdom). - */ - function areNamespaceIgnoredByQuerySelector() { - const doc = new DOMParser().parseFromString("", "text/xml"); - return doc.querySelector("test") !== null; + function escapeQueryNameSpaces(query) { + return query.replaceAll(/([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "NAMESPACE" + "$1" + "NAMESPACE" + "$2"); } class AttributeValue { @@ -28881,14 +29096,20 @@ // The xml file we are currently parsing. We should have one Extractor class by XLSXImportFile, but // the XLSXImportFile contains both the main .xml file, and the .rels file currentFile = undefined; - // If the parser querySelector() implementation ignores tag namespaces or not - areNamespaceIgnored; + /** + * /!\ Important : There should be no namespaces in the tags of the XML files. + * + * This class use native querySelector and querySelectorAll, that's used for HTML (not XML). These aren't supposed to + * handled namespaces, as they are not supported by the HTML specification. Some implementations (most browsers) do + * actually support namespaces, but some don't (e.g. jsdom). + * + * The namespace should be escaped as with NAMESPACE string (eg. => ). + */ constructor(rootFile, xlsxStructure, warningManager) { this.rootFile = rootFile; this.currentFile = rootFile.file.fileName; this.xlsxFileStructure = xlsxStructure; this.warningManager = warningManager; - this.areNamespaceIgnored = areNamespaceIgnoredByQuerySelector(); this.relationships = {}; if (rootFile.rels) { this.extractRelationships(rootFile.rels).map((rel) => { @@ -29114,29 +29335,13 @@ throw new Error("Cannot find target file"); return f; } - /** - * Wrapper of querySelector, but we'll remove the namespaces from the query if areNamespacesIgnored is true. - * - * Why we need to do this : - * - For an XML "" - * - on Jest(jsdom) : xml.querySelector("test") == null, xml.querySelector("t\\:test") == - * - on Browser : xml.querySelector("test") == , xml.querySelector("t\\:test") == null - */ querySelector(element, query) { - query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query); - return element.querySelector(query); + const escapedQuery = escapeQueryNameSpaces(query); + return element.querySelector(escapedQuery); } - /** - * Wrapper of querySelectorAll, but we'll remove the namespaces from the query if areNamespacesIgnored is true. - * - * Why we need to do this : - * - For an XML "" - * - on Jest(jsdom) : xml.querySelectorAll("test") == [], xml.querySelectorAll("t\\:test") == [] - * - on Browser : xml.querySelectorAll("test") == [], xml.querySelectorAll("t\\:test") == [] - */ querySelectorAll(element, query) { - query = this.areNamespaceIgnored ? removeNamespaces(query) : escapeNamespaces(query); - return element.querySelectorAll(query); + const escapedQuery = escapeQueryNameSpaces(query); + return element.querySelectorAll(escapedQuery); } /** * Get a color from its id in the Theme's colorScheme. @@ -29376,7 +29581,7 @@ throw new Error("Missing plot area in the chart definition."); } for (let child of plotAreaElement.children) { - const tag = removeNamespaces(child.tagName); + const tag = removeTagEscapedNamespaces(child.tagName); if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) { return tag; } @@ -29388,7 +29593,7 @@ class XlsxFigureExtractor extends XlsxBaseExtractor { extractFigures() { return this.mapOnElements({ parent: this.rootFile.file.xml, query: "xdr:wsDr", children: true }, (figureElement) => { - const anchorType = removeNamespaces(figureElement.tagName); + const anchorType = removeTagEscapedNamespaces(figureElement.tagName); if (anchorType !== "twoCellAnchor") { throw new Error("Only twoCellAnchor are supported for xlsx drawings."); } @@ -30022,7 +30227,8 @@ for (let key of Object.keys(files)) { // Random files can be in xlsx (like a bin file for printer settings) if (key.endsWith(".xml") || key.endsWith(".rels")) { - this.xmls[key] = parseXML(new XMLString(files[key])); + const contentString = escapeTagNamespaces(files[key]); + this.xmls[key] = parseXML(new XMLString(contentString)); } else if (key.includes("media/image")) { this.images.push({ @@ -31089,7 +31295,7 @@ } /** * Set the borders of a cell. - * It overrides the current border if override == true. + * It overrides the current border if override === true. */ setBorder(sheetId, col, row, border, override = true) { if (override || !this.borders?.[sheetId]?.[col]?.[row]?.vertical) { @@ -32359,7 +32565,9 @@ const numHeader = this.getters.getNumberRows(sheetId); let gridHeight = 0; for (let i = 0; i < numHeader; i++) { - gridHeight += this.getters.getRowSize(sheetId, i); + // TODO : since the row size is an UI value now, this doesn't work anymore. Using the default cell height is + // a temporary solution at best, but is broken. + gridHeight += this.getters.getUserRowSize(sheetId, i) || DEFAULT_CELL_HEIGHT; } const figures = this.getters.getFigures(sheetId); for (const figure of figures) { @@ -32785,33 +32993,15 @@ } class HeaderSizePlugin extends CorePlugin { - static getters = ["getRowSize", "getColSize"]; + static getters = ["getUserRowSize", "getColSize"]; sizes = {}; handle(cmd) { switch (cmd.type) { case "CREATE_SHEET": { - const computedSizes = this.computeSheetSizes(cmd.sheetId); - const sizes = { - COL: computedSizes.COL.map((size) => ({ - manualSize: undefined, - computedSize: lazy(size), - })), - ROW: computedSizes.ROW.map((size) => ({ - manualSize: undefined, - computedSize: lazy(size), - })), - }; - this.history.update("sizes", cmd.sheetId, sizes); + this.history.update("sizes", cmd.sheetId, { COL: [], ROW: [] }); break; } case "DUPLICATE_SHEET": - // make sure the values are computed in case the original sheet is deleted - for (const row of this.sizes[cmd.sheetId].ROW) { - row.computedSize(); - } - for (const col of this.sizes[cmd.sheetId].COL) { - col.computedSize(); - } this.history.update("sizes", cmd.sheetIdTo, deepCopy(this.sizes[cmd.sheetId])); break; case "DELETE_SHEET": @@ -32820,21 +33010,8 @@ this.history.update("sizes", sizes); break; case "REMOVE_COLUMNS_ROWS": { - let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]]; - for (let headerIndex of [...cmd.elements].sort((a, b) => b - a)) { - sizes.splice(headerIndex, 1); - } - const min = Math.min(...cmd.elements); - sizes = sizes.map((size, row) => { - if (cmd.dimension === "ROW" && row >= min) { - // invalidate sizes - return { - manualSize: size.manualSize, - computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)), - }; - } - return size; - }); + const arr = this.sizes[cmd.sheetId][cmd.dimension]; + const sizes = removeIndexesFromArray(arr, cmd.elements); this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes); break; } @@ -32843,51 +33020,18 @@ const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base); const baseSize = sizes[cmd.base]; sizes.splice(addIndex, 0, ...Array(cmd.quantity).fill(baseSize)); - sizes = sizes.map((size, row) => { - if (cmd.dimension === "ROW" && row > cmd.base + cmd.quantity) { - // invalidate sizes - return { - manualSize: size.manualSize, - computedSize: lazy(() => this.getRowTallestCellSize(cmd.sheetId, row)), - }; - } - return size; - }); this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes); break; } case "RESIZE_COLUMNS_ROWS": - for (let el of cmd.elements) { - if (cmd.dimension === "ROW") { - const height = this.getRowTallestCellSize(cmd.sheetId, el); - const size = height; - this.history.update("sizes", cmd.sheetId, cmd.dimension, el, { - manualSize: cmd.size || undefined, - computedSize: lazy(size), - }); - } - else { - this.history.update("sizes", cmd.sheetId, cmd.dimension, el, { - manualSize: cmd.size || undefined, - computedSize: lazy(cmd.size || DEFAULT_CELL_WIDTH), - }); + if (cmd.dimension === "ROW") { + for (const el of cmd.elements) { + this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined); } } - break; - case "UPDATE_CELL": - if (!this.sizes[cmd.sheetId]?.["ROW"]?.[cmd.row]?.manualSize) { - const { sheetId, row } = cmd; - this.history.update("sizes", sheetId, "ROW", row, "computedSize", lazy(() => this.getRowTallestCellSize(sheetId, row))); - } - break; - case "ADD_MERGE": - case "REMOVE_MERGE": - for (let target of cmd.target) { - for (let row of range(target.top, target.bottom + 1)) { - const rowHeight = this.getRowTallestCellSize(cmd.sheetId, row); - if (rowHeight !== this.getRowSize(cmd.sheetId, row)) { - this.history.update("sizes", cmd.sheetId, "ROW", row, "computedSize", lazy(rowHeight)); - } + else { + for (const el of cmd.elements) { + this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined); } } break; @@ -32895,94 +33039,29 @@ return; } getColSize(sheetId, index) { - return this.getHeaderSize(sheetId, "COL", index); + return Math.round(this.sizes[sheetId]?.["COL"][index] || DEFAULT_CELL_WIDTH); } - getRowSize(sheetId, index) { - return this.getHeaderSize(sheetId, "ROW", index); - } - getHeaderSize(sheetId, dimension, index) { - return Math.round(this.sizes[sheetId]?.[dimension][index]?.manualSize || - this.sizes[sheetId]?.[dimension][index]?.computedSize() || - this.getDefaultHeaderSize(dimension)); - } - computeSheetSizes(sheetId) { - const sizes = { COL: [], ROW: [] }; - for (let col of range(0, this.getters.getNumberCols(sheetId))) { - sizes.COL.push(this.getHeaderSize(sheetId, "COL", col)); - } - for (let row of range(0, this.getters.getNumberRows(sheetId))) { - let rowSize = this.sizes[sheetId]?.["ROW"]?.[row].manualSize; - if (!rowSize) { - const height = this.getRowTallestCellSize(sheetId, row); - rowSize = height; - } - sizes.ROW.push(rowSize); - } - return sizes; - } - getDefaultHeaderSize(dimension) { - return dimension === "COL" ? DEFAULT_CELL_WIDTH : DEFAULT_CELL_HEIGHT; - } - /** - * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row - * merge, or the height of the cell computed based on its font size. - */ - getCellHeight(position) { - const merge = this.getters.getMerge(position); - if (merge && merge.bottom !== merge.top) { - return DEFAULT_CELL_HEIGHT; - } - const cell = this.getters.getCell(position); - return getDefaultCellHeight(cell); - } - /** - * Get the tallest cell of a row and its size. - * - * The tallest cell of the row correspond to the cell with the biggest font size, - * and that is not part of a multi-line merge. - */ - getRowTallestCellSize(sheetId, row) { - const cellIds = this.getters.getRowCells(sheetId, row); - let maxHeight = 0; - for (let i = 0; i < cellIds.length; i++) { - const cell = this.getters.getCellById(cellIds[i]); - if (!cell) - continue; - const position = this.getters.getCellPosition(cell.id); - const cellHeight = this.getCellHeight(position); - if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) { - maxHeight = cellHeight; - } - } - if (maxHeight <= DEFAULT_CELL_HEIGHT) { - return DEFAULT_CELL_HEIGHT; - } - return maxHeight; + getUserRowSize(sheetId, index) { + const rowSize = this.sizes[sheetId]?.["ROW"][index]; + return rowSize ? Math.round(rowSize) : undefined; } import(data) { for (let sheet of data.sheets) { - const manualSizes = { COL: [], ROW: [] }; + const sizes = { + COL: Array(sheet.colNumber).fill(undefined), + ROW: Array(sheet.rowNumber).fill(undefined), + }; for (let [rowIndex, row] of Object.entries(sheet.rows)) { if (row.size) { - manualSizes["ROW"][rowIndex] = row.size; + sizes["ROW"][rowIndex] = row.size; } } for (let [colIndex, col] of Object.entries(sheet.cols)) { if (col.size) { - manualSizes["COL"][colIndex] = col.size; + sizes["COL"][colIndex] = col.size; } } - const computedSizes = this.computeSheetSizes(sheet.id); - this.sizes[sheet.id] = { - COL: computedSizes.COL.map((size, i) => ({ - manualSize: manualSizes.COL[i], - computedSize: lazy(size), - })), - ROW: computedSizes.ROW.map((size, i) => ({ - manualSize: manualSizes.ROW[i], - computedSize: lazy(size), - })), - }; + this.sizes[sheet.id] = sizes; } return; } @@ -33003,9 +33082,12 @@ if (sheet.rows === undefined) { sheet.rows = {}; } - for (let row of range(0, this.getters.getNumberRows(sheet.id))) { - if (exportDefaults || this.sizes[sheet.id]["ROW"][row]?.manualSize) { - sheet.rows[row] = { ...sheet.rows[row], size: this.getRowSize(sheet.id, row) }; + for (const row of range(0, this.getters.getNumberRows(sheet.id))) { + if (exportDefaults || this.sizes[sheet.id]["ROW"][row]) { + sheet.rows[row] = { + ...sheet.rows[row], + size: this.getUserRowSize(sheet.id, row) ?? DEFAULT_CELL_HEIGHT, + }; } } // Export col sizes @@ -33013,7 +33095,7 @@ sheet.cols = {}; } for (let col of range(0, this.getters.getNumberCols(sheet.id))) { - if (exportDefaults || this.sizes[sheet.id]["COL"][col]?.manualSize) { + if (exportDefaults || this.sizes[sheet.id]["COL"][col]) { sheet.cols[col] = { ...sheet.cols[col], size: this.getColSize(sheet.id, col) }; } } @@ -36569,7 +36651,7 @@ case "ContainsText": return cell.value.toString().indexOf(values[0].toString()) > -1; case "NotContains": - return !cell.value || cell.value.toString().indexOf(values[0].toString()) == -1; + return !cell.value || cell.value.toString().indexOf(values[0].toString()) === -1; case "GreaterThan": return cell.value > values[0]; case "GreaterThanOrEqual": @@ -36596,6 +36678,156 @@ }; } + class UIRowSizePlugin extends UIPlugin { + static getters = ["getRowSize"]; + tallestCellInRow = {}; + ctx = document.createElement("canvas").getContext("2d"); + handle(cmd) { + switch (cmd.type) { + case "START": + for (const sheetId of this.getters.getSheetIds()) { + this.initializeSheet(sheetId); + } + break; + case "CREATE_SHEET": { + this.initializeSheet(cmd.sheetId); + break; + } + case "DUPLICATE_SHEET": { + const tallestCells = deepCopy(this.tallestCellInRow[cmd.sheetId]); + this.history.update("tallestCellInRow", cmd.sheetIdTo, tallestCells); + break; + } + case "DELETE_SHEET": + const tallestCells = { ...this.tallestCellInRow }; + delete tallestCells[cmd.sheetId]; + this.history.update("tallestCellInRow", tallestCells); + break; + case "REMOVE_COLUMNS_ROWS": { + if (cmd.dimension === "COL") { + return; + } + const tallestCells = removeIndexesFromArray(this.tallestCellInRow[cmd.sheetId], cmd.elements); + this.history.update("tallestCellInRow", cmd.sheetId, tallestCells); + break; + } + case "ADD_COLUMNS_ROWS": { + if (cmd.dimension === "COL") { + return; + } + const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base); + const newCells = Array(cmd.quantity).fill(undefined); + const newTallestCells = insertItemsAtIndex(this.tallestCellInRow[cmd.sheetId], newCells, addIndex); + this.history.update("tallestCellInRow", cmd.sheetId, newTallestCells); + break; + } + case "RESIZE_COLUMNS_ROWS": + { + const sheetId = cmd.sheetId; + if (cmd.dimension === "ROW") { + for (const row of cmd.elements) { + const tallestCell = this.getRowTallestCell(sheetId, row); + this.history.update("tallestCellInRow", sheetId, row, tallestCell); + } + } + else { + // Recompute row heights on col size change, they might have changed because of wrapped text + for (const row of range(0, this.getters.getNumberRows(sheetId))) { + for (const col of cmd.elements) { + this.updateRowSizeForCellChange(sheetId, row, col); + } + } + } + } + break; + case "UPDATE_CELL": + this.updateRowSizeForCellChange(cmd.sheetId, cmd.row, cmd.col); + break; + case "ADD_MERGE": + case "REMOVE_MERGE": + for (const target of cmd.target) { + for (const position of positions(target)) { + this.updateRowSizeForCellChange(cmd.sheetId, position.row, position.col); + } + } + } + return; + } + getRowSize(sheetId, row) { + return Math.round(this.getters.getUserRowSize(sheetId, row) ?? + this.tallestCellInRow[sheetId][row]?.size ?? + DEFAULT_CELL_HEIGHT); + } + updateRowSizeForCellChange(sheetId, row, col) { + const tallestCellInRow = this.tallestCellInRow[sheetId]?.[row]; + if (tallestCellInRow?.cell.col === col) { + const newTallestCell = this.getRowTallestCell(sheetId, row); + this.history.update("tallestCellInRow", sheetId, row, newTallestCell); + } + const updatedCellHeight = this.getCellHeight({ sheetId, col, row }); + if (updatedCellHeight <= DEFAULT_CELL_HEIGHT) { + return; + } + if ((!tallestCellInRow && updatedCellHeight > DEFAULT_CELL_HEIGHT) || + (tallestCellInRow && updatedCellHeight > tallestCellInRow.size)) { + const newTallestCell = { cell: { sheetId, col, row }, size: updatedCellHeight }; + this.history.update("tallestCellInRow", sheetId, row, newTallestCell); + } + } + initializeSheet(sheetId) { + const tallestCells = []; + for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) { + const tallestCell = this.getRowTallestCell(sheetId, row); + tallestCells.push(tallestCell); + } + this.history.update("tallestCellInRow", sheetId, tallestCells); + } + /** + * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row + * merge, or the height of the cell computed based on its style/content. + */ + getCellHeight(position) { + if (this.isInMultiRowMerge(position)) { + return DEFAULT_CELL_HEIGHT; + } + const cell = this.getters.getCell(position); + const colSize = this.getters.getColSize(position.sheetId, position.col); + return getDefaultCellHeight(this.ctx, cell, colSize); + } + isInMultiRowMerge(position) { + const merge = this.getters.getMerge(position); + return !!merge && merge.bottom !== merge.top; + } + /** + * Get the tallest cell of a row and its size. + */ + getRowTallestCell(sheetId, row) { + const userRowSize = this.getters.getUserRowSize(sheetId, row); + if (userRowSize !== undefined) { + return undefined; + } + const cellIds = this.getters.getRowCells(sheetId, row); + let maxHeight = 0; + let tallestCell = undefined; + for (let i = 0; i < cellIds.length; i++) { + const cell = this.getters.getCellById(cellIds[i]); + if (!cell) { + continue; + } + const position = this.getters.getCellPosition(cell.id); + const cellHeight = this.getCellHeight(position); + if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) { + maxHeight = cellHeight; + tallestCell = { cell: position, size: cellHeight }; + } + } + if (tallestCell && tallestCell.size > DEFAULT_CELL_HEIGHT) { + return tallestCell; + } + return undefined; + } + } + /** * This plugin manage the autofill. * @@ -38297,6 +38529,7 @@ searchFormulas: false, }; toSearch = ""; + isSearchDirty = false; // --------------------------------------------------------------------------- // Command Handling // --------------------------------------------------------------------------- @@ -38324,14 +38557,21 @@ case "REDO": case "REMOVE_COLUMNS_ROWS": case "ADD_COLUMNS_ROWS": - this.clearSearch(); + case "EVALUATE_CELLS": + case "UPDATE_CELL": + this.isSearchDirty = true; break; case "ACTIVATE_SHEET": - case "REFRESH_SEARCH": this.refreshSearch(); break; } } + finalize() { + if (this.isSearchDirty) { + this.refreshSearch(); + this.isSearchDirty = false; + } + } // --------------------------------------------------------------------------- // Getters // --------------------------------------------------------------------------- @@ -38387,6 +38627,11 @@ if (this.toSearch) { for (const cell of Object.values(cells)) { const { col, row } = this.getters.getCellPosition(cell.id); + const isColHidden = this.getters.isColHidden(sheetId, col); + const isRowHidden = this.getters.isRowHidden(sheetId, row); + if (isColHidden || isRowHidden) { + continue; + } if (cell && this.currentSearchRegex && this.currentSearchRegex.test(this.getSearchableString({ sheetId, col, row }))) { @@ -40029,10 +40274,8 @@ } updateCellCommands.push(newCellValues); } - for (const cmd of updateCellCommands) { - this.dispatch("UPDATE_CELL", cmd); - } } + updateCellCommands.forEach((cmdPayload) => this.dispatch("UPDATE_CELL", cmdPayload)); } /** * Return the distances between main merge cells in the zone. @@ -40133,24 +40376,28 @@ // Getters // --------------------------------------------------------------------------- getCellWidth(position) { - const text = this.getCellText(position); const style = this.getters.getCellComputedStyle(position); - const multiLineText = text.split(NEWLINE); - let contentWidth = Math.max(...multiLineText.map((line) => this.getTextWidth(line, style))); + let contentWidth = 0; + const content = this.getters.getEvaluatedCell(position).formattedValue; + if (content) { + const multiLineText = splitTextToWidth(this.ctx, content, style, undefined); + contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style))); + } const icon = this.getters.getConditionalIcon(position); if (icon) { - contentWidth += computeIconWidth(this.getters.getCellStyle(position)); + contentWidth += computeIconWidth(style); } const isFilterHeader = this.getters.isFilterHeader(position); if (isFilterHeader) { contentWidth += ICON_EDGE_LENGTH + FILTER_ICON_MARGIN; } - if (contentWidth > 0) { - contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL; - if (this.getters.getCellStyle(position).wrapping === "wrap") { - const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col); - return Math.min(colWidth, contentWidth); - } + if (contentWidth === 0) { + return 0; + } + contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL; + if (style.wrapping === "wrap") { + const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col); + return Math.min(colWidth, contentWidth); } return contentWidth; } @@ -40173,56 +40420,7 @@ getCellMultiLineText(position, width) { const style = this.getters.getCellStyle(position); const text = this.getters.getCellText(position, this.getters.shouldShowFormulas()); - const brokenText = []; - for (const line of text.split("\n")) { - const words = line.split(" "); - if (!width) { - brokenText.push(line); - continue; - } - let textLine = ""; - let availableWidth = width; - for (let word of words) { - const splitWord = this.splitWordToSpecificWidth(this.ctx, word, width, style); - const lastPart = splitWord.pop(); - const lastPartWidth = computeTextWidth(this.ctx, lastPart, style); - // At this step: "splitWord" is an array composed of parts of word whose - // length is at most equal to "width". - // Last part contains the end of the word. - // Note that: When word length is less than width, then lastPart is equal - // to word and splitWord is empty - if (splitWord.length) { - if (textLine !== "") { - brokenText.push(textLine); - textLine = ""; - availableWidth = width; - } - splitWord.forEach((wordPart) => { - brokenText.push(wordPart); - }); - textLine = lastPart; - availableWidth = width - lastPartWidth; - } - else { - // here "lastPart" is equal to "word" and the "word" size is smaller than "width" - const _word = textLine === "" ? lastPart : " " + lastPart; - const wordWidth = computeTextWidth(this.ctx, _word, style); - if (wordWidth <= availableWidth) { - textLine += _word; - availableWidth -= wordWidth; - } - else { - brokenText.push(textLine); - textLine = lastPart; - availableWidth = width - lastPartWidth; - } - } - } - if (textLine !== "") { - brokenText.push(textLine); - } - } - return brokenText; + return splitTextToWidth(this.ctx, text, style, width); } /** * Returns the size, start and end coordinates of a column on an unfolded sheet @@ -40281,26 +40479,6 @@ const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position })); return Math.max(0, ...sizes); } - splitWordToSpecificWidth(ctx, word, width, style) { - const wordWidth = computeTextWidth(ctx, word, style); - if (wordWidth <= width) { - return [word]; - } - const splitWord = []; - let wordPart = ""; - for (let l of word) { - const wordPartWidth = computeTextWidth(ctx, wordPart + l, style); - if (wordPartWidth > width) { - splitWord.push(wordPart); - wordPart = l; - } - else { - wordPart += l; - } - } - splitWord.push(wordPart); - return splitWord; - } /** * Check that any "sheetId" in the command matches an existing * sheet. @@ -41292,7 +41470,7 @@ .join("\n") || "\t"); } getHTMLContent() { - if (this.cells.length == 1 && this.cells[0].length == 1) { + if (this.cells.length === 1 && this.cells[0].length === 1) { return this.getters.getCellText(this.cells[0][0].position); } let htmlTable = ''; @@ -41617,7 +41795,7 @@ status = "invisible"; state; lastPasteState; - _isPaintingFormat = false; + paintFormatStatus = "inactive"; originSheetId; // --------------------------------------------------------------------------- // Command Handling @@ -41632,7 +41810,7 @@ if (!this.state) { return 24 /* CommandResult.EmptyClipboard */; } - const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "onlyFormat" : undefined); + const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined); return this.state.isPasteAllowed(cmd.target, { pasteOption }); case "PASTE_FROM_OS_CLIPBOARD": { const state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection); @@ -41648,6 +41826,11 @@ const state = this.getClipboardStateForCopyCells(cut, "CUT"); return state.isPasteAllowed(paste); } + case "ACTIVATE_PAINT_FORMAT": { + if (this.paintFormatStatus !== "inactive") { + return 95 /* CommandResult.AlreadyInPaintingFormatMode */; + } + } } return 0 /* CommandResult.Success */; } @@ -41664,11 +41847,13 @@ if (!this.state) { break; } - const pasteOption = cmd.pasteOption || (this._isPaintingFormat ? "onlyFormat" : undefined); - this._isPaintingFormat = false; + const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined); this.state.paste(cmd.target, { pasteOption, shouldPasteCF: true, selectTarget: true }); this.lastPasteState = this.state; - this.status = "invisible"; + if (this.paintFormatStatus === "oneOff") { + this.paintFormatStatus = "inactive"; + this.status = "invisible"; + } break; case "CLEAN_CLIPBOARD_HIGHLIGHT": this.status = "invisible"; @@ -41736,8 +41921,13 @@ case "ACTIVATE_PAINT_FORMAT": { const zones = this.getters.getSelectedZones(); this.state = this.getClipboardStateForCopyCells(zones, "COPY"); - this._isPaintingFormat = true; this.status = "visible"; + if (cmd.persistent) { + this.paintFormatStatus = "persistent"; + } + else { + this.paintFormatStatus = "oneOff"; + } break; } case "DELETE_SHEET": @@ -41749,6 +41939,11 @@ this.status = "invisible"; } break; + case "CANCEL_PAINT_FORMAT": { + this.paintFormatStatus = "inactive"; + this.status = "invisible"; + break; + } default: if (isCoreCommand(cmd)) { this.status = "invisible"; @@ -41779,7 +41974,7 @@ return this.state ? this.state.operation === "CUT" : false; } isPaintingFormat() { - return this._isPaintingFormat; + return this.paintFormatStatus !== "inactive"; } // --------------------------------------------------------------------------- // Private methods @@ -44414,6 +44609,7 @@ .add("evaluation", EvaluationPlugin) .add("evaluation_chart", EvaluationChartPlugin) .add("evaluation_cf", EvaluationConditionalFormatPlugin) + .add("row_size", UIRowSizePlugin) .add("custom_colors", CustomColorsPlugin); const clickableCellRegistry = new Registry(); @@ -45536,6 +45732,11 @@ .o-sidePanelButton:enabled { cursor: pointer; } + + .o-sidePanelButton:disabled { + color: ${DISABLED_TEXT_COLOR}; + } + .o-sidePanelButton:last-child { margin-right: 0px; } @@ -45669,11 +45870,11 @@ } get title() { const name = this.actionButton.name(this.env); - const description = this.actionButton.description; + const description = this.actionButton.description(this.env); return name + (description ? ` (${description})` : ""); } get iconTitle() { - return this.actionButton.icon; + return this.actionButton.icon(this.env); } onClick(ev) { if (this.isEnabled) { @@ -46058,6 +46259,27 @@ class: String, }; + class PaintFormatButton extends owl.Component { + static template = "o-spreadsheet-PaintFormatButton"; + get isActive() { + return this.env.model.getters.isPaintingFormat(); + } + onDblClick() { + this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: true }); + } + togglePaintFormat() { + if (this.isActive) { + this.env.model.dispatch("CANCEL_PAINT_FORMAT"); + } + else { + this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: false }); + } + } + } + PaintFormatButton.props = { + class: { type: String, optional: true }, + }; + // ----------------------------------------------------------------------------- // TopBar // ----------------------------------------------------------------------------- @@ -46158,6 +46380,7 @@ TopBarComposer, FontSizeEditor, ActionButton, + PaintFormatButton, BorderEditorWidget, }; state = owl.useState({ @@ -48010,7 +48233,7 @@ : "nextCluster"; while (true) { const nextCellPosition = this.getNextCellPosition(currentPosition, dim, dir); - // Break if nextPosition == currentPosition, which happens if there's no next valid position + // Break if nextPosition === currentPosition, which happens if there's no next valid position if (currentPosition.col === nextCellPosition.col && currentPosition.row === nextCellPosition.row) { break; @@ -49924,6 +50147,7 @@ ...config, mode: config.mode || "normal", custom: config.custom || {}, + defaultCurrencyFormat: config.defaultCurrencyFormat || "[$$]#,##0.00", external: this.setupExternalConfig(config.external || {}), transportService, client, @@ -50221,6 +50445,7 @@ numberToLetters, UuidGenerator, formatValue, + createCurrencyFormat, computeTextWidth, createEmptyWorkbookData, createEmptySheet, @@ -50306,9 +50531,9 @@ Object.defineProperty(exports, '__esModule', { value: true }); - __info__.version = '16.4.0-alpha.7'; - __info__.date = '2023-07-04T14:12:23.240Z'; - __info__.hash = 'b04cf78'; + __info__.version = '16.5.0-alpha.1'; + __info__.date = '2023-07-26T13:08:47.460Z'; + __info__.hash = '9160731'; })(this.o_spreadsheet = this.o_spreadsheet || {}, owl); diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml index 76fd232ba4a..5ebafb39901 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml @@ -599,8 +599,8 @@ t-on-contextmenu.prevent="" t-att-style="getCellClickableStyle(clickableCell.coordinates)" /> + -
@@ -1007,7 +1007,8 @@
@@ -2213,10 +2214,11 @@
+
+ + + + + + + +
-
+
Search
@@ -3796,7 +3808,7 @@
- +
@@ -3937,7 +3949,7 @@