[IMP] web_editor: add failsafe upon CTRL+BACKSPACE
Prevents the user to remove the last child (if it is an empty block) of an editable, in order to avoid the case where the user can add text nodes directly in a block where it should not be possible (i.e. directly under this.editable). Replaces any empty block other than `<P>` by an empty `<P>` which is the default block for the editor. Task-id: 2794990 Part-of: odoo/odoo#86489
This commit is contained in:
@@ -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 <p> if it is the last element of its
|
||||
// parent.
|
||||
ev.preventDefault();
|
||||
if (element.tagName !== 'P') {
|
||||
// Replace an empty block which is not a <p> by a <p>
|
||||
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
|
||||
|
||||
@@ -2351,6 +2351,97 @@ X[]
|
||||
});
|
||||
});
|
||||
|
||||
describe('CTRL+Backspace', () => {
|
||||
it('should not remove the last p with ctrl+backspace', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`<p>[]<br></p>`),
|
||||
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(`<p>[]<br></p>`),
|
||||
});
|
||||
});
|
||||
it('should not remove the last p enclosed in a contenteditable=false with ctrl+backspace', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`
|
||||
<p>text</p>
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>`),
|
||||
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(`
|
||||
<p>text</p>
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>`),
|
||||
});
|
||||
});
|
||||
it('should add a <p><br></p> element when deleting the last child of the editable with ctrl+backspace', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`
|
||||
<blockquote>
|
||||
[]<br>
|
||||
</blockquote>`),
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'Backspace', { ctrlKey: true });
|
||||
},
|
||||
contentAfter: unformat(`<p>[]<br></p>`),
|
||||
});
|
||||
});
|
||||
it('should add a <p><br></p> element when deleting the last child of an element with ctrl+backspace', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<blockquote>
|
||||
[]<br>
|
||||
</blockquote>
|
||||
</div></div>`),
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'Backspace', { ctrlKey: true });
|
||||
},
|
||||
contentAfter: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>`),
|
||||
});
|
||||
});
|
||||
it('should correctly rollback a CTRL+BACKSPACE if the element should not have been removed', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<blockquote class="oe_unremovable">
|
||||
[]<br>
|
||||
</blockquote>
|
||||
</div></div>`),
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'Backspace', { ctrlKey: true });
|
||||
},
|
||||
contentAfter: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<blockquote class="oe_unremovable">
|
||||
[]<br>
|
||||
</blockquote>
|
||||
</div></div>`),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertParagraphBreak', () => {
|
||||
describe('Selection collapsed', () => {
|
||||
describe('Basic', () => {
|
||||
@@ -3359,7 +3450,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '[ab]<span>\u200B</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: '[ab<span>\u200B]</span>cd',
|
||||
// Final state: '[ab<span>\u200B</span>c]d'
|
||||
@@ -3367,7 +3458,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '[ab<span>]\u200B</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: '[ab<span>\u200B]</span>cd',
|
||||
// Final state: '[ab<span>\u200B</span>c]d'
|
||||
@@ -3377,7 +3468,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'a[b]<span>\u200B</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'a[b<span>\u200B]</span>cd',
|
||||
// Final state: 'a[b<span>\u200B</span>c]d'
|
||||
@@ -3385,7 +3476,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'a[b<span>]\u200B</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'a[b<span>\u200B]</span>cd',
|
||||
// Final state: 'a[b<span>\u200B</span>c]d'
|
||||
@@ -3395,7 +3486,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab[]<span>\u200B</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab[<span>\u200B]</span>cd',
|
||||
// Final state: 'ab[<span>\u200B</span>c]d'
|
||||
@@ -3403,7 +3494,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>[]\u200B</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>[\u200B]</span>cd',
|
||||
// Final state: 'ab<span>[\u200B</span>c]d'
|
||||
@@ -3413,7 +3504,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>]\u200B[</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>\u200B[]</span>cd',
|
||||
// Final state: 'ab<span>\u200B</span>[c]d'
|
||||
@@ -3421,7 +3512,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>]\u200B</span>[cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>\u200B[]</span>cd',
|
||||
// Final state: 'ab<span>\u200B</span>[c]d'
|
||||
@@ -3429,7 +3520,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab]<span>\u200B</span>[cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>\u200B[]</span>cd',
|
||||
// Final state: 'ab<span>\u200B</span>[c]d'
|
||||
@@ -3437,7 +3528,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab]<span>\u200B[</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>\u200B[]</span>cd',
|
||||
// Final state: 'ab<span>\u200B</span>[c]d'
|
||||
@@ -3447,7 +3538,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>]\u200B</span>c[d',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>\u200B]</span>c[d',
|
||||
// Final state: 'ab<span>\u200B</span>c[]d'
|
||||
@@ -3455,7 +3546,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab]<span>\u200B</span>c[d',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowRight', true);
|
||||
await keydown(editor.editable, 'ArrowRight', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>\u200B]</span>c[d',
|
||||
// Final state: 'ab<span>\u200B</span>c[]d'
|
||||
@@ -3485,7 +3576,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>\u200B[]</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>]\u200B[</span>cd',
|
||||
// Final state: 'a]b<span>\u200B[</span>cd'
|
||||
@@ -3493,7 +3584,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>\u200B</span>[]cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>]\u200B[</span>cd',
|
||||
// Final state: 'a]b<span>\u200B[</span>cd'
|
||||
@@ -3503,7 +3594,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>\u200B</span>]cd[',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>]\u200B</span>cd[',
|
||||
// Final state: 'a]b<span>\u200B</span>cd['
|
||||
@@ -3511,7 +3602,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>\u200B]</span>cd[',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>]\u200B</span>cd[',
|
||||
// Final state: 'a]b<span>\u200B</span>cd['
|
||||
@@ -3521,7 +3612,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>\u200B</span>]c[d',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>]\u200B</span>c[d',
|
||||
// Final state: 'a]b<span>\u200B</span>c[d'
|
||||
@@ -3529,7 +3620,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>\u200B]</span>c[d',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>]\u200B</span>c[d',
|
||||
// Final state: 'a]b<span>\u200B</span>c[d'
|
||||
@@ -3539,7 +3630,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>[\u200B]</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>[]\u200B</span>cd',
|
||||
// Final state: 'a]b<span>[\u200B</span>cd'
|
||||
@@ -3547,7 +3638,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab<span>[\u200B</span>]cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab<span>[]\u200B</span>cd',
|
||||
// Final state: 'a]b<span>[\u200B</span>cd'
|
||||
@@ -3555,7 +3646,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab[<span>\u200B]</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab[<span>]\u200B</span>cd',
|
||||
// Final state: 'a]b[<span>\u200B</span>cd'
|
||||
@@ -3563,7 +3654,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'ab[<span>\u200B</span>]cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'ab[<span>]\u200B</span>cd',
|
||||
// Final state: 'a]b[<span>\u200B</span>cd'
|
||||
@@ -3573,7 +3664,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'a[b<span>\u200B]</span>cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'a[b<span>]\u200B</span>cd',
|
||||
// Final state: 'a[]b<span>\u200B</span>cd'
|
||||
@@ -3581,7 +3672,7 @@ X[]
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: 'a[b<span>\u200B</span>]cd',
|
||||
stepFunction: async editor => {
|
||||
await keydown(editor.editable, 'ArrowLeft', true);
|
||||
await keydown(editor.editable, 'ArrowLeft', { shiftKey: true });
|
||||
},
|
||||
contentAfter: 'a[b<span>]\u200B</span>cd',
|
||||
// Final state: 'a[]b<span>\u200B</span>cd'
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user