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, + `![${input.value}|$2$3$4]($5)` + ); + + 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, - `![${$(event.target).val()}|$2$3$4]($5)` - ); - - 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 + "![test|690x313](upload://test.png)", + // 1 Image with scaling percentage, should work + "![test|690x313,50%](upload://test.png)", + // 2 image with scaling percentage and a proceeding whitespace, should work + "![test|690x313, 50%](upload://test.png)", + // 3 No dimensions, should not work + "![test](upload://test.jpeg)", + // 4 Wrapped in backticks should not work + "`![test|690x313](upload://test.png)`", + // 5 html image - should not work + "", + // 6 two images one the same line, but both are syntactically correct - both should work + "![onTheSameLine1|200x200](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250](upload://onTheSameLine2.jpeg)", + // 7 & 8 Identical images - both should work + "![identicalImage|300x300](upload://identicalImage.png)", + "![identicalImage|300x300](upload://identicalImage.png)", + // 9 Image with whitespaces in alt - should work + "![image with spaces in alt|690x220](upload://test.png)", + // 10 Image with markdown title - should work + `![image|690x220](upload://test.png "image title")`, + // 11 bbcode - should not work + "[img]/images/avatar.png[/img]", + // 12 Image with data attributes + "![test|foo=bar|690x313,50%|bar=baz](upload://test.png)", + ]; + + 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] = + "![test|690x313, 50%](upload://test.png)"; + 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] = + "![onTheSameLine1|200x200, 50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250](upload://onTheSameLine2.jpeg)"; + 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] = + "![onTheSameLine1|200x200, 50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250, 75%](upload://onTheSameLine2.jpeg)"; + 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] = "![identicalImage|300x300, 50%](upload://identicalImage.png)"; + await click( + queryAll( + ".button-wrapper[data-image-index='5'] .scale-btn[data-scale='50']" + )[0] + ); + assertImageResized(assert, uploads); + + // Try the other dupe + uploads[8] = "![identicalImage|300x300, 75%](upload://identicalImage.png)"; + 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] = `![image|690x220, 75%](upload://test.png "image title")`; + await click( + queryAll( + ".button-wrapper[data-image-index='8'] .scale-btn[data-scale='75']" + )[0] + ); + assertImageResized(assert, uploads); + + // Keep data attributes + uploads[12] = `![test|foo=bar|690x313, 75%|bar=baz](upload://test.png)`; + await click( + queryAll( + ".button-wrapper[data-image-index='9'] .scale-btn[data-scale='75']" + )[0] + ); + assertImageResized(assert, uploads); + + await fillIn( + ".d-editor-input", + ` +![test|690x313](upload://test.png) + +\`\` + ` + ); + + 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", `![zorro|200x200](upload://zorro.png)`); + + 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(), + "![steak|200x200](upload://zorro.png)", + "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", `![zorro|200x200](upload://zorro.png)`); + + await click(editAltTextButton); + + await fillIn(altTextInput, "steak"); + await click(altTextEditOk); + + assert.equal( + queryAll(".d-editor-input").val(), + "![steak|200x200](upload://zorro.png)", + "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", `![zorro|200x200](upload://zorro.png)`); + + await click(editAltTextButton); + + await fillIn(altTextInput, "steak"); + await click(altTextEditCancel); + + assert.equal( + queryAll(".d-editor-input").val(), + "![zorro|200x200](upload://zorro.png)", + "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", + `![zorro|200x200](upload://zorro.png) ![not-zorro|200x200](upload://not-zorro.png)` + ); + await click(editAltTextButton); + + await fillIn(altTextInput, "tomtom"); + await triggerKeyEvent(altTextInput, "keypress", 13); + + assert.equal( + queryAll(".d-editor-input").val(), + `![tomtom|200x200](upload://zorro.png) ![not-zorro|200x200](upload://not-zorro.png)`, + "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", `![zorro|200x200](upload://zorro.png)`); + + await click(editAltTextButton); + + await fillIn(altTextInput, ""); + await triggerKeyEvent(altTextInput, "keypress", 13); + + assert.equal( + queryAll(".d-editor-input").val(), + "![|200x200](upload://zorro.png)", + "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(), + "![tomtom|200x200](upload://zorro.png)", + "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 - "![test|690x313](upload://test.png)", - // 1 Image with scaling percentage, should work - "![test|690x313,50%](upload://test.png)", - // 2 image with scaling percentage and a proceeding whitespace, should work - "![test|690x313, 50%](upload://test.png)", - // 3 No dimensions, should not work - "![test](upload://test.jpeg)", - // 4 Wrapped in backticks should not work - "`![test|690x313](upload://test.png)`", - // 5 html image - should not work - "", - // 6 two images one the same line, but both are syntactically correct - both should work - "![onTheSameLine1|200x200](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250](upload://onTheSameLine2.jpeg)", - // 7 & 8 Identical images - both should work - "![identicalImage|300x300](upload://identicalImage.png)", - "![identicalImage|300x300](upload://identicalImage.png)", - // 9 Image with whitespaces in alt - should work - "![image with spaces in alt|690x220](upload://test.png)", - // 10 Image with markdown title - should work - `![image|690x220](upload://test.png "image title")`, - // 11 bbcode - should not work - "[img]/images/avatar.png[/img]", - // 12 Image with data attributes - "![test|foo=bar|690x313,50%|bar=baz](upload://test.png)", - ]; - - 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] = - "![test|690x313, 50%](upload://test.png)"; - 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] = - "![onTheSameLine1|200x200, 50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250](upload://onTheSameLine2.jpeg)"; - 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] = - "![onTheSameLine1|200x200, 50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250, 75%](upload://onTheSameLine2.jpeg)"; - 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] = "![identicalImage|300x300, 50%](upload://identicalImage.png)"; - await click( - queryAll( - ".button-wrapper[data-image-index='5'] .scale-btn[data-scale='50']" - )[0] - ); - assertImageResized(assert, uploads); - - // Try the other dupe - uploads[8] = "![identicalImage|300x300, 75%](upload://identicalImage.png)"; - 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] = `![image|690x220, 75%](upload://test.png "image title")`; - await click( - queryAll( - ".button-wrapper[data-image-index='8'] .scale-btn[data-scale='75']" - )[0] - ); - assertImageResized(assert, uploads); - - // Keep data attributes - uploads[12] = `![test|foo=bar|690x313, 75%|bar=baz](upload://test.png)`; - await click( - queryAll( - ".button-wrapper[data-image-index='9'] .scale-btn[data-scale='75']" - )[0] - ); - assertImageResized(assert, uploads); - - await fillIn( - ".d-editor-input", - ` -![test|690x313](upload://test.png) - -\`\` - ` - ); - - 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", `![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", - "does not input [ ] keys" - ); - - await fillIn(altTextInput, "steak"); - await triggerKeyEvent(altTextInput, "keypress", 13); - - assert.equal( - queryAll(".d-editor-input").val(), - "![steak|200x200](upload://zorro.png)", - "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", - `![zorro|200x200](upload://zorro.png) ![not-zorro|200x200](upload://not-zorro.png)` - ); - await click(editAltTextButton); - - await fillIn(altTextInput, "tomtom"); - await triggerKeyEvent(altTextInput, "keypress", 13); - - assert.equal( - queryAll(".d-editor-input").val(), - `![tomtom|200x200](upload://zorro.png) ![not-zorro|200x200](upload://not-zorro.png)`, - "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", `![zorro|200x200](upload://zorro.png)`); - - await click(editAltTextButton); - - await fillIn(altTextInput, ""); - await triggerKeyEvent(altTextInput, "keypress", 13); - - assert.equal( - queryAll(".d-editor-input").val(), - "![|200x200](upload://zorro.png)", - "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(), - "![tomtom|200x200](upload://zorro.png)", - "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; + } } }