diff options
| author | Walid (wasa) <wasa@odoo.com> | 2025-01-24 10:13:14 +0100 |
|---|---|---|
| committer | Walid (wasa) <wasa@odoo.com> | 2025-02-07 19:26:35 +0000 |
| commit | 996e7e7c5d53be1e9f851b50f3f16b09f2738e4a (patch) | |
| tree | ed21c0a7a15ffb0ba80d61aea4ec4230b5690011 | |
| parent | ac32c2d5efcef243565193e4e227e13246a94236 (diff) | |
[FIX] html_editor: programmatically add space on automatic link creation
**Problem**:
In Safari, relying on the browser to add a space after automatic link creation
causes the cursor to be incorrectly positioned. This issue arises due to DOM
manipulations (adding and removing nodes) during the process.
**Solution**:
To ensure proper link insertion and cursor positioning, explicitly add the space
after the link and set the selection programmatically.
**Steps to Reproduce**:
1. Open the Editor on Safari.
2. Add the text "test.com".
3. Press the space key.
4. Observe that the link is created, but the cursor moves to the start of the paragraph.
opw-4441817
closes odoo/odoo#194989
Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
4 files changed, 80 insertions, 30 deletions
diff --git a/addons/html_editor/static/src/main/link/link_plugin.js b/addons/html_editor/static/src/main/link/link_plugin.js index 39f8dc8b995..5d7f71de64c 100644 --- a/addons/html_editor/static/src/main/link/link_plugin.js +++ b/addons/html_editor/static/src/main/link/link_plugin.js @@ -658,12 +658,34 @@ export class LinkPlugin extends Plugin { } onBeforeInput(ev) { - if ( - ev.inputType === "insertParagraph" || - ev.inputType === "insertLineBreak" || - (ev.inputType === "insertText" && ev.data === " ") - ) { - this.handleAutomaticLinkInsertion(); + if (ev.inputType === "insertParagraph" || ev.inputType === "insertLineBreak") { + const nodeForSelectionRestore = this.handleAutomaticLinkInsertion(); + if (nodeForSelectionRestore) { + this.dependencies.selection.setCursorStart(nodeForSelectionRestore); + this.dependencies.history.addStep(); + } + } + if (ev.inputType === "insertText" && ev.data === " ") { + const nodeForSelectionRestore = this.handleAutomaticLinkInsertion(); + if (nodeForSelectionRestore) { + // Since we manually insert a space here, we will be adding a history step + // after link creation with selection at the end of the link and another + // after inserting the space. So first undo will remove the space, and the + // second will undo the link creation. + this.dependencies.selection.setSelection({ + anchorNode: nodeForSelectionRestore, + anchorOffset: 0, + }); + this.dependencies.history.addStep(); + nodeForSelectionRestore.textContent = + "\u00A0" + nodeForSelectionRestore.textContent; + this.dependencies.selection.setSelection({ + anchorNode: nodeForSelectionRestore, + anchorOffset: 1, + }); + this.dependencies.history.addStep(); + ev.preventDefault(); + } } } /** @@ -701,8 +723,7 @@ export class LinkPlugin extends Plugin { const textNodeToReplace = selection.anchorNode.splitText(startOffset); textNodeToReplace.splitText(match[0].length); selection.anchorNode.parentElement.replaceChild(link, textNodeToReplace); - this.dependencies.selection.setCursorStart(nodeForSelectionRestore); - this.dependencies.history.addStep(); + return nodeForSelectionRestore; } } } diff --git a/addons/html_editor/static/tests/_helpers/user_actions.js b/addons/html_editor/static/tests/_helpers/user_actions.js index 0c7f44088f5..54891ca12cc 100644 --- a/addons/html_editor/static/tests/_helpers/user_actions.js +++ b/addons/html_editor/static/tests/_helpers/user_actions.js @@ -40,17 +40,32 @@ export async function insertText(editor, text) { }; for (const char of text) { // KeyDownEvent is required to trigger deleteRange. - await manuallyDispatchProgrammaticEvent(editor.editable, "keydown", { key: char }); - // InputEvent is required to simulate the insert text. - await manuallyDispatchProgrammaticEvent(editor.editable, "beforeinput", { - inputType: "insertText", - data: char, + const keydownEvent = await manuallyDispatchProgrammaticEvent(editor.editable, "keydown", { + key: char, }); + if (keydownEvent.defaultPrevented) { + continue; + } + // InputEvent is required to simulate the insert text. + const beforeinputEvent = await manuallyDispatchProgrammaticEvent( + editor.editable, + "beforeinput", + { + inputType: "insertText", + data: char, + } + ); + if (beforeinputEvent.defaultPrevented) { + continue; + } insertChar(char); - await manuallyDispatchProgrammaticEvent(editor.editable, "input", { + const inputEvent = await manuallyDispatchProgrammaticEvent(editor.editable, "input", { inputType: "insertText", data: char, }); + if (inputEvent.defaultPrevented) { + continue; + } // KeyUpEvent is not required but is triggered like the browser would. await manuallyDispatchProgrammaticEvent(editor.editable, "keyup", { key: char }); } diff --git a/addons/html_editor/static/tests/link/popover.test.js b/addons/html_editor/static/tests/link/popover.test.js index d4b1d0952a0..890dac11f1f 100644 --- a/addons/html_editor/static/tests/link/popover.test.js +++ b/addons/html_editor/static/tests/link/popover.test.js @@ -208,7 +208,7 @@ describe("Link creation", () => { await insertText(editor, "http://google.co.in"); await insertText(editor, " "); expect(cleanLinkArtifacts(getContent(el))).toBe( - '<p><a href="http://google.co.in">http://google.co.in</a> []</p>' + '<p><a href="http://google.co.in">http://google.co.in</a> []</p>' ); }); test("typing invalid URL + space should not convert to link", async () => { diff --git a/addons/html_editor/static/tests/link/transform.test.js b/addons/html_editor/static/tests/link/transform.test.js index 8802ec5b240..b17be7b7342 100644 --- a/addons/html_editor/static/tests/link/transform.test.js +++ b/addons/html_editor/static/tests/link/transform.test.js @@ -7,12 +7,24 @@ import { getContent, setSelection } from "../_helpers/selection"; import { insertText, undo } from "../_helpers/user_actions"; async function insertSpace(editor) { - await manuallyDispatchProgrammaticEvent(editor.editable, "keydown", { key: " " }); - // InputEvent is required to simulate the insert text. - await manuallyDispatchProgrammaticEvent(editor.editable, "beforeinput", { - inputType: "insertText", - data: " ", + const keydownEvent = await manuallyDispatchProgrammaticEvent(editor.editable, "keydown", { + key: " ", }); + if (keydownEvent.defaultPrevented) { + return; + } + // InputEvent is required to simulate the insert text. + const beforeinputEvent = await manuallyDispatchProgrammaticEvent( + editor.editable, + "beforeinput", + { + inputType: "insertText", + data: " ", + } + ); + if (beforeinputEvent.defaultPrevented) { + return; + } const range = editor.document.getSelection().getRangeAt(0); if (!range.collapsed) { throw new Error("need to implement something... maybe"); @@ -41,11 +53,13 @@ async function insertSpace(editor) { anchorOffset: offset, }); - await manuallyDispatchProgrammaticEvent(editor.editable, "input", { + const inputEvent = await manuallyDispatchProgrammaticEvent(editor.editable, "input", { inputType: "insertText", data: " ", }); - + if (inputEvent.defaultPrevented) { + return; + } // KeyUpEvent is not required but is triggered like the browser would. await manuallyDispatchProgrammaticEvent(editor.editable, "keyup", { key: " " }); } @@ -60,7 +74,7 @@ test("should transform url after space", async () => { await insertSpace(editor); }, contentAfter: - '<p>a http://test.com b <a href="http://test.com">http://test.com</a> [] c http://test.com d</p>', + '<p>a http://test.com b <a href="http://test.com">http://test.com</a> [] c http://test.com d</p>', }); await testEditor({ contentBefore: "<p>http://test.com[]</p>", @@ -74,7 +88,7 @@ test("should transform url after space", async () => { // Action: insert space await insertSpace(editor); }, - contentAfter: '<p><a href="http://test.com">http://test.com</a> []</p>', + contentAfter: '<p><a href="http://test.com">http://test.com</a> []</p>', }); }); @@ -84,22 +98,22 @@ test("should transform url followed by punctuation characters after space", asyn stepFunction: async (editor) => { await insertSpace(editor); }, - contentAfter: '<p><a href="http://test.com">http://test.com</a>. []</p>', + contentAfter: '<p><a href="http://test.com">http://test.com</a>. []</p>', }); await testEditor({ contentBefore: "<p>test.com...[]</p>", stepFunction: (editor) => insertSpace(editor), - contentAfter: '<p><a href="http://test.com">test.com</a>... []</p>', + contentAfter: '<p><a href="http://test.com">test.com</a>... []</p>', }); await testEditor({ contentBefore: "<p>test.com,[]</p>", stepFunction: (editor) => insertSpace(editor), - contentAfter: '<p><a href="http://test.com">test.com</a>, []</p>', + contentAfter: '<p><a href="http://test.com">test.com</a>, []</p>', }); await testEditor({ contentBefore: "<p>test.com,hello[]</p>", stepFunction: (editor) => insertSpace(editor), - contentAfter: '<p><a href="http://test.com">test.com</a>,hello []</p>', + contentAfter: '<p><a href="http://test.com">test.com</a>,hello []</p>', }); await testEditor({ contentBefore: "<p>http://test.com[]</p>", @@ -113,7 +127,7 @@ test("should transform url followed by punctuation characters after space", asyn // Action: insert space await insertSpace(editor); }, - contentAfter: '<p><a href="http://test.com">http://test.com</a> []</p>', + contentAfter: '<p><a href="http://test.com">http://test.com</a> []</p>', }); }); @@ -166,7 +180,7 @@ test("transform text url into link and undo it", async () => { const { el, editor } = await setupEditor(`<p>[]</p>`); await insertText(editor, "www.abc.jpg "); expect(cleanLinkArtifacts(getContent(el))).toBe( - '<p><a href="http://www.abc.jpg">www.abc.jpg</a> []</p>' + '<p><a href="http://www.abc.jpg">www.abc.jpg</a> []</p>' ); undo(editor); |
