diff --git a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js index 36c929c540f..ab4ba780854 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js @@ -20,6 +20,7 @@ import { endPos, ensureFocus, getCursorDirection, + getFurthestUneditableParent, getListMode, getOuid, insertText, @@ -1323,6 +1324,28 @@ export class OdooEditor extends EventTarget { correctTripleClick: true, }); if (!range) return; + // Expand the range to fully include all contentEditable=False elements. + const commonAncestorContainer = this.editable.contains(range.commonAncestorContainer) ? + range.commonAncestorContainer : + this.editable; + const startUneditable = getFurthestUneditableParent(range.startContainer, commonAncestorContainer); + if (startUneditable) { + let leaf = previousLeaf(startUneditable); + if (leaf) { + range.setStart(leaf, nodeSize(leaf)); + } else { + range.setStart(commonAncestorContainer, 0); + } + } + const endUneditable = getFurthestUneditableParent(range.endContainer, commonAncestorContainer); + if (endUneditable) { + let leaf = nextLeaf(endUneditable); + if (leaf) { + range.setEnd(leaf, 0); + } else { + range.setEnd(commonAncestorContainer, nodeSize(commonAncestorContainer)); + } + } let start = range.startContainer; let end = range.endContainer; // Let the DOM split and delete the range. diff --git a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js index 28bcc8e643b..5fe2b3170e2 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js @@ -230,6 +230,29 @@ export function findNode(domPath, findCallback = () => true, stopCallback = () = * @param {Node} node */ +/** + * Return the furthest uneditable parent of node contained within parentLimit. + * @see deleteRange Used to guarantee that uneditables are fully contained in + * the range (so that it is not possible to partially remove them) + * + * @param {Node} node + * @param {Node} parentLimit non-inclusive furthest parent allowed + * @returns {Node} uneditable parent if it exists + */ +export function getFurthestUneditableParent(node, parentLimit) { + if (node === parentLimit || !parentLimit.contains(node)) { + return undefined; + } + let parent = node && node.parentElement; + let nonEditableElement; + while (parent && parent !== parentLimit) { + if (!parent.isContentEditable) { + nonEditableElement = parent; + } + parent = parent.parentElement; + } + return nonEditableElement; +} /** * Returns the closest HTMLElement of the provided Node * if a 'selector' is provided, Returns the closest HTMLElement that match the selector diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js index 439b9dcd7a5..fe799300391 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js @@ -2351,6 +2351,36 @@ X[]
before[]after
`), }); }); + it('should extend the range to fully include contenteditable=false that are partially selected at the end of the range', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +before[o
+intruder]
after
`), + stepFunction: async editor => { + await deleteBackward(editor); + }, + contentAfter: unformat(` +before[]
after
`), + }); + }); + it('should extend the range to fully include contenteditable=false that are partially selected at the start of the range', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +before
+[intruder
o]after
`), + stepFunction: async editor => { + await deleteBackward(editor); + }, + contentAfter: unformat(` +before[]after
`), + }); + }); }); describe('CTRL+Backspace', () => {