mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FIX: Add a confirm and cancel button when editing alt text (#15003)
This commit is contained in:
parent
1d0faedfbc
commit
da9a9a8e65
@ -677,6 +677,37 @@ export default Component.extend(ComposerUpload, {
|
|||||||
return;
|
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
|
@bind
|
||||||
_handleAltTextInputKeypress(event) {
|
_handleAltTextInputKeypress(event) {
|
||||||
if (!event.target.classList.contains("alt-text-input")) {
|
if (!event.target.classList.contains("alt-text-input")) {
|
||||||
@ -688,29 +719,8 @@ export default Component.extend(ComposerUpload, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
const index = parseInt(
|
const buttonWrapper = event.target.closest(".button-wrapper");
|
||||||
$(event.target).closest(".button-wrapper").attr("data-image-index"),
|
this.commitAltText(buttonWrapper);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -720,21 +730,51 @@ export default Component.extend(ComposerUpload, {
|
|||||||
return;
|
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"
|
".alt-text-readonly-container"
|
||||||
);
|
);
|
||||||
const altText = parentContainer.find(".alt-text");
|
const altText = readonlyContainer.querySelector(".alt-text");
|
||||||
const correspondingInput = parentContainer.find(".alt-text-input");
|
|
||||||
|
|
||||||
$(event.target).hide();
|
const editContainer = buttonWrapper.querySelector(
|
||||||
altText.hide();
|
".alt-text-edit-container"
|
||||||
correspondingInput.val(altText.text());
|
);
|
||||||
correspondingInput.show();
|
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();
|
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) {
|
_registerImageAltTextButtonClick(preview) {
|
||||||
preview.addEventListener("click", this._handleAltTextEditButtonClick);
|
preview.addEventListener("click", this._handleAltTextEditButtonClick);
|
||||||
|
preview.addEventListener("click", this._handleAltTextOkButtonClick);
|
||||||
|
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
|
||||||
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
|
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -766,6 +806,8 @@ export default Component.extend(ComposerUpload, {
|
|||||||
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
||||||
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
|
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
|
||||||
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
|
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
|
||||||
|
preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
|
||||||
|
preview?.removeEventListener("click", this._handleAltTextCancelButtonClick);
|
||||||
preview?.removeEventListener("keypress", this._handleAltTextInputKeypress);
|
preview?.removeEventListener("keypress", this._handleAltTextInputKeypress);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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'></a>",
|
||||||
|
// 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
|
||||||
|
"<img src='/images/avatar.png' wight='20' height='20'>",
|
||||||
|
// 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] =
|
||||||
|
"<a href='https://example.com'></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] =
|
||||||
|
" ";
|
||||||
|
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",
|
||||||
|
`
|
||||||
|

|
||||||
|
|
||||||
|
\`<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", ``);
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -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 {
|
import {
|
||||||
acceptance,
|
acceptance,
|
||||||
count,
|
count,
|
||||||
@ -8,24 +15,11 @@ import {
|
|||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
visible,
|
visible,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} 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 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 sinon from "sinon";
|
||||||
import { toggleCheckDraftPopup } from "discourse/controllers/composer";
|
|
||||||
import LinkLookup from "discourse/lib/link-lookup";
|
|
||||||
|
|
||||||
acceptance("Composer", function (needs) {
|
acceptance("Composer", function (needs) {
|
||||||
needs.user();
|
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) {
|
test("reply button has envelope icon when replying to private message", async function (assert) {
|
||||||
await visit("/t/34");
|
await visit("/t/34");
|
||||||
await click("article#post_3 button.reply");
|
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'></a>",
|
|
||||||
// 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
|
|
||||||
"<img src='/images/avatar.png' wight='20' height='20'>",
|
|
||||||
// 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] =
|
|
||||||
"<a href='https://example.com'></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] =
|
|
||||||
" ";
|
|
||||||
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",
|
|
||||||
`
|
|
||||||

|
|
||||||
|
|
||||||
\`<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", ``);
|
|
||||||
|
|
||||||
// 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) {
|
skip("Shows duplicate_link notice", async function (assert) {
|
||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
await click("#topic-footer-buttons .create");
|
await click("#topic-footer-buttons .create");
|
||||||
|
@ -67,14 +67,29 @@ function buildScaleButton(selectedScale, scale) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildImageAltTextButton(altText) {
|
function buildImageShowAltTextControls(altText) {
|
||||||
return `
|
return `
|
||||||
<span class="alt-text-readonly-container">
|
<span class="alt-text-readonly-container">
|
||||||
<span class="alt-text" aria-label="${I18n.t(
|
<span class="alt-text" aria-label="${I18n.t(
|
||||||
"composer.image_alt_text.aria_label"
|
"composer.image_alt_text.aria_label"
|
||||||
)}">${altText}</span>
|
)}">${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 class="alt-text-edit-btn">
|
||||||
<input class="alt-text-input" hidden="true" type="text" value="${altText}" />
|
<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>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -104,7 +119,12 @@ function ruleWithImageControls(oldRule) {
|
|||||||
).join("");
|
).join("");
|
||||||
result += `</span>`;
|
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>";
|
result += "</span></span>";
|
||||||
|
|
||||||
@ -128,14 +148,25 @@ export function setup(helper) {
|
|||||||
"span.scale-btn[data-scale]",
|
"span.scale-btn[data-scale]",
|
||||||
"span.button-wrapper[data-image-index]",
|
"span.button-wrapper[data-image-index]",
|
||||||
"span[aria-label]",
|
"span[aria-label]",
|
||||||
|
|
||||||
|
"span.alt-text-container",
|
||||||
|
|
||||||
"span.alt-text-readonly-container",
|
"span.alt-text-readonly-container",
|
||||||
"span.alt-text-readonly-container.alt-text",
|
"span.alt-text-readonly-container.alt-text",
|
||||||
"span.alt-text-readonly-container.alt-text-edit-btn",
|
"span.alt-text-readonly-container.alt-text-edit-btn",
|
||||||
"svg[class=fa d-icon d-icon-pencil svg-icon svg-string]",
|
"svg[class=fa d-icon d-icon-pencil svg-icon svg-string]",
|
||||||
"use[href=#pencil-alt]",
|
"use[href=#pencil-alt]",
|
||||||
|
|
||||||
|
"span.alt-text-edit-container",
|
||||||
|
"span[hidden=true]",
|
||||||
"input[type=text]",
|
"input[type=text]",
|
||||||
"input[hidden=true]",
|
|
||||||
"input[class=alt-text-input]",
|
"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) => {
|
helper.registerPlugin((md) => {
|
||||||
|
@ -179,6 +179,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-wrapper {
|
.button-wrapper {
|
||||||
|
min-width: 10em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0 0.5em;
|
gap: 0 0.5em;
|
||||||
@ -190,15 +193,19 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all 0.25s;
|
transition: all 0.25s;
|
||||||
z-index: 1; // needs to be higher than image
|
z-index: 1; // needs to be higher than image
|
||||||
width: 100%;
|
|
||||||
background: var(--secondary); // for when images are wider than controls
|
background: var(--secondary); // for when images are wider than controls
|
||||||
|
|
||||||
.scale-btn-container,
|
.scale-btn-container,
|
||||||
.alt-text-readonly-container {
|
.alt-text-readonly-container,
|
||||||
|
.alt-text-edit-container {
|
||||||
background: var(--secondary);
|
background: var(--secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
height: var(--resizer-height);
|
height: var(--resizer-height);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scale-btn {
|
.scale-btn {
|
||||||
@ -222,8 +229,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alt-text-readonly-container {
|
.alt-text-readonly-container {
|
||||||
max-width: 100%;
|
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.alt-text {
|
.alt-text {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
@ -233,25 +240,48 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text-edit-btn svg {
|
.alt-text-edit-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
pointer-events: none;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
.alt-text-input {
|
||||||
height: var(--resizer-height);
|
flex: 1;
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&[hidden="true"] {
|
.alt-text-edit-ok,
|
||||||
display: none;
|
.alt-text-edit-cancel {
|
||||||
|
border: none;
|
||||||
|
width: var(--resizer-height);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user