[FIX] web_editor: prevent pasting blocks into paragraph-related elements

When pasting HTML, we have to be careful not to introduce invalid HTML.
The editor corrected for blocks within inlines but failed to prevent the
invalid pasting of blocks within paragraph-related elements
(eg., `<p><h1>text</h1></p>`). This fixes that issue by unwrapping all
blocks from the copied content when trying to paste within an element
that doesn't accept paragraph-related elements as children.

Notably, this fixes an issue that was caused by that problem, in which
pasted HTML content could not be saved because it was invalid.

task-2632676

closes odoo/odoo#79070

X-original-commit: 9c77834e03516f831a9600f0ba4131c957b3ba74
Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Antoine Guenet
2021-10-27 14:01:39 +00:00
parent 406850f083
commit 218e9bb2aa
2 changed files with 58 additions and 3 deletions
@@ -33,6 +33,9 @@ import {
splitTextNode,
startPos,
nodeSize,
allowsParagraphRelatedElements,
isUnbreakable,
makeContentsInline,
} from '../utils/utils.js';
const TEXT_CLASSES_REGEX = /\btext-[^\s]*\b/g;
@@ -71,13 +74,18 @@ function insert(editor, data, isText = true) {
let nodeToInsert;
const insertedNodes = [...fakeEl.childNodes];
while ((nodeToInsert = fakeEl.childNodes[0])) {
if (isBlock(nodeToInsert) && !isBlock(startNode)) {
if (isBlock(nodeToInsert) && !allowsParagraphRelatedElements(startNode)) {
// Split blocks at the edges if inserting new blocks (preventing
// <p><p>text</p></p> scenarios).
while (
startNode.parentElement !== editor.editable &&
!isBlock(startNode.parentElement)
!allowsParagraphRelatedElements(startNode.parentElement)
) {
if (isUnbreakable(startNode.parentElement)) {
makeContentsInline(fakeEl);
nodeToInsert = fakeEl.childNodes[0];
break;
}
let offset = childNodeIndex(startNode);
if (!insertBefore) {
offset += 1;
@@ -800,7 +800,7 @@ export function preserveCursor(document) {
* Source:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
*
* */
**/
const blockTagNames = [
'ADDRESS',
'ARTICLE',
@@ -985,6 +985,53 @@ export function getInSelection(document, selector) {
);
}
// This is a list of "paragraph-related elements", defined as elements that
// behave like paragraphs.
const paragraphRelatedElements = [
'P',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
];
/**
* Return true if the given node allows "paragraph-related elements".
*
* @see paragraphRelatedElements
* @param {Node} node
* @returns {boolean}
*/
export function allowsParagraphRelatedElements(node) {
return isBlock(node) && !paragraphRelatedElements.includes(node.nodeName);
}
/**
* Take a node and unwrap all of its block contents recursively. All blocks
* (except for firstChilds) are preceded by a <br> in order to preserve the line
* breaks.
*
* @param {Node} node
*/
export function makeContentsInline(node) {
let childIndex = 0;
for (const child of node.childNodes) {
if (isBlock(child)) {
if (childIndex && paragraphRelatedElements.includes(child.nodeName)) {
child.before(document.createElement('br'));
}
for (const grandChild of child.childNodes) {
child.before(grandChild);
makeContentsInline(grandChild);
}
child.remove();
}
childIndex += 1;
}
}
/**
* Returns an array of url infos for url matched in the given string.
*