summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWalid (wasa) <wasa@odoo.com>2025-01-24 10:13:14 +0100
committerWalid (wasa) <wasa@odoo.com>2025-02-07 19:26:35 +0000
commit996e7e7c5d53be1e9f851b50f3f16b09f2738e4a (patch)
treeed21c0a7a15ffb0ba80d61aea4ec4230b5690011
parentac32c2d5efcef243565193e4e227e13246a94236 (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>
-rw-r--r--addons/html_editor/static/src/main/link/link_plugin.js37
-rw-r--r--addons/html_editor/static/tests/_helpers/user_actions.js27
-rw-r--r--addons/html_editor/static/tests/link/popover.test.js2
-rw-r--r--addons/html_editor/static/tests/link/transform.test.js44
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>&nbsp;[]</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> []&nbsp;c http://test.com d</p>',
+ '<p>a http://test.com b <a href="http://test.com">http://test.com</a>&nbsp;[] 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>&nbsp;[]</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>.&nbsp;[]</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>...&nbsp;[]</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>,&nbsp;[]</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&nbsp;[]</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>&nbsp;[]</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>&nbsp;[]</p>'
);
undo(editor);