1class NxtUnsubscribe { 2 3 _root; 4 _form; 5 _submitButton; 6 _successMessage; 7 _errorMessage; 8 _formUrl = '/api/unsubscribe'; 9 10 constructor(root) { 11 this._root = root; 12 this._submitButton = root.querySelector('input[type="submit"]'); 13 this._form = root.querySelector('form'); 14 this._successMessage = root.querySelector('.nxt-newsletter-message.success'); 15 this._errorMessage = root.querySelector('.nxt-newsletter-message.error'); 16 17 if (this._form) { 18 this._form.setAttribute('novalidate', 'novalidate'); 19 } 20 21 this.initEventListeners(); 22 this.initFieldValidation(); 23 this._recaptcha = new NxtRecaptcha(this._root); 24 } 25 26 // ââ Live error-clearing listeners ââââââââââââââââââââââââââââââââââââââââ 27 initFieldValidation() { 28 this._root.querySelectorAll('input[required]:not([type="checkbox"]):not([type="radio"]), select[required]').forEach((input) => { 29 input.addEventListener('input', () => { 30 if (input.value.trim()) this._clearError(input); 31 }); 32 }); 33 34 this._root.querySelectorAll('input[type="checkbox"][required]').forEach((checkbox) => { 35 checkbox.addEventListener('change', () => { 36 if (checkbox.checked) this._clearError(checkbox); 37 }); 38 }); 39 40 const radioNames = new Set(); 41 this._root.querySelectorAll('input[type="radio"][required]').forEach((r) => radioNames.add(r.name)); 42 radioNames.forEach((name) => { 43 this._root.querySelectorAll(`input[type="radio"][name="${name}"]`).forEach((radio) => { 44 radio.addEventListener('change', () => this._clearErrorByName(name)); 45 }); 46 }); 47 } 48 49 _markError(field) { 50 if (field.type !== 'checkbox' && field.type !== 'radio') { 51 field.classList.add('nxt-field-error'); 52 const label = this._root.querySelector(`label[for="${field.id}"]`); 53 if (label) label.classList.add('nxt-label-error'); 54 return; 55 } 56 const label = this._root.querySelector(`label[for="${field.id}"]`) || field.closest('label'); 57 if (label) label.classList.add('nxt-label-error'); 58 } 59 60 _clearError(field) { 61 field.classList.remove('nxt-field-error'); 62 const label = this._root.querySelector(`label[for="${field.id}"]`) || field.closest('label'); 63 if (label) label.classList.remove('nxt-label-error'); 64 } 65 66 _clearErrorByName(name) { 67 this._root.querySelectorAll(`input[name="${name}"]`).forEach((r) => this._clearError(r)); 68 const wrapper = this._root.querySelector(`[data-radio-group="${name}"]`); 69 if (wrapper) wrapper.classList.remove('nxt-label-error'); 70 } 71 72 validateFields() { 73 let valid = true; 74 75 this._root.querySelectorAll('input[required]:not([type="checkbox"]):not([type="radio"]), select[required]').forEach((input) => { 76 if (input.closest('.hidden') || input.offsetParent === null) return; 77 if (!input.value.trim()) { 78 this._markError(input); 79 valid = false; 80 } else { 81 this._clearError(input); 82 } 83 }); 84 85 this._root.querySelectorAll('input[type="checkbox"][required]').forEach((checkbox) => { 86 if (!checkbox.checked) { 87 this._markError(checkbox); 88 valid = false; 89 } else { 90 this._clearError(checkbox); 91 } 92 }); 93 94 const radioNames = new Set(); 95 this._root.querySelectorAll('input[type="radio"][required]').forEach((r) => radioNames.add(r.name)); 96 radioNames.forEach((name) => { 97 const radios = [...this._root.querySelectorAll(`input[type="radio"][name="${name}"]`)]; 98 const visibleRadios = radios.filter(r => !r.closest('.hidden') && r.offsetParent !== null); 99 if (visibleRadios.length === 0) return; 100 if (!radios.some(r => r.checked)) { 101 visibleRadios.forEach(r => this._markError(r)); 102 valid = false; 103 } else { 104 radios.forEach(r => this._clearError(r)); 105 } 106 }); 107 108 return valid; 109 } 110 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 111 112 initEventListeners() { 113 if (this._form != null) { 114 this._form.addEventListener('submit', (e) => { 115 e.preventDefault();
116 this.submitForm(); 117 }); 118 } 119 120 const emailInput = this._root.querySelector('input[name="email"]'); 121 if (emailInput != null) { 122 emailInput.addEventListener('input', (e) => { 123 this.toggleCollapse(e.target.value.length > 0); 124 this.toggleButton(e.target.value.length > 0); 125 }); 126 } 127 } 128 129 toggleButton(show) { 130 if (show) { 131 this._submitButton.classList.add('active'); 132 } else { 133 this._submitButton.classList.remove('active'); 134 } 135 } 136 137 submitForm() { 138 if (!this.validateFields()) return; 139 140 const email = this._form.querySelector('input[name="email"]').value.trim(); 141 if (!email) return; 142 143 const recaptchaToken = this._recaptcha.getToken(); 144 145 const body = new FormData(); 146 body.append('email', email); 147 body.append('nonce', nxtUnsubscribe.nonce); 148 body.append('recaptcha_token', recaptchaToken); 149 150 fetch(this._formUrl, { 151 method: 'POST', 152 body: body, 153 }) 154 .then(res => { 155 this._form.style.display = 'none'; 156 if (res.ok || res.redirected) { 157 this.showMessage('success'); 158 this._form.reset(); 159 this._recaptcha.reset(); 160 } else { 161 this.showMessage('error'); 162 } 163 }) 164 .catch(() => { 165 this.showMessage('error'); 166 }); 167 } 168 169 showMessage(type) { 170 if (type === 'success') { 171 this._errorMessage.classList.add('hidden'); 172 this._errorMessage.setAttribute('aria-hidden', 'true'); 173 this._successMessage.classList.remove('hidden'); 174 this._successMessage.setAttribute('aria-hidden', 'false'); 175 } else { 176 this._errorMessage.classList.remove('hidden'); 177 this._errorMessage.setAttribute('aria-hidden', 'false'); 178 this._successMessage.classList.add('hidden'); 179 this._successMessage.setAttribute('aria-hidden', 'true'); 180 } 181 } 182 183 toggleCollapse(show) { 184 const collapse = this._root.querySelector('.form-collapse'); 185 if (collapse == null) return; 186 187 if (show) { 188 collapse.classList.remove('hidden'); 189 collapse.setAttribute('aria-hidden', 'false'); 190 this._submitButton.classList.add('active'); 191 } else { 192 collapse.classList.add('hidden'); 193 collapse.setAttribute('aria-hidden', 'true'); 194 this._submitButton.classList.remove('active'); 195 } 196 } 197} 198 199document.addEventListener('DOMContentLoaded', function () { 200 document.querySelectorAll('.nxt-newsletter-unsubscribe-element').forEach((el) => { 201 new NxtUnsubscribe(el); 202 }); 203});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.