From 3d89a2ff91bbe0ab36090538bebee60eb456006d Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Tue, 10 Jan 2023 10:02:46 +0000 Subject: [PATCH] [FIX] web_editor: preserve style on empty element Make sure not to remove inline styles when emptying an element. task-3102841 X-original-commit: 8d0397c99798f05a41276d62a66f47c41f255ef8 Part-of: odoo/odoo#116558 --- .../js/editor/odoo-editor/src/OdooEditor.js | 21 +++++++++++++++++++ .../odoo-editor/test/spec/editor.test.js | 7 ++++++- .../odoo-editor/test/spec/insert.test.js | 21 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) 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 2c699d2423f..d452fff29be 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 @@ -1853,6 +1853,19 @@ export class OdooEditor extends EventTarget { range.setEnd(commonAncestorContainer, nodeSize(commonAncestorContainer)); } } + let insertedZws; + if (sel && !sel.isCollapsed && !range.startOffset && !range.startContainer.previousSibling) { + // Insert a zero-width space before the selection if the selection + // is non-collapsed and at the beginning of its parent, so said + // parent will have content after extraction. This ensures that the + // parent will not be removed by "tricking" `range.extractContents`. + // Eg,

[...]

will preserve the styles of the + // node. If it remains empty, it will be cleaned up later by + // the sanitizer. + const zws = document.createTextNode('\u200B'); + range.startContainer.before(zws); + insertedZws = zws; + } let start = range.startContainer; let end = range.endContainer; // Let the DOM split and delete the range. @@ -1925,6 +1938,14 @@ export class OdooEditor extends EventTarget { break; } } + if (insertedZws) { + // Remove the zero-width space (zws) that was added to preserve the + // parent styles, then call `fillEmpty` to properly add a flagged + // zws if still needed. + const el = closestElement(insertedZws); + insertedZws.remove(); + el && fillEmpty(el); + } next = range.endContainer && rightLeafOnlyNotBlockPath(range.endContainer).next().value; if ( shouldPreserveSpace && next && !(next && next.nodeType === Node.TEXT_NODE && next.textContent.startsWith(' ')) 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 e17d8074a7e..8041867dfa7 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 @@ -1003,11 +1003,16 @@ X[] contentAfter: '
a

b[]e

f
', }); }); - it('should delete empty nodes ', async () => { + it('should not delete single remaining empty inline', async () => { // Forward selection await testEditor(BasicEditor, { contentBefore: '

[abcdef]

', stepFunction: deleteForward, + // The flagged 200B is there to preserve the font so if we + // write now, we still write in the font element's style. + contentAfterEdit: '

[]\u200B

', + // The flagged 200B is removed by the sanitizer if its + // parent remains empty. contentAfter: '

[]

', }); }); diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/insert.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/insert.test.js index f0d0cb7a4a5..d5e2e9552e3 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/insert.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/insert.test.js @@ -1,5 +1,5 @@ import { parseHTML } from '../../src/utils/utils.js'; -import { BasicEditor, testEditor, unformat } from '../utils.js'; +import { BasicEditor, testEditor, unformat, insertText, deleteBackward } from '../utils.js'; const span = text => { const span = document.createElement('span'); @@ -264,3 +264,22 @@ describe('insert HTML', () => { }); }); }); +describe('insert text', () => { + describe('not collapsed selection', () => { + it('should insert a character in a fully selected font in a heading, preserving its style', async () => { + await testEditor(BasicEditor, { + contentBefore: '

[abc

]def

', + stepFunction: async editor => insertText(editor, 'g'), + contentAfter: '

g[]

def

', + }); + await testEditor(BasicEditor, { + contentBefore: '

[abc

]def

', + stepFunction: async editor => { + await deleteBackward(editor); + await insertText(editor, 'g'); + }, + contentAfter: '

g[]

def

', + }); + }); + }); +});