1/** 2 * Global Form Handler for Custom Forms 3 * Automatically handles all forms with data-custom-form attribute 4 */ 5(function() { 6 'use strict'; 7 8 // Initialize all forms on page load 9 document.addEventListener('DOMContentLoaded', function() { 10 const forms = document.querySelectorAll('[data-custom-form]'); 11 forms.forEach(initializeForm); 12 }); 13 14 /** 15 * Initialize a single form 16 */ 17 function initializeForm(formElement) { 18 const formId = formElement.dataset.formId; 19 const responseDiv = formElement.querySelector('[data-form-response]'); 20 const actualForm = formElement.querySelector('form'); 21 22 if (!actualForm) { 23 return; 24 } 25 26 // Determine token endpoint (contact forms use different endpoint) 27 const isContactForm = formId === 'contact'; 28 const tokenEndpoint = isContactForm 29 ? window.location.origin + '/contact/token' 30 : window.location.origin + '/form/' + formId + '/token'; 31 32 // Determine submit endpoint 33 const submitEndpoint = isContactForm 34 ? window.location.origin + '/contact' 35 : window.location.origin + '/form/' + formId; 36 37 // Fetch initial token on page load (also updates captcha image for session sync) 38 fetchAndSetToken(tokenEndpoint, actualForm, formElement); 39 40 actualForm.addEventListener('submit', async function(e) { 41 e.preventDefault(); 42 43 // Disable submit button 44 const submitBtn = actualForm.querySelector('[type="submit"]'); 45 const originalBtnText = submitBtn.innerHTML; 46 submitBtn.disabled = true; 47 submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Sending...'; 48 49 // Get form data - use existing tokens (set on page load or after previous submit) 50 // DO NOT fetch new tokens here - it would regenerate the captcha! 51 const formData = new FormData(actualForm); 52 const data = Object.fromEntries(formData.entries()); 53 54 // Use existing CSRF token from form 55 let csrfToken = data._token || ''; 56 57 // Ensure honeypot is empty 58 if (!data.website_url) { 59 data.website_url = ''; 60 } 61 62 // Submit form 63 try { 64 const headers = { 65 'Content-Type': 'application/json', 66 'Accept': 'application/json', 67 'X-Requested-With': 'XMLHttpRequest' 68 }; 69 70 // Add CSRF token to headers (Laravel checks X-CSRF-TOKEN) 71 if (csrfToken) { 72 headers['X-CSRF-TOKEN'] = csrfToken; 73 } 74 75 // Remove _token from data (it's in headers now) 76 delete data._token; 77 78 const response = await fetch(submitEndpoint, { 79 method: 'POST', 80 headers: headers, 81 body: JSON.stringify(data), 82 credentials: 'same-origin' // Important: send cookies to maintain session 83 }); 84 85 const result = await response.json(); 86 87 // Clear previous messages 88 responseDiv.innerHTML = ''; 89 90 if (result.success) { 91 // Success message 92 responseDiv.innerHTML = ` 93 <div class="alert alert-success alert-dismissible fade show" role="alert"> 94 <i class="bi bi-check-circle-fill me-2"></i>${result.message} 95 <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> 96 </div> 97 `; 98 99 // Reset form 100 actualForm.reset(); 101 102 // Fetch fresh token for next submission (also regenerates captcha) 103 // The token endpoint creates a new captcha and returns its image URL 104 await fetchAndSetToken(tokenEndpoint, actualForm, formElement); 105 106 // Scroll to message 107 responseDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 108 } else { 109 // Error messages 110 let errorHtml = '<div class="alert alert-danger alert-dismissible fade show" role="alert">'; 111 errorHtml += '<div class="d-flex align-items-start">'; 112 errorHtml += '<i class="bi bi-exclamation-circle-fill me-2 mt-0"></i>'; 113 errorHtml += '<div>'; 114 115 if (result.errors) { 116 errorHtml += '<ul class="mb-0 ps-0" style="list-style-position: inside;">'; 117 for (let field in result.errors) {
118 result.errors[field].forEach(error => { 119 errorHtml += `<li>${error}</li>`; 120 }); 121 } 122 errorHtml += '</ul>'; 123 } else { 124 errorHtml += result.message || 'An error occurred. Please try again.'; 125 } 126 127 errorHtml += '</div></div>'; 128 errorHtml += '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>'; 129 responseDiv.innerHTML = errorHtml; 130 131 // Refresh captcha on error (captcha is consumed even on failed attempts) 132 await fetchAndSetToken(tokenEndpoint, actualForm, formElement); 133 134 // Clear the captcha answer field so user knows to type new answer 135 const captchaInput = actualForm.querySelector('input[name="custom_answer"]'); 136 if (captchaInput) { 137 captchaInput.value = ''; 138 captchaInput.focus(); 139 } 140 141 // Scroll to message 142 responseDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 143 } 144 145 } catch (error) { 146 console.error('Form submission error:', error); 147 responseDiv.innerHTML = ` 148 <div class="alert alert-danger alert-dismissible fade show" role="alert"> 149 <i class="bi bi-exclamation-circle-fill me-2"></i> 150 An error occurred while submitting the form. Please try again. 151 <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> 152 </div> 153 `; 154 } finally { 155 // Re-enable submit button 156 submitBtn.disabled = false; 157 submitBtn.innerHTML = originalBtnText; 158 } 159 }); 160 } 161 162 /** 163 * Fetch and set tokens for a form (both form token and CSRF token) 164 * Also updates captcha image to ensure it's in sync with session 165 */ 166 async function fetchAndSetToken(tokenEndpoint, form, formContainer) { 167 try { 168 const tokenResponse = await fetch(tokenEndpoint, { 169 credentials: 'same-origin', 170 headers: { 171 'Accept': 'application/json', 172 'X-Requested-With': 'XMLHttpRequest' 173 } 174 }); 175 if (tokenResponse.ok) { 176 const tokenData = await tokenResponse.json(); 177 178 // Set form token 179 const tokenField = form.querySelector('input[name="_form_token"]'); 180 if (tokenField) { 181 tokenField.value = tokenData.token; 182 } 183 184 // Set CSRF token 185 const csrfField = form.querySelector('input[name="_token"]'); 186 if (csrfField) { 187 csrfField.value = tokenData.csrf_token; 188 } 189 190 // Update captcha image if present (ensures captcha is in sync with session) 191 if (tokenData.captcha_image_url && formContainer) { 192 const captchaImage = formContainer.querySelector('img[alt="Security Question"]'); 193 if (captchaImage) { 194 captchaImage.style.opacity = '0.5'; 195 captchaImage.src = tokenData.captcha_image_url + '?v=' + Date.now(); 196 captchaImage.onload = function() { 197 captchaImage.style.opacity = '1'; 198 }; 199 } 200 } 201 } 202 } catch (error) { 203 // Silent fail - form will still work, just might have stale tokens 204 } 205 } 206 207})(); 208
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.