From 65d2bb7618fbbae13de688fc0c713d0dfe5645f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Fri, 12 May 2023 11:53:12 +0000 Subject: [PATCH] [IMP] spreadsheet: update o_spreadsheet to latest version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/odoo/o-spreadsheet/commit/afee7d57 [REL] 16.3.0-alpha.8 https://github.com/odoo/o-spreadsheet/commit/3f5b6c41 [FIX] topbar: menu items are now sorted by sequence https://github.com/odoo/o-spreadsheet/commit/88edc8b0 [IMP] core: add timestamp to remote revisions https://github.com/odoo/o-spreadsheet/commit/289044a3 [FIX] sidepanel: switching from on sidepanel to another should trigger onCloseSidePanel https://github.com/odoo/o-spreadsheet/commit/5848dcb7 [FIX] composer: fix placeholder scrollbar Task: 3258920 https://github.com/odoo/o-spreadsheet/commit/378bdd89 [FIX] menu items: text overflow not working https://github.com/odoo/o-spreadsheet/commit/c7524c5a [FIX] evaluation: compute format from empty cell https://github.com/odoo/o-spreadsheet/commit/bca6ad64 [IMP] TopBar: Add missing icons an gray disabled icons https://github.com/odoo/o-spreadsheet/commit/e672e183 [FIX] menus: mark automatic format as active https://github.com/odoo/o-spreadsheet/commit/fac211e1 [FIX] SelectionInput: Fix cross symbol https://github.com/odoo/o-spreadsheet/commit/53ce0f02 [FIX] SelectionInputPlugin: prevent multiple range in 'singleRange' input Task: 3237798 https://github.com/odoo/o-spreadsheet/commit/4aa957bd [FIX] topbar: fix borderEditor props https://github.com/odoo/o-spreadsheet/commit/803bc1a8 [FIX] topbar: active item color https://github.com/odoo/o-spreadsheet/commit/f6705fa4 [FIX] ContentEditableHelper: fix getCurrentSelection for Firefox Task: 3071026 https://github.com/odoo/o-spreadsheet/commit/61766daa [REF] css: move hoverable-button to spreadsheet https://github.com/odoo/o-spreadsheet/commit/34fd1982 [IMP] borders: add support for different line styles Task: 2714331 https://github.com/odoo/o-spreadsheet/commit/5539367a [FIX] lookup: horizontal search in LOOKUP function Task: 3277145 https://github.com/odoo/o-spreadsheet/commit/a7d7c0e5 [IMP] menu: disable insertion via menus when separate cols/rows selected https://github.com/odoo/o-spreadsheet/commit/605ae100 [REF] selective_history: name constructor arguments Task: 3301044 https://github.com/odoo/o-spreadsheet/commit/23ec1c47 [FIX] collaborative: ignore bad snapshot revision https://github.com/odoo/o-spreadsheet/commit/ed9ee1f3 [IMP] model: dispatch initial revision to core only https://github.com/odoo/o-spreadsheet/commit/8dbd4801 [FIX] chart: no dataset with single cell closes odoo/odoo#121273 Related: odoo/enterprise#41001 Signed-off-by: Rémi Rahir (rar) Co-authored-by: anhe-odoo --- .../static/src/helpers/constants.js | 6 +- .../src/list/plugins/list_core_plugin.js | 23 +- .../static/src/o_spreadsheet/o_spreadsheet.js | 900 +++++++++++++----- .../src/o_spreadsheet/o_spreadsheet.xml | 359 +++++-- .../src/pivot/plugins/pivot_core_plugin.js | 7 +- .../static/tests/lists/list_plugin_test.js | 8 +- .../tests/pivots/model/pivot_plugin_test.js | 8 +- 7 files changed, 967 insertions(+), 344 deletions(-) diff --git a/addons/spreadsheet/static/src/helpers/constants.js b/addons/spreadsheet/static/src/helpers/constants.js index 30d2daeaeb7..1ea8d21c877 100644 --- a/addons/spreadsheet/static/src/helpers/constants.js +++ b/addons/spreadsheet/static/src/helpers/constants.js @@ -12,9 +12,9 @@ export const FORMATS = { year: { out: "YYYY", display: "YYYY", interval: "y" }, }; -export const HEADER_STYLE = { fillColor: "#f2f2f2" }; -export const TOP_LEVEL_STYLE = { bold: true, fillColor: "#f2f2f2" }; -export const MEASURE_STYLE = { fillColor: "#f2f2f2", textColor: "#756f6f" }; +export const HEADER_STYLE = { fillColor: "#E6F2F3" }; +export const TOP_LEVEL_STYLE = { bold: true, fillColor: "#E6F2F3" }; +export const MEASURE_STYLE = { fillColor: "#E6F2F3", textColor: "#756f6f" }; export const UNTITLED_SPREADSHEET_NAME = _lt("Untitled spreadsheet"); diff --git a/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js b/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js index 812fd6216a2..e423961d69f 100644 --- a/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js +++ b/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js @@ -288,7 +288,21 @@ export default class ListCorePlugin extends CorePlugin { right: anchor[0] + columns.length - 1, }, ], - border: "external", + }); + this.dispatch("SET_ZONE_BORDERS", { + sheetId, + target: [ + { + top: anchor[1], + bottom: anchor[1], + left: anchor[0], + right: anchor[0] + columns.length - 1, + }, + ], + border: { + position: "external", + color: "#2D7E84", + }, }); } @@ -308,7 +322,7 @@ export default class ListCorePlugin extends CorePlugin { } row++; } - this.dispatch("SET_FORMATTING", { + this.dispatch("SET_ZONE_BORDERS", { sheetId, target: [ { @@ -318,7 +332,10 @@ export default class ListCorePlugin extends CorePlugin { right: anchor[0] + columns.length - 1, }, ], - border: "external", + border: { + position: "external", + color: "#2D7E84", + }, }); } diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js index 83c08258a3e..6a06a788ab5 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js @@ -237,8 +237,8 @@ const DEFAULT_FONT = "'Roboto', arial"; const DEFAULT_VERTICAL_ALIGN = "bottom"; // Borders - const DEFAULT_BORDER_DESC = ["thin", "#000"]; - const DEFAULT_FILTER_BORDER_DESC = ["thin", FILTERS_COLOR]; + const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" }; + const DEFAULT_FILTER_BORDER_DESC = { style: "thin", color: FILTERS_COLOR }; // Ranges const INCORRECT_RANGE_STRING = CellErrorType.InvalidReference; // Max Number of history steps kept in memory @@ -1103,6 +1103,18 @@ text = text.replace(newLineRegex, NEWLINE); return text; } + /** + * Determine if the numbers are consecutive. + */ + function isConsecutive(iterable) { + const array = Array.from(iterable).sort((a, b) => a - b); // sort numerically rather than lexicographically + for (let i = 1; i < array.length; i++) { + if (array[i] - array[i - 1] !== 1) { + return false; + } + } + return true; + } const RBA_REGEX = /rgba?\(|\s+|\)/gi; const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/; @@ -4244,6 +4256,7 @@ "SET_FORMATTING", "CLEAR_FORMATTING", "SET_BORDER", + "SET_ZONE_BORDERS", /** CHART */ "CREATE_CHART", "UPDATE_CHART", @@ -4386,6 +4399,7 @@ CommandResult[CommandResult["NoSplitSeparatorInSelection"] = 91] = "NoSplitSeparatorInSelection"; })(exports.CommandResult || (exports.CommandResult = {})); + const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"]; var DIRECTION; (function (DIRECTION) { DIRECTION["UP"] = "up"; @@ -5479,12 +5493,6 @@ cursor: pointer; user-select: none; - .o-menu-item-name { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - &.o-menu-root { display: flex; justify-content: space-between; @@ -5495,10 +5503,6 @@ margin: 0px 8px 0px 0px; width: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px; line-height: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px; - - .o-icon { - color: ${ICONS_COLOR}; - } } .o-menu-item-root { width: 10px; @@ -5512,6 +5516,11 @@ .o-menu-item-description { color: grey; } + .o-menu-item-icon { + .o-icon { + color: ${ICONS_COLOR}; + } + } } &.disabled { color: ${MENU_ITEM_DISABLED_COLOR}; @@ -6328,8 +6337,14 @@ : undefined)); } } + else if (zone.left === zone.right && zone.top === zone.bottom) { + // A single cell. If it's only the title, the dataset is not added. + if (!dataSetsHaveTitle) { + dataSets.push(createDataSet(getters, dataSetSheetId, zone, undefined)); + } + } else { - /* 1 cell, 1 row or 1 column */ + /* 1 row or 1 column */ dataSets.push(createDataSet(getters, dataSetSheetId, zone, dataSetsHaveTitle ? { top: zone.top, @@ -8940,7 +8955,7 @@ handlePasteResult(env, result); }; const MENU_INSERT_ROWS_NAME = (env) => { - const number = getColumnsNumber(env); + const number = getRowsNumber(env); return number === 1 ? _lt("Insert row") : _lt("Insert %s rows", number.toString()); }; const MENU_INSERT_ROWS_BEFORE_NAME = (env) => { @@ -15305,7 +15320,11 @@ : getNormalizedValueFromRowRange; const rangeLength = verticalSearch ? nbRow : nbCol; const index = dichotomicSearch(searchArray, _searchKey, "nextSmaller", "asc", rangeLength, getElement); - assertAvailable(searchArray[0][index], searchKey); + if (index === -1) + assertAvailable(undefined, searchKey); + verticalSearch + ? assertAvailable(searchArray[0][index], searchKey) + : assertAvailable(searchArray[index][nbRow - 1], searchKey); if (resultRange === undefined) { return (verticalSearch ? searchArray[nbCol - 1][index] : searchArray[index][nbRow - 1]); } @@ -16212,75 +16231,97 @@ const insertRow = { name: MENU_INSERT_ROWS_NAME, - isVisible: IS_ONLY_ONE_RANGE, + isVisible: (env) => isConsecutive(env.model.getters.getActiveRows()) && + IS_ONLY_ONE_RANGE(env) && + env.model.getters.getActiveCols().size === 0, icon: "o-spreadsheet-Icon.INSERT_ROW", }; const rowInsertRowBefore = { name: ROW_INSERT_ROWS_BEFORE_NAME, execute: INSERT_ROWS_BEFORE_ACTION, + isVisible: (env) => isConsecutive(env.model.getters.getActiveRows()) && + IS_ONLY_ONE_RANGE(env) && + env.model.getters.getActiveCols().size === 0, + icon: "o-spreadsheet-Icon.INSERT_ROW_BEFORE", }; const topBarInsertRowsBefore = { ...rowInsertRowBefore, name: MENU_INSERT_ROWS_BEFORE_NAME, - isVisible: (env) => env.model.getters.getActiveCols().size === 0, }; const cellInsertRowsBefore = { ...rowInsertRowBefore, name: CELL_INSERT_ROWS_BEFORE_NAME, isVisible: IS_ONLY_ONE_RANGE, - icon: "o-spreadsheet-Icon.INSERT_ROW", + icon: "o-spreadsheet-Icon.INSERT_ROW_BEFORE", }; const rowInsertRowsAfter = { execute: INSERT_ROWS_AFTER_ACTION, name: ROW_INSERT_ROWS_AFTER_NAME, + isVisible: (env) => isConsecutive(env.model.getters.getActiveRows()) && + IS_ONLY_ONE_RANGE(env) && + env.model.getters.getActiveCols().size === 0, + icon: "o-spreadsheet-Icon.INSERT_ROW_AFTER", }; const topBarInsertRowsAfter = { ...rowInsertRowsAfter, name: MENU_INSERT_ROWS_AFTER_NAME, - isVisible: (env) => env.model.getters.getActiveCols().size === 0, }; const insertCol = { name: MENU_INSERT_COLUMNS_NAME, - isVisible: IS_ONLY_ONE_RANGE, + isVisible: (env) => isConsecutive(env.model.getters.getActiveCols()) && + IS_ONLY_ONE_RANGE(env) && + env.model.getters.getActiveRows().size === 0, icon: "o-spreadsheet-Icon.INSERT_COL", }; const colInsertColsBefore = { name: COLUMN_INSERT_COLUMNS_BEFORE_NAME, execute: INSERT_COLUMNS_BEFORE_ACTION, + isVisible: (env) => isConsecutive(env.model.getters.getActiveCols()) && + IS_ONLY_ONE_RANGE(env) && + env.model.getters.getActiveRows().size === 0, + icon: "o-spreadsheet-Icon.INSERT_COL_BEFORE", }; const topBarInsertColsBefore = { ...colInsertColsBefore, name: MENU_INSERT_COLUMNS_BEFORE_NAME, - isVisible: (env) => env.model.getters.getActiveRows().size === 0, }; const cellInsertColsBefore = { ...colInsertColsBefore, name: CELL_INSERT_COLUMNS_BEFORE_NAME, isVisible: IS_ONLY_ONE_RANGE, - icon: "o-spreadsheet-Icon.INSERT_COL", + icon: "o-spreadsheet-Icon.INSERT_COL_BEFORE", }; const colInsertColsAfter = { name: COLUMN_INSERT_COLUMNS_AFTER_NAME, execute: INSERT_COLUMNS_AFTER_ACTION, + isVisible: (env) => isConsecutive(env.model.getters.getActiveCols()) && + IS_ONLY_ONE_RANGE(env) && + env.model.getters.getActiveRows().size === 0, + icon: "o-spreadsheet-Icon.INSERT_COL_AFTER", }; const topBarInsertColsAfter = { ...colInsertColsAfter, name: MENU_INSERT_COLUMNS_AFTER_NAME, execute: INSERT_COLUMNS_AFTER_ACTION, - isVisible: (env) => env.model.getters.getActiveRows().size === 0, }; const insertCell = { name: _lt("Insert cells"), - isVisible: IS_ONLY_ONE_RANGE, + isVisible: (env) => IS_ONLY_ONE_RANGE(env) && + env.model.getters.getActiveCols().size === 0 && + env.model.getters.getActiveRows().size === 0, icon: "o-spreadsheet-Icon.INSERT_CELL", }; const insertCellShiftDown = { name: _lt("Insert cells and shift down"), execute: INSERT_CELL_SHIFT_DOWN, + isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0, + icon: "o-spreadsheet-Icon.INSERT_CELL_SHIFT_DOWN", }; const insertCellShiftRight = { name: _lt("Insert cells and shift right"), execute: INSERT_CELL_SHIFT_RIGHT, + isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0, + icon: "o-spreadsheet-Icon.INSERT_CELL_SHIFT_RIGHT", }; const insertChart = { name: _lt("Chart"), @@ -16478,6 +16519,7 @@ sequence: 1, execute: (env) => env.openSidePanel("SplitToColumns", {}), isEnabled: (env) => env.model.getters.isSingleColSelected(), + icon: "o-spreadsheet-Icon.SPLIT_TEXT", }; const formatNumberAutomatic = { @@ -16693,60 +16735,6 @@ execute: FORMAT_CLEARFORMAT_ACTION, icon: "o-spreadsheet-Icon.CLEAR_FORMAT", }; - const borders = { - name: _lt("Borders"), - icon: "o-spreadsheet-Icon.BORDERS", - }; - const bordersAll = { - name: _lt("All borders"), - execute: (env) => setBorder(env, "all"), - icon: "o-spreadsheet-Icon.BORDERS", - }; - const bordersInner = { - name: _lt("Inner borders"), - execute: (env) => setBorder(env, "hv"), - icon: "o-spreadsheet-Icon.BORDER_HV", - }; - const bordersHorizontal = { - name: _lt("Horizontal borders"), - execute: (env) => setBorder(env, "h"), - icon: "o-spreadsheet-Icon.BORDER_H", - }; - const bordersVertical = { - name: _lt("Vertical borders"), - execute: (env) => setBorder(env, "v"), - icon: "o-spreadsheet-Icon.BORDER_V", - }; - const bordersExternal = { - name: _lt("External borders"), - execute: (env) => setBorder(env, "external"), - icon: "o-spreadsheet-Icon.BORDER_EXTERNAL", - }; - const bordersLeft = { - name: _lt("Left borders"), - execute: (env) => setBorder(env, "left"), - icon: "o-spreadsheet-Icon.BORDER_LEFT", - }; - const bordersTop = { - name: _lt("Top borders"), - execute: (env) => setBorder(env, "top"), - icon: "o-spreadsheet-Icon.BORDER_TOP", - }; - const bordersRight = { - name: _lt("Right borders"), - execute: (env) => setBorder(env, "right"), - icon: "o-spreadsheet-Icon.BORDER_RIGHT", - }; - const bordersBottom = { - name: _lt("Bottom borders"), - execute: (env) => setBorder(env, "bottom"), - icon: "o-spreadsheet-Icon.BORDER_BOTTOM", - }; - const bordersClear = { - name: _lt("Clear borders"), - execute: (env) => setBorder(env, "clear"), - icon: "o-spreadsheet-Icon.BORDER_CLEAR", - }; function fontSizeMenuBuilder() { return FONT_SIZES.map((fs) => { return { @@ -16759,24 +16747,17 @@ }); } function isAutomaticFormatSelected(env) { - const activeCell = env.model.getters.getActiveCell(); + const activeCell = env.model.getters.getCell(env.model.getters.getActivePosition()); return !activeCell || !activeCell.format; } function isFormatSelected(env, format) { - const activeCell = env.model.getters.getActiveCell(); - return activeCell && activeCell.format === format; + const activeCell = env.model.getters.getCell(env.model.getters.getActivePosition()); + return activeCell?.format === format; } function isFontSizeSelected(env, fontSize) { const currentFontSize = env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE; return currentFontSize === fontSize; } - function setBorder(env, command) { - env.model.dispatch("SET_FORMATTING", { - sheetId: env.model.getters.getActiveSheetId(), - target: env.model.getters.getSelectedZones(), - border: command, - }); - } function getHorizontalAlign(env) { const style = env.model.getters.getCurrentStyle(); if (style.align) { @@ -16823,18 +16804,7 @@ fillColor: fillColor, formatCF: formatCF, paintFormat: paintFormat, - clearFormat: clearFormat, - borders: borders, - bordersAll: bordersAll, - bordersInner: bordersInner, - bordersHorizontal: bordersHorizontal, - bordersVertical: bordersVertical, - bordersExternal: bordersExternal, - bordersLeft: bordersLeft, - bordersTop: bordersTop, - bordersRight: bordersRight, - bordersBottom: bordersBottom, - bordersClear: bordersClear + clearFormat: clearFormat }); function interactiveFreezeColumnsRows(env, dimension, base) { @@ -16850,6 +16820,7 @@ name: HIDE_COLUMNS_NAME, execute: HIDE_COLUMNS_ACTION, isVisible: NOT_ALL_VISIBLE_COLS_SELECTED, + icon: "o-spreadsheet-Icon.HIDE_COL", }; const unhideCols = { name: _lt("Unhide columns"), @@ -16871,6 +16842,7 @@ name: HIDE_ROWS_NAME, execute: HIDE_ROWS_ACTION, isVisible: NOT_ALL_VISIBLE_ROWS_SELECTED, + icon: "o-spreadsheet-Icon.HIDE_ROW", }; const unhideRows = { name: _lt("Unhide rows"), @@ -17074,19 +17046,21 @@ .add("delete_column", { ...deleteCols, sequence: 90, + icon: "o-spreadsheet-Icon.DELETE", }) .add("clear_column", { ...clearCols, sequence: 100, + icon: "o-spreadsheet-Icon.CLEAR", }) .add("hide_columns", { ...hideCols, - sequence: 85, + sequence: 105, separator: true, }) .add("unhide_columns", { ...unhideCols, - sequence: 86, + sequence: 106, separator: true, }) .add("conditional_formatting", { @@ -17184,10 +17158,12 @@ .add("delete_row", { ...deleteRows, sequence: 70, + icon: "o-spreadsheet-Icon.DELETE", }) .add("clear_row", { ...clearRows, sequence: 80, + icon: "o-spreadsheet-Icon.CLEAR", }) .add("hide_rows", { ...hideRows, @@ -17680,6 +17656,12 @@ height: 25px; } } + /** Make the character a bit bigger + compared to its neighbor INPUT box */ + .o-remove-selection { + font-weight: bold; + font-size: calc(100% + 4px); + } } `; /** @@ -17775,6 +17757,9 @@ this.confirm(); } } + extractRanges(value) { + return this.props.hasSingleRange ? value.split(",")[0] : value; + } focus(rangeId) { this.state.isMissing = false; this.state.mode = "select-range"; @@ -17793,10 +17778,11 @@ } onInputChanged(rangeId, ev) { const target = ev.target; + const value = this.extractRanges(target.value); this.env.model.dispatch("CHANGE_RANGE", { id: this.id, rangeId, - value: target.value, + value, }); this.triggerChange(); } @@ -20565,14 +20551,31 @@ * */ getCurrentSelection() { let { startElement, endElement, startSelectionOffset, endSelectionOffset } = this.getStartAndEndSelection(); - let startSizeBefore = this.findSizeBeforeElement(startElement); - let endSizeBefore = this.findSizeBeforeElement(endElement); + let startSizeBefore = this.findSelectionIndex(startElement, startSelectionOffset); + let endSizeBefore = this.findSelectionIndex(endElement, endSelectionOffset); return { - start: startSizeBefore + startSelectionOffset, - end: endSizeBefore + endSelectionOffset, + start: startSizeBefore, + end: endSizeBefore, }; } - findSizeBeforeElement(nodeToFind) { + /** + * Computes the text 'index' inside this.el based on the currently selected node and its offset. + * The selected node is either a Text node or an Element node. + * + * case 1 -Text node: + * the offset is the number of characters from the start of the node. We have to add this offset to the + * content length of all previous nodes. + * + * case 2 - Element node: + * the offset is the number of child nodes before the selected node. We have to add the content length of + * all the bnodes prior to the selected node as well as the content of the child node before the offset. + * + * See the MDN documentation for more details. + * https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset + * https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset + * + */ + findSelectionIndex(nodeToFind, nodeOffset) { let usedCharacters = 0; let it = iterateChildren(this.el); let current = it.next(); @@ -20599,6 +20602,28 @@ if (current.value !== nodeToFind) { throw new Error("Cannot find the node in the children of the element"); } + else { + if (!current.value.hasChildNodes()) { + usedCharacters += nodeOffset; + } + else { + const children = [...current.value.childNodes].slice(0, nodeOffset); + usedCharacters += children.reduce((acc, child, index) => { + if (child.textContent !== null) { + // need to account for paragraph nodes that implicitely add a new line + // except for the last paragraph + let chars = child.textContent.length; + if (child.nodeName === "P" && index !== children.length - 1) { + chars++; + } + return acc + chars; + } + else { + return acc; + } + }, 0); + } + } if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent == "") { usedCharacters++; } @@ -21399,13 +21424,13 @@ const CSS$1 = css /* scss */ ` .o-filter-icon { + color: ${FILTERS_COLOR}; position: absolute; display: flex; align-items: center; justify-content: center; width: ${FILTER_ICON_EDGE_LENGTH}px; height: ${FILTER_ICON_EDGE_LENGTH}px; - color: #4a4f59; } .o-filter-icon:hover { background: ${FILTERS_COLOR}; @@ -24136,17 +24161,17 @@ const BORDER_STYLE_CONVERSION_MAP = { dashDot: "thin", dashDotDot: "thin", - dashed: "thin", - dotted: "thin", + dashed: "dashed", + dotted: "dotted", double: "thin", hair: "thin", - medium: "thin", + medium: "medium", mediumDashDot: "thin", mediumDashDotDot: "thin", mediumDashed: "thin", none: undefined, slantDashDot: "thin", - thick: "thin", + thick: "thick", thin: "thin", }; /** Conversion map Horizontal Alignment in XLSX <=> Horizontal Alignment in o_spreadsheet*/ @@ -24645,7 +24670,7 @@ return undefined; addBorderDescrWarnings(borderDescr, warningManager); const style = BORDER_STYLE_CONVERSION_MAP[borderDescr.style]; - return style ? [style, convertColor(borderDescr.color)] : undefined; + return style ? { style, color: convertColor(borderDescr.color) } : undefined; } function convertStyles(data, warningManager) { const stylesArray = data.styles.map((style) => { @@ -26043,8 +26068,8 @@ return undefined; } return { - style: descr[0], - color: { rgb: descr[1] }, + style: descr.style, + color: { rgb: descr.color }, }; } function extractStyle(cell, data) { @@ -26622,7 +26647,7 @@ const TABLE_HIGHLIGHTED_CELL_STYLE = { bold: true, }; - const TABLE_BORDER_STYLE = ["thin", "#000000FF"]; + const TABLE_BORDER_STYLE = { style: "thin", color: "#000000FF" }; /** * Convert the imported XLSX tables. * @@ -28191,7 +28216,7 @@ * a breaking change is made in the way the state is handled, and an upgrade * function should be defined */ - const CURRENT_VERSION = 12; + const CURRENT_VERSION = 13; const INITIAL_SHEET_ID = "Sheet1"; /** * This function tries to load anything that could look like a valid @@ -28455,6 +28480,25 @@ return data; }, }, + { + description: "Change Border description structure", + from: 12, + to: 13, + applyMigration(data) { + for (const borderId in data.borders) { + const border = data.borders[borderId]; + for (const position in border) { + if (Array.isArray(border[position])) { + border[position] = { + style: border[position][0], + color: border[position][1], + }; + } + } + } + return data; + }, + }, ]; /** * This function is used to repair faulty data independently of the migration. @@ -28790,7 +28834,7 @@ * - borders */ class BordersPlugin extends CorePlugin { - static getters = ["getCellBorder"]; + static getters = ["getCellBorder", "getBordersColors"]; borders = {}; // --------------------------------------------------------------------------- // Command Handling @@ -28821,10 +28865,15 @@ case "SET_BORDER": this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border); break; - case "SET_FORMATTING": + case "SET_ZONE_BORDERS": if (cmd.border) { const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone)); - this.setBorders(cmd.sheetId, target, cmd.border); + this.setBorders(cmd.sheetId, target, cmd.border.position, cmd.border.color === "" + ? undefined + : { + style: cmd.border.style || DEFAULT_BORDER_DESC.style, + color: cmd.border.color || DEFAULT_BORDER_DESC.color, + }); } break; case "CLEAR_FORMATTING": @@ -28913,6 +28962,23 @@ } return border; } + getBordersColors(sheetId) { + const colors = []; + const sheetBorders = this.borders[sheetId]; + if (sheetBorders) { + for (const borders of sheetBorders.filter(isDefined$1)) { + for (const cellBorder of borders) { + if (cellBorder?.horizontal) { + colors.push(cellBorder.horizontal.color); + } + if (cellBorder?.vertical) { + colors.push(cellBorder.vertical.color); + } + } + } + } + return colors; + } // --------------------------------------------------------------------------- // Private // --------------------------------------------------------------------------- @@ -29124,43 +29190,43 @@ * Set the borders of a zone by computing the borders to add from the given * command */ - setBorders(sheetId, zones, command) { - if (command === "clear") { + setBorders(sheetId, zones, position, border) { + if (position === "clear") { return this.clearBorders(sheetId, zones); } for (let zone of zones) { - if (command === "h" || command === "hv" || command === "all") { + if (position === "h" || position === "hv" || position === "all") { for (let row = zone.top + 1; row <= zone.bottom; row++) { for (let col = zone.left; col <= zone.right; col++) { - this.addBorder(sheetId, col, row, { top: DEFAULT_BORDER_DESC }); + this.addBorder(sheetId, col, row, { top: border }); } } } - if (command === "v" || command === "hv" || command === "all") { + if (position === "v" || position === "hv" || position === "all") { for (let row = zone.top; row <= zone.bottom; row++) { for (let col = zone.left + 1; col <= zone.right; col++) { - this.addBorder(sheetId, col, row, { left: DEFAULT_BORDER_DESC }); + this.addBorder(sheetId, col, row, { left: border }); } } } - if (command === "left" || command === "all" || command === "external") { + if (position === "left" || position === "all" || position === "external") { for (let row = zone.top; row <= zone.bottom; row++) { - this.addBorder(sheetId, zone.left, row, { left: DEFAULT_BORDER_DESC }); + this.addBorder(sheetId, zone.left, row, { left: border }); } } - if (command === "right" || command === "all" || command === "external") { + if (position === "right" || position === "all" || position === "external") { for (let row = zone.top; row <= zone.bottom; row++) { - this.addBorder(sheetId, zone.right + 1, row, { left: DEFAULT_BORDER_DESC }); + this.addBorder(sheetId, zone.right + 1, row, { left: border }); } } - if (command === "top" || command === "all" || command === "external") { + if (position === "top" || position === "all" || position === "external") { for (let col = zone.left; col <= zone.right; col++) { - this.addBorder(sheetId, col, zone.top, { top: DEFAULT_BORDER_DESC }); + this.addBorder(sheetId, col, zone.top, { top: border }); } } - if (command === "bottom" || command === "all" || command === "external") { + if (position === "bottom" || position === "all" || position === "external") { for (let col = zone.left; col <= zone.right; col++) { - this.addBorder(sheetId, col, zone.bottom + 1, { top: DEFAULT_BORDER_DESC }); + this.addBorder(sheetId, col, zone.bottom + 1, { top: border }); } } } @@ -29174,16 +29240,22 @@ const bordersBottomRight = this.getCellBorder({ sheetId, col: right, row: bottom }); this.clearBorders(sheetId, [zone]); if (bordersTopLeft?.top) { - this.setBorders(sheetId, [{ ...zone, bottom: top }], "top"); + this.setBorders(sheetId, [{ ...zone, bottom: top }], "top", bordersTopLeft.top); } if (bordersTopLeft?.left) { - this.setBorders(sheetId, [{ ...zone, right: left }], "left"); + this.setBorders(sheetId, [{ ...zone, right: left }], "left", bordersTopLeft.left); } - if (bordersBottomRight?.bottom || bordersTopLeft?.bottom) { - this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom"); + if (bordersBottomRight?.bottom) { + this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersBottomRight.bottom); } - if (bordersBottomRight?.right || bordersTopLeft?.right) { - this.setBorders(sheetId, [{ ...zone, left: right }], "right"); + else if (bordersTopLeft?.bottom) { + this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersTopLeft.bottom); + } + if (bordersBottomRight?.right) { + this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersBottomRight.right); + } + else if (bordersTopLeft?.right) { + this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersTopLeft.right); } } // --------------------------------------------------------------------------- @@ -33036,6 +33108,9 @@ case "UPDATE_CHART": case "CREATE_CHART": case "ADD_CONDITIONAL_FORMAT": + case "SET_BORDER": + case "SET_ZONE_BORDERS": + case "SET_FORMATTING": this.shouldUpdateColors = true; } } @@ -33050,8 +33125,7 @@ getCustomColors() { let usedColors = []; for (const sheetId of this.getters.getSheetIds()) { - const cells = Object.values(this.getters.getCells(sheetId)); - usedColors = usedColors.concat(this.getColorsFromCells(cells), this.getFormattingColors(sheetId), this.getChartColors(sheetId)); + usedColors = usedColors.concat(this.getColorsFromCells(sheetId), this.getFormattingColors(sheetId), this.getChartColors(sheetId)); } return sortWithClusters([ ...new Set( @@ -33061,7 +33135,8 @@ [...new Set([...usedColors, ...this.customColors])].filter(isColorValid).map(toHex)), ]).filter((color) => !COLOR_PICKER_DEFAULTS.includes(color)); } - getColorsFromCells(cells) { + getColorsFromCells(sheetId) { + const cells = Object.values(this.getters.getCells(sheetId)); const colors = new Set(); for (const cell of cells) { if (cell.style?.textColor) { @@ -33071,6 +33146,9 @@ colors.add(cell.style.fillColor); } } + for (const color of this.getters.getBordersColors(sheetId)) { + colors.add(color); + } return [...colors]; } getFormattingColors(sheetId) { @@ -33344,7 +33422,7 @@ // magic "empty" value // Returning {value: null} instead of undefined will ensure that we don't // fall back on the default value of the argument provided to the formula's compute function - return { value: null }; + return { value: null, format: cell?.format }; } return getEvaluatedCell(cell); } @@ -36173,6 +36251,7 @@ class Revision { rootCommand; + timestamp; id; clientId; _commands = []; @@ -36186,8 +36265,9 @@ * - clientId: Client who initiated the action * - changes: List of changes applied on the state. */ - constructor(id, clientId, commands, rootCommand, changes) { + constructor(id, clientId, commands, rootCommand, changes, timestamp) { this.rootCommand = rootCommand; + this.timestamp = timestamp; this.id = id; this.clientId = clientId; this._commands = [...commands]; @@ -36259,7 +36339,7 @@ save(rootCommand, commands, changes) { if (!commands.length || !changes.length || !this.canApplyOptimisticUpdate()) return; - const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, rootCommand, changes); + const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, rootCommand, changes, Date.now()); this.revisions.append(revision.id, revision); this.trigger("new-local-state-update", { id: revision.id }); this.sendUpdateMessage({ @@ -36310,13 +36390,9 @@ } loadInitialMessages(messages) { this.isReplayingInitialRevisions = true; - this.on("unexpected-revision-id", this, ({ revisionId }) => { - throw new Error(`The spreadsheet could not be loaded. Revision ${revisionId} is corrupted.`); - }); for (const message of messages) { this.onMessageReceived(message); } - this.off("unexpected-revision-id", this); this.isReplayingInitialRevisions = false; } /** @@ -36402,6 +36478,10 @@ onMessageReceived(message) { if (this.isAlreadyProcessed(message)) return; + if (this.isWrongServerRevisionId(message)) { + this.trigger("unexpected-revision-id"); + return; + } switch (message.type) { case "CLIENT_MOVED": this.onClientMoved(message); @@ -36428,12 +36508,8 @@ }); break; case "REMOTE_REVISION": - if (message.serverRevisionId !== this.serverRevisionId) { - this.trigger("unexpected-revision-id", { revisionId: message.serverRevisionId }); - return; - } - const { clientId, commands } = message; - const revision = new Revision(message.nextRevisionId, clientId, commands, "REMOTE"); + const { clientId, commands, timestamp } = message; + const revision = new Revision(message.nextRevisionId, clientId, commands, "REMOTE", undefined, timestamp); if (revision.clientId !== this.clientId) { this.revisions.insert(revision.id, revision, message.serverRevisionId); const pendingCommands = this.pendingMessages @@ -36446,7 +36522,7 @@ } break; case "SNAPSHOT_CREATED": { - const revision = new Revision(message.nextRevisionId, "server", [], "SNAPSHOT"); + const revision = new Revision(message.nextRevisionId, "server", [], "SNAPSHOT", undefined, Date.now()); this.revisions.insert(revision.id, revision, message.serverRevisionId); this.dropPendingHistoryMessages(); this.trigger("snapshot"); @@ -36563,6 +36639,17 @@ return false; } } + isWrongServerRevisionId(message) { + switch (message.type) { + case "REMOTE_REVISION": + case "REVISION_REDONE": + case "REVISION_UNDONE": + case "SNAPSHOT_CREATED": + return message.serverRevisionId !== this.serverRevisionId; + default: + return false; + } + } dropPendingHistoryMessages() { this.waitingUndoRedoAck = false; this.pendingMessages = this.pendingMessages.filter(({ type }) => type !== "REVISION_REDONE" && type !== "REVISION_UNDONE"); @@ -37278,7 +37365,7 @@ } } drawBorders(renderingContext) { - const { ctx, thinLineWidth } = renderingContext; + const { ctx } = renderingContext; for (let box of this.boxes) { const border = box.border; if (border) { @@ -37297,13 +37384,66 @@ } } } - function drawBorder([style, color], x1, y1, x2, y2) { + /** + * Following https://usefulangle.com/post/17/html5-canvas-drawing-1px-crisp-straight-lines, + * we need to make sure that a "single" pixel line is drawn on a "half" pixel coordinate, + * while a "double" pixel line is drawn on a "full" pixel coordinate. As, in the rendering + * process, we always had 0.5 before rendering line (to make sure it is drawn on a "half" + * pixel), we need to correct this behavior for the "medium" and the "dotted" styles, as + * they are drawing a two pixels width line. + * We also adapt here the coordinates of the line to make sure corner are correctly drawn, + * avoiding a "round corners" effect. This is done by subtracting 1 pixel to the origin of + * each line and adding 1 pixel to the end of each line (depending on the direction of the + * line). + */ + function drawBorder({ style, color }, x1, y1, x2, y2) { ctx.strokeStyle = color; - ctx.lineWidth = (style === "thin" ? 2 : 3) * thinLineWidth; + switch (style) { + case "medium": + ctx.lineWidth = 2; + x1 += y1 === y2 ? -0.5 : 0.5; + x2 += y1 === y2 ? 1.5 : 0.5; + y1 += x1 === x2 ? -0.5 : 0.5; + y2 += x1 === x2 ? 1.5 : 0.5; + break; + case "thick": + ctx.lineWidth = 3; + if (y1 === y2) { + x1--; + x2++; + } + if (x1 === x2) { + y1--; + y2++; + } + break; + case "dashed": + ctx.lineWidth = 1; + ctx.setLineDash([1, 3]); + break; + case "dotted": + ctx.lineWidth = 1; + if (y1 === y2) { + x1 += 0.5; + x2 += 0.5; + } + if (x1 === x2) { + y1 += 0.5; + y2 += 0.5; + } + ctx.setLineDash([1, 1]); + break; + case "thin": + default: + ctx.lineWidth = 1; + break; + } ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); + ctx.lineWidth = 1; + ctx.setLineDash([]); } } drawTexts(renderingContext) { @@ -37781,6 +37921,9 @@ activeSheet; willAddNewRange = false; constructor(config, initialRanges, inputHasSingleRange) { + if (inputHasSingleRange && initialRanges.length > 1) { + throw new Error("Input with a single range cannot be instantiated with several range references."); + } super(config); this.inputHasSingleRange = inputHasSingleRange; this.insertNewRange(0, initialRanges); @@ -37800,6 +37943,11 @@ return 30 /* CommandResult.MaximumRangesReached */; } break; + case "CHANGE_RANGE": + if (this.inputHasSingleRange && cmd.value.split(",").length > 1) { + return 30 /* CommandResult.MaximumRangesReached */; + } + break; } return 0 /* CommandResult.Success */; } @@ -41286,7 +41434,8 @@ }, zone: selectedZone, }; - this.setSelectionMixin(anchor, [selectedZone]); + const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, "left", [...cmd.elements])); + this.setSelectionMixin(anchor, selections); } onRowsRemoved(cmd) { const { cell, zone } = this.gridSelection.anchor; @@ -41300,13 +41449,18 @@ }, zone: selectedZone, }; - this.setSelectionMixin(anchor, [selectedZone]); + const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, "top", [...cmd.elements])); + this.setSelectionMixin(anchor, selections); } onAddElements(cmd) { - const selection = this.gridSelection.anchor.zone; - const zone = updateSelectionOnInsertion(selection, cmd.dimension === "COL" ? "left" : "top", cmd.base, cmd.position, cmd.quantity); - const anchor = { cell: { col: zone.left, row: zone.top }, zone }; - this.setSelectionMixin(anchor, [zone]); + const start = cmd.dimension === "COL" ? "left" : "top"; + const anchorZone = updateSelectionOnInsertion(this.gridSelection.anchor.zone, start, cmd.base, cmd.position, cmd.quantity); + const selection = this.gridSelection.zones.map((zone) => updateSelectionOnInsertion(zone, start, cmd.base, cmd.position, cmd.quantity)); + const anchor = { + cell: { col: anchorZone.left, row: anchorZone.top }, + zone: anchorZone, + }; + this.setSelectionMixin(anchor, selection); } onMoveElements(cmd) { const thickness = cmd.elements.length; @@ -42552,7 +42706,6 @@ color: dimgrey; } .o-sidePanelClose { - font-size: 1.5rem; padding: 5px 10px; cursor: pointer; &:hover { @@ -42737,7 +42890,6 @@ justify-content: center; align-items: center; margin: 2px; - padding: 0px 3px; border-radius: 2px; min-width: 20px; } @@ -42794,7 +42946,228 @@ onClick: { type: Function, optional: true }, }; + /** + * List the available borders positions and the corresponding icons. + * The structure of this array is defined to match the order/lines we want + * to display in the topbar's border tool. + */ + const BORDER_POSITIONS = [ + [ + ["all", "o-spreadsheet-Icon.BORDERS"], + ["hv", "o-spreadsheet-Icon.BORDER_HV"], + ["h", "o-spreadsheet-Icon.BORDER_H"], + ["v", "o-spreadsheet-Icon.BORDER_V"], + ["external", "o-spreadsheet-Icon.BORDER_EXTERNAL"], + ], + [ + ["left", "o-spreadsheet-Icon.BORDER_LEFT"], + ["top", "o-spreadsheet-Icon.BORDER_TOP"], + ["right", "o-spreadsheet-Icon.BORDER_RIGHT"], + ["bottom", "o-spreadsheet-Icon.BORDER_BOTTOM"], + ["clear", "o-spreadsheet-Icon.BORDER_CLEAR"], + ], + ]; + // ----------------------------------------------------------------------------- + // Border Editor + // ----------------------------------------------------------------------------- + css /* scss */ ` + .o-border-selector { + padding: 4px; + } + .o-divider { + border-right: 1px solid #e0e2e4; + margin: 0 6px; + } + .o-border-selector-section { + .o-dropdown-line { + height: 30px; + margin: 1px; + .o-line-item { + padding: 4px; + width: 18px; + height: 18px; + } + } + .o-border-style-tool { + padding: 0px 3px; + margin: 2px; + height: 25px; + } + } + .o-style-preview { + margin: 7px 5px 7px 5px; + width: 60px; + height: 5px; + } + .o-style-thin { + border-bottom: 1px solid #000000; + } + .o-style-medium { + border-bottom: 2px solid #000000; + } + .o-style-thick { + border-bottom: 3px solid #000000; + } + .o-style-dashed { + border-bottom: 1px dashed #000000; + } + .o-style-dotted { + border-bottom: 1px dotted #000000; + } + .o-dropdown-border-type { + &:not(.o-disabled):not(.active):hover { + background-color: ${BG_HOVER_COLOR}; + } + } + .o-dropdown-border-check { + width: 20px; + font-size: 12px; + } + .o-border-style-dropdown { + background: #ffffff; + padding: 4px; + .o-dropdown-line { + .o-line-item.active { + background-color: rgba(0, 0, 0, 0.2); + } + } + } + .o-border-picker-button { + padding: 0px !important; + margin: 5px 0px 0px 0px !important; + height: 25px !important; + } +`; + class BorderEditor extends owl.Component { + static template = "o-spreadsheet-BorderEditor"; + static components = { ColorPickerWidget, Popover }; + BORDER_POSITIONS = BORDER_POSITIONS; + lineStyleButtonRef = owl.useRef("lineStyleButton"); + borderStyles = borderStyles; + state = owl.useState({ + activeTool: undefined, + }); + toggleDropdownTool(tool) { + const isOpen = this.state.activeTool === tool; + this.state.activeTool = isOpen ? undefined : tool; + } + closeDropdown() { + this.state.activeTool = undefined; + } + setBorderPosition(position) { + this.props.onBorderPositionPicked(position); + this.closeDropdown(); + } + setBorderColor(color) { + this.props.onBorderColorPicked(color); + this.closeDropdown(); + } + setBorderStyle(style) { + this.props.onBorderStylePicked(style); + this.closeDropdown(); + } + get lineStylePickerPopoverProps() { + return { + anchorRect: this.lineStylePickerAnchorRect, + positioning: "BottomLeft", + verticalOffset: 0, + }; + } + get popoverProps() { + return { + anchorRect: this.props.anchorRect, + maxHeight: this.props.maxHeight, + positioning: "BottomLeft", + verticalOffset: 0, + }; + } + get lineStylePickerAnchorRect() { + const button = this.lineStyleButtonRef.el; + if (button === null) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + const buttonRect = button.getBoundingClientRect(); + return { + x: buttonRect.x, + y: buttonRect.y, + width: buttonRect.width, + height: buttonRect.height, + }; + } + } + BorderEditor.props = { + class: { type: String, optional: true }, + currentBorderColor: { type: String, optional: false }, + currentBorderStyle: { type: String, optional: false }, + currentBorderPosition: { type: String, optional: true }, + onBorderColorPicked: Function, + onBorderStylePicked: Function, + onBorderPositionPicked: Function, + maxHeight: { type: Number, optional: true }, + anchorRect: Object, + }; + + class BorderEditorWidget extends owl.Component { + static template = "o-spreadsheet-BorderEditorWidget"; + static components = { BorderEditor }; + borderEditorButtonRef = owl.useRef("borderEditorButton"); + state = owl.useState({ + currentColor: DEFAULT_BORDER_DESC.color, + currentStyle: DEFAULT_BORDER_DESC.style, + currentPosition: undefined, + }); + get borderEditorAnchorRect() { + const button = this.borderEditorButtonRef.el; + const buttonRect = button.getBoundingClientRect(); + return { + x: buttonRect.x, + y: buttonRect.y, + width: buttonRect.width, + height: buttonRect.height, + }; + } + onBorderPositionPicked(position) { + this.state.currentPosition = position; + this.updateBorder(); + } + onBorderColorPicked(color) { + this.state.currentColor = color; + this.updateBorder(); + } + onBorderStylePicked(style) { + this.state.currentStyle = style; + this.updateBorder(); + } + updateBorder() { + if (this.state.currentPosition === undefined) { + return; + } + this.env.model.dispatch("SET_ZONE_BORDERS", { + sheetId: this.env.model.getters.getActiveSheetId(), + target: this.env.model.getters.getSelectedZones(), + border: { + position: this.state.currentPosition, + color: this.state.currentColor, + style: this.state.currentStyle, + }, + }); + } + } + BorderEditorWidget.props = { + toggleBorderEditor: Function, + showBorderEditor: Boolean, + disabled: { type: Boolean, optional: true }, + dropdownMaxHeight: { type: Number, optional: true }, + class: { type: String, optional: true }, + }; + const COMPOSER_MAX_HEIGHT = 100; + /* svg free of use from https://uxwing.com/formula-fx-icon/ */ + const FX_SVG = /*xml*/ ` + + + +`; css /* scss */ ` .o-topbar-composer { height: fit-content; @@ -42802,9 +43175,10 @@ border: 1px solid; z-index: ${ComponentsImportance.TopBarComposer}; - .o-composer:empty:not(:focus)::before { - /* svg free of use from https://uxwing.com/formula-fx-icon/ */ - content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -10 121.8 142.9' width='24' height='24' focusable='false'%3E%3Cpath d='m28 34-4 5v2h10l-6 40c-4 22-6 28-7 30-2 2-3 3-5 3-3 0-7-2-9-4H4c-2 2-4 4-4 7s4 6 8 6 9-2 15-8c8-7 13-17 18-39l7-35 13-1 3-6H49c4-23 7-27 11-27 2 0 5 2 8 6h4c1-1 4-4 4-7 0-2-3-6-9-6-5 0-13 4-20 10-6 7-9 14-11 24h-8zm41 16c4-5 7-7 8-7s2 1 5 9l3 12c-7 11-12 17-16 17l-3-1-2-1c-3 0-6 3-6 7s3 7 7 7c6 0 12-6 22-23l3 10c3 9 6 13 10 13 5 0 11-4 18-15l-3-4c-4 6-7 8-8 8-2 0-4-3-6-10l-5-15 8-10 6-4 3 1 3 2c2 0 6-3 6-7s-2-7-6-7c-6 0-11 5-21 20l-2-6c-3-9-5-14-9-14-5 0-12 6-18 15l3 3z' fill='%23BDBDBD' /%3E%3C/svg%3E"); + .o-composer:empty:not(:focus):not(.active)::before { + content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}"); + position: relative; + top: 20%; } } `; @@ -42927,11 +43301,6 @@ dropdownStyle: String, }; - // If we ever change these colors, make sure the filter tool stays green to match the icon in the grid - const ACTIVE_BG_COLOR = BACKGROUND_HEADER_FILTER_COLOR; - const ACTIVE_FONT_COLOR = FILTERS_COLOR; - const HOVERED_BG_COLOR = BG_HOVER_COLOR; - const HOVERED_FONT_COLOR = "#000"; // ----------------------------------------------------------------------------- // TopBar // ----------------------------------------------------------------------------- @@ -42941,27 +43310,6 @@ font-size: 13px; font-weight: 500; - .o-topbar-hoverable { - cursor: pointer; - .o-icon { - color: ${ICONS_COLOR}; - } - &:not(.o-disabled):not(.active):hover { - background-color: ${HOVERED_BG_COLOR}; - color: ${HOVERED_FONT_COLOR}; - .o-icon { - color: ${HOVERED_FONT_COLOR}; - } - } - &.active { - background-color: ${ACTIVE_BG_COLOR}; - color: ${ACTIVE_FONT_COLOR}; - .o-icon { - color: ${ACTIVE_FONT_COLOR}; - } - } - } - .o-topbar-top { border-bottom: 1px solid ${SEPARATOR_COLOR}; padding: 2px 10px; @@ -42971,6 +43319,11 @@ .o-topbar-menu { padding: 4px 6px; margin: 0 2px; + + &.active { + background-color: ${BG_HOVER_COLOR}; + color: #000; + } } } } @@ -43048,6 +43401,7 @@ TopBarComposer, FontSizeEditor, ActionButton, + BorderEditorWidget, }; state = owl.useState({ menuState: { isOpen: false, position: null, menuItems: [] }, @@ -43123,7 +43477,9 @@ this.state.activeTool = ""; this.state.menuState.isOpen = true; this.state.menuState.position = { x: left, y: top + height }; - this.state.menuState.menuItems = menu.children(this.env); + this.state.menuState.menuItems = menu + .children(this.env) + .sort((a, b) => a.sequence - b.sequence); this.state.menuState.parentMenu = menu; this.isSelectingMenu = true; this.openedEl = ev.target; @@ -43209,6 +43565,11 @@ } } + // If we ever change these colors, make sure the filter tool stays green to match the icon in the grid + const ACTIVE_BG_COLOR = BACKGROUND_HEADER_FILTER_COLOR; + const ACTIVE_FONT_COLOR = FILTERS_COLOR; + const HOVERED_BG_COLOR = BG_HOVER_COLOR; + const HOVERED_FONT_COLOR = "#000"; css /* scss */ ` .o-spreadsheet { position: relative; @@ -43241,6 +43602,27 @@ margin-top: ${MENU_SEPARATOR_PADDING}px; margin-bottom: ${MENU_SEPARATOR_PADDING}px; } + .o-hoverable-button { + border-radius: 2px; + cursor: pointer; + .o-icon { + color: ${ICONS_COLOR}; + } + &:not(.o-disabled):not(.active):hover { + background-color: ${HOVERED_BG_COLOR}; + color: ${HOVERED_FONT_COLOR}; + .o-icon { + color: ${HOVERED_FONT_COLOR}; + } + } + &.active { + background-color: ${ACTIVE_BG_COLOR}; + color: ${ACTIVE_FONT_COLOR}; + .o-icon { + color: ${ACTIVE_FONT_COLOR}; + } + } + } } .o-two-columns { @@ -43390,6 +43772,9 @@ } } openSidePanel(panel, panelProps) { + if (this.sidePanel.isOpen && panel !== this.sidePanel.component) { + this.sidePanel.panelProps?.onCloseSidePanel?.(); + } this.sidePanel.component = panel; this.sidePanel.panelProps = panelProps; this.sidePanel.isOpen = true; @@ -44099,13 +44484,13 @@ } class SelectiveHistory { + HEAD_BRANCH; + HEAD_OPERATION; + tree; applyOperation; revertOperation; buildEmpty; buildTransformation; - HEAD_BRANCH; - HEAD_OPERATION; - tree; /** * The selective history is a data structure used to register changes/updates of a state. * Each change/update is called an "operation". @@ -44126,14 +44511,15 @@ * (used for internal implementation) * @param buildTransformation Factory used to build transformations */ - constructor(initialOperationId, applyOperation, revertOperation, buildEmpty, buildTransformation) { - this.applyOperation = applyOperation; - this.revertOperation = revertOperation; - this.buildEmpty = buildEmpty; - this.buildTransformation = buildTransformation; + constructor(args) { + this.applyOperation = args.applyOperation; + this.revertOperation = args.revertOperation; + this.buildEmpty = args.buildEmpty; + this.buildTransformation = args.buildTransformation; this.HEAD_BRANCH = new Branch(this.buildTransformation); - this.tree = new Tree(buildTransformation, this.HEAD_BRANCH); - const initial = new Operation(initialOperationId, buildEmpty(initialOperationId)); + this.tree = new Tree(this.buildTransformation, this.HEAD_BRANCH); + const initialOperationId = args.initialOperationId; + const initial = new Operation(initialOperationId, this.buildEmpty(initialOperationId)); this.tree.insertOperationLast(this.HEAD_BRANCH, initial); this.HEAD_OPERATION = initial; } @@ -44247,21 +44633,27 @@ } } - function buildRevisionLog(initialRevisionId, recordChanges, dispatch) { - return new SelectiveHistory(initialRevisionId, (revision) => { - const commands = revision.commands.slice(); - const { changes } = recordChanges(() => { - for (const command of commands) { - dispatch(command); - } - }); - revision.setChanges(changes); - }, (revision) => revertChanges([revision]), (id) => new Revision(id, "empty", []), { - with: (revision) => (toTransform) => { - return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands), toTransform.rootCommand); + function buildRevisionLog(args) { + return new SelectiveHistory({ + initialOperationId: args.initialRevisionId, + applyOperation: (revision) => { + const commands = revision.commands.slice(); + const { changes } = args.recordChanges(() => { + for (const command of commands) { + args.dispatch(command); + } + }); + revision.setChanges(changes); }, - without: (revision) => (toTransform) => { - return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands.map(inverseCommand).flat()), toTransform.rootCommand); + revertOperation: (revision) => revertChanges([revision]), + buildEmpty: (id) => new Revision(id, "empty", []), + buildTransformation: { + with: (revision) => (toTransform) => { + return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands), toTransform.rootCommand, undefined, toTransform.timestamp); + }, + without: (revision) => (toTransform) => { + return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands.map(inverseCommand).flat()), toTransform.rootCommand, undefined, toTransform.timestamp); + }, }, }); } @@ -45934,11 +46326,21 @@ for (let border of Object.values(borders)) { borderNodes.push(escapeXml /*xml*/ ` - - - - - + + ${addBorderColor(border["left"])} + + + ${addBorderColor(border["right"])} + + + ${addBorderColor(border["top"])} + + + ${addBorderColor(border["bottom"])} + + + ${addBorderColor(border["diagonal"])} + `); } @@ -45952,10 +46354,15 @@ if (!description) { return escapeXml ``; } - return formatAttributes([ - ["style", description.style], - ["color", toXlsxHexColor(description.color.rgb)], - ]); + return formatAttributes([["style", description.style]]); + } + function addBorderColor(description) { + if (!description) { + return escapeXml ``; + } + return escapeXml /*xml*/ ` + + `; } function addStyles(styles) { const styleNodes = []; @@ -46599,6 +47006,7 @@ this.setupCorePlugin(Plugin, workbookData); } Object.assign(this.getters, this.coreGetters); + this.session.loadInitialMessages(stateUpdateMessages); for (let Plugin of statefulUIPluginRegistry.getAll()) { const plugin = this.setupUiPlugin(Plugin); this.statefulUIPlugins.push(plugin); @@ -46626,8 +47034,6 @@ // This should be done after construction of LocalHistory due to order of // events this.setupSessionEvents(); - // Load the initial revisions - this.session.loadInitialMessages(stateUpdateMessages); this.joinSession(); if (config.snapshotRequested) { this.session.snapshot(this.exportData()); @@ -46691,14 +47097,18 @@ this.finalize(); } setupSession(revisionId) { - const session = new Session(buildRevisionLog(revisionId, this.state.recordChanges.bind(this.state), (command) => { - const result = this.checkDispatchAllowed(command); - if (!result.isSuccessful) { - return; - } - this.isReplayingCommand = true; - this.dispatchToHandlers(this.coreHandlers, command); - this.isReplayingCommand = false; + const session = new Session(buildRevisionLog({ + initialRevisionId: revisionId, + recordChanges: this.state.recordChanges.bind(this.state), + dispatch: (command) => { + const result = this.checkDispatchAllowed(command); + if (!result.isSuccessful) { + return; + } + this.isReplayingCommand = true; + this.dispatchToHandlers(this.coreHandlers, command); + this.isReplayingCommand = false; + }, }), this.config.transportService, revisionId); return session; } @@ -47099,9 +47509,9 @@ Object.defineProperty(exports, '__esModule', { value: true }); - __info__.version = '16.3.0-alpha.7'; - __info__.date = '2023-05-02T13:11:44.262Z'; - __info__.hash = '77c8f5f'; + __info__.version = '16.3.0-alpha.8'; + __info__.date = '2023-05-12T11:53:07.768Z'; + __info__.hash = 'afee7d5'; })(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 6c9b1d8fc20..2dab14cfefc 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml @@ -55,6 +55,107 @@ + + +
+
+
+ + + +
+
+
+
+
+ + +
+
+ + + +
+ +
+
+
+ +
+
+
+
+ +
+ +
+
+
+ + + + +
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + - + @@ -1533,6 +1707,38 @@ /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1723,7 +1929,7 @@ A - Z + Z @@ -1734,8 +1940,8 @@ } - A - Z + Z + A @@ -1746,6 +1952,7 @@ focusable="false" viewBox="0 0 850 850"> @@ -1838,6 +2045,14 @@ /> + + + + +