diff --git a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js index 3ab43be5d72..a86e501b5d9 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js +++ b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js @@ -354,6 +354,19 @@ export function toggleFormat(editor, format) { } } if (wasCollapsed) { + const siblings = [...zws.parentElement.childNodes]; + if ( + selectedTextNodes.includes(siblings[0]) && + selectedTextNodes.includes(siblings[siblings.length - 1]) + ) { + zws.parentElement.setAttribute('oe-zws-empty-inline', ''); + } else { + const span = document.createElement('span'); + span.setAttribute('oe-zws-empty-inline', ''); + zws.before(span); + span.append(zws); + + } setSelection(zws, 1); } else { setSelection(anchorNode, anchorOffset, focusNode, focusOffset); @@ -376,6 +389,21 @@ export function toggleFormat(editor, format) { } else { el.style[style.name] = style.value; } + if (zws) { + const siblings = [...zws.parentElement.childNodes]; + if ( + selectedTextNodes.includes(siblings[0]) && + selectedTextNodes.includes(siblings[siblings.length - 1]) + ) { + zws.parentElement.setAttribute('oe-zws-empty-inline', ''); + } else { + const span = document.createElement('span'); + span.setAttribute('oe-zws-empty-inline', ''); + zws.before(span); + span.append(zws); + + } + } }, format, !isAlreadyFormatted); } } diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js index a8a52978e88..5a86afac58d 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js @@ -15,8 +15,8 @@ const strikeThrough = async editor => { }; describe('Format', () => { - const b = content => `${content}`; - const notB = (content, weight) => `${content}`; + const b = (content, zws) => `${content}`; + const notB = (content, weight, zws) => `${content}`; describe('bold', () => { it('should make a few characters bold', async () => { await testEditor(BasicEditor, { @@ -86,19 +86,21 @@ describe('Format', () => { await testEditor(BasicEditor, { contentBefore: '
ab[]cd
', stepFunction: bold, - contentAfter: `ab${b(`[]\u200B`)}cd
`, + contentAfterEdit: `ab${b(`[]\u200B`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); }); it('should get ready to type in not bold', async () => { await testEditor(BasicEditor, { contentBefore: `${b(`ab[]cd`)}
`, stepFunction: bold, - contentAfter: `${b(`ab${notB(`[]\u200B`)}cd`)}
`, + contentAfterEdit: `${b(`ab${notB(`[]\u200B`, undefined, 'first')}cd`)}
`, + contentAfter: `${b(`ab[]cd`)}
`, }); }); }); - const i = content => `${content}`; - const notI = content => `${content}`; + const i = (content, zws) => `${content}`; + const notI = (content, zws) => `${content}`; describe('italic', () => { it('should make a few characters italic', async () => { await testEditor(BasicEditor, { @@ -168,18 +170,20 @@ describe('Format', () => { await testEditor(BasicEditor, { contentBefore: `ab[]cd
`, stepFunction: italic, - contentAfter: `ab${i(`[]\u200B`)}cd
`, + contentAfterEdit: `ab${i(`[]\u200B`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); }); it('should get ready to type in not italic', async () => { await testEditor(BasicEditor, { contentBefore: `${i(`ab[]cd`)}
`, stepFunction: italic, - contentAfter: `${i(`ab${notI(`[]\u200B`)}cd`)}
`, + contentAfterEdit: `${i(`ab${notI(`[]\u200B`, 'first')}cd`)}
`, + contentAfter: `${i(`ab[]cd`)}
`, }); }); }); - const u = content => `${content}`; + const u = (content, zws) => `${content}`; describe('underline', () => { it('should make a few characters underline', async () => { await testEditor(BasicEditor, { @@ -248,18 +252,20 @@ describe('Format', () => { await testEditor(BasicEditor, { contentBefore: `ab[]cd
`, stepFunction: underline, - contentAfter: `ab${u(`[]\u200B`)}cd
`, + contentAfterEdit: `ab${u(`[]\u200B`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); }); it('should get ready to type in not underline', async () => { await testEditor(BasicEditor, { contentBefore: `${u(`ab[]cd`)}
`, stepFunction: underline, - contentAfter: `${u(`ab`)}\u200B[]${u(`cd`)}
`, + contentAfterEdit: `${u(`ab`)}\u200B[]${u(`cd`)}
`, + contentAfter: `${u(`ab`)}[]${u(`cd`)}
`, }); }); }); - const s = content => `${content}`; + const s = (content, zws) => `${content}`; describe('strikeThrough', () => { it('should make a few characters strikeThrough', async () => { await testEditor(BasicEditor, { @@ -328,14 +334,16 @@ describe('Format', () => { await testEditor(BasicEditor, { contentBefore: `ab[]cd
`, stepFunction: strikeThrough, - contentAfter: `ab${s(`[]\u200B`)}cd
`, + contentAfterEdit: `ab${s(`[]\u200B`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); }); - it('should get ready to type in not strikeThrough', async () => { + it('should get ready to type in not underline', async () => { await testEditor(BasicEditor, { contentBefore: `${s(`ab[]cd`)}
`, stepFunction: strikeThrough, - contentAfter: `${s(`ab`)}\u200B[]${s(`cd`)}
`, + contentAfterEdit: `${s(`ab`)}\u200B[]${s(`cd`)}
`, + contentAfter: `${s(`ab`)}[]${s(`cd`)}
`, }); }); }); @@ -344,32 +352,37 @@ describe('Format', () => { await testEditor(BasicEditor, { contentBefore: `ab${u(s(`cd[]ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(s(`cd`))}${s(`\u200b[]`)}${u(s(`ef`))}
`, + contentAfterEdit: `ab${u(s(`cd`))}${s(`\u200b[]`, 'last')}${u(s(`ef`))}
`, + contentAfter: `ab${u(s(`cd`))}[]${u(s(`ef`))}
`, }); }); it('should restore underline after removing it (collapsed, strikeThrough)', async () => { await testEditor(BasicEditor, { - contentBefore: `ab${u(s(`cd`))}${s(`\u200b[]`)}${u(s(`ef`))}
`, + contentBefore: `ab${u(s(`cd`))}${s(`\u200b[]`, 'first')}${u(s(`ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(s(`cd`))}${s(u(`[]\u200b`))}${u(s(`ef`))}
`, + contentAfterEdit: `ab${u(s(`cd`))}${s(u(`[]\u200b`, 'first'), 'first')}${u(s(`ef`))}
`, + contentAfter: `ab${u(s(`cd`))}[]${u(s(`ef`))}
`, }); }); it('should remove underline after restoring it after removing it (collapsed, strikeThrough)', async () => { await testEditor(BasicEditor, { - contentBefore: `ab${u(s(`cd`))}${s(u(`[]\u200b`))}${u(s(`ef`))}
`, + contentBefore: `ab${u(s(`cd`))}${s(u(`[]\u200b`, 'first'))}${u(s(`ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(s(`cd`))}${s(`\u200b[]`)}${u(s(`ef`))}
`, + contentAfterEdit: `ab${u(s(`cd`))}${s(`\u200b[]`, 'last')}${u(s(`ef`))}
`, + contentAfter: `ab${u(s(`cd`))}[]${u(s(`ef`))}
`, }); }); it('should remove underline after restoring it and writing after removing it (collapsed, strikeThrough)', async () => { await testEditor(BasicEditor, { contentBefore: `ab${u(s(`cd`))}${s(u(`ghi[]`))}${u(s(`ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(s(`cd`))}${s(u(`ghi`) + `\u200b[]`)}${u(s(`ef`))}
`, + contentAfterEdit: `ab${u(s(`cd`))}${s(u(`ghi`) + `\u200b[]`)}${u(s(`ef`))}
`, + contentAfter: `ab${u(s(`cd`))}${s(u(`ghi`) + `[]`)}${u(s(`ef`))}
`, }); }); it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, strikeThrough)', async () => { - const uselessSpan = u(''); // TODO: clean + const uselessSpan = content => `${content}`; // TODO: clean + const uselessU = u(''); // TODO: clean await testEditor(BasicEditor, { contentBefore: `ab${u(s(`cd[]ef`))}
`, stepFunction: async editor => { @@ -380,12 +393,12 @@ describe('Format', () => { await editor.execCommand('underline'); await editor.execCommand('insertText', 'C'); }, - contentAfterEdit: `ab${u(s(`cd`))}${s(`A${u(`B`)}C[]${uselessSpan}`)}${u(s(`ef`))}
`, + contentAfterEdit: `ab${u(s(`cd`))}${s(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(s(`ef`))}
`, }); }); }); describe('underline + italic', () => { - const iAndU = content => `${content}`; + const iAndU = (content, zws) => `${content}`; it('should get ready to write in italic and underline', async () => { await testEditor(BasicEditor, { contentBefore: `ab[]cd
`, @@ -393,7 +406,8 @@ describe('Format', () => { await editor.execCommand('italic'); await editor.execCommand('underline'); }, - contentAfter: `ab${iAndU(`[]\u200B`)}cd
`, + contentAfterEdit: `ab${iAndU(`[]\u200B`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); }); it('should get ready to write in italic, after changing one\'s mind about underline', async () => { @@ -404,7 +418,8 @@ describe('Format', () => { await editor.execCommand('underline'); await editor.execCommand('underline'); }, - contentAfter: `ab${i(`\u200B[]`)}cd
`, + contentAfterEdit: `ab${i(`\u200B[]`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); await testEditor(BasicEditor, { contentBefore: `ab[]cd
`, @@ -413,7 +428,8 @@ describe('Format', () => { await editor.execCommand('italic'); await editor.execCommand('underline'); }, - contentAfter: `ab${i(`\u200B[]`)}cd
`, + contentAfterEdit: `ab${i(`\u200B[]`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); await testEditor(BasicEditor, { contentBefore: `ab[]cd
`, @@ -422,39 +438,45 @@ describe('Format', () => { await editor.execCommand('underline'); await editor.execCommand('italic'); }, - contentAfter: `ab${i(`[]\u200B`)}cd
`, + contentAfterEdit: `ab${i(`[]\u200B`, 'first')}cd
`, + contentAfter: `ab[]cd
`, }); }); it('should get ready to write in italic without underline (underline was first)', async () => { await testEditor(BasicEditor, { contentBefore: `ab${u(i(`cd[]ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(i(`cd`))}${i(`\u200b[]`)}${u(i(`ef`))}
`, + contentAfterEdit: `ab${u(i(`cd`))}${i(`\u200b[]`, 'last')}${u(i(`ef`))}
`, + contentAfter: `ab${u(i(`cd`))}[]${u(i(`ef`))}
`, }); }); it('should restore underline after removing it (collapsed, italic)', async () => { await testEditor(BasicEditor, { contentBefore: `ab${u(i(`cd`))}${i(`\u200b[]`)}${u(i(`ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(i(`cd`))}${iAndU(`[]\u200b`)}${u(i(`ef`))}
`, + contentAfterEdit: `ab${u(i(`cd`))}${iAndU(`[]\u200b`, 'last')}${u(i(`ef`))}
`, + contentAfter: `ab${u(i(`cd`))}[]${u(i(`ef`))}
`, }); }); it('should remove underline after restoring it after removing it (collapsed, italic)', async () => { await testEditor(BasicEditor, { contentBefore: `ab${u(i(`cd`))}${i(u(`[]\u200b`))}${u(i(`ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(i(`cd`))}${i(`\u200b[]`)}${u(i(`ef`))}
`, + contentAfterEdit: `ab${u(i(`cd`))}${i(`\u200b[]`, 'last')}${u(i(`ef`))}
`, + contentAfter: `ab${u(i(`cd`))}[]${u(i(`ef`))}
`, }); }); it('should remove underline after restoring it and writing after removing it (collapsed, italic)', async () => { await testEditor(BasicEditor, { contentBefore: `ab${u(i(`cd`))}${i(u(`ghi[]`))}${u(i(`ef`))}
`, stepFunction: underline, - contentAfter: `ab${u(i(`cd`))}${i(u(`ghi`) + `\u200b[]`)}${u(i(`ef`))}
`, + contentAfterEdit: `ab${u(i(`cd`))}${i(u(`ghi`) + `\u200b[]`)}${u(i(`ef`))}
`, + contentAfter: `ab${u(i(`cd`))}${i(u(`ghi`) + `[]`)}${u(i(`ef`))}
`, }); }); it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, italic)', async () => { - const uselessSpan = u(''); // TODO: clean + const uselessSpan = content => `${content}`; + const uselessU = u(''); // TODO: clean await testEditor(BasicEditor, { contentBefore: `ab${u(i(`cd[]ef`))}
`, stepFunction: async editor => { @@ -465,7 +487,7 @@ describe('Format', () => { await editor.execCommand('underline'); await editor.execCommand('insertText', 'C'); }, - contentAfterEdit: `ab${u(i(`cd`))}${i(`A${u(`B`)}C[]${uselessSpan}`)}${u(i(`ef`))}
`, + contentAfter: `ab${u(i(`cd`))}${i(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(i(`ef`))}
`, }); }); });