|
7 | 7 | * Author: Raspgot |
8 | 8 | */ |
9 | 9 |
|
10 | | -const RECAPTCHA_SITE_KEY = 'YOUR_RECAPTCHA_SITE_KEY'; // Replace with your public reCAPTCHA key |
| 10 | +const RECAPTCHA_SITE_KEY = 'YOUR_RECAPTCHA_SITE_KEY'; // Replace with your public reCAPTCHA site key |
11 | 11 |
|
12 | 12 | document.addEventListener('DOMContentLoaded', () => { |
13 | 13 | 'use strict'; |
14 | 14 |
|
15 | | - // Get the form element that uses Bootstrap's validation classes |
16 | 15 | const form = document.querySelector('.needs-validation'); |
17 | | - if (!form) return; // Stop if no form is found on the page |
| 16 | + if (!form) return; |
18 | 17 |
|
19 | | - // Select additional elements: loading spinner, submit button, and alert box |
20 | 18 | const spinner = document.getElementById('loading-spinner'); |
21 | 19 | const submitButton = form.querySelector('button[type="submit"]'); |
22 | 20 | const alertContainer = document.getElementById('alert-status'); |
23 | 21 |
|
24 | | - /** |
25 | | - * Enable live validation as the user types or selects options |
26 | | - */ |
27 | | - form.querySelectorAll('input, select, textarea').forEach((field) => { |
28 | | - const eventType = field.tagName === 'SELECT' ? 'change' : 'input'; |
| 22 | + let inFlight = false; |
29 | 23 |
|
30 | | - field.addEventListener(eventType, () => { |
| 24 | + // Live validation |
| 25 | + form.querySelectorAll('input, select, textarea').forEach((field) => { |
| 26 | + field.addEventListener(field.tagName === 'SELECT' ? 'change' : 'input', () => { |
31 | 27 | if (!field.value.trim()) { |
32 | | - // Clear feedback if field is empty |
33 | 28 | field.classList.remove('is-valid', 'is-invalid'); |
34 | 29 | } else if (field.checkValidity()) { |
35 | | - // Field is valid |
36 | 30 | field.classList.add('is-valid'); |
37 | 31 | field.classList.remove('is-invalid'); |
38 | 32 | } else { |
39 | | - // Field is invalid |
40 | 33 | field.classList.add('is-invalid'); |
41 | 34 | field.classList.remove('is-valid'); |
42 | 35 | } |
43 | 36 | }); |
44 | 37 | }); |
45 | 38 |
|
46 | | - /** |
47 | | - * Handle form submission with AJAX |
48 | | - */ |
| 39 | + // Handle form submission with AJAX |
49 | 40 | form.addEventListener('submit', async (event) => { |
50 | | - event.preventDefault(); // Stop the default form submission |
51 | | - event.stopPropagation(); // Prevent the event from bubbling up |
| 41 | + event.preventDefault(); |
| 42 | + event.stopPropagation(); |
52 | 43 |
|
53 | | - // Clear any previous validation feedback |
| 44 | + if (inFlight) return; |
| 45 | + |
| 46 | + form.classList.remove('was-validated'); |
54 | 47 | form.querySelectorAll('.is-valid, .is-invalid').forEach((el) => el.classList.remove('is-valid', 'is-invalid')); |
55 | 48 |
|
56 | | - // Check HTML5 field validity (required, pattern, etc.) |
| 49 | + // Native HTML5 validity check |
57 | 50 | if (!form.checkValidity()) { |
58 | | - form.classList.add('was-validated'); // Bootstrap validation feedback |
59 | | - form.querySelector(':invalid')?.focus(); // Focus first invalid input |
| 51 | + form.classList.add('was-validated'); |
| 52 | + form.querySelector(':invalid')?.focus(); |
60 | 53 | return; |
61 | 54 | } |
62 | 55 |
|
63 | | - // Show loading spinner and disable the submit button |
64 | | - spinner.classList.remove('d-none'); |
65 | | - submitButton.disabled = true; |
| 56 | + const formData = new FormData(form); |
| 57 | + const endpoint = 'AjaxForm.php'; |
| 58 | + |
| 59 | + // Show loading spinner and disable form |
| 60 | + if (spinner) spinner.classList.remove('d-none'); |
| 61 | + if (submitButton) submitButton.disabled = true; |
| 62 | + form.querySelectorAll('input, select, textarea, button').forEach((el) => { |
| 63 | + if (el !== submitButton) el.disabled = true; |
| 64 | + }); |
| 65 | + inFlight = true; |
66 | 66 |
|
67 | 67 | try { |
| 68 | + if (!RECAPTCHA_SITE_KEY || RECAPTCHA_SITE_KEY === 'YOUR_RECAPTCHA_SITE_KEY') { |
| 69 | + throw new Error('⚠️ Missing reCAPTCHA site key.'); |
| 70 | + } |
| 71 | + if (typeof grecaptcha === 'undefined' || !grecaptcha?.ready) { |
| 72 | + throw new Error('⚠️ reCAPTCHA not loaded.'); |
| 73 | + } |
| 74 | + |
68 | 75 | // Wait for reCAPTCHA to be ready and get the token |
69 | | - const token = await new Promise((resolve) => { |
70 | | - grecaptcha.ready(() => { |
71 | | - grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'submit' }).then(resolve); |
72 | | - }); |
| 76 | + const token = await new Promise((resolve, reject) => { |
| 77 | + try { |
| 78 | + grecaptcha.ready(() => { |
| 79 | + grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'submit' }).then(resolve).catch(reject); |
| 80 | + }); |
| 81 | + } catch (e) { |
| 82 | + reject(e); |
| 83 | + } |
73 | 84 | }); |
74 | 85 |
|
75 | | - // Prepare form data, including the reCAPTCHA token |
76 | | - const formData = new FormData(form); |
| 86 | + // Append token to input form |
77 | 87 | formData.append('recaptcha_token', token); |
78 | 88 |
|
79 | 89 | // Send data using Fetch API (AJAX) |
80 | | - const response = await fetch('AjaxForm.php', { |
| 90 | + const response = await fetch(endpoint, { |
81 | 91 | method: 'POST', |
82 | 92 | body: formData, |
| 93 | + headers: { Accept: 'application/json' }, |
83 | 94 | }); |
84 | 95 |
|
85 | | - // If server responded with an error (like 500), throw an exception |
86 | | - if (!response.ok) throw new Error('Network error: ' + response.status); |
| 96 | + if (!response.ok) { |
| 97 | + throw new Error(`⚠️ Network error: ${response.status}`); |
| 98 | + } |
| 99 | + |
| 100 | + let result; |
| 101 | + try { |
| 102 | + result = await response.json(); |
| 103 | + } catch { |
| 104 | + throw new Error('⚠️ Invalid JSON response.'); |
| 105 | + } |
87 | 106 |
|
88 | | - // Parse the JSON response from the server |
89 | | - const result = await response.json(); |
90 | | - const { message, success, field } = result; |
91 | | - const alertType = success ? 'success' : 'danger'; |
| 107 | + const success = !!result?.success; |
| 108 | + const message = result?.message || (success ? 'Success.' : 'An error occurred.'); |
| 109 | + const field = result?.field; |
92 | 110 |
|
93 | | - // Highlight the invalid field if the server specified one |
| 111 | + // Highlight the invalid field |
94 | 112 | if (field) { |
95 | | - const fieldEl = form.querySelector(`[name="${field}"]`); |
96 | | - if (fieldEl) { |
97 | | - fieldEl.classList.add('is-invalid'); |
98 | | - fieldEl.reportValidity(); // Show native tooltip message |
99 | | - fieldEl.focus(); |
| 113 | + const target = form.querySelector(`[name="${CSS.escape(field)}"]`); |
| 114 | + if (target) { |
| 115 | + target.classList.add('is-invalid'); |
| 116 | + target.focus(); |
100 | 117 | form.classList.remove('was-validated'); |
101 | 118 | } |
102 | 119 | } |
103 | 120 |
|
104 | | - // Display the success or error message |
105 | | - alertContainer.className = `alert alert-${alertType} fade show`; |
106 | | - alertContainer.textContent = message; |
107 | | - alertContainer.classList.remove('d-none'); |
108 | | - alertContainer.scrollIntoView({ behavior: 'smooth' }); |
| 121 | + if (alertContainer) { |
| 122 | + alertContainer.className = `alert alert-${success ? 'success' : 'danger'} fade show`; |
| 123 | + alertContainer.textContent = message; |
| 124 | + alertContainer.classList.remove('d-none'); |
| 125 | + alertContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| 126 | + } |
109 | 127 |
|
110 | 128 | // If the form was submitted successfully, reset it |
111 | 129 | if (success) { |
112 | 130 | form.reset(); |
113 | 131 | form.classList.remove('was-validated'); |
114 | 132 | form.querySelectorAll('.is-valid, .is-invalid').forEach((el) => el.classList.remove('is-valid', 'is-invalid')); |
115 | 133 | } |
116 | | - } catch (error) { |
117 | | - // Log unexpected errors (network, parse errors, etc.) |
118 | | - console.error('An error occurred:', error); |
| 134 | + } catch (err) { |
| 135 | + console.error(err); |
| 136 | + if (alertContainer) { |
| 137 | + alertContainer.className = 'alert alert-danger fade show'; |
| 138 | + alertContainer.textContent = err?.message || 'Unexpected error.'; |
| 139 | + alertContainer.classList.remove('d-none'); |
| 140 | + } |
119 | 141 | } finally { |
120 | | - // Always hide the spinner and re-enable the submit button |
121 | | - spinner.classList.add('d-none'); |
122 | | - submitButton.disabled = false; |
| 142 | + // Hide loading spinner and enable form |
| 143 | + if (spinner) spinner.classList.add('d-none'); |
| 144 | + if (submitButton) submitButton.disabled = false; |
| 145 | + form.querySelectorAll('input, select, textarea, button').forEach((el) => { |
| 146 | + if (el !== submitButton) el.disabled = false; |
| 147 | + }); |
| 148 | + inFlight = false; |
123 | 149 | } |
124 | 150 | }); |
125 | 151 | }); |
0 commit comments