diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js
index fe511fd6f2f..548556785fd 100644
--- a/app/assets/javascripts/discourse/app/components/composer-editor.js
+++ b/app/assets/javascripts/discourse/app/components/composer-editor.js
@@ -677,6 +677,37 @@ export default Component.extend(ComposerUpload, {
return;
},
+ resetImageControls(buttonWrapper) {
+ const imageResize = buttonWrapper.querySelector(".scale-btn-container");
+ const readonlyContainer = buttonWrapper.querySelector(
+ ".alt-text-readonly-container"
+ );
+ const editContainer = buttonWrapper.querySelector(
+ ".alt-text-edit-container"
+ );
+
+ imageResize.removeAttribute("hidden");
+ readonlyContainer.removeAttribute("hidden");
+ editContainer.setAttribute("hidden", "true");
+ },
+
+ commitAltText(buttonWrapper) {
+ const index = parseInt(buttonWrapper.getAttribute("data-image-index"), 10);
+ const matchingPlaceholder = this.get("composer.reply").match(
+ IMAGE_MARKDOWN_REGEX
+ );
+ const match = matchingPlaceholder[index];
+ const input = buttonWrapper.querySelector("input.alt-text-input");
+ const replacement = match.replace(
+ IMAGE_MARKDOWN_REGEX,
+ ``
+ );
+
+ this.appEvents.trigger("composer:replace-text", match, replacement);
+
+ this.resetImageControls(buttonWrapper);
+ },
+
@bind
_handleAltTextInputKeypress(event) {
if (!event.target.classList.contains("alt-text-input")) {
@@ -688,29 +719,8 @@ export default Component.extend(ComposerUpload, {
}
if (event.key === "Enter") {
- const index = parseInt(
- $(event.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,
- ``
- );
-
- this.appEvents.trigger("composer:replace-text", match, replacement);
-
- const parentContainer = $(event.target).closest(
- ".alt-text-readonly-container"
- );
- const altText = parentContainer.find(".alt-text");
- const altTextButton = parentContainer.find(".alt-text-edit-btn");
- altText.show();
- altTextButton.show();
- $(event.target).hide();
+ const buttonWrapper = event.target.closest(".button-wrapper");
+ this.commitAltText(buttonWrapper);
}
},
@@ -720,21 +730,51 @@ export default Component.extend(ComposerUpload, {
return;
}
- const parentContainer = $(event.target).closest(
+ const buttonWrapper = event.target.closest(".button-wrapper");
+ const imageResize = buttonWrapper.querySelector(".scale-btn-container");
+
+ const readonlyContainer = buttonWrapper.querySelector(
".alt-text-readonly-container"
);
- const altText = parentContainer.find(".alt-text");
- const correspondingInput = parentContainer.find(".alt-text-input");
+ const altText = readonlyContainer.querySelector(".alt-text");
- $(event.target).hide();
- altText.hide();
- correspondingInput.val(altText.text());
- correspondingInput.show();
+ const editContainer = buttonWrapper.querySelector(
+ ".alt-text-edit-container"
+ );
+ const editContainerInput = editContainer.querySelector(".alt-text-input");
+
+ imageResize.setAttribute("hidden", "true");
+ readonlyContainer.setAttribute("hidden", "true");
+ editContainerInput.value = altText.textContent;
+ editContainer.removeAttribute("hidden");
+ editContainerInput.focus();
event.preventDefault();
},
+ @bind
+ _handleAltTextOkButtonClick(event) {
+ if (!event.target.classList.contains("alt-text-edit-ok")) {
+ return;
+ }
+
+ const buttonWrapper = event.target.closest(".button-wrapper");
+ this.commitAltText(buttonWrapper);
+ },
+
+ @bind
+ _handleAltTextCancelButtonClick(event) {
+ if (!event.target.classList.contains("alt-text-edit-cancel")) {
+ return;
+ }
+
+ const buttonWrapper = event.target.closest(".button-wrapper");
+ this.resetImageControls(buttonWrapper);
+ },
+
_registerImageAltTextButtonClick(preview) {
preview.addEventListener("click", this._handleAltTextEditButtonClick);
+ preview.addEventListener("click", this._handleAltTextOkButtonClick);
+ preview.addEventListener("click", this._handleAltTextCancelButtonClick);
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
},
@@ -766,6 +806,8 @@ export default Component.extend(ComposerUpload, {
const preview = this.element.querySelector(".d-editor-preview-wrapper");
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
+ preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
+ preview?.removeEventListener("click", this._handleAltTextCancelButtonClick);
preview?.removeEventListener("keypress", this._handleAltTextInputKeypress);
},
diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js
new file mode 100644
index 00000000000..b5148fa888d
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js
@@ -0,0 +1,362 @@
+import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
+import {
+ acceptance,
+ count,
+ exists,
+ invisible,
+ query,
+ queryAll,
+ visible,
+} from "discourse/tests/helpers/qunit-helpers";
+import { test } from "qunit";
+
+acceptance("Composer - Image Preview", function (needs) {
+ needs.user();
+ needs.settings({ enable_whispers: true });
+ needs.site({ can_tag_topics: true });
+ needs.pretender((server, helper) => {
+ server.post("/uploads/lookup-urls", () => {
+ return helper.response([]);
+ });
+ server.get("/posts/419", () => {
+ return helper.response({ id: 419 });
+ });
+ server.get("/u/is_local_username", () => {
+ return helper.response({
+ valid: [],
+ valid_groups: ["staff"],
+ mentionable_groups: [{ name: "staff", user_count: 30 }],
+ cannot_see: [],
+ max_users_notified_per_group_mention: 100,
+ });
+ });
+ });
+
+ const assertImageResized = (assert, uploads) => {
+ assert.strictEqual(
+ queryAll(".d-editor-input").val(),
+ uploads.join("\n"),
+ "it resizes uploaded image"
+ );
+ };
+
+ test("Image resizing buttons", async function (assert) {
+ await visit("/");
+ await click("#create-topic");
+
+ let uploads = [
+ // 0 Default markdown with dimensions- should work
+ "",
+ // 1 Image with scaling percentage, should work
+ "",
+ // 2 image with scaling percentage and a proceeding whitespace, should work
+ "",
+ // 3 No dimensions, should not work
+ "",
+ // 4 Wrapped in backticks should not work
+ "``",
+ // 5 html image - should not work
+ "
",
+ // 6 two images one the same line, but both are syntactically correct - both should work
+ " ",
+ // 7 & 8 Identical images - both should work
+ "",
+ "",
+ // 9 Image with whitespaces in alt - should work
+ "",
+ // 10 Image with markdown title - should work
+ ``,
+ // 11 bbcode - should not work
+ "[img]/images/avatar.png[/img]",
+ // 12 Image with data attributes
+ "",
+ ];
+
+ await fillIn(".d-editor-input", uploads.join("\n"));
+
+ assert.strictEqual(
+ count(".button-wrapper"),
+ 10,
+ "it adds correct amount of scaling button groups"
+ );
+
+ // Default
+ uploads[0] =
+ "";
+ await click(
+ queryAll(
+ ".button-wrapper[data-image-index='0'] .scale-btn[data-scale='50']"
+ )[0]
+ );
+ assertImageResized(assert, uploads);
+
+ // Targets the correct image if two on the same line
+ uploads[6] =
+ " ";
+ await click(
+ queryAll(
+ ".button-wrapper[data-image-index='3'] .scale-btn[data-scale='50']"
+ )[0]
+ );
+ assertImageResized(assert, uploads);
+
+ // Try the other image on the same line
+ uploads[6] =
+ " ";
+ await click(
+ queryAll(
+ ".button-wrapper[data-image-index='4'] .scale-btn[data-scale='75']"
+ )[0]
+ );
+ assertImageResized(assert, uploads);
+
+ // Make sure we target the correct image if there are duplicates
+ uploads[7] = "";
+ await click(
+ queryAll(
+ ".button-wrapper[data-image-index='5'] .scale-btn[data-scale='50']"
+ )[0]
+ );
+ assertImageResized(assert, uploads);
+
+ // Try the other dupe
+ uploads[8] = "";
+ await click(
+ queryAll(
+ ".button-wrapper[data-image-index='6'] .scale-btn[data-scale='75']"
+ )[0]
+ );
+ assertImageResized(assert, uploads);
+
+ // Don't mess with image titles
+ uploads[10] = ``;
+ await click(
+ queryAll(
+ ".button-wrapper[data-image-index='8'] .scale-btn[data-scale='75']"
+ )[0]
+ );
+ assertImageResized(assert, uploads);
+
+ // Keep data attributes
+ uploads[12] = ``;
+ await click(
+ queryAll(
+ ".button-wrapper[data-image-index='9'] .scale-btn[data-scale='75']"
+ )[0]
+ );
+ assertImageResized(assert, uploads);
+
+ await fillIn(
+ ".d-editor-input",
+ `
+
+
+\`\`
+ `
+ );
+
+ assert.ok(
+ !exists("script"),
+ "it does not unescape script tags in code blocks"
+ );
+ });
+
+ test("Editing alt text (with enter key) for single image in preview updates alt text in composer", async function (assert) {
+ const scaleButtonContainer = ".scale-btn-container";
+
+ const readonlyAltText = ".alt-text";
+ const editAltTextButton = ".alt-text-edit-btn";
+
+ const altTextInput = ".alt-text-input";
+ const altTextEditOk = ".alt-text-edit-ok";
+ const altTextEditCancel = ".alt-text-edit-cancel";
+
+ await visit("/");
+
+ await click("#create-topic");
+ await fillIn(".d-editor-input", ``);
+
+ assert.equal(query(readonlyAltText).innerText, "zorro", "correct alt text");
+ assert.ok(visible(readonlyAltText), "alt text is visible");
+ assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+ assert.ok(invisible(altTextInput), "alt text input is hidden");
+ assert.ok(invisible(altTextEditOk), "alt text edit ok button is hidden");
+ assert.ok(invisible(altTextEditCancel), "alt text edit cancel is hidden");
+
+ await click(editAltTextButton);
+
+ assert.ok(invisible(scaleButtonContainer), "scale buttons are hidden");
+ assert.ok(invisible(readonlyAltText), "alt text is hidden");
+ assert.ok(invisible(editAltTextButton), "alt text edit button is hidden");
+ assert.ok(visible(altTextInput), "alt text input is visible");
+ assert.ok(visible(altTextEditOk), "alt text edit ok button is visible");
+ assert.ok(visible(altTextEditCancel), "alt text edit cancel is hidden");
+ 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",
+ "does not input [ ] keys"
+ );
+
+ await fillIn(altTextInput, "steak");
+ await triggerKeyEvent(altTextInput, "keypress", 13);
+
+ assert.equal(
+ queryAll(".d-editor-input").val(),
+ "",
+ "alt text updated"
+ );
+ assert.equal(
+ query(readonlyAltText).innerText,
+ "steak",
+ "shows the alt text"
+ );
+ assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+ assert.ok(visible(scaleButtonContainer), "scale buttons are visible");
+ assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+ assert.ok(invisible(altTextInput), "alt text input is hidden");
+ assert.ok(invisible(altTextEditOk), "alt text edit ok button is hidden");
+ assert.ok(invisible(altTextEditCancel), "alt text edit cancel is hidden");
+ });
+
+ test("Editing alt text (with check button) in preview updates alt text in composer", async function (assert) {
+ const scaleButtonContainer = ".scale-btn-container";
+ const readonlyAltText = ".alt-text";
+ const editAltTextButton = ".alt-text-edit-btn";
+
+ const altTextInput = ".alt-text-input";
+ const altTextEditOk = ".alt-text-edit-ok";
+ const altTextEditCancel = ".alt-text-edit-cancel";
+
+ await visit("/");
+
+ await click("#create-topic");
+ await fillIn(".d-editor-input", ``);
+
+ await click(editAltTextButton);
+
+ await fillIn(altTextInput, "steak");
+ await click(altTextEditOk);
+
+ assert.equal(
+ queryAll(".d-editor-input").val(),
+ "",
+ "alt text updated"
+ );
+ assert.equal(
+ query(readonlyAltText).innerText,
+ "steak",
+ "shows the alt text"
+ );
+
+ assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+ assert.ok(visible(scaleButtonContainer), "scale buttons are visible");
+ assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+ assert.ok(invisible(altTextInput), "alt text input is hidden");
+ assert.ok(invisible(altTextEditOk), "alt text edit ok button is hidden");
+ assert.ok(invisible(altTextEditCancel), "alt text edit cancel is hidden");
+ });
+
+ test("Cancel alt text edit in preview does not update alt text in composer", async function (assert) {
+ const scaleButtonContainer = ".scale-btn-container";
+
+ const readonlyAltText = ".alt-text";
+ const editAltTextButton = ".alt-text-edit-btn";
+
+ const altTextInput = ".alt-text-input";
+ const altTextEditOk = ".alt-text-edit-ok";
+ const altTextEditCancel = ".alt-text-edit-cancel";
+
+ await visit("/");
+
+ await click("#create-topic");
+ await fillIn(".d-editor-input", ``);
+
+ await click(editAltTextButton);
+
+ await fillIn(altTextInput, "steak");
+ await click(altTextEditCancel);
+
+ assert.equal(
+ queryAll(".d-editor-input").val(),
+ "",
+ "alt text not updated"
+ );
+ assert.equal(
+ query(readonlyAltText).innerText,
+ "zorro",
+ "shows the unedited alt text"
+ );
+
+ assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+ assert.ok(visible(scaleButtonContainer), "scale buttons are visible");
+ assert.ok(visible(editAltTextButton), "alt text edit button is visible");
+ assert.ok(invisible(altTextInput), "alt text input is hidden");
+ assert.ok(invisible(altTextEditOk), "alt text edit ok button is hidden");
+ assert.ok(invisible(altTextEditCancel), "alt text edit cancel is hidden");
+ });
+
+ test("Editing alt text for one of two images in preview updates correct alt text in composer", async function (assert) {
+ const editAltTextButton = ".alt-text-edit-btn";
+ const altTextInput = ".alt-text-input";
+
+ await visit("/");
+ await click("#create-topic");
+
+ await fillIn(
+ ".d-editor-input",
+ ` `
+ );
+ await click(editAltTextButton);
+
+ await fillIn(altTextInput, "tomtom");
+ await triggerKeyEvent(altTextInput, "keypress", 13);
+
+ assert.equal(
+ queryAll(".d-editor-input").val(),
+ ` `,
+ "the correct image's alt text updated"
+ );
+ });
+
+ test("Deleting alt text for image empties alt text in composer and allows further modification", async function (assert) {
+ const altText = ".alt-text";
+ const editAltTextButton = ".alt-text-edit-btn";
+ const altTextInput = ".alt-text-input";
+
+ await visit("/");
+
+ await click("#create-topic");
+ await fillIn(".d-editor-input", ``);
+
+ await click(editAltTextButton);
+
+ await fillIn(altTextInput, "");
+ await triggerKeyEvent(altTextInput, "keypress", 13);
+
+ assert.equal(
+ queryAll(".d-editor-input").val(),
+ "",
+ "alt text updated"
+ );
+ assert.equal(query(altText).innerText, "", "shows the alt text");
+
+ await click(editAltTextButton);
+
+ await fillIn(altTextInput, "tomtom");
+ await triggerKeyEvent(altTextInput, "keypress", 13);
+
+ assert.equal(
+ queryAll(".d-editor-input").val(),
+ "",
+ "alt text updated"
+ );
+ });
+});
diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
index 44d87a77d37..00d1eea0759 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
@@ -1,3 +1,10 @@
+import { run } from "@ember/runloop";
+import { click, currentURL, fillIn, 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 Draft from "discourse/models/draft";
import {
acceptance,
count,
@@ -8,24 +15,11 @@ import {
updateCurrentUser,
visible,
} from "discourse/tests/helpers/qunit-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";
-import { CREATE_TOPIC, NEW_TOPIC_KEY } from "discourse/models/composer";
-import { withPluginApi } from "discourse/lib/plugin-api";
-import { Promise } from "rsvp";
-import { run } from "@ember/runloop";
import selectKit from "discourse/tests/helpers/select-kit-helper";
+import I18n from "I18n";
+import { skip, test } from "qunit";
+import { Promise } from "rsvp";
import sinon from "sinon";
-import { toggleCheckDraftPopup } from "discourse/controllers/composer";
-import LinkLookup from "discourse/lib/link-lookup";
acceptance("Composer", function (needs) {
needs.user();
@@ -808,14 +802,6 @@ acceptance("Composer", function (needs) {
);
});
- const assertImageResized = (assert, uploads) => {
- assert.strictEqual(
- queryAll(".d-editor-input").val(),
- uploads.join("\n"),
- "it resizes uploaded image"
- );
- };
-
test("reply button has envelope icon when replying to private message", async function (assert) {
await visit("/t/34");
await click("article#post_3 button.reply");
@@ -848,256 +834,6 @@ acceptance("Composer", function (needs) {
);
});
- test("Image resizing buttons", async function (assert) {
- await visit("/");
- await click("#create-topic");
-
- let uploads = [
- // 0 Default markdown with dimensions- should work
- "",
- // 1 Image with scaling percentage, should work
- "",
- // 2 image with scaling percentage and a proceeding whitespace, should work
- "",
- // 3 No dimensions, should not work
- "",
- // 4 Wrapped in backticks should not work
- "``",
- // 5 html image - should not work
- "
",
- // 6 two images one the same line, but both are syntactically correct - both should work
- " ",
- // 7 & 8 Identical images - both should work
- "",
- "",
- // 9 Image with whitespaces in alt - should work
- "",
- // 10 Image with markdown title - should work
- ``,
- // 11 bbcode - should not work
- "[img]/images/avatar.png[/img]",
- // 12 Image with data attributes
- "",
- ];
-
- await fillIn(".d-editor-input", uploads.join("\n"));
-
- assert.strictEqual(
- count(".button-wrapper"),
- 10,
- "it adds correct amount of scaling button groups"
- );
-
- // Default
- uploads[0] =
- "";
- await click(
- queryAll(
- ".button-wrapper[data-image-index='0'] .scale-btn[data-scale='50']"
- )[0]
- );
- assertImageResized(assert, uploads);
-
- // Targets the correct image if two on the same line
- uploads[6] =
- " ";
- await click(
- queryAll(
- ".button-wrapper[data-image-index='3'] .scale-btn[data-scale='50']"
- )[0]
- );
- assertImageResized(assert, uploads);
-
- // Try the other image on the same line
- uploads[6] =
- " ";
- await click(
- queryAll(
- ".button-wrapper[data-image-index='4'] .scale-btn[data-scale='75']"
- )[0]
- );
- assertImageResized(assert, uploads);
-
- // Make sure we target the correct image if there are duplicates
- uploads[7] = "";
- await click(
- queryAll(
- ".button-wrapper[data-image-index='5'] .scale-btn[data-scale='50']"
- )[0]
- );
- assertImageResized(assert, uploads);
-
- // Try the other dupe
- uploads[8] = "";
- await click(
- queryAll(
- ".button-wrapper[data-image-index='6'] .scale-btn[data-scale='75']"
- )[0]
- );
- assertImageResized(assert, uploads);
-
- // Don't mess with image titles
- uploads[10] = ``;
- await click(
- queryAll(
- ".button-wrapper[data-image-index='8'] .scale-btn[data-scale='75']"
- )[0]
- );
- assertImageResized(assert, uploads);
-
- // Keep data attributes
- uploads[12] = ``;
- await click(
- queryAll(
- ".button-wrapper[data-image-index='9'] .scale-btn[data-scale='75']"
- )[0]
- );
- assertImageResized(assert, uploads);
-
- await fillIn(
- ".d-editor-input",
- `
-
-
-\`\`
- `
- );
-
- assert.ok(
- !exists("script"),
- "it does not unescape script tags in code blocks"
- );
- });
-
- 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", ``);
-
- // 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",
- "does not input [ ] keys"
- );
-
- await fillIn(altTextInput, "steak");
- await triggerKeyEvent(altTextInput, "keypress", 13);
-
- assert.equal(
- queryAll(".d-editor-input").val(),
- "",
- "alt text updated"
- );
- assert.equal(query(altText).innerText, "steak", "shows the alt text");
- assert.ok(visible(editAltTextButton), "alt text edit button is visible");
- assert.ok(invisible(altTextInput), "alt text input is not visible");
- });
-
- test("Editing alt text for one of two images in preview updates correct alt text in composer", async function (assert) {
- 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",
- ` `
- );
- await click(editAltTextButton);
-
- await fillIn(altTextInput, "tomtom");
- await triggerKeyEvent(altTextInput, "keypress", 13);
-
- assert.equal(
- queryAll(".d-editor-input").val(),
- ` `,
- "the correct image's alt text updated"
- );
- });
-
- test("Deleting alt text for image empties alt text in composer and allows further modification", 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", ``);
-
- await click(editAltTextButton);
-
- await fillIn(altTextInput, "");
- await triggerKeyEvent(altTextInput, "keypress", 13);
-
- assert.equal(
- queryAll(".d-editor-input").val(),
- "",
- "alt text updated"
- );
- assert.equal(query(altText).innerText, "", "shows the alt text");
-
- await click(editAltTextButton);
-
- await fillIn(altTextInput, "tomtom");
- await triggerKeyEvent(altTextInput, "keypress", 13);
-
- assert.equal(
- queryAll(".d-editor-input").val(),
- "",
- "alt text updated"
- );
- });
-
skip("Shows duplicate_link notice", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-buttons .create");
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-controls.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-controls.js
index 6c49d5df004..56f51b8d207 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-controls.js
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/image-controls.js
@@ -67,16 +67,31 @@ function buildScaleButton(selectedScale, scale) {
);
}
-function buildImageAltTextButton(altText) {
+function buildImageShowAltTextControls(altText) {
return `
-
- ${altText}
-
-
-
-`;
+
+ ${altText}
+
+
+
+
+ `;
+}
+
+function buildImageEditAltTextControls(altText) {
+ return `
+
+
+
+
+
+ `;
}
// We need this to load after `upload-protocol` which is priority 0
@@ -104,7 +119,12 @@ function ruleWithImageControls(oldRule) {
).join("");
result += ``;
- result += buildImageAltTextButton(token.attrs[token.attrIndex("alt")][1]);
+ result += buildImageShowAltTextControls(
+ token.attrs[token.attrIndex("alt")][1]
+ );
+ result += buildImageEditAltTextControls(
+ token.attrs[token.attrIndex("alt")][1]
+ );
result += "";
@@ -128,14 +148,25 @@ export function setup(helper) {
"span.scale-btn[data-scale]",
"span.button-wrapper[data-image-index]",
"span[aria-label]",
+
+ "span.alt-text-container",
+
"span.alt-text-readonly-container",
"span.alt-text-readonly-container.alt-text",
"span.alt-text-readonly-container.alt-text-edit-btn",
"svg[class=fa d-icon d-icon-pencil svg-icon svg-string]",
"use[href=#pencil-alt]",
+
+ "span.alt-text-edit-container",
+ "span[hidden=true]",
"input[type=text]",
- "input[hidden=true]",
"input[class=alt-text-input]",
+ "button[class=alt-text-edit-ok btn-primary]",
+ "svg[class=fa d-icon d-icon-check svg-icon svg-string]",
+ "use[href=#check]",
+ "button[class=alt-text-edit-cancel btn-default]",
+ "svg[class=fa d-icon d-icon-times svg-icon svg-string]",
+ "use[href=#times]",
]);
helper.registerPlugin((md) => {
diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss
index 48446ae7ae7..54611375944 100644
--- a/app/assets/stylesheets/common/d-editor.scss
+++ b/app/assets/stylesheets/common/d-editor.scss
@@ -179,6 +179,9 @@
}
.button-wrapper {
+ min-width: 10em;
+ width: 100%;
+
display: flex;
flex-wrap: wrap;
gap: 0 0.5em;
@@ -190,15 +193,19 @@
opacity: 0;
transition: all 0.25s;
z-index: 1; // needs to be higher than image
- width: 100%;
background: var(--secondary); // for when images are wider than controls
.scale-btn-container,
- .alt-text-readonly-container {
+ .alt-text-readonly-container,
+ .alt-text-edit-container {
background: var(--secondary);
display: flex;
height: var(--resizer-height);
align-items: center;
+
+ &[hidden] {
+ display: none;
+ }
}
.scale-btn {
@@ -222,8 +229,8 @@
}
.alt-text-readonly-container {
- max-width: 100%;
flex: 1 1;
+ width: 100%;
.alt-text {
margin-right: 0.5em;
@@ -233,25 +240,48 @@
max-width: 100%;
}
- .alt-text-edit-btn svg {
- padding-right: 0.5em;
- pointer-events: none;
+ .alt-text-edit-btn {
+ cursor: pointer;
+
+ svg {
+ padding-right: 0.5em;
+ }
+ }
+ }
+
+ .alt-text-edit-container {
+ margin-top: 0.25em;
+ gap: 0 0.25em;
+ flex: 1;
+
+ .alt-text-input,
+ .alt-text-edit-ok,
+ .alt-text-edit-cancel {
+ height: var(--resizer-height);
}
.alt-text-input {
- height: var(--resizer-height);
- width: 100%;
+ flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
- max-width: 100%;
margin: 0;
+ }
- &[hidden="true"] {
- display: none;
+ .alt-text-edit-ok,
+ .alt-text-edit-cancel {
+ border: none;
+ width: var(--resizer-height);
+
+ svg {
+ margin: 0;
}
}
}
+
+ svg {
+ pointer-events: none;
+ }
}
}