1/** 2 * validate-forms.js 3 * 8-17-26 4 * 5 * Handles client-side validation and submission for all forms marked 6 * with [data-form-wrapper] on a page (main-page forms and modal forms). 7 * 8 * Requirements for each form: 9 * - Wrapper element has the `data-form-wrapper` attribute. 10 * - Wrapper contains a hidden input with id="processid" (required â 11 * forms without this are skipped entirely). 12 * - Wrapper contains a hidden input with id="postLocation" (optional; 13 * defaults to /SpecialFunctions/Forms/MasterForm.aspx). 14 * - Wrapper contains a submit <button> or <input type="submit">. 15 * 16 * Multi-form pages (e.g. several modals on one page): 17 * - On submit, every OTHER [data-form-wrapper] on the page is removed 18 * from the DOM before the active form is processed. This prevents 19 * duplicate id/name collisions between forms â most notably 20 * reCAPTCHA's g-recaptcha-response textarea, which api.js renders 21 * with the same name (and near-duplicate id) for every widget on 22 * the page. 23 * - Any fields that must survive outside of every wrapper (e.g. a 24 * sitewide widget) are NOT touched by this script and need their 25 * own cleanup â see beforeSubmit() in the page/control markup. 26 * 27 * Optional global hooks (defined elsewhere, called if present): 28 * - getMessage() â reCAPTCHA include; verifies the captcha, then 29 * calls submitThisForm() with no args. Only 30 * called when the active wrapper contains a 31 * .g-recaptcha-response element. 32 * - beforeSubmit() â page-specific cleanup, run just before the 33 * form actually submits. 34 * - ga() â Google Analytics event tracking. 35 */ 36 37// Grab the main form tag (usually just one in ASP.NET environments) 38const mainForm = document.querySelector("form"); 39 40function submitThisForm(wrapper, contactformSession, postValue, contactformID) { 41 // Fall back to window-stored values when called with no args (e.g. from getMessage in recaptcha include) 42 wrapper = wrapper || window._activeFormWrapper; 43 contactformSession = contactformSession || window._activeFormSession; 44 postValue = postValue || window._activeFormPostValue; 45 contactformID = contactformID || window._activeFormId; 46 47 sessionStorage.setItem(contactformSession, `${contactformSession}Sent`); 48 49 // Set form.action 50 mainForm.action = postValue || "/SpecialFunctions/Forms/MasterForm.aspx"; 51 52 // GA tracking Event 53 if (typeof ga === 'function') { 54 ga('send', 'event', 'Form ID:' + contactformID + ' Submitted', 'Submitted', 'Contact Form', contactformID); 55 } else { 56 console.warn('Google Analytics is not defined.'); 57 } 58 59 if (typeof beforeSubmit === 'function') { 60 beforeSubmit(); 61 } 62 63 // Safety net: disable inputs outside the active form in case this was 64 // called without the removal step having already run. 65 const allFormWrappers = document.querySelectorAll('[data-form-wrapper]'); 66 allFormWrappers.forEach(otherWrapper => { 67 if (otherWrapper !== wrapper) { 68 const inputsToDisable = otherWrapper.querySelectorAll('input, textarea, select'); 69 inputsToDisable.forEach(el => el.disabled = true); 70 } 71 }); 72 73 // Submit the main form 74 mainForm.submit(); 75} 76 77function updateLegacyForms(wrapper) { 78 // Scoped to look ONLY inside the current wrapper 79 const legacyInputs = wrapper.querySelectorAll('[class^="validate"]'); 80 81 legacyInputs.forEach(input => { 82 if (!input.hasAttribute('required')) { 83 input.setAttribute('required', true); 84 85 let labelText = ''; 86 const label = input.closest('label'); 87 if (label) { 88 labelText = label.textContent.trim(); 89 } else { 90 labelText = input.getAttribute('name') || ''; 91 } 92 93 input.setAttribute('oninvalid', `this.setCustomValidity('Please enter your ${labelText}')`); 94 input.setAttribute('oninput', 'this.setCustomValidity("")'); 95 96 const inputId = input.getAttribute('id'); 97 if (labelText.toLowerCase().includes('email') || (inputId && inputId.toLowerCase().includes('email'))) { 98 input.setAttribute('pattern', '[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'); 99 input.setAttribute('oninvalid', 'this.setCustomValidity("Please enter a valid email address")'); 100 } 101 102 if (labelText.toLowerCase().includes('phone') || (inputId && inputId.toLowerCase().includes('phone'))) { 103 input.setAttribute('pattern', '^(?:\\d{3}-\\d{3}-\\d{4}|\\(\\d{3}\\)-\\d{3}-\\d{4}|\\d{10}|[0-9]-\\d{3}-\\d{3}-\\d{4}|[0-9]-\\(\\d{3}\\)-\\d{3}-\\d{4}|\\d{11})$'); 104 input.setAttribute('oninvalid', 'this.setCustomValidity("Please enter a valid phone number")'); 105 } 106 } 107 });
108} 109 110// Stamps formnovalidate on any submit button that is NOT inside a managed 111// form wrapper (i.e. a [data-form-wrapper] containing a processid). 112// Accepts an optional root element to scope the search (for MutationObserver). 113function stampUnmanagedButtons(root) { 114 root = root || document; 115 const candidates = root.querySelectorAll('input[type="submit"], button[type="submit"]'); 116 117 candidates.forEach(btn => { 118 const parentWrapper = btn.closest('[data-form-wrapper]'); 119 const isManaged = parentWrapper && parentWrapper.querySelector('[id="processid"]'); 120 121 if (!isManaged) { 122 btn.setAttribute('formnovalidate', ''); 123 } 124 }); 125} 126 127function validateForm() { 128 // Find EVERY logical form wrapper on the page (main page and modals). 129 // Falls back to the single page-wide <form> when no [data-form-wrapper] 130 // markup exists at all (legacy pages). Pages with a MIX of wrapped and 131 // unwrapped forms are not supported â the unwrapped ones need the 132 // attribute added. 133 let formWrappers = document.querySelectorAll('[data-form-wrapper]'); 134 if (formWrappers.length === 0 && mainForm) { 135 formWrappers = [mainForm]; 136 } 137 138 formWrappers.forEach(wrapper => { 139 const processIdElement = wrapper.querySelector('[id="processid"]'); 140 const postInput = wrapper.querySelector('[id="postLocation"]'); 141 142 if (!processIdElement) return; // Skip if this wrapper doesn't have a process id 143 144 const contactformID = processIdElement.value; 145 const contactformSession = contactformID ? `contactForm${contactformID}` : ""; 146 const postValue = postInput ? postInput.value : ""; 147 148 // See if form already sent 149 if (sessionStorage.getItem(contactformSession)) { 150 wrapper.innerHTML = "<div class='thank-you-message'><h4>Thank you for contacting us!</h4><p>We will be in touch.</p></div>"; 151 sessionStorage.removeItem(contactformSession); 152 return; 153 } 154 155 updateLegacyForms(wrapper); 156 157 const submitButton = wrapper.querySelector('button, input[type="submit"]'); 158 159 if (submitButton) { 160 submitButton.addEventListener('click', (event) => { 161 event.preventDefault(); 162 163 let isFormValid = true; 164 165 // Validate ONLY the inputs inside this specific wrapper 166 const inputsToValidate = wrapper.querySelectorAll('input, textarea, select'); 167 168 inputsToValidate.forEach(element => { 169 let errorMessageDiv = element.parentNode.querySelector('.input-error-msg'); 170 171 element.addEventListener('input', () => { 172 if (element.checkValidity()) { 173 if (errorMessageDiv) errorMessageDiv.remove(); 174 element.classList.remove("input-error"); 175 } 176 }); 177 178 if (!element.checkValidity()) { 179 isFormValid = false; 180 if (errorMessageDiv) { 181 errorMessageDiv.textContent = element.validationMessage; 182 } else { 183 errorMessageDiv = document.createElement("div"); 184 errorMessageDiv.className = "input-error-msg"; 185 errorMessageDiv.textContent = element.validationMessage; 186 element.parentNode.appendChild(errorMessageDiv); 187 } 188 element.classList.add("input-error"); 189 } 190 }); 191 192 if (isFormValid) { 193 // Store scoped variables on window so getMessage's no-arg 194 // submitThisForm() call can retrieve them from the recaptcha include 195 window._activeFormWrapper = wrapper; 196 window._activeFormSession = contactformSession; 197 window._activeFormPostValue = postValue; 198 window._activeFormId = contactformID; 199 200 // Remove other form wrappers so duplicate IDs (e.g. reCAPTCHA's
201 // g-recaptcha-response) can't collide during submission. 202 document.querySelectorAll('[data-form-wrapper]').forEach(otherWrapper => { 203 if (otherWrapper !== wrapper) { 204 otherWrapper.remove(); 205 } 206 }); 207 208 // Only run the captcha flow if this wrapper actually has one. 209 const captchaResponseEl = wrapper.querySelector('.g-recaptcha-response'); 210 211 if (captchaResponseEl && typeof getMessage === 'function') { 212 // Normalize the id so getMessage()'s hardcoded 213 // getElementById('g-recaptcha-response') finds THIS 214 // wrapper's textarea, regardless of the -1/-2 suffix 215 // reCAPTCHA's api.js may have assigned it. 216 captchaResponseEl.id = 'g-recaptcha-response'; 217 getMessage(); 218 } else { 219 submitThisForm(wrapper, contactformSession, postValue, contactformID); 220 } 221 } 222 }); 223 } 224 }); 225 226 // Stamp all unmanaged submit buttons present at init time 227 stampUnmanagedButtons(); 228 229 // Watch for dynamically added content (e.g. UpdatePanel re-renders, AJAX loads) 230 // and re-stamp any new unmanaged submit buttons as they appear 231 const observer = new MutationObserver(mutations => { 232 mutations.forEach(mutation => { 233 mutation.addedNodes.forEach(node => { 234 if (node.nodeType !== Node.ELEMENT_NODE) return; 235 236 if ( 237 (node.tagName === 'INPUT' && node.type === 'submit') || 238 (node.tagName === 'BUTTON' && (node.type === 'submit' || !node.type)) 239 ) { 240 stampUnmanagedButtons(node.parentNode); 241 return; 242 } 243 244 const hasSubmits = node.querySelector('input[type="submit"], button[type="submit"]'); 245 if (hasSubmits) { 246 stampUnmanagedButtons(node); 247 } 248 }); 249 }); 250 }); 251 252 observer.observe(document.body, { childList: true, subtree: true }); 253} 254 255// Initialize 256validateForm();
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.