From c189fc1b318eab173ad195b74e227bb0bf3eee41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Geelen=20=28sge=29?= Date: Wed, 7 Sep 2022 13:08:17 +0000 Subject: [PATCH] [IMP] web_editor: better tabs Refactor the tabs to be : * Automatically aligned on a 40px grid. * A tab only need one delete to remove. * Arrow navigation around tabs is working as intended. task-2871848 closes odoo/odoo#99460 Signed-off-by: David Monjoie (dmo) --- .../js/editor/odoo-editor/src/OdooEditor.js | 8 +++-- .../js/editor/odoo-editor/src/base_style.scss | 6 ++++ .../src/commands/deleteBackward.js | 3 -- .../odoo-editor/src/commands/deleteForward.js | 11 +++++-- .../editor/odoo-editor/src/utils/sanitize.js | 33 +++++++++++++++++-- .../js/editor/odoo-editor/src/utils/utils.js | 26 ++++++++++----- .../editor/odoo-editor/test/spec/tabs.test.js | 3 -- 7 files changed, 68 insertions(+), 22 deletions(-) diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js index 6799c384c69..b398b7a2463 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js @@ -3031,7 +3031,7 @@ export class OdooEditor extends EventTarget { } else if (closestTag === 'TABLE') { this._onTabulationInTable(ev); } else if (!ev.shiftKey) { - this.execCommand('insert', '\u00A0 \u00A0\u00A0'); + this.execCommand('insert', parseHTML('\u0009\u200B')); } ev.preventDefault(); ev.stopPropagation(); @@ -3159,7 +3159,7 @@ export class OdooEditor extends EventTarget { * @returns {boolean} */ isSelectionInEditable(selection) { - selection = selection || this.document.getSelection() + selection = selection || this.document.getSelection(); return selection && selection.anchorNode && this.editable.contains(selection.anchorNode) && this.editable.contains(selection.focusNode); } @@ -3260,6 +3260,10 @@ export class OdooEditor extends EventTarget { for (const el of element.querySelectorAll('[contenteditable="false"]')) { el.setAttribute('data-oe-keep-contenteditable', ''); } + // Flag elements .oe-tabs contenteditable=false. + for (const el of element.querySelectorAll('.oe-tabs')) { + el.setAttribute('contenteditable', 'false'); + } } cleanForSave(element = this.editable) { diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/base_style.scss b/addons/web_editor/static/src/js/editor/odoo-editor/src/base_style.scss index 76dcd592032..350269110c9 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/base_style.scss +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/base_style.scss @@ -5,3 +5,9 @@ li.oe-nested { padding: 0.5rem; border-color: #c9ccd2; } +.oe-tabs { + display: inline-block; + white-space: pre-wrap; + max-width: 40px; + width: 40px; +} diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js index ddf18fbd8bb..da4bf6913aa 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js @@ -12,18 +12,15 @@ import { getState, isBlock, isEmptyBlock, - isInPre, isUnbreakable, isUnremovable, isVisible, - isVisibleStr, leftPos, rightPos, moveNodes, nodeSize, prepareUpdate, setSelection, - splitTextNode, isMediaElement, isVisibleEmpty, isNotEditableNode, diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteForward.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteForward.js index 4f7ca432a44..cd329acf41d 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteForward.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteForward.js @@ -71,8 +71,8 @@ Text.prototype.oDeleteForward = function (offset, alreadyMoved = false) { const parentElement = this.parentElement; if (offset === this.nodeValue.length) { - // Delete at the end of a text node is not a specific case to - // handle, let the element implementation handle it. + // Delete at the end of a text node is not a specific case to handle, + // let the element implementation handle it. parentElement.oDeleteForward([...parentElement.childNodes].indexOf(this) + 1); return; } @@ -124,7 +124,14 @@ HTMLElement.prototype.oDeleteForward = function (offset) { } if (firstLeafNode && (isFontAwesome(firstLeafNode) || isNotEditableNode(firstLeafNode))) { + const nextSibling = firstLeafNode.nextSibling; + const nextSiblingText = nextSibling ? nextSibling.textContent : ''; firstLeafNode.remove(); + if (isEditorTab(firstLeafNode) && nextSiblingText[0] === '\u200B') { + // When deleting an editor tab, we need to ensure it's related ZWS + // il deleted as well. + nextSibling.textContent = nextSiblingText.replace('\u200B', ''); + } return; } if ( diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js index 0582d2344d5..87eabeee287 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { closestBlock, + closestElement, endPos, fillEmpty, getListMode, @@ -13,7 +14,8 @@ import { isMediaElement, getDeepRange, isUnbreakable, - closestElement, + isEditorTab, + isZWS, getUrlsInfosInString, URL_REGEX, } from './utils.js'; @@ -107,10 +109,11 @@ class Sanitize { _parse(node) { while (node) { - // Merge identical elements together + // Merge identical elements together. while ( areSimilarElements(node, node.previousSibling) && - !isUnbreakable(node) + !isUnbreakable(node) && + !isEditorTab(node) ) { getDeepRange(this.root, { select: true }); const restoreCursor = node.isConnected && @@ -184,6 +187,30 @@ class Sanitize { node.textContent = '\u200B'; } + // Ensure the editor tabs align on a 40px grid. + if (isEditorTab(node)) { + let tabPreviousSibling = node.previousSibling; + while (isZWS(tabPreviousSibling)) { + tabPreviousSibling = tabPreviousSibling.previousSibling; + } + if (isEditorTab(tabPreviousSibling)) { + node.style.width = '40px'; + } else { + const editable = closestElement(node, '.odoo-editor-editable', true); + if (editable && editable.firstElementChild) { + const nodeRect = node.getBoundingClientRect(); + const referenceRect = editable.firstElementChild.getBoundingClientRect(); + // Values from getBoundingClientRect() are all zeros + // during Editor startup or saving. We cannot + // recalculate the tabs width in thoses cases. + if (nodeRect.width && referenceRect.width) { + const width = (nodeRect.left - referenceRect.left) % 40; + node.style.width = (40 - width) + 'px'; + } + } + } + } + // Ensure elements which should not contain any content are tagged // contenteditable=false to avoid any hiccup. if ( diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js index f029e1ee9cd..1d17cbaeb44 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js @@ -1120,6 +1120,13 @@ export function isZWS(node) { node.textContent === '\u200B' ); } +export function isEditorTab(node) { + return ( + node && + (node.nodeName === 'SPAN') && + node.classList.contains('oe-tabs') + ); +} export function isMediaElement(node) { return ( isFontAwesome(node) || @@ -1284,7 +1291,8 @@ export function isInPre(node) { * Returns whether the given string (or given text node value) * has at least one visible character or one non colapsed whitespace characters in it. */ -const nonWhitespacesRegex = /[\S\u00A0]/; +const nonWhitespaces = '\\S\\u00A0\\u0009'; +const nonWhitespacesRegex = new RegExp(`[${nonWhitespaces}]`); export function isVisibleStr(value) { const str = typeof value === 'string' ? value : value.nodeValue; return nonWhitespacesRegex.test(str); @@ -1511,9 +1519,9 @@ export function isColorGradient(value) { * Note: if split after or before whitespace, that whitespace may become * invisible, it is up to the caller to replace it by nbsp if needed. * - * @param {Text} textNode + * @param {Node} textNode * @param {number} offset - * @param {DIRECTION} originalNodeSide Whether the original node ends up on left + * @param {DIRECTIONS} originalNodeSide Whether the original node ends up on left * or right after the split * @returns {number} The parentOffset if the cursor was between the two text * node parts after the split. @@ -1835,8 +1843,8 @@ export function prepareUpdate(...args) { * * @param {HTMLElement} el * @param {number} offset - * @param {number} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT - * @param {CTYPES} leftCType + * @param {DIRECTIONS} direction @see DIRECTIONS.LEFT @see DIRECTIONS.RIGHT + * @param {CTYPES} [leftCType] * @returns {Object} */ export function getState(el, offset, direction, leftCType) { @@ -1850,11 +1858,11 @@ export function getState(el, offset, direction, leftCType) { if (direction === DIRECTIONS.LEFT) { domPath = leftDOMPath(el, offset, reasons); inverseDOMPath = rightDOMPath(el, offset); - expr = /[^\S\u00A0]$/; + expr = new RegExp(`[^${nonWhitespaces}]$`); } else { domPath = rightDOMPath(el, offset, reasons); inverseDOMPath = leftDOMPath(el, offset); - expr = /^[^\S\u00A0]/; + expr = new RegExp(`^[^${nonWhitespaces}]`); } // TODO I think sometimes, the node we have to consider as the @@ -2092,10 +2100,10 @@ export function enforceWhitespace(el, offset, direction, rule) { let expr; if (direction === DIRECTIONS.LEFT) { domPath = leftLeafOnlyNotBlockPath(el, offset); - expr = /[^\S\u00A0]+$/; + expr = new RegExp(`[^${nonWhitespaces}]+$`); } else { domPath = rightLeafOnlyNotBlockPath(el, offset); - expr = /^[^\S\u00A0]+/; + expr = new RegExp(`^[^${nonWhitespaces}]+`); } const invisibleSpaceTextNodes = []; diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js index 93325a9ad96..93a2787d340 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js @@ -1,13 +1,10 @@ -import { OdooEditor } from '../../src/OdooEditor.js'; import { BasicEditor, deleteBackward, deleteForward, insertText, triggerEvent, - redo, testEditor, - undo, } from '../utils.js'; describe('Tabs', () => {