1/** 2 * FormIt 3 * 4 * Client-side functionality for FormIt forms. 5 * 6 * @package formit 7 */ 8(function (window, document) { 9 'use strict'; 10 11 /** 12 * Global config. Set by PHP via Object.assign(FormIt, {...}). 13 */ 14 window.FormIt = { 15 actionUrl: '/assets/components/formit/action.php', 16 recaptchaDefaultAction: 'submit' 17 }; 18 19 /** 20 * @param {HTMLFormElement} form 21 * @constructor 22 */ 23 function FormItForm(form) { 24 if (!(form instanceof HTMLFormElement)) { 25 console.error('[FormIt] First argument must be a form element.'); 26 return; 27 } 28 29 this.form = form; 30 this._lastSubmitter = null; 31 this.form.addEventListener('click', this._onClickSubmit.bind(this)); 32 this.form.addEventListener('submit', this._onSubmit.bind(this)); 33 } 34 35 /** 36 * Track which submit button was clicked. 37 * @param {MouseEvent} e 38 * @private 39 */ 40 FormItForm.prototype._onClickSubmit = function (e) { 41 var btn = e.target.closest('[type="submit"]'); 42 this._lastSubmitter = btn && btn.name ? btn : null; 43 }; 44 45 /** 46 * @param {SubmitEvent} e 47 * @private 48 */ 49 FormItForm.prototype._onSubmit = function (e) { 50 this.ajaxToken = this.form.getAttribute('data-formit-ajax-token'); 51 52 if (!this.ajaxToken && !this.form.querySelector('[name="g-recaptcha-response"]')) { 53 return; 54 } 55 56 e.preventDefault(); 57 58 // beforesubmit event (cancelable) 59 var beforeEvent = this._dispatch('formit:beforesubmit', { form: this.form }, true); 60 if (beforeEvent.defaultPrevented) return; 61 62 this.submitter = e.submitter || this._lastSubmitter || this.form.querySelector('[type="submit"]'); 63 var formData = new FormData(this.form); 64 65 if (this.submitter && this.submitter.name) { 66 formData.append(this.submitter.name, this.submitter.value || ''); 67 } 68 69 var self = this; 70 71 this._clearMessages(); 72 this._setLoading(true); 73 74 // _resolveRecaptcha has its own guards â resolves immediately if not configured 75 this._resolveRecaptcha(formData) 76 .then(function () { 77 self._submit(formData); 78 }) 79 .catch(function (error) { 80 console.error('[FormIt] reCAPTCHA failed:', error); 81 self._setLoading(false); 82 self._showMessage('[data-formit-error-message]', error.message || 'Request failed'); 83 self._dispatch('formit:error', { data: null, error: error }); 84 }); 85 }; 86 87 /** 88 * Execute reCAPTCHA v3 and fill both formData and the response field. 89 * Resolves immediately when reCAPTCHA is not configured. 90 * 91 * @param {FormData} formData 92 * @returns {Promise<void>} 93 * @private 94 */ 95 FormItForm.prototype._resolveRecaptcha = function (formData) { 96 var recaptchaResponseField = this.form.querySelector('[name="g-recaptcha-response"]'); 97 98 if (!recaptchaResponseField || typeof grecaptcha === 'undefined') { 99 return Promise.resolve(); 100 } 101 102 var actionField = this.form.querySelector('[name="g-recaptcha-action"]'); 103 var recaptchaAction = (actionField && actionField.value) || FormIt.recaptchaDefaultAction; 104 105 return new Promise(function (resolve, reject) { 106 grecaptcha.ready(function () { 107 grecaptcha.execute(FormIt.recaptchaSiteKey, { action: recaptchaAction }) 108 .then(function (token) { 109 recaptchaResponseField.value = token; 110 formData.set('g-recaptcha-response', token); 111 resolve(); 112 }) 113 .catch(reject); 114 }); 115 }); 116 }; 117 118 /** 119 * Submit the form: native submit or AJAX fetch depending on ajaxToken. 120 * 121 * @param {FormData} formData 122 * @private 123 */ 124 FormItForm.prototype._injectSubmitter = function () { 125 if (!this.submitter || !this.submitter.name) return; 126 var existing = this.form.querySelector('[name="' + this.submitter.name + '"]'); 127 if (existing && existing !== this.submitter) return; 128 var hidden = document.createElement('input'); 129 hidden.type = 'hidden'; 130 hidden.name = this.submitter.name; 131 hidden.value = this.submitter.value || ''; 132 this.form.appendChild(hidden); 133 }; 134 135 FormItForm.prototype._submit = function (formData) { 136 if (!this.ajaxToken) { 137 this._injectSubmitter(); 138 this.form.submit(); 139 return; 140 } 141 142 var self = this; 143 var form = this.form; 144 145 fetch(FormIt.actionUrl, { 146 method: 'POST', 147 headers: { 148 'X-Requested-With': 'XMLHttpRequest', 149 'X-FormIt-Token': this.ajaxToken 150 }, 151 body: formData 152 }) 153 .then(function (response) { 154 if (!response.ok) { 155 return response.text().then(function (text) { 156 throw new Error('[FormIt] Server returned HTTP ' + response.status + ': ' + text.substring(0, 200)); 157 }); 158 } 159 160 var contentType = response.headers.get('Content-Type') || ''; 161 if (contentType.indexOf('application/json') === -1) { 162 return response.text().then(function (text) { 163 throw new Error('[FormIt] Expected JSON but received ' + contentType + ': ' + text.substring(0, 200)); 164 }); 165 } 166 167 return response.json(); 168 }) 169 .then(function (data) { 170 self._handleResponse(data); 171 }) 172 .catch(function (error) { 173 // AJAX failed â fall back to native submit so the request still goes through. 174 // reCAPTCHA token is already set in the response field value. 175 console.warn('[FormIt] AJAX failed, falling back to native submit:', error); 176 self._injectSubmitter(); 177 form.submit(); 178 }) 179 .finally(function () { 180 self._setLoading(false); 181 self._dispatch('formit:complete', {}); 182 }); 183 }; 184 185 /** 186 * @param {Object} data - Response from processForm() 187 * @private 188 */ 189 FormItForm.prototype._handleResponse = function (data) { 190 var placeholders = data.placeholders || {}; 191 var hasFieldErrors = false;
192 193 for (var key in placeholders) { 194 if (placeholders.hasOwnProperty(key) && key.indexOf('error.') === 0) { 195 hasFieldErrors = true; 196 var fieldName = key.substring(6); 197 var el = this.form.querySelector('[data-formit-error="' + fieldName + '"]'); 198 if (el) el.innerHTML = placeholders[key]; 199 } 200 } 201 202 this._showMessage('[data-formit-success-message]', placeholders.successMessage || ''); 203 this._showMessage('[data-formit-validation-error-message]', placeholders.validation_error_message || ''); 204 this._showMessage('[data-formit-error-message]', placeholders.error_message || ''); 205 206 var isError = !data.success || hasFieldErrors || placeholders.validation_error || placeholders.error_message; 207 208 if (isError) { 209 this._dispatch('formit:error', { data: data }); 210 } else { 211 this._dispatch('formit:success', { data: data }); 212 213 this.form.reset(); 214 215 if (data.redirect_url) { 216 var redirectEvent = this._dispatch('formit:redirect', { url: data.redirect_url }, true); 217 if (redirectEvent.defaultPrevented) return; 218 219 window.location.href = data.redirect_url; 220 } 221 } 222 }; 223 224 /** 225 * @param {string} selector 226 * @param {string} message 227 * @private 228 */ 229 FormItForm.prototype._showMessage = function (selector, message) { 230 if (!message) return; 231 var el = this.form.querySelector(selector); 232 if (el) { 233 el.innerHTML = message; 234 } else { 235 var stripEl = document.createElement('div'); 236 stripEl.innerHTML = message; 237 alert(stripEl.textContent || stripEl.innerText || message); 238 } 239 }; 240 241 /** 242 * @private 243 */ 244 FormItForm.prototype._clearMessages = function () { 245 this.form.querySelectorAll('[data-formit-error]').forEach(function (el) { 246 el.textContent = ''; 247 }); 248 this.form.querySelectorAll('[data-formit-success-message], [data-formit-validation-error-message], [data-formit-error-message]') 249 .forEach(function (el) { 250 el.textContent = ''; 251 }); 252 }; 253 254 /** 255 * @param {boolean} loading 256 * @private 257 */ 258 FormItForm.prototype._setLoading = function (loading) { 259 if (loading) { 260 this.form.classList.add('formit-loading'); 261 } else { 262 this.form.classList.remove('formit-loading'); 263 } 264 265 var buttons = this.form.querySelectorAll('[type="submit"]'); 266 for (var i = 0; i < buttons.length; i++) { 267 buttons[i].disabled = loading; 268 } 269 }; 270 271 /** 272 * @param {string} name 273 * @param {Object} detail 274 * @param {boolean} [cancelable] 275 * @returns {CustomEvent} 276 * @private 277 */ 278 FormItForm.prototype._dispatch = function (name, detail, cancelable) { 279 var event = new CustomEvent(name, { 280 detail: detail, 281 bubbles: true, 282 cancelable: !!cancelable 283 }); 284 this.form.dispatchEvent(event); 285 return event; 286 }; 287 288 // Auto-initialize all forms 289 document.addEventListener('DOMContentLoaded', function () { 290 var forms = document.querySelectorAll('form'); 291 for (var i = 0; i < forms.length; i++) { 292 new FormItForm(forms[i]); 293 } 294 }); 295 296})(window, document);
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.