diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index e7c5b6cec3..cd81ba155c 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -6,7 +6,7 @@ import { authorizesOneOrMoreExtensions, uploadIcon, } from "discourse/lib/uploads"; -import { cancel, run } from "@ember/runloop"; +import { cancel, run, scheduleOnce } from "@ember/runloop"; import { cannotPostAgain, durationTextFromSeconds, @@ -396,6 +396,52 @@ export default Controller.extend({ return uploadIcon(this.currentUser.staff, this.siteSettings); }, + // Use this to open the composer when you are not sure whether it is + // already open and whether it already has a draft being worked on. Supports + // options to append text once the composer is open if required. + // + // opts: + // + // - fallbackToNewTopic: if true, and there is no draft, the composer will + // be opened with the create_topic action and a new topic draft key + // - insertText: the text to append to the composer once it is opened + // - openOpts: this object will be passed to this.open if fallbackToNewTopic is + // true + @action + focusComposer(opts = {}) { + if (this.get("model.viewOpen")) { + this._focusAndInsertText(opts.insertText); + } else { + const opened = this.openIfDraft(); + if (!opened && opts.fallbackToNewTopic) { + this.open( + Object.assign( + { + action: Composer.CREATE_TOPIC, + draftKey: Composer.NEW_TOPIC_KEY, + }, + opts.openOpts || {} + ) + ).then(() => { + this._focusAndInsertText(opts.insertText); + }); + } else if (opened) { + this._focusAndInsertText(opts.insertText); + } + } + }, + + _focusAndInsertText(insertText) { + scheduleOnce("afterRender", () => { + const input = document.querySelector("textarea.d-editor-input"); + input && input.focus(); + + if (insertText) { + this.model.appendText(insertText, null, { new_line: true }); + } + }); + }, + @action openIfDraft(event) { if (this.get("model.viewDraft")) { @@ -407,7 +453,10 @@ export default Controller.extend({ } this.set("model.composeState", Composer.OPEN); + return true; } + + return false; }, actions: { diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index baa2cc69b8..a60af675ed 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -1,7 +1,7 @@ import { bind } from "discourse-common/utils/decorators"; import discourseDebounce from "discourse-common/lib/debounce"; import { isAppWebview } from "discourse/lib/utilities"; -import { later, run, schedule, throttle } from "@ember/runloop"; +import { later, run, throttle } from "@ember/runloop"; import { nextTopicUrl, previousTopicUrl, @@ -413,16 +413,11 @@ export default { focusComposer(event) { const composer = this.container.lookup("controller:composer"); - if (composer.get("model.viewOpen")) { - preventKeyboardEvent(event); - - schedule("afterRender", () => { - const input = document.querySelector("textarea.d-editor-input"); - input && input.focus(); - }); - } else { - composer.openIfDraft(event); + if (event) { + event.preventDefault(); + event.stopPropagation(); } + composer.focusComposer(event); }, fullscreenComposer() { diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index 4f9194b414..98612deefe 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -660,6 +660,10 @@ const Composer = RestModel.extend({ } } + if (opts && opts.new_line) { + text = "\n\n" + text.trim(); + } + this.set("reply", before + text + after); return before.length + text.length; diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index 36af0bda53..2006c85467 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -1,9 +1,12 @@ import { run } from "@ember/runloop"; -import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; +import { click, currentURL, fillIn, settled, visit } from "@ember/test-helpers"; import { toggleCheckDraftPopup } from "discourse/controllers/composer"; import LinkLookup from "discourse/lib/link-lookup"; import { withPluginApi } from "discourse/lib/plugin-api"; -import { CREATE_TOPIC, NEW_TOPIC_KEY } from "discourse/models/composer"; +import Composer, { + CREATE_TOPIC, + NEW_TOPIC_KEY, +} from "discourse/models/composer"; import Draft from "discourse/models/draft"; import { acceptance, @@ -17,7 +20,7 @@ import { } from "discourse/tests/helpers/qunit-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import I18n from "I18n"; -import { test } from "qunit"; +import { skip, test } from "qunit"; import { Promise } from "rsvp"; import sinon from "sinon"; @@ -918,3 +921,101 @@ acceptance("Composer - Customizations", function (needs) { ); }); }); + +// all of these are broken on legacy ember qunit for...some reason. commenting +// until we are fully on ember cli. +acceptance("Composer - Focus Open and Closed", function (needs) { + needs.user(); + + skip("Focusing a composer which is not open with create topic", async function (assert) { + await visit("/t/internationalization-localization/280"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ fallbackToNewTopic: true }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual(composer.model.action, Composer.CREATE_TOPIC); + }); + + skip("Focusing a composer which is not open with create topic and append text", async function (assert) { + await visit("/t/internationalization-localization/280"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ + fallbackToNewTopic: true, + insertText: "this is appended", + }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual( + query("textarea.d-editor-input").value.trim(), + "this is appended" + ); + }); + + skip("Focusing a composer which is already open", async function (assert) { + await visit("/"); + await click("#create-topic"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer(); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + }); + + skip("Focusing a composer which is already open and append text", async function (assert) { + await visit("/"); + await click("#create-topic"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ insertText: "this is some appended text" }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual( + query("textarea.d-editor-input").value.trim(), + "this is some appended text" + ); + }); + + skip("Focusing a composer which is not open that has a draft", async function (assert) { + await visit("/t/this-is-a-test-topic/9"); + + await click(".topic-post:nth-of-type(1) button.edit"); + await fillIn(".d-editor-input", "This is a dirty reply"); + await click(".toggle-minimize"); + + const composer = this.container.lookup("controller:composer"); + composer.focusComposer({ insertText: "this is some appended text" }); + + await settled(); + assert.strictEqual( + document.activeElement.classList.contains("d-editor-input"), + true, + "composer is opened and focused" + ); + assert.strictEqual( + query("textarea.d-editor-input").value.trim(), + "This is a dirty reply\n\nthis is some appended text" + ); + }); +});