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 4149edf2245..f933c0c6530 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 @@ -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(); } } diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js index 269466c328f..67b9daf1926 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js @@ -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'; diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js index c17c3a37480..22c54a99d1f 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js @@ -1540,7 +1540,7 @@ X[] await deleteBackward(editor); await insertText(editor, 'x'); }, - contentAfterEdit: '
cd
cd
cd
uvi[]xy
', + contentAfterEdit: 'uvi[]\u200Bxy
', contentAfter: 'uvi[]xy
', }); await testEditor(BasicEditor, { @@ -1581,7 +1581,7 @@ X[] await deleteBackward(editor); await insertText(editor, 'x'); }, - contentAfterEdit: 'abx[]ef
', + contentAfterEdit: 'abx[]\u200Bef
', contentAfter: 'abx[]ef
', }); }); @@ -2504,7 +2504,7 @@ X[] await deleteBackward(editor); await insertText(editor, 'x'); }, - contentAfterEdit: 'ab x[] d
ab x[]\u200B d
ab x[] d
abx[]d
abx[]\u200Bd
abx[]d
ab x[] f
ab x[]\u200B f
ab x[] f
ab${u(s(`cd[]ef`))}
`, stepFunction: async editor => { @@ -589,7 +589,7 @@ describe('Format', () => { await editor.execCommand('underline'); await editor.execCommand('insert', 'C'); }, - contentAfterEdit: `ab${u(s(`cd`))}${s(`A${u(`B`)}C[]${uselessU}`)}${u(s(`ef`))}
`, + contentAfterEdit: `ab${u(s(`cd`))}${s(`A${u(`B`, 'first')}C[]\u200B`, 'first')}${uselessS}${u(s(`ef`))}
`, }); }); 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: `ab${u(em(`cd[]ef`))}
`, stepFunction: async editor => { @@ -693,7 +692,7 @@ describe('Format', () => { await editor.execCommand('underline'); await editor.execCommand('insert', 'C'); }, - contentAfter: `ab${u(em(`cd`))}${em(`A${u(`B`)}C[]${uselessU}`)}${u(em(`ef`))}
`, + contentAfter: `ab${u(em(`cd`))}${em(`A${u(`B`)}C[]`)}${u(em(`ef`))}
`, }); }); });