FEATURE: Allow users to edit alt text from the image preview in the editor (#14480)

FEATURE: Allow users to edit alt text from the image preview in the editor (#14480)

diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js
index 2aed00d..a0164cd 100644
--- a/app/assets/javascripts/discourse/app/components/composer-editor.js
+++ b/app/assets/javascripts/discourse/app/components/composer-editor.js
@@ -38,6 +38,18 @@ import { loadOneboxes } from "discourse/lib/load-oneboxes";
 import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
 import userSearch from "discourse/lib/user-search";
 
+// original string `![image|foo=bar|690x220, 50%|bar=baz](upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title")`
+// group 1 `image|foo=bar`
+// group 2 `690x220`
+// group 3 `, 50%`
+// group 4 '|bar=baz'
+// group 5 'upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title"'
+
+// Notes:
+// Group 3 is optional. group 4 can match images with or without a markdown title.
+// All matches are whitespace tolerant as long it's still valid markdown.
+// If the image is inside a code block, we'll ignore it `(?!(.*`))`.
+const IMAGE_MARKDOWN_REGEX = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?(.*?)\]\((upload:\/\/.*?)\)(?!(.*`))/g;
 const REBUILD_SCROLL_MAP_EVENTS = ["composer:resized", "composer:typed-reply"];
 
 let uploadHandlers = [];
@@ -589,24 +601,15 @@ export default Component.extend(ComposerUpload, {
   },
 
   _registerImageScaleButtonClick($preview) {
-    // original string `![image|foo=bar|690x220, 50%|bar=baz](upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title")`
-    // group 1 `image|foo=bar`
-    // group 2 `690x220`
-    // group 3 `, 50%`
-    // group 4 '|bar=baz'
-    // group 5 'upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title"'
-
-    // Notes:
-    // Group 3 is optional. group 4 can match images with or without a markdown title.
-    // All matches are whitespace tolerant as long it's still valid markdown.
-    // If the image is inside a code block, we'll ignore it `(?!(.*`))`.
-    const imageScaleRegex = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?(.*?)\]\((upload:\/\/.*?)\)(?!(.*`))/g;
     $preview.off("click", ".scale-btn").on("click", ".scale-btn", (e) => {
-      const index = parseInt($(e.target).parent().attr("data-image-index"), 10);
+      const index = parseInt(
+        $(e.target).closest(".button-wrapper").attr("data-image-index"),
+        10
+      );
 
       const scale = e.target.attributes["data-scale"].value;
       const matchingPlaceholder = this.get("composer.reply").match(
-        imageScaleRegex
+        IMAGE_MARKDOWN_REGEX
       );
 
       if (matchingPlaceholder) {
@@ -614,7 +617,7 @@ export default Component.extend(ComposerUpload, {
 
         if (match) {
           const replacement = match.replace(
-            imageScaleRegex,
+            IMAGE_MARKDOWN_REGEX,
             `![$1|$2, ${scale}%$4]($5)`
           );
 
@@ -622,7 +625,7 @@ export default Component.extend(ComposerUpload, {
             "composer:replace-text",
             matchingPlaceholder[index],
             replacement,
-            { regex: imageScaleRegex, index }
+            { regex: IMAGE_MARKDOWN_REGEX, index }
           );
         }
       }
@@ -632,6 +635,58 @@ export default Component.extend(ComposerUpload, {
     });
   },
 
+  _registerImageAltTextButtonClick($preview) {
+    $preview
+      .off("click", ".alt-text-edit-btn")
+      .on("click", ".alt-text-edit-btn", (e) => {
+        const parentContainer = $(e.target).closest(
+          ".alt-text-readonly-container"
+        );
+        const altText = parentContainer.find(".alt-text");
+        const correspondingInput = parentContainer.find(".alt-text-input");
+
+        $(e.target).hide();
+        altText.hide();
+        correspondingInput.val(altText.text());
+        correspondingInput.show();
+        e.preventDefault();
+      });
+
+    $preview
+      .off("keypress", ".alt-text-input")
+      .on("keypress", ".alt-text-input", (e) => {
+        if (e.key === "[" || e.key === "]") {
+          e.preventDefault();
+        }
+
+        if (e.key === "Enter") {
+          const index = parseInt(
+            $(e.target).closest(".button-wrapper").attr("data-image-index"),
+            10
+          );
+          const matchingPlaceholder = this.get("composer.reply").match(
+            IMAGE_MARKDOWN_REGEX
+          );
+          const match = matchingPlaceholder[index];
+          const replacement = match.replace(
+            IMAGE_MARKDOWN_REGEX,
+            `![${$(e.target).val()}|$2$3$4]($5)`
+          );
+
+          this.appEvents.trigger("composer:replace-text", match, replacement);
+
+          const parentContainer = $(e.target).closest(
+            ".alt-text-readonly-container"
+          );
+          const altText = parentContainer.find(".alt-text");
+          const altTextButton = parentContainer.find(".alt-text-edit-btn");
+          altText.show();
+          altTextButton.show();
+          $(e.target).hide();
+        }
+      });
+  },
+
   @on("willDestroyElement")
   _composerClosed() {
     this._unbindMobileUploadButton();
@@ -807,6 +862,7 @@ export default Component.extend(ComposerUpload, {
       }
 
       this._registerImageScaleButtonClick($preview);
+      this._registerImageAltTextButtonClick($preview);
 
       this.trigger("previewRefreshed", $preview[0]);
       this.afterRefresh($preview);
diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
index 2883124..b011c28 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
@@ -8,7 +8,13 @@ import {
   updateCurrentUser,
   visible,
 } from "discourse/tests/helpers/qunit-helpers";
-import { click, currentURL, fillIn, visit } from "@ember/test-helpers";
+import {
+  click,
+  currentURL,
+  fillIn,
+  triggerKeyEvent,
+  visit,
+} from "@ember/test-helpers";
 import { skip, test } from "qunit";
 import Draft from "discourse/models/draft";
 import I18n from "I18n";
@@ -968,6 +974,135 @@ acceptance("Composer", function (needs) {
     );
   });
 
+  test("Editing alt text for single image in preview edits alt text in composer", async function (assert) {
+    const altText = ".image-wrapper .button-wrapper .alt-text";
+    const editAltTextButton =
+      ".image-wrapper .button-wrapper .alt-text-edit-btn";
+    const altTextInput = ".image-wrapper .button-wrapper .alt-text-input";
+
+    await visit("/");
+
+    await click("#create-topic");
+    await fillIn(".d-editor-input", `![zorro|200x200](upload://zorro.png)`);
+
+    // placement of elements
+
+    assert.ok(
+      exists(altText),
+      "shows alt text in the image wrapper's button wrapper"
+    );
+
+    assert.ok(
+      exists(editAltTextButton + " .d-icon-pencil"),
+      "alt text edit button with icon is in the image wrapper's button wrapper"
+    );
+
+    assert.ok(
+      exists(altTextInput),
+      "alt text input is in the image wrapper's button wrapper"
+    );
+
+    // logical
+
+    assert.equal(query(altText).innerText, "zorro", "correct alt text");
+    assert.ok(visible(altText), "alt text is visible");
+    assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+    assert.ok(invisible(altTextInput), "alt text input is not visible");
+
+    await click(editAltTextButton);
+
+    assert.ok(invisible(altText), "readonly alt text is not visible");
+    assert.ok(
+      invisible(editAltTextButton),
+      "alt text edit button is not visible"
+    );
+    assert.ok(visible(altTextInput), "alt text input is visible");
+    assert.equal(
+      queryAll(altTextInput).val(),
+      "zorro",
+      "correct alt text in input"
+    );
+
+    await triggerKeyEvent(altTextInput, "keypress", "[".charCodeAt(0));
+    await triggerKeyEvent(altTextInput, "keypress", "]".charCodeAt(0));
+    assert.equal(
+      queryAll(altTextInput).val(),
+      "zorro",

[... diff too long, it was truncated ...]

GitHub sha: 0b495e9ad421317b8d6ce5b49d6df497f012b05f

This commit appears in #14480 which was approved by jjaffeux. It was merged by nattsw.