diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js index ef0957787c8..3e032b0b244 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js @@ -47,6 +47,7 @@ import { isEmptyBlock, isWhitespace, isVisibleTextNode, + getCursorDirection, } from '../utils/utils.js'; const TEXT_CLASSES_REGEX = /\btext-[^\s]*\b/g; @@ -429,25 +430,37 @@ export const editorCommands = { }, unlink: editor => { const sel = editor.document.getSelection(); - const closestEl = closestElement(sel.focusNode, 'a'); - if (closestEl) { - // Remove the class otherwise Firefox transforms the link in a span. - closestEl.removeAttribute('class'); - // Remove the contentEditable isolation of links before unlinking, - // otherwise the command fails since the link is the editable root. - if (closestEl.getAttribute('contenteditable') === 'true') { - editor._activateContenteditable(); + const isCollapsed = sel.isCollapsed; + // If the selection is collapsed, unlink the whole link: + // `a[]b` => `a[]b`. + getDeepRange(editor.editable, { sel, splitText: true, select: true }); + if (!isCollapsed) { + // If not, unlink only the part(s) of the link(s) that are selected: + // `a[bcdef]g` => `a[bcdef]g`. + let { anchorNode, focusNode, anchorOffset, focusOffset } = sel; + const direction = getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset); + // Split the links around the selection. + const [startLink, endLink] = [closestElement(anchorNode, 'a'), closestElement(focusNode, 'a')]; + if (startLink) { + anchorNode = splitAroundUntil(anchorNode, startLink); + anchorOffset = direction === DIRECTIONS.RIGHT ? 0 : nodeSize(anchorNode); + setSelection(anchorNode, anchorOffset, focusNode, focusOffset, true); + } + // Only split the end link if it was not already done above. + if (endLink && endLink.isConnected) { + focusNode = splitAroundUntil(focusNode, endLink); + focusOffset = direction === DIRECTIONS.RIGHT ? nodeSize(focusNode) : 0; + setSelection(anchorNode, anchorOffset, focusNode, focusOffset, true); } } - if (sel.isCollapsed) { + const targetedNodes = isCollapsed ? [sel.anchorNode] : getSelectedNodes(editor.editable); + const links = new Set(targetedNodes.map(node => closestElement(node, 'a')).filter(a => a)); + if (links.size) { const cr = preserveCursor(editor.document); - const node = closestElement(sel.focusNode, 'a'); - setSelection(node, 0, node, node.childNodes.length, false); - editor.document.execCommand('unlink'); + for (const link of links) { + unwrapContents(link); + } cr(); - } else { - editor.document.execCommand('unlink'); - setSelection(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset); } }, diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js index 03bce980a8d..50ce2b31ca3 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js @@ -301,6 +301,30 @@ describe('Link', () => { }, contentAfter: '

abcd[]e

', }); + // With fontawesome at the start of the link. + await testEditor(BasicEditor, { + contentBefore: '

a\u200Bbcd[]e

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abcd[]e

', + }); + // With fontawesome at the middle of the link. + await testEditor(BasicEditor, { + contentBefore: '

abc\u200Bd[]e

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abcd[]e

', + }); + // With fontawesome at the end of the link. + await testEditor(BasicEditor, { + contentBefore: '

abcd[]\u200Be

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abcd[]e

', + }); }); it('should remove the link if collapsed range in the middle a link', async () => { await testEditor(BasicEditor, { @@ -310,6 +334,30 @@ describe('Link', () => { }, contentAfter: '

ab[]cde

', }); + // With fontawesome at the start of the link. + await testEditor(BasicEditor, { + contentBefore: '

a\u200Bb[]cde

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

ab[]cde

', + }); + // With fontawesome at the middle of the link. + await testEditor(BasicEditor, { + contentBefore: '

ab[]c\u200Bde

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

ab[]cde

', + }); + // With fontawesome at the end of the link. + await testEditor(BasicEditor, { + contentBefore: '

ab[]cd\u200Be

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

ab[]cde

', + }); }); it('should remove the link if collapsed range at the start of a link', async () => { await testEditor(BasicEditor, { @@ -319,6 +367,30 @@ describe('Link', () => { }, contentAfter: '

a[]bcde

', }); + // With fontawesome at the start of the link. + await testEditor(BasicEditor, { + contentBefore: '

a\u200B[]bcde

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

a[]bcde

', + }); + // With fontawesome at the middle of the link. + await testEditor(BasicEditor, { + contentBefore: '

a[]bc\u200Bde

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

a[]bcde

', + }); + // With fontawesome at the end of the link. + await testEditor(BasicEditor, { + contentBefore: '

a[]bcd\u200Be

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

a[]bcde

', + }); }); it('should remove only the current link if collapsed range in the middle of a link', async () => { await testEditor(BasicEditor, { @@ -329,10 +401,38 @@ describe('Link', () => { }, contentAfter: '

abc[]def

', }); + // With fontawesome at the start of the link. + await testEditor(BasicEditor, { + contentBefore: + '

ab\u200Bc[]def

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abc[]def

', + }); + // With fontawesome at the middle of the link. + await testEditor(BasicEditor, { + contentBefore: + '

abc\u200Bd[]efg

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abcd[]efg

', + }); + // With fontawesome at the end of the link. + await testEditor(BasicEditor, { + contentBefore: + '

abc[]d\u200Bef

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abc[]def

', + }); }); }); describe('range not collapsed', () => { it('should remove the link in the selected range at the end of a link', async () => { + // FORWARD await testEditor(BasicEditor, { contentBefore: '

abc[d]e

', stepFunction: async editor => { @@ -340,8 +440,17 @@ describe('Link', () => { }, contentAfter: '

abc[d]e

', }); + // BACKWARD + await testEditor(BasicEditor, { + contentBefore: '

abc]d[e

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abc]d[e

', + }); }); it('should remove the link in the selected range in the middle of a link', async () => { + // FORWARD await testEditor(BasicEditor, { contentBefore: '

ab[c]de

', stepFunction: async editor => { @@ -349,8 +458,17 @@ describe('Link', () => { }, contentAfter: '

ab[c]de

', }); + // BACKWARD + await testEditor(BasicEditor, { + contentBefore: '

ab]c[de

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

ab]c[de

', + }); }); it('should remove the link in the selected range at the start of a link', async () => { + // FORWARD await testEditor(BasicEditor, { contentBefore: '

a[b]cde

', stepFunction: async editor => { @@ -358,8 +476,17 @@ describe('Link', () => { }, contentAfter: '

a[b]cde

', }); + // BACKWARD + await testEditor(BasicEditor, { + contentBefore: '

a]b[cde

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

a]b[cde

', + }); }); it('should remove the link in the selected range overlapping the end of a link', async () => { + // FORWARD await testEditor(BasicEditor, { contentBefore: '

abc[de]f

', stepFunction: async editor => { @@ -367,8 +494,17 @@ describe('Link', () => { }, contentAfter: '

abc[de]f

', }); + // BACKWARD + await testEditor(BasicEditor, { + contentBefore: '

abc]de[f

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

abc]de[f

', + }); }); it('should remove the link in the selected range overlapping the start of a link', async () => { + // FORWARD await testEditor(BasicEditor, { contentBefore: '

a[bc]def

', stepFunction: async editor => { @@ -376,6 +512,14 @@ describe('Link', () => { }, contentAfter: '

a[bc]def

', }); + // BACKWARD + await testEditor(BasicEditor, { + contentBefore: '

a]bc[def

', + stepFunction: async editor => { + await unlink(editor); + }, + contentAfter: '

a]bc[def

', + }); }); }); });