[REF] web_editor: remove sanitization of zws in favor of cleanForSave

Superfluous zero-width spaces were removed by the sanitizer and then
upon saving (`cleanForSave`). This commit removes the code dedicated to
that from the sanitizer in favor of the code in `cleanForSave` (adapted
to cover all cases).

Note: this means that with this commit, superfluous zero-width spaces
are not removed from the editor during edition anymore, but only when
saving.

task-3081457

Part-of: odoo/odoo#111119
This commit is contained in:
Antoine Guenet
2023-06-08 13:10:02 +02:00
parent 9d644624dd
commit 508c77fa32
4 changed files with 28 additions and 52 deletions
@@ -3878,23 +3878,21 @@ export class OdooEditor extends EventTarget {
hint.removeAttribute('placeholder');
}
this._pluginCall('cleanForSave', [element]);
// Clean the remaining ZeroWidthspaces added by the `fillEmpty` function
// ( contain "data-oe-zws-empty-inline" attr)
// If the element contain more than just a ZWS,
// we remove it and clean the attribute.
// If the element have a class,
// we only remove the attribute to ensure we don't break some style.
// Otherwise we remove the entire inline element.
for (const emptyElement of element.querySelectorAll('[data-oe-zws-empty-inline]')) {
if (isZWS(emptyElement)) {
if (emptyElement.classList.length > 0) {
emptyElement.removeAttribute('data-oe-zws-empty-inline');
} else {
emptyElement.remove();
}
} else {
// Clean the zero-width spaces added by the `fillEmpty` function
// (flagged with the "data-oe-zws-empty-inline" attributes). Reverse the
// list to start from the deepest elements (for emptiness checks).
const allWhitespaceRegex = /^[\s\u200b]*$/;
for (const emptyElement of [...element.querySelectorAll('[data-oe-zws-empty-inline]')].reverse()) {
emptyElement.removeAttribute('data-oe-zws-empty-inline');
if (!allWhitespaceRegex.test(emptyElement.textContent)) {
// The element has some meaningful text. Remove the ZWS in it.
cleanZWS(emptyElement);
emptyElement.removeAttribute('data-oe-zws-empty-inline');
} else if (!emptyElement.classList.length) {
// We only remove the empty element if it has no class, to
// ensure we don't break visual styles (in that case, its
// ZWS was kept to ensure the cursor can be placed in it).
emptyElement.remove();
}
}
@@ -91,7 +91,11 @@ function sanitizeNode(node, root) {
if (
areSimilarElements(node, node.previousSibling) &&
!isUnbreakable(node) &&
!isEditorTab(node)
!isEditorTab(node) &&
!(
node.attributes?.length === 1 &&
node.hasAttribute('data-oe-zws-empty-inline') &&
(node.textContent === '\u200B' || node.previousSibling.textContent === '\u200B'))
) {
// Merge identical elements together.
getDeepRange(root, { select: true });
@@ -121,31 +125,6 @@ function sanitizeNode(node, root) {
paragraph.replaceChildren(...node.childNodes);
node.replaceWith(paragraph);
node = paragraph; // The node has been removed, update the reference.
} else if (
node.nodeType === Node.TEXT_NODE &&
node.textContent.includes('\u200B') &&
node.parentElement.hasAttribute('data-oe-zws-empty-inline') &&
node !== root.ownerDocument.getSelection()?.anchorNode &&
!isBlock(node.parentElement) &&
(
node.textContent.length > 1 ||
// There can be multiple ajacent text nodes, in which case
// the zero-width space is not needed either, despite being
// alone (length === 1) in its own text node.
Array.from(node.parentNode.childNodes).find(
sibling =>
sibling !== node &&
sibling.nodeType === Node.TEXT_NODE &&
sibling.length > 0
)
)
) {
// Remove zero-width spaces added by `fillEmpty` when there is
// content and the selection is not next to it.
const restoreCursor = node.isConnected && preserveCursor(root.ownerDocument);
node.textContent = node.textContent.replace('\u200B', '');
node.parentElement.removeAttribute("data-oe-zws-empty-inline");
restoreCursor?.();
} else if (isFontAwesome(node) && node.textContent !== '\u200B') {
// Ensure a zero width space is present inside the FA element.
node.textContent = '\u200B';
@@ -1540,7 +1540,7 @@ X[]
await deleteBackward(editor);
await insertText(editor, 'x');
},
contentAfterEdit: '<div><p>cd</p><br><span>x[]</span></div>',
contentAfterEdit: '<div><p>cd</p><br><span data-oe-zws-empty-inline="">x[]\u200B</span></div>',
contentAfter: '<div><p>cd</p><br><span>x[]</span></div>',
});
});
@@ -1562,7 +1562,7 @@ X[]
await deleteBackward(editor);
await insertText(editor, 'i');
},
contentAfterEdit: '<p>uv<i style="color:red">i[]</i>xy</p>',
contentAfterEdit: '<p>uv<i style="color:red" data-oe-zws-empty-inline="">i[]\u200B</i>xy</p>',
contentAfter: '<p>uv<i style="color:red">i[]</i>xy</p>',
});
await testEditor(BasicEditor, {
@@ -1581,7 +1581,7 @@ X[]
await deleteBackward(editor);
await insertText(editor, 'x');
},
contentAfterEdit: '<p>ab<span class="style">x[]</span>ef</p>',
contentAfterEdit: '<p>ab<span class="style" data-oe-zws-empty-inline="">x[]\u200B</span>ef</p>',
contentAfter: '<p>ab<span class="style">x[]</span>ef</p>',
});
});
@@ -2504,7 +2504,7 @@ X[]
await deleteBackward(editor);
await insertText(editor, 'x');
},
contentAfterEdit: '<div><p>ab <span class="style">x[]</span> d</p></div>',
contentAfterEdit: '<div><p>ab <span class="style" data-oe-zws-empty-inline="">x[]\u200B</span> d</p></div>',
contentAfter: '<div><p>ab <span class="style">x[]</span> d</p></div>',
});
await testEditor(BasicEditor, {
@@ -2513,7 +2513,7 @@ X[]
await deleteBackward(editor);
await insertText(editor, 'x');
},
contentAfterEdit: '<div><p>ab<span class="style">x[]</span>d</p></div>',
contentAfterEdit: '<div><p>ab<span class="style" data-oe-zws-empty-inline="">x[]\u200B</span>d</p></div>',
contentAfter: '<div><p>ab<span class="style">x[]</span>d</p></div>',
});
await testEditor(BasicEditor, {
@@ -2522,7 +2522,7 @@ X[]
await deleteBackward(editor);
await insertText(editor, 'x');
},
contentAfterEdit: '<div><p>ab <span class="style">x[]</span> f</p></div>',
contentAfterEdit: '<div><p>ab <span class="style" data-oe-zws-empty-inline="">x[]\u200B</span> f</p></div>',
contentAfter: '<div><p>ab <span class="style">x[]</span> f</p></div>',
});
});
@@ -578,7 +578,7 @@ describe('Format', () => {
});
});
it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, strikeThrough)', async () => {
const uselessU = u(''); // TODO: clean
const uselessS = s(u('\u200B', 'first'), 'first'); // TODO: clean
await testEditor(BasicEditor, {
contentBefore: `<p>ab${u(s(`cd[]ef`))}</p>`,
stepFunction: async editor => {
@@ -589,7 +589,7 @@ describe('Format', () => {
await editor.execCommand('underline');
await editor.execCommand('insert', 'C');
},
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`A${u(`B`)}C[]${uselessU}`)}${u(s(`ef`))}</p>`,
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`A${u(`B`, 'first')}C[]\u200B`, 'first')}${uselessS}${u(s(`ef`))}</p>`,
});
});
it('should remove only underline decoration on a span', async () => {
@@ -682,7 +682,6 @@ describe('Format', () => {
});
});
it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, italic)', async () => {
const uselessU = u(''); // TODO: clean
await testEditor(BasicEditor, {
contentBefore: `<p>ab${u(em(`cd[]ef`))}</p>`,
stepFunction: async editor => {
@@ -693,7 +692,7 @@ describe('Format', () => {
await editor.execCommand('underline');
await editor.execCommand('insert', 'C');
},
contentAfter: `<p>ab${u(em(`cd`))}${em(`A${u(`B`)}C[]${uselessU}`)}${u(em(`ef`))}</p>`,
contentAfter: `<p>ab${u(em(`cd`))}${em(`A${u(`B`)}C[]`)}${u(em(`ef`))}</p>`,
});
});
});