[FIX] web_editor: implement unlink without relying on execCommand
Before this commit when a button containing a text then an icon was unlinked in Chrome, all further buttons were also unlinked (see steps below). It turns out this bug is due to the browser's `execCommand` implementation so this commit reimplements the feature without relying on it. Steps to reproduce (in website module): * drop Text+Image snippet twice * inside first button type /image and insert a pictogram after the text * click on the text of the first button => the link popover appears * click on the unlink button of the popover => both buttons become flat texts in Chrome => nothing happens visually in Firefox, but the <a> actually gets turned into a <span> task-2778912 Part-of: odoo/odoo#118248 Co-authored-by: Antoine Guenet <age@odoo.com> Co-authored-by: Benoît Socias <bso@odoo.com>
This commit is contained in:
co-authored by
Benoît Socias
parent
5632ad99e3
commit
bd21ea8323
@@ -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>a[]b</a>` => `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>a[b</a>c<a>d</a>e<a>f]g</a>` => `<a>a</a>[bcdef]<a>g</a>`.
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -301,6 +301,30 @@ describe('Link', () => {
|
||||
},
|
||||
contentAfter: '<p>abcd[]e</p>',
|
||||
});
|
||||
// With fontawesome at the start of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist"><span class="fa fa-music" contenteditable="false">\u200B</span>bcd[]</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a<span class="fa fa-music" contenteditable="false"></span>bcd[]e</p>',
|
||||
});
|
||||
// With fontawesome at the middle of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">bc<span class="fa fa-music" contenteditable="false">\u200B</span>d[]</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>abc<span class="fa fa-music" contenteditable="false"></span>d[]e</p>',
|
||||
});
|
||||
// With fontawesome at the end of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">bcd[]<span class="fa fa-music" contenteditable="false">\u200B</span></a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>abcd[]<span class="fa fa-music" contenteditable="false"></span>e</p>',
|
||||
});
|
||||
});
|
||||
it('should remove the link if collapsed range in the middle a link', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
@@ -310,6 +334,30 @@ describe('Link', () => {
|
||||
},
|
||||
contentAfter: '<p>ab[]cde</p>',
|
||||
});
|
||||
// With fontawesome at the start of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist"><span class="fa fa-music" contenteditable="false">\u200B</span>b[]cd</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a<span class="fa fa-music" contenteditable="false"></span>b[]cde</p>',
|
||||
});
|
||||
// With fontawesome at the middle of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">b[]c<span class="fa fa-music" contenteditable="false">\u200B</span>d</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>ab[]c<span class="fa fa-music" contenteditable="false"></span>de</p>',
|
||||
});
|
||||
// With fontawesome at the end of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">b[]cd<span class="fa fa-music" contenteditable="false">\u200B</span></a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>ab[]cd<span class="fa fa-music" contenteditable="false"></span>e</p>',
|
||||
});
|
||||
});
|
||||
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: '<p>a[]bcde</p>',
|
||||
});
|
||||
// With fontawesome at the start of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist"><span class="fa fa-music" contenteditable="false">\u200B</span>[]bcd</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a<span class="fa fa-music" contenteditable="false"></span>[]bcde</p>',
|
||||
});
|
||||
// With fontawesome at the middle of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">[]bc<span class="fa fa-music" contenteditable="false">\u200B</span>d</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a[]bc<span class="fa fa-music" contenteditable="false"></span>de</p>',
|
||||
});
|
||||
// With fontawesome at the end of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">[]bcd<span class="fa fa-music" contenteditable="false">\u200B</span></a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a[]bcd<span class="fa fa-music" contenteditable="false"></span>e</p>',
|
||||
});
|
||||
});
|
||||
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: '<p><a href="exist">a</a>bc[]de<a href="exist">f</a></p>',
|
||||
});
|
||||
// With fontawesome at the start of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore:
|
||||
'<p><a href="exist">a</a>b<a href="exist"><span class="fa fa-music" contenteditable="false">\u200B</span>c[]d</a>e<a href="exist">f</a></p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p><a href="exist">a</a>b<span class="fa fa-music" contenteditable="false"></span>c[]de<a href="exist">f</a></p>',
|
||||
});
|
||||
// With fontawesome at the middle of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore:
|
||||
'<p><a href="exist">a</a>b<a href="exist">c<span class="fa fa-music" contenteditable="false">\u200B</span>d[]e</a>f<a href="exist">g</a></p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p><a href="exist">a</a>bc<span class="fa fa-music" contenteditable="false"></span>d[]ef<a href="exist">g</a></p>',
|
||||
});
|
||||
// With fontawesome at the end of the link.
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore:
|
||||
'<p><a href="exist">a</a>b<a href="exist">c[]d<span class="fa fa-music" contenteditable="false">\u200B</span></a>e<a href="exist">f</a></p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p><a href="exist">a</a>bc[]d<span class="fa fa-music" contenteditable="false"></span>e<a href="exist">f</a></p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
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: '<p>a<a href="exist">bc[d]</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
@@ -340,8 +440,17 @@ describe('Link', () => {
|
||||
},
|
||||
contentAfter: '<p>a<a href="exist">bc[</a>d]e</p>',
|
||||
});
|
||||
// BACKWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">bc]d[</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a<a href="exist">bc]</a>d[e</p>',
|
||||
});
|
||||
});
|
||||
it('should remove the link in the selected range in the middle of a link', async () => {
|
||||
// FORWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">b[c]d</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
@@ -349,8 +458,17 @@ describe('Link', () => {
|
||||
},
|
||||
contentAfter: '<p>a<a href="exist">b[</a>c]<a href="exist">d</a>e</p>',
|
||||
});
|
||||
// BACKWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">b]c[d</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a<a href="exist">b]</a>c[<a href="exist">d</a>e</p>',
|
||||
});
|
||||
});
|
||||
it('should remove the link in the selected range at the start of a link', async () => {
|
||||
// FORWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">[b]cd</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
@@ -358,8 +476,17 @@ describe('Link', () => {
|
||||
},
|
||||
contentAfter: '<p>a[b]<a href="exist">cd</a>e</p>',
|
||||
});
|
||||
// BACKWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">]b[cd</a>e</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a]b[<a href="exist">cd</a>e</p>',
|
||||
});
|
||||
});
|
||||
it('should remove the link in the selected range overlapping the end of a link', async () => {
|
||||
// FORWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">bc[d</a>e]f</p>',
|
||||
stepFunction: async editor => {
|
||||
@@ -367,8 +494,17 @@ describe('Link', () => {
|
||||
},
|
||||
contentAfter: '<p>a<a href="exist">bc[</a>de]f</p>',
|
||||
});
|
||||
// BACKWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<a href="exist">bc]d</a>e[f</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a<a href="exist">bc]</a>de[f</p>',
|
||||
});
|
||||
});
|
||||
it('should remove the link in the selected range overlapping the start of a link', async () => {
|
||||
// FORWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a[b<a href="exist">c]de</a>f</p>',
|
||||
stepFunction: async editor => {
|
||||
@@ -376,6 +512,14 @@ describe('Link', () => {
|
||||
},
|
||||
contentAfter: '<p>a[bc]<a href="exist">de</a>f</p>',
|
||||
});
|
||||
// BACKWARD
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a]b<a href="exist">c[de</a>f</p>',
|
||||
stepFunction: async editor => {
|
||||
await unlink(editor);
|
||||
},
|
||||
contentAfter: '<p>a]bc[<a href="exist">de</a>f</p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user