PageSourceSearch

https://www.acttallergy.com/js/form-handler.js

js acttallergy.com collected 2026-10-10 01:04:08 UTC 9,025 bytes, 208 lines download raw bytes

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.