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


x[]
', + contentAfterEdit: '

cd


x[]\u200B
', contentAfter: '

cd


x[]
', }); }); @@ -1562,7 +1562,7 @@ X[] await deleteBackward(editor); await insertText(editor, 'i'); }, - contentAfterEdit: '

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

', + contentAfterEdit: '

ab x[]\u200B d

', contentAfter: '

ab x[] d

', }); await testEditor(BasicEditor, { @@ -2513,7 +2513,7 @@ X[] await deleteBackward(editor); await insertText(editor, 'x'); }, - contentAfterEdit: '

abx[]d

', + contentAfterEdit: '

abx[]\u200Bd

', contentAfter: '

abx[]d

', }); await testEditor(BasicEditor, { @@ -2522,7 +2522,7 @@ X[] await deleteBackward(editor); await insertText(editor, 'x'); }, - contentAfterEdit: '

ab x[] f

', + contentAfterEdit: '

ab x[]\u200B f

', contentAfter: '

ab x[] f

', }); }); diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/format.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/format.test.js index b593eb07a15..3381b64652f 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/format.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/format.test.js @@ -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: `

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`))}

`, }); }); });