diff --git a/assets/js/__tests__/upload.spec.ts b/assets/js/__tests__/upload.spec.ts
index 06d14d64..bd5d9c5e 100644
--- a/assets/js/__tests__/upload.spec.ts
+++ b/assets/js/__tests__/upload.spec.ts
@@ -25,6 +25,9 @@ const errorResponse = {
};
/* eslint-enable camelcase */
+const tagSets = ['', 'a tag', 'safe', 'one, two, three', 'safe, explicit', 'safe, explicit, three', 'safe, two, three'];
+const tagErrorCounts = [1, 2, 1, 1, 2, 1, 0];
+
describe('Image upload form', () => {
let mockPng: File;
let mockWebm: File;
@@ -58,13 +61,23 @@ describe('Image upload form', () => {
let scraperError: HTMLDivElement;
let fetchButton: HTMLButtonElement;
let tagsEl: HTMLTextAreaElement;
+ let taginputEl: HTMLDivElement;
let sourceEl: HTMLInputElement;
let descrEl: HTMLTextAreaElement;
+ let submitButton: HTMLButtonElement;
const assertFetchButtonIsDisabled = () => {
if (!fetchButton.hasAttribute('disabled')) throw new Error('fetchButton is not disabled');
};
+ const assertSubmitButtonIsDisabled = () => {
+ if (!submitButton.hasAttribute('disabled')) throw new Error('submitButton is not disabled');
+ };
+
+ const assertSubmitButtonIsEnabled = () => {
+ if (submitButton.hasAttribute('disabled')) throw new Error('submitButton is disabled');
+ };
+
beforeEach(() => {
document.documentElement.insertAdjacentHTML(
'beforeend',
@@ -77,7 +90,12 @@ describe('Image upload form', () => {
+
+
+
+
+
`,
);
@@ -87,9 +105,11 @@ describe('Image upload form', () => {
remoteUrl = assertNotUndefined($$('.js-scraper')[1]);
scraperError = assertNotUndefined($$('.js-scraper')[2]);
tagsEl = assertNotNull($('.js-image-tags-input'));
+ taginputEl = assertNotNull($('.js-taginput'));
sourceEl = assertNotNull($('.js-source-url'));
descrEl = assertNotNull($('.js-image-descr-input'));
fetchButton = assertNotNull($('#js-scraper-preview'));
+ submitButton = assertNotNull($('.actions > .button'));
setupImageUpload();
fetchMock.resetMocks();
@@ -193,4 +213,42 @@ describe('Image upload form', () => {
expect(scraperError.innerText).toEqual('Error 1 Error 2');
});
});
+
+ async function submitForm(frm): Promise {
+ return new Promise(resolve => {
+ function onSubmit() {
+ frm.removeEventListener('submit', onSubmit);
+ resolve(true);
+ }
+
+ frm.addEventListener('submit', onSubmit);
+
+ if (!fireEvent.submit(frm)) {
+ frm.removeEventListener('submit', onSubmit);
+ resolve(false);
+ }
+ });
+ }
+
+ it('should prevent form submission if tag checks fail', async () => {
+ for (let i = 0; i < tagSets.length; i += 1) {
+ taginputEl.value = tagSets[i];
+
+ if (await submitForm(form)) {
+ // form submit succeeded
+ await waitFor(() => {
+ assertSubmitButtonIsDisabled();
+ const succeededUnloadEvent = new Event('beforeunload', { cancelable: true });
+ expect(fireEvent(window, succeededUnloadEvent)).toBe(true);
+ });
+ } else {
+ // form submit prevented
+ frm = form;
+ await waitFor(() => {
+ assertSubmitButtonIsEnabled();
+ expect(frm.querySelectorAll('.help-block')).toHaveLength(tagErrorCounts[i]);
+ });
+ }
+ }
+ });
});
diff --git a/assets/js/upload.js b/assets/js/upload.js
index 16d33959..0f931037 100644
--- a/assets/js/upload.js
+++ b/assets/js/upload.js
@@ -2,6 +2,7 @@
* Fetch and display preview images for various image upload forms.
*/
+import { assertNotNull } from './utils/assert';
import { fetchJson, handleError } from './utils/requests';
import { $, $$, clearEl, hideEl, makeEl, showEl } from './utils/dom';
import { addTag } from './tagsinput';
@@ -171,9 +172,98 @@ function setupImageUpload() {
window.removeEventListener('beforeunload', beforeUnload);
}
+ function createTagError(message) {
+ const buttonAfter = $('#tagsinput-save');
+ const errorElement = makeEl('span', { className: 'help-block tag-error', innerText: message });
+
+ buttonAfter.insertAdjacentElement('beforebegin', errorElement);
+ }
+
+ function clearTagErrors() {
+ $$('.tag-error').forEach(el => el.remove());
+ }
+
+ const ratingsTags = ['safe', 'suggestive', 'questionable', 'explicit', 'semi-grimdark', 'grimdark', 'grotesque'];
+
+ // populate tag error helper bars as necessary
+ // return true if all checks pass
+ // return false if any check fails
+ function validateTags() {
+ const tagInput = $('textarea.js-taginput');
+
+ if (!tagInput) {
+ return true;
+ }
+
+ const tagsArr = tagInput.value.split(',').map(t => t.trim());
+
+ const errors = [];
+
+ let hasRating = false;
+ let hasSafe = false;
+ let hasOtherRating = false;
+
+ tagsArr.forEach(tag => {
+ if (ratingsTags.includes(tag)) {
+ hasRating = true;
+ if (tag === 'safe') {
+ hasSafe = true;
+ } else {
+ hasOtherRating = true;
+ }
+ }
+ });
+
+ if (!hasRating) {
+ errors.push('Tag input must contain at least one rating tag');
+ } else if (hasSafe && hasOtherRating) {
+ errors.push('Tag input may not contain any other rating if safe');
+ }
+
+ if (tagsArr.length < 3) {
+ errors.push('Tag input must contain at least 3 tags');
+ }
+
+ errors.forEach(msg => createTagError(msg));
+
+ return errors.length === 0; // true: valid if no errors
+ }
+
+ function disableUploadButton() {
+ const submitButton = $('.button.input--separate-top');
+ if (submitButton !== null) {
+ submitButton.disabled = true;
+ submitButton.innerText = 'Please wait...';
+ }
+
+ // delay is needed because Safari stops the submit if the button is immediately disabled
+ requestAnimationFrame(() => submitButton.setAttribute('disabled', 'disabled'));
+ }
+
+ function submitHandler(event) {
+ // Remove any existing tag error elements
+ clearTagErrors();
+
+ if (validateTags()) {
+ // Disable navigation check
+ unregisterBeforeUnload();
+
+ // Prevent duplicate attempts to submit the form
+ disableUploadButton();
+
+ // Let the form submission complete
+ } else {
+ // Scroll to view validation errors
+ assertNotNull($('.fancy-tag-upload')).scrollIntoView();
+
+ // Prevent the form from being submitted
+ event.preventDefault();
+ }
+ }
+
fileField.addEventListener('change', registerBeforeUnload);
fetchButton.addEventListener('click', registerBeforeUnload);
- form.addEventListener('submit', unregisterBeforeUnload);
+ form.addEventListener('submit', submitHandler);
}
export { setupImageUpload };
diff --git a/lib/philomena_web/templates/image/new.html.slime b/lib/philomena_web/templates/image/new.html.slime
index dfb664d9..2a080eb1 100644
--- a/lib/philomena_web/templates/image/new.html.slime
+++ b/lib/philomena_web/templates/image/new.html.slime
@@ -88,4 +88,4 @@
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "image", conn: @conn
.actions
- = submit "Upload", class: "button input--separate-top", autocomplete: "off", data: [disable_with: "Please wait..."]
+ = submit "Upload", class: "button input--separate-top", autocomplete: "off"