1// Elementor 4 "Atomic" forms are elements rather than widgets, so they carry no .elementor-form class. 2var CFT_ATOMIC_FORM_SELECTOR = 'form[data-element_type="e-form"]'; 3var CFT_ATOMIC_AJAX_ACTION = 'elementor_pro_atomic_forms_send_form'; 4 5function cfturnstile_elementor_set_submit(btn, enabled) { 6 if (!btn) return; 7 btn.style.pointerEvents = enabled ? 'auto' : 'none'; 8 btn.style.opacity = enabled ? '1' : '0.5'; 9} 10 11/** 12 * Current Turnstile token for a form, whichever way it is available. 13 */ 14function cfturnstile_elementor_token(form) { 15 var widget = form ? form.querySelector('.cf-turnstile') : null; 16 if (!widget) return ''; 17 var token = ''; 18 if (window.turnstile) { 19 try { token = turnstile.getResponse(widget) || ''; } catch (e) {} 20 } 21 if (!token) { 22 var responseInput = form.querySelector('[name="cf-turnstile-response"]'); 23 token = responseInput ? (responseInput.value || '') : ''; 24 } 25 return token; 26} 27 28function cfturnstile_init_elementor_forms() { 29 var settings = window.cfturnstileElementorSettings || {}; 30 var sitekey = settings.sitekey || ''; 31 var position = settings.position || 'before'; 32 var mode = settings.mode || 'turnstile'; 33 var recaptchaSiteKey = settings.recaptchaSiteKey || ''; 34 var disableSubmit = settings.disableSubmit || false; 35 var widgetSize = settings.size || 'normal'; 36 37 if (!window._cft_elementor_idx) { window._cft_elementor_idx = 0; } 38 var elementorForms = document.querySelectorAll('.elementor-form:not(.cft-processed)'); 39 elementorForms.forEach(function(form) { 40 var index = window._cft_elementor_idx++; 41 if (form.querySelector('.cf-turnstile') || form.querySelector('.g-recaptcha') || form.querySelector('input[name="cfturnstile_failsafe"]')) { 42 form.classList.add('cft-processed'); 43 return; 44 } 45 46 var submitButton = form.querySelector('button[type="submit"]'); 47 48 // Failsafe modes: inject marker and optionally reCAPTCHA widget. 49 if (submitButton && (mode === 'allow' || mode === 'recaptcha')) { 50 var marker = document.createElement('input'); 51 marker.type = 'hidden'; 52 marker.name = 'cfturnstile_failsafe'; 53 marker.value = mode; 54 form.appendChild(marker); 55 56 if (mode === 'recaptcha' && recaptchaSiteKey) { 57 var recaptchaDiv = document.createElement('div'); 58 recaptchaDiv.className = 'g-recaptcha'; 59 recaptchaDiv.setAttribute('data-sitekey', recaptchaSiteKey); 60 recaptchaDiv.style.cssText = 'display: block; margin: 10px 0 15px 0; width: 100%;'; 61 62 if (position === 'after') { 63 submitButton.parentNode.insertBefore(recaptchaDiv, submitButton.nextSibling); 64 } else if (position === 'afterform') { 65 form.appendChild(recaptchaDiv); 66 } else { 67 submitButton.parentNode.insertBefore(recaptchaDiv, submitButton); 68 } 69 } 70 71 form.classList.add('cft-processed'); 72 return; 73 } 74 75 if (submitButton && window.turnstile && sitekey) { 76 // Disable submit button if option is enabled 77 if (disableSubmit) { 78 cfturnstile_elementor_set_submit(submitButton, false); 79 } 80 81 var labelEl = null; 82 if (settings.labelEnable && settings.labelText) { 83 labelEl = document.createElement('p'); 84 labelEl.className = 'cfturnstile-widget-label'; 85 labelEl.style.cssText = 'font-size: 14px; margin: 0 0 6px 0; width: 100%;'; 86 if ((settings.appearance || 'always') === 'interaction-only') { 87 labelEl.className += ' cfturnstile-widget-label-interaction'; 88 labelEl.style.display = 'none'; 89 } 90 var smallEl = document.createElement('small'); 91 smallEl.textContent = settings.labelText; 92 labelEl.appendChild(smallEl); 93 } 94 95 var turnstileDiv = document.createElement('div'); 96 turnstileDiv.className = 'elementor-turnstile-field cf-turnstile'; 97 turnstileDiv.id = 'cf-turnstile-elementor-fallback-' + index; 98 turnstileDiv.style.cssText = 'display: block; margin: 10px 0 15px 0; width: 100%;'; 99 100 if (position === 'after') { 101 if (labelEl) submitButton.parentNode.insertBefore(labelEl, submitButton.nextSibling); 102 submitButton.parentNode.insertBefore(turnstileDiv, labelEl ? labelEl.nextSibling : submitButton.nextSibling); 103 } else if (position === 'afterform') { 104 if (labelEl) form.appendChild(labelEl); 105 form.appendChild(turnstileDiv); 106 } else { 107 if (labelEl) submitButton.parentNode.insertBefore(labelEl, submitButton); 108 submitButton.parentNode.insertBefore(turnstileDiv, submitButton); 109 } 110 111 turnstile.render('#cf-turnstile-elementor-fallback-' + index, { 112 sitekey: sitekey, 113 theme: settings.theme || 'auto', 114 size: widgetSize, 115 appearance: settings.appearance || 'always', 116 callback: function(token) { 117 // Re-enable submit button when Turnstile is complete 118 if (disableSubmit && submitButton) { 119 cfturnstile_elementor_set_submit(submitButton, true); 120 } 121 if (typeof turnstileElementorCallback === 'function') { 122 turnstileElementorCallback(token); 123 } 124 }, 125 'error-callback': function() { 126 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 127 }, 128 'expired-callback': function() { 129 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 130 } 131 }); 132 133 if (labelEl && labelEl.classList.contains('cfturnstile-widget-label-interaction') && typeof window.cfturnstileInitInteractionLabels === 'function') { 134 window.cfturnstileInitInteractionLabels(); 135 } 136 137 form.classList.add('cft-processed'); 138 } 139 }); 140} 141 142document.addEventListener('DOMContentLoaded', function() { 143 cfturnstile_init_elementor_forms(); 144}); 145 146// Listen to Elementor frontend init to handle cached elements 147jQuery(window).on('elementor/frontend/init', function() { 148 cfturnstile_init_elementor_forms(); 149 150 // Hook into Elementor's widget ready system for forms loaded dynamically (e.g. in popups) 151 if (window.elementorFrontend && elementorFrontend.hooks) { 152 elementorFrontend.hooks.addAction('frontend/element_ready/form.default', function($scope) { 153 cfturnstile_init_elementor_forms(); 154 });
155 elementorFrontend.hooks.addAction('frontend/element_ready/login.default', function($scope) { 156 cfturnstile_init_elementor_forms(); 157 }); 158 } 159}); 160 161// Re-render Turnstile only after Elementor reports a submit error (e.g. failed validation) 162function cfturnstile_elementor_rerender(form) { 163 if (!form || !window.turnstile) return; 164 var settings = window.cfturnstileElementorSettings || {}; 165 var widget = form.querySelector('.cf-turnstile'); 166 if (!widget) return; 167 var submitButton = form.querySelector('button[type="submit"]'); 168 var disableSubmit = settings.disableSubmit || false; 169 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 170 try { turnstile.remove(widget); } catch (e) {} 171 turnstile.render(widget, { 172 sitekey: settings.sitekey, 173 theme: settings.theme || 'auto', 174 size: settings.size || 'normal', 175 appearance: settings.appearance || 'always', 176 callback: function(token) { 177 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, true); } 178 if (typeof turnstileElementorCallback === 'function') { 179 turnstileElementorCallback(token); 180 } 181 }, 182 'error-callback': function() { 183 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 184 }, 185 'expired-callback': function() { 186 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 187 } 188 }); 189} 190 191jQuery(document).on('submit_error submit_success', '.elementor-form', function() { 192 var settings = window.cfturnstileElementorSettings || {}; 193 if ((settings.mode || 'turnstile') !== 'turnstile') return; 194 cfturnstile_elementor_rerender(this); 195}); 196 197// Block submission until Turnstile is completed (client-side guard). 198// Server-side validation in cfturnstile_elementor_check is the source of truth; 199// this just avoids a wasted round-trip and gives immediate feedback. 200document.addEventListener('submit', function(event) { 201 var form = event.target; 202 if (!form.classList) return; 203 var isClassic = form.classList.contains('elementor-form'); 204 var isAtomic = !isClassic && form.matches && form.matches(CFT_ATOMIC_FORM_SELECTOR); 205 if (!isClassic && !isAtomic) return; 206 var settings = window.cfturnstileElementorSettings || {}; 207 if ((settings.mode || 'turnstile') !== 'turnstile' || !window.turnstile) return; 208 209 var widget = form.querySelector('.cf-turnstile'); 210 if (!widget) return; 211 if (!cfturnstile_elementor_token(form)) { 212 event.preventDefault(); 213 event.stopImmediatePropagation(); 214 try { widget.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (e) {} 215 } 216}, true); 217 218// Handle Elementor popup show events (jQuery event - must use jQuery to listen) 219jQuery(document).on('elementor/popup/show', function(event, id, instance) { 220 setTimeout(function() { 221 // First, inject Turnstile into any unprocessed forms inside the popup 222 cfturnstile_init_elementor_forms(); 223 224 var settings = window.cfturnstileElementorSettings || {}; 225 var mode = settings.mode || 'turnstile'; 226 var disableSubmit = settings.disableSubmit || false; 227 if (mode !== 'turnstile' || !window.turnstile) { 228 return; 229 } 230 231 // Re-render all Turnstile widgets inside popup modals (they need explicit render after DOM insertion) 232 var popupTurnstiles = document.querySelectorAll('.elementor-popup-modal .cf-turnstile'); 233 popupTurnstiles.forEach(function(widget) { 234 var failedText = widget.parentNode ? widget.parentNode.querySelector('.cf-turnstile-failed-text') : null; 235 if (failedText) { 236 failedText.style.display = 'none'; 237 } 238 239 // Find the form and submit button for this widget 240 var form = widget.closest('.elementor-form'); 241 var submitButton = form ? form.querySelector('button[type="submit"]') : null; 242 243 // Disable submit button if option is enabled 244 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 245 246 turnstile.remove(widget); 247 turnstile.render(widget, { 248 sitekey: cfturnstileElementorSettings.sitekey, 249 appearance: cfturnstileElementorSettings.appearance || 'always', 250 callback: function(token) { 251 // Re-enable submit button when Turnstile is complete 252 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, true); } 253 if (typeof turnstileElementorCallback === 'function') { 254 turnstileElementorCallback(token); 255 } 256 }, 257 'error-callback': function() { 258 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 259 }, 260 'expired-callback': function() { 261 if (disableSubmit && submitButton) { cfturnstile_elementor_set_submit(submitButton, false); } 262 }, 263 theme: cfturnstileElementorSettings.theme || 'auto', 264 size: cfturnstileElementorSettings.size || 'normal' 265 }); 266 });
267 }, 500); 268}); 269 270/* --------------------------------------------------------------------------- 271 * Elementor Atomic Forms (Elementor 4 "e-form" elements) 272 * 273 * Atomic forms are submitted by an Alpine handler bound to the form, which posts 274 * a FormData payload built only from the field widgets Elementor itself rendered 275 * (input/textarea/select carrying data-interaction-id). A hidden token input 276 * sitting inside the form is therefore never sent, so the token is appended to 277 * the outgoing request instead - see cfturnstile_atomic_patch_fetch() below. 278 * ------------------------------------------------------------------------ */ 279 280function cfturnstile_atomic_submit_button(form) { 281 return form ? form.querySelector('button[type="submit"], input[type="submit"]') : null; 282} 283 284/** 285 * Horizontal alignment of the widget inside its (column flex) wrapper. 286 */ 287function cfturnstile_atomic_align(align) { 288 if (align === 'center') return 'center'; 289 if (align === 'right') return 'flex-end'; 290 return 'flex-start'; 291} 292 293/** 294 * Place a node according to the configured widget position. Atomic forms are flex 295 * containers, so the node is inserted as a full width row of its own. 296 */ 297function cfturnstile_atomic_insert(form, node, position, submitButton) { 298 if (position === 'afterform' || !submitButton || !submitButton.parentNode) { 299 form.appendChild(node); 300 } else if (position === 'after') { 301 submitButton.parentNode.insertBefore(node, submitButton.nextSibling); 302 } else { 303 submitButton.parentNode.insertBefore(node, submitButton); 304 } 305} 306 307function cfturnstile_atomic_render_options(form) { 308 var settings = window.cfturnstileElementorSettings || {}; 309 var disableSubmit = settings.disableSubmit || false; 310 var submitButton = cfturnstile_atomic_submit_button(form); 311 return { 312 sitekey: settings.sitekey, 313 theme: settings.theme || 'auto', 314 size: settings.size || 'normal', 315 appearance: settings.appearance || 'always', 316 action: 'elementor-atomic-form', 317 callback: function(token) { 318 if (disableSubmit) { cfturnstile_elementor_set_submit(submitButton, true); } 319 if (typeof turnstileElementorCallback === 'function') { 320 turnstileElementorCallback(token); 321 } 322 }, 323 'error-callback': function() { 324 if (disableSubmit) { cfturnstile_elementor_set_submit(submitButton, false); } 325 }, 326 'expired-callback': function() { 327 if (disableSubmit) { cfturnstile_elementor_set_submit(submitButton, false); } 328 } 329 }; 330} 331 332function cfturnstile_init_elementor_atomic_forms() { 333 var settings = window.cfturnstileElementorSettings || {}; 334 var sitekey = settings.sitekey || ''; 335 var position = settings.position || 'before'; 336 var mode = settings.mode || 'turnstile'; 337 var recaptchaSiteKey = settings.recaptchaSiteKey || ''; 338 var disableSubmit = settings.disableSubmit || false; 339 340 if (!window._cft_atomic_idx) { window._cft_atomic_idx = 0; } 341 342 // Elementor re-renders form elements in place (editor edits, popups, Alpine refreshes), 343 // which drops the injected widget. Release those forms so they are processed again. 344 document.querySelectorAll(CFT_ATOMIC_FORM_SELECTOR + '.cft-processed').forEach(function(form) { 345 if (!form.querySelector('.cf-turnstile, .g-recaptcha, input[name="cfturnstile_failsafe"]')) { 346 form.classList.remove('cft-processed'); 347 } 348 }); 349 350 document.querySelectorAll(CFT_ATOMIC_FORM_SELECTOR + ':not(.cft-processed)').forEach(function(form) { 351 if (form.querySelector('.cf-turnstile, .g-recaptcha, input[name="cfturnstile_failsafe"]')) { 352 form.classList.add('cft-processed'); 353 return; 354 } 355 356 var submitButton = cfturnstile_atomic_submit_button(form); 357 358 // Failsafe modes: post a marker, and render reCAPTCHA in its place if configured. 359 if (mode === 'allow' || mode === 'recaptcha') { 360 var marker = document.createElement('input'); 361 marker.type = 'hidden'; 362 marker.name = 'cfturnstile_failsafe'; 363 marker.value = mode; 364 form.appendChild(marker); 365 366 if (mode === 'recaptcha' && recaptchaSiteKey) { 367 var recaptchaWrap = document.createElement('div'); 368 recaptchaWrap.className = 'cfturnstile-atomic-field'; 369 recaptchaWrap.style.cssText = 'display: flex; flex-direction: column; align-items: ' + cfturnstile_atomic_align(settings.align) + '; flex: 0 0 100%; width: 100%; margin: 10px 0 15px 0;'; 370 var recaptchaDiv = document.createElement('div'); 371 recaptchaDiv.className = 'g-recaptcha'; 372 recaptchaDiv.setAttribute('data-sitekey', recaptchaSiteKey); 373 recaptchaWrap.appendChild(recaptchaDiv); 374 cfturnstile_atomic_insert(form, recaptchaWrap, position, submitButton); 375 } 376 377 form.classList.add('cft-processed'); 378 return; 379 } 380 381 // The submit button is rendered as a child widget, so it can still be missing on an
382 // early pass. Wait for the next pass rather than injecting into a half built form. 383 if (!submitButton || !window.turnstile || !sitekey) return; 384 385 if (disableSubmit) { 386 cfturnstile_elementor_set_submit(submitButton, false); 387 } 388 389 var index = window._cft_atomic_idx++; 390 391 var wrap = document.createElement('div'); 392 wrap.className = 'cfturnstile-atomic-field'; 393 wrap.style.cssText = 'display: flex; flex-direction: column; align-items: ' + cfturnstile_atomic_align(settings.align) + '; flex: 0 0 100%; width: 100%; margin: 10px 0 15px 0;'; 394 395 if (settings.labelEnable && settings.labelText) { 396 var labelEl = document.createElement('p'); 397 labelEl.className = 'cfturnstile-widget-label'; 398 labelEl.style.cssText = 'font-size: 14px; margin: 0 0 6px 0;'; 399 if ((settings.appearance || 'always') === 'interaction-only') { 400 labelEl.className += ' cfturnstile-widget-label-interaction'; 401 labelEl.style.display = 'none'; 402 } 403 var smallEl = document.createElement('small'); 404 smallEl.textContent = settings.labelText; 405 labelEl.appendChild(smallEl); 406 wrap.appendChild(labelEl); 407 } 408 409 var turnstileDiv = document.createElement('div'); 410 turnstileDiv.className = 'elementor-turnstile-field cf-turnstile'; 411 turnstileDiv.id = 'cf-turnstile-elementor-atomic-' + index; 412 wrap.appendChild(turnstileDiv); 413 414 cfturnstile_atomic_insert(form, wrap, position, submitButton); 415 416 turnstile.render(turnstileDiv, cfturnstile_atomic_render_options(form)); 417 418 if (settings.labelEnable && (settings.appearance || 'always') === 'interaction-only' && typeof window.cfturnstileInitInteractionLabels === 'function') { 419 window.cfturnstileInitInteractionLabels(); 420 } 421 422 form.classList.add('cft-processed'); 423 }); 424} 425 426/** 427 * Reset a widget after its form has been submitted, so a retry gets a fresh token. 428 * Elementor calls form.reset() on success, which empties the token input while the 429 * widget still holds the spent token - the global refresh helper cannot detect that. 430 */ 431function cfturnstile_atomic_reset(form) { 432 if (!form || !window.turnstile) return; 433 var widget = form.querySelector('.cf-turnstile'); 434 if (!widget) return; 435 var settings = window.cfturnstileElementorSettings || {}; 436 if (settings.disableSubmit) { 437 cfturnstile_elementor_set_submit(cfturnstile_atomic_submit_button(form), false); 438 } 439 try { turnstile.reset(widget); } catch (e) {} 440} 441 442/** 443 * Attach the token to the atomic form request. 444 * 445 * Elementor builds the FormData itself and only includes its own field widgets, so 446 * there is no markup based way to get the token to the server. The wrapper only 447 * touches requests carrying the atomic form action and leaves everything else alone. 448 */ 449function cfturnstile_atomic_patch_fetch() { 450 if (window._cfturnstileAtomicFetchPatched) return; 451 var originalFetch = window.fetch; 452 if (typeof originalFetch !== 'function') return; 453 window._cfturnstileAtomicFetchPatched = true; 454 455 window.fetch = function(input, init) { 456 var form = null; 457 try { 458 var body = init && init.body; 459 if (body instanceof FormData && body.get('action') === CFT_ATOMIC_AJAX_ACTION) { 460 var formId = body.get('form_id'); 461 if (formId && /^[A-Za-z0-9_-]+$/.test(formId)) { 462 form = document.querySelector(CFT_ATOMIC_FORM_SELECTOR + '[data-id="' + formId + '"]'); 463 } 464 if (form) { 465 var token = cfturnstile_elementor_token(form); 466 if (token) { 467 body.set('cf-turnstile-response', token); 468 } 469 var failsafe = form.querySelector('input[name="cfturnstile_failsafe"]'); 470 if (failsafe && failsafe.value) { 471 body.set('cfturnstile_failsafe', failsafe.value); 472 } 473 var recaptcha = form.querySelector('textarea[name="g-recaptcha-response"], input[name="g-recaptcha-response"]'); 474 if (recaptcha && recaptcha.value) { 475 body.set('g-recaptcha-response', recaptcha.value); 476 } 477 } 478 } 479 } catch (e) {} 480
481 // Callers reaching fetch from strict mode code (bundles, ES modules) invoke it bare, so 482 // `this` arrives undefined. Current Chrome tolerates that, but engines that hold fetch to 483 // its WebIDL receiver throw "Illegal invocation" - fall back to the global rather than 484 // making that a property of whether this plugin happens to be active. 485 var request = originalFetch.apply(this || window, arguments); 486 487 if (form) { 488 var reset = function() { 489 // Let Elementor apply its own result handling (including form.reset()) first. 490 setTimeout(function() { cfturnstile_atomic_reset(form); }, 500); 491 }; 492 request.then(reset, reset); 493 } 494 495 return request; 496 }; 497} 498 499(function() { 500 cfturnstile_atomic_patch_fetch(); 501 502 var scheduled = null; 503 function schedule() { 504 if (scheduled) return; 505 scheduled = setTimeout(function() { 506 scheduled = null; 507 cfturnstile_init_elementor_atomic_forms(); 508 }, 100); 509 } 510 511 if (document.readyState === 'loading') { 512 document.addEventListener('DOMContentLoaded', schedule); 513 } else { 514 schedule(); 515 } 516 517 // Atomic forms are mounted by Elementor's own handler system and can appear or be 518 // re-rendered at any point (popups, editor edits, lazy loaded content), so watch the 519 // document rather than hooking into a specific lifecycle event. 520 if (window.MutationObserver) { 521 new MutationObserver(schedule).observe(document.documentElement, { childList: true, subtree: true }); 522 } 523})();
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.