From 7850ee318fc480d820d256ab01e39cfc046f2d5b Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Wed, 9 Feb 2022 15:25:03 +1000 Subject: [PATCH] DEV: Add focusComposer to composer controller (#15872) This commit adds a new helpful function to the composer controller which can be used to focus the composer and insert text, regardless of whether the consumer knows whether the composer is open or has a draft. This is good for cases where an action needs to copy text to the composer or open it with text after navigating to a URL. The inspiration for this addition is the discourse-chat plugin, which needs to be able to copy quoted markdown from the chat and insert it into the composer, and unlike in the topic controller we have no idea of the state of the composer from chat. --- .../discourse/app/controllers/composer.js | 51 ++++++++- .../discourse/app/lib/keyboard-shortcuts.js | 15 +-- .../discourse/app/models/composer.js | 4 + .../tests/acceptance/composer-test.js | 107 +++++++++++++++++- 4 files changed, 163 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index e7c5b6cec33..cd81ba155cf 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 baa2cc69b81..a60af675ed7 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 4f9194b414d..98612deefe4 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 36af0bda53f..2006c85467f 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" + ); + }); +});