FIX: Add a confirm and cancel button when editing alt text (#15003)

This commit is contained in:
Natalie Tay 2021-11-19 10:57:09 +08:00 committed by GitHub
parent 1d0faedfbc
commit da9a9a8e65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 527 additions and 326 deletions

View File

@ -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);
},

View File

@ -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
"<a href='https://example.com'>![test|690x313](upload://test.png)</a>",
// 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
"<img src='/images/avatar.png' wight='20' height='20'>",
// 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] =
"<a href='https://example.com'>![test|690x313, 50%](upload://test.png)</a>";
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)
\`<script>alert("xss")</script>\`
`
);
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"
);
});
});

View File

@ -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
"<a href='https://example.com'>![test|690x313](upload://test.png)</a>",
// 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
"<img src='/images/avatar.png' wight='20' height='20'>",
// 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] =
"<a href='https://example.com'>![test|690x313, 50%](upload://test.png)</a>";
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)
\`<script>alert("xss")</script>\`
`
);
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");

View File

@ -67,16 +67,31 @@ function buildScaleButton(selectedScale, scale) {
);
}
function buildImageAltTextButton(altText) {
function buildImageShowAltTextControls(altText) {
return `
<span class="alt-text-readonly-container">
<span class="alt-text" aria-label="${I18n.t(
"composer.image_alt_text.aria_label"
)}">${altText}</span>
<span class="alt-text-edit-btn"><svg aria-hidden="true" class="fa d-icon d-icon-pencil svg-icon svg-string"><use href="#pencil-alt"></use></svg></span>
<input class="alt-text-input" hidden="true" type="text" value="${altText}" />
</span>
`;
<span class="alt-text-readonly-container">
<span class="alt-text" aria-label="${I18n.t(
"composer.image_alt_text.aria_label"
)}">${altText}</span>
<span class="alt-text-edit-btn">
<svg aria-hidden="true" class="fa d-icon d-icon-pencil svg-icon svg-string"><use href="#pencil-alt"></use></svg>
</span>
</span>
`;
}
function buildImageEditAltTextControls(altText) {
return `
<span class="alt-text-edit-container" hidden="true">
<input class="alt-text-input" type="text" value="${altText}" />
<button class="alt-text-edit-ok btn-primary">
<svg class="fa d-icon d-icon-check svg-icon svg-string"><use href="#check"></use></svg>
</button>
<button class="alt-text-edit-cancel btn-default">
<svg class="fa d-icon d-icon-times svg-icon svg-string"><use href="#times"></use></svg>
</button>
</span>
`;
}
// We need this to load after `upload-protocol` which is priority 0
@ -104,7 +119,12 @@ function ruleWithImageControls(oldRule) {
).join("");
result += `</span>`;
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 += "</span></span>";
@ -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) => {

View File

@ -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;
}
}
}