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 ee68cd8acbb..0098ded098c 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
@@ -775,7 +775,6 @@ export class OdooEditor extends EventTarget {
sanitize(target) {
this.observerFlush();
-
let record;
if (!target) {
// If the target is not given,
@@ -783,6 +782,9 @@ export class OdooEditor extends EventTarget {
// in the mutations from the last step.
for (record of this._currentStep.mutations) {
const node = this.idFind(record.parentId || record.id) || this.editable;
+ if (!this.editable.contains(node)) {
+ continue;
+ }
target = target
? commonParentGet(target, node, this.editable)
: node;
@@ -2028,7 +2030,9 @@ export class OdooEditor extends EventTarget {
insertedZws = zws;
}
let start = range.startContainer;
+ const startBlock = closestBlock(start);
let end = range.endContainer;
+ const endBlock = closestBlock(end);
// Let the DOM split and delete the range.
const doJoin =
(closestBlock(start) !== closestBlock(range.commonAncestorContainer) ||
@@ -2100,6 +2104,21 @@ export class OdooEditor extends EventTarget {
break;
}
}
+ // If the oDeleteBackward loop have emptied the start block and the
+ // range end in another element (rangeStart != rangeEnd), we delete
+ // the start block and move the cursor to the end block.
+ if (
+ startBlock &&
+ startBlock.textContent === '\u200B' &&
+ endBlock &&
+ startBlock !== endBlock &&
+ !isEmptyBlock(endBlock) &&
+ paragraphRelatedElements.includes(endBlock.nodeName)
+ ) {
+ startBlock.remove();
+ setSelection(endBlock, 0);
+ fillEmpty(endBlock);
+ }
if (insertedZws) {
// Remove the zero-width space (zws) that was added to preserve the
// parent styles, then call `fillEmpty` to properly add a flagged
diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js
index 35feccad89f..661ef46f35d 100644
--- a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js
+++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js
@@ -27,6 +27,7 @@ import {
isNotEditableNode,
createDOMPathGenerator,
closestElement,
+ closestBlock,
} from '../utils/utils.js';
Text.prototype.oDeleteBackward = function (offset, alreadyMoved = false) {
@@ -141,6 +142,29 @@ HTMLElement.prototype.oDeleteBackward = function (offset, alreadyMoved = false,
return;
}
+ /** If we are at the beninning of a block node,
+ * And the previous node is empty, remove it.
+ *
+ * E.g. (previousEl == empty)
+ *
[]def
+ BACKSPACE
+ * <=> []def
+ *
+ * E.g. (previousEl != empty)
+ * abc
[]def
+ BACKSPACE
+ * <=> abc[]def
+ */
+ const previousElementSiblingClosestBlock = closestBlock(this.previousElementSibling);
+ if (
+ previousElementSiblingClosestBlock &&
+ (isEmptyBlock(previousElementSiblingClosestBlock) ||
+ previousElementSiblingClosestBlock.textContent === '\u200B') &&
+ paragraphRelatedElements.includes(this.nodeName)
+ ) {
+ previousElementSiblingClosestBlock.remove();
+ setSelection(this, 0);
+ return;
+ }
+
/**
* Backspace at the beginning of a block node. If it doesn't have a left
* block and it is one of the special block formatting tags below then
diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js
index 1d131757570..0384598605f 100644
--- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js
+++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js
@@ -57,6 +57,8 @@ export const YOUTUBE_URL_GET_VIDEO_ID =
/^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:youtube\.com|youtu\.be)(?:\/(?:[\w-]+\?v=|embed\/|v\/)?)([^\s?]+)(?:\S+)?$/i;
export const EMAIL_REGEX = /^(mailto:)?[\w-.]+@(?:[\w-]+\.)+[\w-]{2,4}$/i;
+export const PROTECTED_BLOCK_TAG = ['TR','TD','TABLE','TBODY','UL','OL','LI'];
+
//------------------------------------------------------------------------------
// Position and sizes
//------------------------------------------------------------------------------
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 33923d69590..1d55b7c778c 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
@@ -836,12 +836,11 @@ X[]
contentAfter: 'ab[]
',
});
});
- it('should merge a heading1 with text into an empty paragraph (keeping the heading)', async () => {
+ it('should remove empty paragraph (keeping the heading)', async () => {
await testEditor(BasicEditor, {
contentBefore: '
[]
ab
',
stepFunction: deleteForward,
- // JW cAfter: '[]ab
',
- contentAfter: '[]ab
',
+ contentAfter: '[]ab
',
});
});
it('should merge a text following a paragraph (keeping the text)', async () => {
@@ -858,12 +857,11 @@ X[]
});
});
describe('With attributes', () => {
- it('should merge a paragraph without class into an empty paragraph with a class', async () => {
+ it('should remove empty paragraph with class', async () => {
await testEditor(BasicEditor, {
contentBefore: '
[]
abc
',
stepFunction: deleteForward,
- // JW cAfter: '[]abc
',
- contentAfter: '[]abc
',
+ contentAfter: '[]abc
',
});
});
it('should merge two paragraphs with spans of same classes', async () => {
@@ -1248,28 +1246,92 @@ X[]
contentAfter: 'ab []gh
',
});
});
- it('should delete a selection from the beginning of a heading1 with a format to the middle of a paragraph', async () => {
- // Forward selection
+ it('should delete a selection from the beginning of a heading1 with a format to the middle of a paragraph + start of editable', async () => {
+ //Forward selection
await testEditor(BasicEditor, {
- contentBefore: '[abcd
ef]gh
',
+ contentBefore: '[abcd
ef]gh1
',
stepFunction: deleteForward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh1
',
});
await testEditor(BasicEditor, {
- contentBefore: '[abcd
ef]gh
',
+ contentBefore: '[abcd
ef]gh2
',
stepFunction: deleteForward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh2
',
});
// Backward selection
await testEditor(BasicEditor, {
- contentBefore: ']abcd
ef[gh
',
+ contentBefore: ']abcd
ef[gh3
',
stepFunction: deleteForward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh3
',
});
await testEditor(BasicEditor, {
- contentBefore: ']abcd
ef[gh
',
+ contentBefore: ']abcd
ef[gh4
',
stepFunction: deleteForward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh4
',
+ });
+ });
+ it('should delete a selection from the beginning of a heading1 with a format to the middle of a paragraph + content', async () => {
+ await testEditor(BasicEditor, {
+ contentBefore: 'content
[abcd
ef]gh1
',
+ stepFunction: deleteForward,
+ contentAfter: 'content
[]gh1
',
+ });
+ await testEditor(BasicEditor, {
+ contentBefore: 'content
[abcd
ef]gh2
',
+ stepFunction: deleteForward,
+ contentAfter: 'content
[]gh2
',
+ });
+ });
+ it('should delete a selection from the beginning of a heading1 to the end of a paragraph', async () => {
+ //Forward selection
+ await testEditor(BasicEditor, {
+ contentBefore: '[abcd
ef]
1
',
+ stepFunction: deleteForward,
+ contentAfter: '[]
1
',
+ });
+ await testEditor(BasicEditor, {
+ contentBefore: '[abcd
ef]
2
',
+ stepFunction: deleteForward,
+ contentAfter: '[]
2
',
+ });
+ // Backward selection
+ await testEditor(BasicEditor, {
+ contentBefore: ']abcd
ef[
3
',
+ stepFunction: deleteForward,
+ contentAfter: '[]
3
',
+ });
+ await testEditor(BasicEditor, {
+ contentBefore: ']abcd
ef[
4
',
+ stepFunction: deleteForward,
+ contentAfter: '[]
4
',
+ });
+ });
+ it('should delete a selection from the beginning of a heading1 with a format to the end of a paragraph', async () => {
+ //Forward selection
+ await testEditor(BasicEditor, {
+ contentBefore: '[abcd
ef]
1
',
+ stepFunction: deleteForward,
+ contentAfterEdit: '[]\u200B
1
',
+ contentAfter: '[]
1
',
+ });
+ await testEditor(BasicEditor, {
+ contentBefore: '[abcd
ef]
2
',
+ stepFunction: deleteForward,
+ contentAfterEdit: '[]\u200B
2
',
+ contentAfter: '[]
2
',
+ });
+ // Backward selection
+ await testEditor(BasicEditor, {
+ contentBefore: ']abcd
ef[
3
',
+ stepFunction: deleteForward,
+ contentAfterEdit: '[]\u200B
3
',
+ contentAfter: '[]
3
',
+ });
+ await testEditor(BasicEditor, {
+ contentBefore: ']abcd
ef[
4
',
+ stepFunction: deleteForward,
+ contentAfterEdit: '[]\u200B
4
',
+ contentAfter: '[]
4
',
});
});
it('should not break unbreakables', async () => {
@@ -1726,94 +1788,115 @@ X[]
});
it('should not break unbreakables', async () => {
await testEditor(BasicEditor, {
- contentBefore: ``,
+ contentBefore: `` +
+ `
[]
` +
+ `
abc
` +
+ `
`,
stepFunction: deleteBackward,
- contentAfter: ``,
+ contentAfter: `` +
+ `
[]
` +
+ `
abc
` +
+ `
`,
});
await testEditor(BasicEditor, {
- contentBefore: `` +
- `
[ab
` +
+ contentBefore: `
` +
+ `
[ab
` +
`
cd
` +
- `
e]f
`,
+ `
e]f1
` +
+ `
`,
stepFunction: deleteBackward,
- contentAfter: ``,
+ contentAfter: `` +
+ `
[]
` +
+ `
f1
` +
+ `
`,
});
await testEditor(BasicEditor, {
- contentBefore: `` +
- `
a[b
` +
+ contentBefore: `
` +
+ `
a[b
` +
`
cd
` +
- `
e]f
`,
+ `
e]f2
` +
+ `
`,
stepFunction: deleteBackward,
- contentAfter: ``,
+ contentAfter: `` +
+ `
a[]
` +
+ `
f2
` +
+ `
`,
});
await testEditor(BasicEditor, {
- contentBefore: `` +
- `
a[b
` +
+ contentBefore: `
` +
+ `
3a[b
` +
`
cd
` +
- `
ef]
`,
+ `
ef]
` +
+ `
`,
stepFunction: deleteBackward,
- contentAfter: ``,
+ contentAfter: ``,
});
await testEditor(BasicEditor, {
- contentBefore: `` +
- `
[ab
` +
+ contentBefore: `
` +
+ `
[ab
` +
`
cd
` +
- `
ef]
`,
+ `
ef4]
` +
+ `
`,
stepFunction: deleteBackward,
- contentAfter: ``,
+ contentAfter: ``,
});
await testEditor(BasicEditor, {
- contentBefore: `` +
- `
[ab
` +
+ contentBefore: `
` +
+ `
[ab
` +
`
cd
` +
- `
ef
` +
- `
` +
+ `
` +
+ `
gh
` +
`
ij
` +
- `
k]l
`,
+ `
k]l5
` +
+ `
`,
stepFunction: deleteBackward,
- contentAfter: ``,
+ contentAfter: `` +
+ ``,
});
await testEditor(BasicEditor, {
- contentBefore: `` +
- `
a[b
` +
+ contentBefore: `
` +
+ `
a[b
` +
`
cd
` +
- `
ef
` +
- `
` +
+ `
` +
+ `
gh
` +
`
ij
` +
- `
k]l
`,
+ `
k]l6
` +
+ `
`,
stepFunction: deleteBackward,
- contentAfter: ``,
+ contentAfter: `` +
+ ``,
});
await testEditor(BasicEditor, {
- contentBefore: `` +
- `
a[b
` +
+ contentBefore: `
` +
+ `
7a[b
` +
`
cd
` +
- `
ef
` +
- `
` +
+ `
` +
+ `
gh
` +
`
ij
` +
- `
kl]
`,
+ `
kl]
` +
+ `
`,
stepFunction: editor => deleteBackward(editor),
- contentAfter: ``,
+ contentAfter: ``,
});
});
it('should merge the following inline text node', async () => {
@@ -2278,12 +2361,11 @@ X[]
contentAfter: 'ab[]
',
});
});
- it('should merge a heading1 with text into an empty paragraph (keeping the heading)', async () => {
+ it('should remove empty paragraph (keeping the heading)', async () => {
await testEditor(BasicEditor, {
contentBefore: '
[]ab
',
stepFunction: deleteBackward,
- // JW cAfter: '[]ab
',
- contentAfter: '[]ab
',
+ contentAfter: '[]ab
',
});
});
it('should merge with previous node (default behaviour)', async () => {
@@ -2337,12 +2419,11 @@ X[]
});
});
describe('With attributes', () => {
- it('should merge a paragraph without class into an empty paragraph with a class', async () => {
+ it('should remove paragraph with class', async () => {
await testEditor(BasicEditor, {
contentBefore: '
[]abc
',
stepFunction: deleteBackward,
- // JW cAfter: '[]abc
',
- contentAfter: '[]abc
',
+ contentAfter: '[]abc
',
});
});
it('should merge two paragraphs with spans of same classes', async () => {
@@ -2602,7 +2683,7 @@ X[]
await testEditor(BasicEditor, {
contentBefore: 'abc
[]def
',
stepFunction: deleteBackward,
- contentAfter: 'abc
[]def
',
+ contentAfter: 'abc
[]def
',
});
});
it('should remove only one br between contents', async () => {
@@ -2829,25 +2910,25 @@ X[]
it('should delete a selection from the beginning of a heading1 with a format to the middle of a paragraph', async () => {
// Forward selection
await testEditor(BasicEditor, {
- contentBefore: '[abcd
ef]gh
',
+ contentBefore: '[abcd
ef]gh1
',
stepFunction: deleteBackward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh1
',
});
await testEditor(BasicEditor, {
- contentBefore: '[abcd
ef]gh
',
+ contentBefore: '[abcd
ef]gh2
',
stepFunction: deleteBackward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh2
',
});
// Backward selection
await testEditor(BasicEditor, {
- contentBefore: ']abcd
ef[gh
',
+ contentBefore: ']abcd
ef[gh3
',
stepFunction: deleteBackward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh3
',
});
await testEditor(BasicEditor, {
- contentBefore: ']abcd
ef[gh
',
+ contentBefore: ']abcd
ef[gh4
',
stepFunction: deleteBackward,
- contentAfter: '[]gh
',
+ contentAfter: '[]gh4
',
});
});
it('should delete a heading (triple click backspace)', async () => {