[IMP] web_editor: expand deleterange for uneditables

In Chrome, it is possible to partially select the contents of a
contenteditable=false element if it has a contenteditable=true child.

This commit prevents breaking such uneditables by expanding the range in a
deleterange to fully contain such elements.

It will be useful with the new blocks introduced with the Knowledge module.

Task-id: 2794990
Part-of: odoo/odoo#86489
This commit is contained in:
abd-msyukyu-odoo
2022-05-05 11:41:57 +02:00
parent f4d72dd1ed
commit f75462eeaf
3 changed files with 76 additions and 0 deletions
@@ -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.
@@ -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
@@ -2351,6 +2351,36 @@ X[]
<p>before[]after</p>`),
});
});
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(`
<p>before[o</p>
<div contenteditable="false">
<div contenteditable="true"><p>intruder]</p></div>
</div>
<p>after</p>`),
stepFunction: async editor => {
await deleteBackward(editor);
},
contentAfter: unformat(`
<p>before[]</p><p>after</p>`),
});
});
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(`
<p>before</p>
<div contenteditable="false">
<div contenteditable="true"><p>[intruder</p></div>
</div>
<p>o]after</p>`),
stepFunction: async editor => {
await deleteBackward(editor);
},
contentAfter: unformat(`
<p>before[]after</p>`),
});
});
});
describe('CTRL+Backspace', () => {