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();
}