diff --git a/app/assets/javascripts/discourse/app/lib/autocomplete.js b/app/assets/javascripts/discourse/app/lib/autocomplete.js index 38ab5601e01..5914a5b281c 100644 --- a/app/assets/javascripts/discourse/app/lib/autocomplete.js +++ b/app/assets/javascripts/discourse/app/lib/autocomplete.js @@ -201,17 +201,22 @@ export default function (options) { if (term) { let text = me.val(); + text = text.substring(0, completeStart) + (options.preserveKey ? options.key || "" : "") + term + " " + text.substring(completeEnd + 1, text.length); + me.val(text); + let newCaretPos = completeStart + 1 + term.length; + if (options.key) { newCaretPos++; } + setCaretPosition(me[0], newCaretPos); if (options && options.afterComplete) { @@ -487,6 +492,7 @@ export default function (options) { if (options.key) { if (options.onKeyUp && key !== options.key) { let match = options.onKeyUp(me.val(), cp); + if (match) { completeStart = cp - match[0].length; completeEnd = completeStart + match[0].length - 1; @@ -543,26 +549,36 @@ export default function (options) { if (!options.key) { completeStart = 0; } + if (e.which === keys.shift) { return; } + if (completeStart === null && e.which === keys.backSpace && options.key) { c = caretPosition(me[0]); c -= 1; initial = c; prevIsGood = true; + while (prevIsGood && c >= 0) { c -= 1; prev = me[0].value[c]; stopFound = prev === options.key; + if (stopFound) { prev = me[0].value[c - 1]; + if ( checkTriggerRule({ backSpace: true }) && (!prev || allowedLettersRegex.test(prev)) ) { completeStart = c; term = me[0].value.substring(c + 1, initial); + + if (!completeEnd) { + completeEnd = c + term.length; + } + updateAutoComplete(dataSource(term, options)); return true; } @@ -640,11 +656,12 @@ export default function (options) { return false; case keys.backSpace: autocompleteOptions = null; - completeEnd = cp; cp--; + completeEnd = cp; if (cp < 0) { closeAutocomplete(); + if (isInput) { i = wrap.find("a:last"); if (i) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js new file mode 100644 index 00000000000..ea3c28edda0 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-editor-mentions-test.js @@ -0,0 +1,109 @@ +import { test } from "qunit"; +import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { setCaretPosition } from "discourse/lib/utilities"; + +const BACKSPACE_KEYCODE = 8; + +acceptance("Composer - editor mentions", function (needs) { + needs.user(); + needs.settings({ enable_mentions: true }); + + needs.pretender((server, helper) => { + server.get("/u/search/users", () => { + return helper.response({ + users: [ + { + username: "user", + name: "Some User", + avatar_template: + "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png", + }, + { + username: "user2", + name: "Some User", + avatar_template: + "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png", + }, + ], + }); + }); + }); + + test("selecting user mentions", async function (assert) { + await visit("/"); + await click("#create-topic"); + + // Emulate user pressing backspace in the editor + const editor = query(".d-editor-input"); + + await triggerKeyEvent(".d-editor-input", "keydown", "@".charCodeAt(0)); + await fillIn(".d-editor-input", "abc @"); + await setCaretPosition(editor, 5); + await triggerKeyEvent(".d-editor-input", "keyup", "@".charCodeAt(0)); + + await triggerKeyEvent(".d-editor-input", "keydown", "u".charCodeAt(0)); + await fillIn(".d-editor-input", "abc @u"); + await setCaretPosition(editor, 6); + await triggerKeyEvent(".d-editor-input", "keyup", "u".charCodeAt(0)); + + await click(".autocomplete.ac-user .selected"); + + assert.strictEqual( + query(".d-editor-input").value, + "abc @user ", + "should replace mention correctly" + ); + }); + + test("selecting user mentions after deleting characters", async function (assert) { + await visit("/"); + await click("#create-topic"); + await fillIn(".d-editor-input", "abc @user a"); + + // Emulate user typing `@` and `u` in the editor + await triggerKeyEvent(".d-editor-input", "keydown", BACKSPACE_KEYCODE); + await fillIn(".d-editor-input", "abc @user "); + await triggerKeyEvent(".d-editor-input", "keyup", BACKSPACE_KEYCODE); + + await triggerKeyEvent(".d-editor-input", "keydown", BACKSPACE_KEYCODE); + await fillIn(".d-editor-input", "abc @user"); + await triggerKeyEvent(".d-editor-input", "keyup", BACKSPACE_KEYCODE); + + await click(".autocomplete.ac-user .selected"); + + assert.strictEqual( + query(".d-editor-input").value, + "abc @user ", + "should replace mention correctly" + ); + }); + + test("selecting user mentions after deleting characters mid sentence", async function (assert) { + await visit("/"); + await click("#create-topic"); + + // Emulate user pressing backspace in the editor + const editor = query(".d-editor-input"); + await fillIn(".d-editor-input", "abc @user 123"); + await setCaretPosition(editor, 9); + + await triggerKeyEvent(".d-editor-input", "keydown", BACKSPACE_KEYCODE); + await fillIn(".d-editor-input", "abc @use 123"); + await triggerKeyEvent(".d-editor-input", "keyup", BACKSPACE_KEYCODE); + await setCaretPosition(editor, 8); + + await triggerKeyEvent(".d-editor-input", "keydown", BACKSPACE_KEYCODE); + await fillIn(".d-editor-input", "abc @us 123"); + await triggerKeyEvent(".d-editor-input", "keyup", BACKSPACE_KEYCODE); + await setCaretPosition(editor, 7); + + await click(".autocomplete.ac-user .selected"); + + assert.strictEqual( + query(".d-editor-input").value, + "abc @user 123", + "should replace mention correctly" + ); + }); +});