diff --git a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js index a3eb46ded77..b716bbbcafd 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js @@ -25,6 +25,7 @@ import { isColorGradient, nodeSize, preserveCursor, + setCursorStart, setSelection, startPos, toggleClass, @@ -2403,15 +2404,38 @@ export class OdooEditor extends EventTarget { this.deleteRange(selection); } } - if (ev.key === 'Backspace' && !ev.ctrlKey && !ev.metaKey) { + if (ev.key === 'Backspace') { // backspace - // We need to hijack it because firefox doesn't trigger a - // deleteBackward input event with a collapsed selection in front of - // a contentEditable="false" (eg: font awesome). const selection = this.document.getSelection(); - if (selection.isCollapsed) { - ev.preventDefault(); - this._applyCommand('oDeleteBackward'); + if (!ev.ctrlKey && !ev.metaKey) { + if (selection.isCollapsed) { + // We need to hijack it because firefox doesn't trigger a + // deleteBackward input event with a collapsed selection in + // front of a contentEditable="false" (eg: font awesome). + ev.preventDefault(); + this._applyCommand('oDeleteBackward'); + } + } else if (selection.isCollapsed && selection.anchorNode) { + const anchor = (selection.anchorNode.nodeType !== Node.TEXT_NODE && selection.anchorOffset) ? + selection.anchorNode[selection.anchorOffset] : selection.anchorNode; + const element = closestBlock(anchor); + if (isEmptyBlock(element) && element.parentElement.children.length === 1) { + // Prevent removing a

if it is the last element of its + // parent. + ev.preventDefault(); + if (element.tagName !== 'P') { + // Replace an empty block which is not a

by a

+ const paragraph = this.document.createElement('P'); + const br = this.document.createElement('BR'); + paragraph.append(br); + element.before(paragraph); + const result = this._protect(() => element.remove()); + if (result !== UNBREAKABLE_ROLLBACK_CODE && result !== UNREMOVABLE_ROLLBACK_CODE) { + setCursorStart(paragraph); + this.historyStep(); + } + } + } } } else if (ev.key === 'Tab') { // Tab diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js index 4e60b982902..7fb041b37ab 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js @@ -2351,6 +2351,97 @@ X[] }); }); + describe('CTRL+Backspace', () => { + it('should not remove the last p with ctrl+backspace', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(`

[]

`), + stepFunction: async editor => { + editor.editable.addEventListener('keydown', ev => { + // simulation of the browser default behavior + if (ev.key === 'Backspace' && ev.ctrlKey === true && !ev.defaultPrevented) { + const sel = document.getSelection(); + sel.anchorNode.remove(); + } + }); + await keydown(editor.editable, 'Backspace', { ctrlKey: true, cancelable: true }); + }, + contentAfter: unformat(`

[]

`), + }); + }); + it('should not remove the last p enclosed in a contenteditable=false with ctrl+backspace', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +

text

+
+

[]

+
`), + stepFunction: async editor => { + editor.editable.addEventListener('keydown', ev => { + // simulation of the browser default behavior + if (ev.key === 'Backspace' && ev.ctrlKey === true && !ev.defaultPrevented) { + const sel = document.getSelection(); + sel.anchorNode.remove(); + } + }); + await keydown(editor.editable, 'Backspace', { ctrlKey: true, cancelable: true }); + }, + contentAfter: unformat(` +

text

+
+

[]

+
`), + }); + }); + it('should add a


element when deleting the last child of the editable with ctrl+backspace', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +
+ []
+
`), + stepFunction: async editor => { + await keydown(editor.editable, 'Backspace', { ctrlKey: true }); + }, + contentAfter: unformat(`

[]

`), + }); + }); + it('should add a


element when deleting the last child of an element with ctrl+backspace', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +
+
+ []
+
+
`), + stepFunction: async editor => { + await keydown(editor.editable, 'Backspace', { ctrlKey: true }); + }, + contentAfter: unformat(` +
+

[]

+
`), + }); + }); + it('should correctly rollback a CTRL+BACKSPACE if the element should not have been removed', async () => { + await testEditor(BasicEditor, { + contentBefore: unformat(` +
+
+ []
+
+
`), + stepFunction: async editor => { + await keydown(editor.editable, 'Backspace', { ctrlKey: true }); + }, + contentAfter: unformat(` +
+
+ []
+
+
`), + }); + }); + }); + describe('insertParagraphBreak', () => { describe('Selection collapsed', () => { describe('Basic', () => { @@ -3359,7 +3450,7 @@ X[] await testEditor(BasicEditor, { contentBefore: '[ab]\u200Bcd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: '[ab\u200B]cd', // Final state: '[ab\u200Bc]d' @@ -3367,7 +3458,7 @@ X[] await testEditor(BasicEditor, { contentBefore: '[ab]\u200Bcd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: '[ab\u200B]cd', // Final state: '[ab\u200Bc]d' @@ -3377,7 +3468,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'a[b]\u200Bcd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'a[b\u200B]cd', // Final state: 'a[b\u200Bc]d' @@ -3385,7 +3476,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'a[b]\u200Bcd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'a[b\u200B]cd', // Final state: 'a[b\u200Bc]d' @@ -3395,7 +3486,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab[]\u200Bcd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab[\u200B]cd', // Final state: 'ab[\u200Bc]d' @@ -3403,7 +3494,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab[]\u200Bcd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab[\u200B]cd', // Final state: 'ab[\u200Bc]d' @@ -3413,7 +3504,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab]\u200B[cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab\u200B[]cd', // Final state: 'ab\u200B[c]d' @@ -3421,7 +3512,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab]\u200B[cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab\u200B[]cd', // Final state: 'ab\u200B[c]d' @@ -3429,7 +3520,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab]\u200B[cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab\u200B[]cd', // Final state: 'ab\u200B[c]d' @@ -3437,7 +3528,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab]\u200B[cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab\u200B[]cd', // Final state: 'ab\u200B[c]d' @@ -3447,7 +3538,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab]\u200Bc[d', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab\u200B]c[d', // Final state: 'ab\u200Bc[]d' @@ -3455,7 +3546,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab]\u200Bc[d', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowRight', true); + await keydown(editor.editable, 'ArrowRight', { shiftKey: true }); }, contentAfter: 'ab\u200B]c[d', // Final state: 'ab\u200Bc[]d' @@ -3485,7 +3576,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab\u200B[]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab]\u200B[cd', // Final state: 'a]b\u200B[cd' @@ -3493,7 +3584,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab\u200B[]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab]\u200B[cd', // Final state: 'a]b\u200B[cd' @@ -3503,7 +3594,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab\u200B]cd[', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab]\u200Bcd[', // Final state: 'a]b\u200Bcd[' @@ -3511,7 +3602,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab\u200B]cd[', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab]\u200Bcd[', // Final state: 'a]b\u200Bcd[' @@ -3521,7 +3612,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab\u200B]c[d', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab]\u200Bc[d', // Final state: 'a]b\u200Bc[d' @@ -3529,7 +3620,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab\u200B]c[d', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab]\u200Bc[d', // Final state: 'a]b\u200Bc[d' @@ -3539,7 +3630,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab[\u200B]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab[]\u200Bcd', // Final state: 'a]b[\u200Bcd' @@ -3547,7 +3638,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab[\u200B]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab[]\u200Bcd', // Final state: 'a]b[\u200Bcd' @@ -3555,7 +3646,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab[\u200B]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab[]\u200Bcd', // Final state: 'a]b[\u200Bcd' @@ -3563,7 +3654,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'ab[\u200B]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'ab[]\u200Bcd', // Final state: 'a]b[\u200Bcd' @@ -3573,7 +3664,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'a[b\u200B]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'a[b]\u200Bcd', // Final state: 'a[]b\u200Bcd' @@ -3581,7 +3672,7 @@ X[] await testEditor(BasicEditor, { contentBefore: 'a[b\u200B]cd', stepFunction: async editor => { - await keydown(editor.editable, 'ArrowLeft', true); + await keydown(editor.editable, 'ArrowLeft', { shiftKey: true }); }, contentAfter: 'a[b]\u200Bcd', // Final state: 'a[]b\u200Bcd' diff --git a/addons/web_editor/static/lib/odoo-editor/test/utils.js b/addons/web_editor/static/lib/odoo-editor/test/utils.js index 16006651136..503ce197ca4 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/utils.js +++ b/addons/web_editor/static/lib/odoo-editor/test/utils.js @@ -412,8 +412,12 @@ export async function click(el, options) { await nextTickFrame(); } -export async function keydown(editable, key, shiftKey) { - const ev = new KeyboardEvent('keydown', { key, shiftKey: !!shiftKey }); +export async function keydown(editable, key, options = {}) { + options = Object.assign( + { key: key }, + options + ); + const ev = new KeyboardEvent('keydown', options); editable.dispatchEvent(ev); await nextTickFrame(); }