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 853c38b8bfe..49cdbf303c3 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 @@ -73,8 +73,11 @@ import { isArtificialVoidElement, cleanZWS, isZWS, + setCursorEnd, + paragraphRelatedElements, getDeepestPosition, leftPos, + isNotAllowedContent, } from './utils/utils.js'; import { editorCommands } from './commands/commands.js'; import { Powerbox } from './powerbox/Powerbox.js'; @@ -3494,6 +3497,7 @@ export class OdooEditor extends EventTarget { _onKeyDown(ev) { this.keyboardType = ev.key === 'Unidentified' ? KEYBOARD_TYPES.VIRTUAL : KEYBOARD_TYPES.PHYSICAL; + this._currentKeyPress = ev.key; // If the pressed key has a printed representation, the returned value // is a non-empty Unicode character string containing the printable // representation of the key. In this case, call `deleteRange` before @@ -3707,6 +3711,8 @@ export class OdooEditor extends EventTarget { * @private */ _onSelectionChange() { + const currentKeyPress = this._currentKeyPress; + delete this._currentKeyPress; const selection = this.document.getSelection(); if (!selection) { // Because the `selectionchange` event is async, the selection can @@ -3718,7 +3724,16 @@ export class OdooEditor extends EventTarget { if (isProtected(anchorNode)) { return; } - + // Correct cursor if at editable root. + if ( + selection.isCollapsed && + anchorNode === this.editable && + !this.options.allowInlineAtRoot + ) { + this._fixSelectionOnEditableRoot(selection, currentKeyPress); + // The _onSelectionChange handler is going to be triggered again. + return; + } let appliedCustomSelection = false; if (selection.rangeCount && selection.getRangeAt(0)) { appliedCustomSelection = this._handleSelectionInTable(); @@ -4124,6 +4139,67 @@ export class OdooEditor extends EventTarget { } } + /** + * Places the cursor in a safe place (not the editable root). + * Inserts an empty paragraph if selection results from mouse click and + * there's no other way to insert text before/after a block. + * + * @param {Selection} selection - Collapsed selection at the editable root. + * @param {String} currentKeyPress + */ + _fixSelectionOnEditableRoot(selection, currentKeyPress) { + let nodeAfterCursor = this.editable.childNodes[selection.anchorOffset]; + let nodeBeforeCursor = nodeAfterCursor && nodeAfterCursor.previousElementSibling; + // Handle arrow key presses. + if (currentKeyPress === 'ArrowRight' || currentKeyPress === 'ArrowDown') { + while (nodeAfterCursor && isNotAllowedContent(nodeAfterCursor)) { + nodeAfterCursor = nodeAfterCursor.nextElementSibling; + } + if (nodeAfterCursor) { + setSelection(...getDeepestPosition(nodeAfterCursor, 0)); + } else { + this.historyResetLatestComputedSelection(true); + } + } else if (currentKeyPress === 'ArrowLeft' || currentKeyPress === 'ArrowUp') { + while (nodeBeforeCursor && isNotAllowedContent(nodeBeforeCursor)) { + nodeBeforeCursor = nodeBeforeCursor.previousElementSibling; + } + if (nodeBeforeCursor) { + setSelection(...getDeepestPosition(nodeBeforeCursor, nodeSize(nodeBeforeCursor))); + } else { + this.historyResetLatestComputedSelection(true); + } + // Handle cursor next to a 'P'. + } else if (nodeAfterCursor && paragraphRelatedElements.includes(nodeAfterCursor.nodeName)) { + // Cursor is right before a 'P'. + setCursorStart(nodeAfterCursor); + } else if (nodeBeforeCursor && paragraphRelatedElements.includes(nodeBeforeCursor.nodeName)) { + // Cursor is right after a 'P'. + setCursorEnd(nodeBeforeCursor); + // Handle cursor not next to a 'P'. + // Insert a new 'P' if selection resulted from a mouse click. + } else if (this._currentMouseState === 'mousedown') { + this._recordHistorySelection(true); + const p = this.document.createElement('p'); + p.append(this.document.createElement('br')); + if (!nodeAfterCursor) { + // Cursor is at the end of the editable. + this.editable.append(p); + } else if (!nodeBeforeCursor) { + // Cursor is at the beginning of the editable. + this.editable.prepend(p); + } else { + // Cursor is between two non-p blocks + nodeAfterCursor.before(p); + } + setCursorStart(p); + this.historyStep(); + } else { + // Remove selection as a fallback. + selection.removeAllRanges(); + } + } + _onMouseup(ev) { this._currentMouseState = ev.type; 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 13f331e6251..54ee168c3b9 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 @@ -1429,7 +1429,7 @@ export function getColumnIndex(td) { // This is a list of "paragraph-related elements", defined as elements that // behave like paragraphs. -const paragraphRelatedElements = [ +export const paragraphRelatedElements = [ 'P', 'H1', 'H2',