From 7e6e9fdda156bf9286365da54dd2d2834f705652 Mon Sep 17 00:00:00 2001 From: Alex Gibson Date: Thu, 26 Oct 2023 13:32:01 +0100 Subject: [PATCH] Port basket newsletter JS to Protocol (Fixes #839) --- .eslintrc.js | 15 +- CHANGELOG.md | 1 + assets/js/protocol/newsletter.js | 366 +++++++++++++-- .../sass/protocol/base/elements/_forms.scss | 8 +- .../protocol/components/_newsletter-form.scss | 18 +- components/forms/02-select/select.config.yml | 4 +- components/newsletter/newsletter--errors.html | 52 ++- .../newsletter/newsletter--success.html | 53 ++- components/newsletter/newsletter.config.yml | 16 +- components/newsletter/newsletter.html | 69 ++- components/newsletter/readme.md | 28 +- package-lock.json | 148 ++++++ package.json | 1 + tests/karma.conf.js | 8 +- tests/unit/newsletter.js | 427 ++++++++++++++++++ 15 files changed, 1093 insertions(+), 121 deletions(-) create mode 100644 tests/unit/newsletter.js diff --git a/.eslintrc.js b/.eslintrc.js index da0a6981..83959ae7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,6 @@ module.exports = { 'browser': true, 'commonjs': true, 'es2017': true, - 'jasmine': true, 'node': true }, extends: [ @@ -74,5 +73,17 @@ module.exports = { // Disallow the use of `console` // https://eslint.org/docs/rules/no-console 'no-console': 'error' - } + }, + overrides: [ + { + // JS Jasmine test files. + files: ['tests/unit/**/*.js'], + env: { + jasmine: true + }, + globals: { + sinon: 'writable' + } + } + ] }; diff --git a/CHANGELOG.md b/CHANGELOG.md index a92b109d..bf9e7a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +* **js:** Update newsletter component to include JS to post directly to Basket (#839). * **css:** Add CSS utility class for centered text and document existing title utility classes (#897). ## Bug Fixes diff --git a/assets/js/protocol/newsletter.js b/assets/js/protocol/newsletter.js index 15dfcc42..3b4c7211 100644 --- a/assets/js/protocol/newsletter.js +++ b/assets/js/protocol/newsletter.js @@ -2,49 +2,349 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -const MzpNewsletter = {}; - -// !! This file assumes only one signup form per page !! -// Expand email form on input focus or submit if details aren't visible -MzpNewsletter.init = () => { - const newsletterForm = document.getElementById('newsletter-form'); - let submitButton; - let formDetails; - let emailField; - let formExpanded; - - function emailFormShowDetails() { - if (!formExpanded) { - formDetails.style.display = 'block'; - formExpanded = true; +let form; + +const ERROR_LIST = { + COUNTRY_ERROR: 'Country not selected', + EMAIL_INVALID_ERROR: 'Invalid email address', + EMAIL_UNKNOWN_ERROR: 'Email address not known', + LANGUAGE_ERROR: 'Language not selected', + LEGAL_TERMS_ERROR: 'Terms not checked', + NEWSLETTER_ERROR: 'Newsletter not selected', + NOT_FOUND: 'Not Found', + PRIVACY_POLICY_ERROR: 'Privacy policy not checked', + REASON_ERROR: 'Reason not selected' +}; + +const MzpNewsletter = { + /** + * Really primitive validation (e.g a@a) + * matches built-in validation in Firefox + * @param {String} email + * @returns {Boolean} + */ + checkEmailValidity: (email) => { + return /\S+@\S+/.test(email); + }, + + /** + * Add disabled property to all form fields. + * @param {HTMLFormElement} form + */ + disableFormFields: (form) => { + const formFields = form.querySelectorAll('input, button, select'); + + for (let i = 0; i < formFields.length; i++) { + formFields[i].disabled = true; } - } + }, - if (newsletterForm) { - submitButton = document.getElementById('newsletter-submit'); - formDetails = document.getElementById('newsletter-details'); - emailField = document.querySelector('.mzp-js-email-field'); - formExpanded = window.getComputedStyle(formDetails).display === 'none' ? false : true; + /** + * Remove disabled property to all form fields. + * @param {HTMLFormElement} form + */ + enableFormFields: (form) => { + const formFields = form.querySelectorAll('input, button, select'); - emailField.addEventListener('focus', () => { - emailFormShowDetails(); - }, false); + for (let i = 0; i < formFields.length; i++) { + formFields[i].disabled = false; + } + }, - submitButton.addEventListener('click', (e) => { - if (!formExpanded) { - e.preventDefault(); - emailFormShowDetails(); + /** + * Hide all visible form error labels. + * @param {HTMLFormElement} form + */ + clearFormErrors: (form) => { + const errorMsgs = form.querySelectorAll('.mzp-c-form-errors li'); + + form.querySelector('.mzp-c-form-errors').classList.add('hidden'); + + for (let i = 0; i < errorMsgs.length; i++) { + errorMsgs[i].classList.add('hidden'); + } + }, + + handleFormError: (msg) => { + let error; + + MzpNewsletter.enableFormFields(form); + + form.querySelector('.mzp-c-form-errors').classList.remove('hidden'); + + switch (msg) { + case ERROR_LIST.EMAIL_INVALID_ERROR: + error = form.querySelector('.error-email-invalid'); + break; + case ERROR_LIST.NEWSLETTER_ERROR: + form.querySelector( + '.error-newsletter-checkbox' + ).classList.remove('hidden'); + break; + case ERROR_LIST.COUNTRY_ERROR: + error = form.querySelector('.error-select-country'); + break; + case ERROR_LIST.LANGUAGE_ERROR: + error = form.querySelector('.error-select-language'); + break; + case ERROR_LIST.PRIVACY_POLICY_ERROR: + error = form.querySelector('.error-privacy-policy'); + break; + case ERROR_LIST.LEGAL_TERMS_ERROR: + error = form.querySelector('.error-terms'); + break; + default: + error = form.querySelector('.error-try-again-later'); + } + + if (error) { + error.classList.remove('hidden'); + } + + if (typeof MzpNewsletter.customErrorCallback === 'function') { + MzpNewsletter.customErrorCallback(msg); + } + }, + + handleFormSuccess: () => { + form.classList.add('hidden'); + document.getElementById('newsletter-thanks').classList.remove('hidden'); + + if (typeof MzpNewsletter.customSuccessCallback === 'function') { + MzpNewsletter.customSuccessCallback(); + } + }, + + /** + * Perform an AJAX POST to Basket + * @param {String} email + * @param {String} params (URI encoded query string) + * @param {String} url (Basket API endpoint) + * @param {Function} successCallback + * @param {Function} errorCallback + */ + postToBasket: (email, params, url, successCallback, errorCallback) => { + const xhr = new XMLHttpRequest(); + + // Emails used in automation for page-level integration tests + // should avoid hitting basket directly. + if (email === 'success@example.com') { + successCallback(); + return; + } else if (email === 'failure@example.com') { + errorCallback(); + return; + } + + xhr.onload = function (e) { + let response = e.target.response || e.target.responseText; + + if (typeof response !== 'object') { + response = JSON.parse(response); } - }, false); - newsletterForm.addEventListener('submit', (e) => { + if (response) { + if ( + response.status === 'ok' && + e.target.status >= 200 && + e.target.status < 300 + ) { + successCallback(); + } else if (response.status === 'error' && response.desc) { + errorCallback(response.desc); + } else { + errorCallback(); + } + } else { + errorCallback(); + } + }; + + xhr.onerror = errorCallback; + xhr.open('POST', url, true); + xhr.setRequestHeader( + 'Content-type', + 'application/x-www-form-urlencoded' + ); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.timeout = 5000; + xhr.ontimeout = errorCallback; + xhr.responseType = 'json'; + xhr.send(params); + }, + + serialize: () => { + // Email address + const email = encodeURIComponent( + form.querySelector('input[type="email"]').value + ); + + // Newsletter format + const format = form.querySelector('input[name="format"]:checked').value; + + // Country (optional form or +
@@ -20,28 +15,43 @@ - - + + + + -