PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.26.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.26.1
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / js / formidable.js

formidable.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.26.1, at js/formidable.js

2,158 lines 59.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* exported frmRecaptcha, frmAfterRecaptcha */
2 /* eslint-disable prefer-const */
3
4 function frmFrontFormJS() {
5 'use strict';
6
7 /*global jQuery:false, frm_js, grecaptcha, hcaptcha, turnstile, frmProForm, tinyMCE */
8 /*global frmThemeOverride_jsErrors, frmThemeOverride_frmPlaceError, frmThemeOverride_frmAfterSubmit */
9
10 let jsErrors = [];
11
12 /**
13 * Triggers custom JS event.
14 *
15 * @since 5.5.3
16 *
17 * @param {HTMLElement} el The HTML element.
18 * @param {string} eventName Event name.
19 * @param {*} data The passed data.
20 */
21 function triggerCustomEvent( el, eventName, data ) {
22 if ( typeof window.CustomEvent !== 'function' ) {
23 return;
24 }
25
26 const event = new CustomEvent( eventName );
27 event.frmData = data;
28
29 el.dispatchEvent( event );
30 }
31
32 /**
33 * Get the ID of the field that changed.
34 *
35 * @param {HTMLElement|jQuery} field
36 * @param {boolean} fullID
37 * @return {string|number} Field ID.
38 */
39 function getFieldId( field, fullID ) {
40 let nameParts, fieldId,
41 isRepeating = false,
42 fieldName = '';
43
44 if ( field instanceof jQuery ) {
45 field = field.get( 0 );
46 }
47
48 fieldName = field.name;
49
50 if ( typeof fieldName === 'undefined' ) {
51 fieldName = '';
52 }
53
54 if ( fieldName === '' ) {
55 fieldName = field.getAttribute( 'data-name' );
56
57 if ( typeof fieldName === 'undefined' ) {
58 fieldName = '';
59 }
60
61 if ( fieldName !== '' && fieldName ) {
62 return fieldName;
63 }
64 return 0;
65 }
66
67 nameParts = fieldName.replace( 'item_meta[', '' ).replace( '[]', '' ).split( ']' );
68 //TODO: Fix this for checkboxes and address fields
69 if ( nameParts.length < 1 ) {
70 return 0;
71 }
72 nameParts = nameParts.filter( function( n ) {
73 return n !== '';
74 } );
75
76 fieldId = nameParts[ 0 ];
77
78 if ( nameParts.length === 1 ) {
79 return fieldId;
80 }
81
82 if ( nameParts[ 1 ] === '[form' || nameParts[ 1 ] === '[row_ids' ) {
83 return 0;
84 }
85
86 // Check if 'this' is in a repeating section
87 if ( document.querySelector( 'input[name="item_meta[' + fieldId + '][form]"]' ) ) {
88 // this is a repeatable section with name: item_meta[repeating-section-id][row-id][field-id]
89 fieldId = nameParts[ 2 ].replace( '[', '' );
90 isRepeating = true;
91 }
92
93 // Check if 'this' is an other text field and get field ID for it
94 if ( 'other' === fieldId ) {
95 if ( isRepeating ) {
96 // name for other fields: item_meta[370][0][other][414]
97 fieldId = nameParts[ 3 ].replace( '[', '' );
98 } else {
99 // Other field name: item_meta[other][370]
100 fieldId = nameParts[ 1 ].replace( '[', '' );
101 }
102 }
103
104 if ( fullID === true ) {
105 // For use in the container div id
106 if ( fieldId === nameParts[ 0 ] ) {
107 fieldId = fieldId + '-' + nameParts[ 1 ].replace( '[', '' );
108 } else {
109 fieldId = fieldId + '-' + nameParts[ 0 ] + '-' + nameParts[ 1 ].replace( '[', '' );
110 }
111 }
112
113 return fieldId;
114 }
115
116 /**
117 * Disable the submit button for a given jQuery form object
118 *
119 * @since 2.03.02
120 *
121 * @param {Object} $form
122 */
123 function disableSubmitButton( $form ) {
124 const form = $form instanceof jQuery ? $form.get( 0 ) : $form;
125 if ( ! form ) {
126 return;
127 }
128 form.querySelectorAll( 'input[type="submit"], input[type="button"], button[type="submit"], button.frm_save_draft' ).forEach(
129 button => button.disabled = true
130 );
131 }
132
133 /**
134 * Enable the submit button for a given jQuery form object
135 *
136 * @since 2.03.02
137 *
138 * @param {HTMLElement} form
139 *
140 * @return {void}
141 */
142 function enableSubmitButton( form ) {
143 form.querySelectorAll( 'input[type="submit"], input[type="button"], button[type="submit"]' ).forEach(
144 button => button.disabled = false
145 );
146 }
147
148 /**
149 * Disable the save draft link for a given jQuery form object
150 *
151 * @since 4.04.03
152 *
153 * @param {Object} $form
154 */
155 function disableSaveDraft( $form ) {
156 const form = $form instanceof jQuery ? $form.get( 0 ) : $form;
157 if ( ! form ) {
158 return;
159 }
160 form.querySelectorAll( 'a.frm_save_draft' ).forEach(
161 link => link.style.pointerEvents = 'none'
162 );
163 }
164
165 /**
166 * Enable the save draft link for a given form object.
167 *
168 * @since 4.04.03
169 *
170 * @param {jQuery|HTMLElement} $form
171 */
172 function enableSaveDraft( $form ) {
173 const form = $form instanceof jQuery ? $form.get( 0 ) : $form;
174 if ( ! form ) {
175 return;
176 }
177 form.querySelectorAll( '.frm_save_draft' ).forEach( saveDraftButton => {
178 saveDraftButton.disabled = false;
179 saveDraftButton.style.pointerEvents = '';
180 } );
181 }
182
183 /**
184 * Validate form with JS.
185 *
186 * @param {HTMLElement|jQuery} object
187 * @return {Array} Errors.
188 */
189 function validateForm( object ) {
190 let errors = [];
191
192 const vanillaJsObject = 'function' === typeof object.get ? object.get( 0 ) : object;
193
194 // Required field validation.
195 vanillaJsObject?.querySelectorAll( '.frm_required_field' ).forEach(
196 requiredField => {
197 const isVisible = requiredField.offsetParent !== null;
198 if ( ! isVisible ) {
199 return;
200 }
201
202 requiredField.querySelectorAll( 'input, select, textarea' ).forEach(
203 requiredInput => {
204 if ( hasClass( requiredInput, 'frm_optional' ) || hasClass( requiredInput, 'ed_button' ) ) {
205 // skip rich text field buttons.
206 return;
207 }
208
209 errors = checkRequiredField( requiredInput, errors );
210 }
211 );
212 }
213 );
214
215 vanillaJsObject?.querySelectorAll( 'input,select,textarea' ).forEach(
216 field => {
217 if ( '' === field.value ) {
218 if ( 'number' === field.type ) {
219 // A number field will return an empty string when it is invalid.
220 checkValidity( field, errors );
221 }
222
223 const isConfirmationField = field.name && 0 === field.name.indexOf( 'item_meta[conf_' );
224 if ( ! isConfirmationField ) {
225 // Allow a blank confirmation field to still call validateFieldValue.
226 // If we continue for a confirmation field there are issues with forms submitting with a blank confirmation field.
227 return;
228 }
229 }
230
231 validateFieldValue( field, errors, true );
232 checkValidity( field, errors );
233 }
234 );
235
236 // Invisible captchas are processed after validation.
237 // We only want to validate a visible captcha on submit.
238 if ( ! hasInvisibleRecaptcha( object ) ) {
239 errors = validateRecaptcha( object, errors );
240 }
241
242 return errors;
243 }
244
245 /**
246 * Check the ValidityState interface for the field.
247 * If it is invalid, show an error for it.
248 *
249 * @param {HTMLElement} field
250 * @param {Array} errors
251 * @return {void}
252 */
253 function checkValidity( field, errors ) {
254 let fieldID;
255 if ( 'object' !== typeof field.validity || false !== field.validity.valid ) {
256 return;
257 }
258
259 fieldID = getFieldId( field, true );
260 if ( 'undefined' === typeof errors[ fieldID ] ) {
261 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
262 }
263
264 if ( 'function' === typeof field.reportValidity ) {
265 // This triggers an error pop up.
266 field.reportValidity();
267 }
268 }
269
270 /**
271 * @since 5.0.10
272 *
273 * @param {Object} element
274 * @param {string} targetClass
275 * @return {boolean} True if the element has the target class.
276 */
277 function hasClass( element, targetClass ) {
278 return element.classList && element.classList.contains( targetClass );
279 }
280
281 /**
282 * @param {HTMLElement} field
283 */
284 function maybeValidateChange( field ) {
285 if ( field.type === 'url' ) {
286 maybeAddHttpsToUrl( field );
287 }
288 const form = field.closest( 'form' );
289 if ( form && hasClass( form, 'frm_js_validate' ) ) {
290 validateField( field );
291 }
292 }
293
294 /**
295 * @param {HTMLElement} field
296 */
297 function maybeAddHttpsToUrl( field ) {
298 const url = field.value;
299 const matches = url.match( /^(https?|ftps?|mailto|news|feed|telnet):/ );
300 if ( field.value !== '' && matches === null ) {
301 field.value = 'https://' + url;
302 }
303 }
304
305 /**
306 * Validate a field with JS.
307 *
308 * @param {HTMLElement} field
309 *
310 * @return {void}
311 */
312 function validateField( field ) {
313 let errors, key;
314
315 errors = [];
316 const fieldContainer = field.closest( '.frm_form_field' );
317
318 if ( ! fieldContainer ) {
319 // Hidden fields do not have a field container and do not require JS validation.
320 return;
321 }
322
323 if ( hasClass( fieldContainer, 'frm_required_field' ) && ! hasClass( field, 'frm_optional' ) ) {
324 errors = checkRequiredField( field, errors );
325 }
326
327 if ( errors.length < 1 ) {
328 validateFieldValue( field, errors, false );
329 }
330
331 removeFieldError( fieldContainer );
332 if ( Object.keys( errors ).length > 0 ) {
333 for ( key in errors ) {
334 addFieldError( fieldContainer, key, errors );
335 }
336 }
337 }
338
339 /**
340 * Validates a field value.
341 *
342 * @since 6.15 Added `onSubmit` parameter.
343 *
344 * @param {HTMLElement} field Field input.
345 * @param {Object} errors Errors data.
346 * @param {boolean} onSubmit Is `true` if the form is being submitted.
347 */
348 function validateFieldValue( field, errors, onSubmit ) {
349 if ( field.type === 'hidden' ) {
350 // don't validate
351 } else if ( field.type === 'number' ) {
352 checkNumberField( field, errors );
353 } else if ( field.type === 'email' ) {
354 checkEmailField( field, errors, onSubmit );
355 } else if ( field.type === 'password' ) {
356 checkPasswordField( field, errors, onSubmit );
357 } else if ( field.type === 'url' ) {
358 checkUrlField( field, errors );
359 } else if ( field.pattern !== null ) {
360 checkPatternField( field, errors );
361 }
362
363 if ( 'tel' === field.type && shouldCheckConfirmField( field, onSubmit ) ) {
364 confirmField( field, errors );
365 }
366
367 /**
368 * @since 6.15 Added `onSubmit` to the data.
369 */
370 triggerCustomEvent( document, 'frm_validate_field_value', {
371 field: field,
372 errors: errors,
373 onSubmit: onSubmit
374 } );
375 }
376
377 /**
378 * @param {HTMLElement} field
379 * @param {Array} errors
380 * @return {Array} Errors
381 */
382 function checkRequiredField( field, errors ) {
383 let tempVal, i, placeholder,
384 val = '',
385 fieldID = '',
386 fileID = field.getAttribute( 'data-frmfile' );
387
388 if ( field.type === 'hidden' && fileID === null && ! isAppointmentField( field ) && ! isInlineDatepickerField( field ) ) {
389 return errors;
390 }
391
392 if ( field.type === 'checkbox' || field.type === 'radio' ) {
393 document.querySelectorAll( 'input[name="' + field.name + '"]' ).forEach( function( input ) {
394 const requiredField = input.closest( '.frm_required_field' );
395 if ( ! requiredField ) {
396 return;
397 }
398
399 const checkedInputs = requiredField.querySelectorAll( 'input:checked' );
400 checkedInputs.forEach( function( checkedInput ) {
401 val = checkedInput.value;
402 } );
403 } );
404 } else if ( field.type === 'file' || fileID ) {
405 if ( typeof fileID === 'undefined' ) {
406 fileID = getFieldId( field, true );
407 fileID = fileID.replace( 'file', '' );
408 }
409
410 if ( typeof errors[ fileID ] === 'undefined' ) {
411 val = getFileVals( fileID );
412 }
413 fieldID = fileID;
414 } else {
415 if ( hasClass( field, 'frm_pos_none' ) ) {
416 // skip hidden other fields
417 return errors;
418 }
419
420 val = jQuery( field ).val();
421
422 if ( val === null ) {
423 val = '';
424 } else if ( typeof val !== 'string' ) {
425 tempVal = val;
426 val = '';
427 for ( i = 0; i < tempVal.length; i++ ) {
428 if ( tempVal[ i ] !== '' ) {
429 val = tempVal[ i ];
430 }
431 }
432 }
433
434 if ( hasClass( field, 'frm_other_input' ) ) {
435 fieldID = getFieldId( field, false );
436
437 if ( val === '' ) {
438 field = document.getElementById( field.id.replace( '-otext', '' ) );
439 }
440 } else {
441 fieldID = getFieldId( field, true );
442 }
443
444 // Make sure fieldID is a string.
445 // fieldID may be a number which doesn't include a .replace function.
446 if ( 'function' !== typeof fieldID.replace ) {
447 fieldID = fieldID.toString();
448 }
449
450 if ( hasClass( field, 'frm_time_select' ) ) {
451 // set id for time field
452 fieldID = fieldID.replace( '-H', '' ).replace( '-m', '' );
453 } else if ( isSignatureField( field ) ) {
454 if ( val === '' ) {
455 const fieldContainer = field.closest( '.frm_form_field' );
456 const outputField = fieldContainer ? fieldContainer.querySelector( '[name="' + field.getAttribute( 'name' ).replace( '[typed]', '[output]' ) + '"]' ) : null;
457 val = outputField ? outputField.value : '';
458 }
459 fieldID = fieldID.replace( '-typed', '' );
460 }
461
462 placeholder = field.getAttribute( 'data-frmplaceholder' );
463 if ( placeholder !== null && val === placeholder ) {
464 val = '';
465 }
466 }
467
468 if ( val === '' ) {
469 if ( fieldID === '' ) {
470 fieldID = getFieldId( field, true );
471 }
472 if ( ! ( fieldID in errors ) ) {
473 errors[ fieldID ] = getFieldValidationMessage( field, 'data-reqmsg' );
474 }
475 }
476
477 return errors;
478 }
479
480 /**
481 * @param {HTMLElement} field
482 * @return {boolean} True if the input is a typed signature input.
483 */
484 function isSignatureField( field ) {
485 const name = field.getAttribute( 'name' );
486 return 'string' === typeof name && '[typed]' === name.substr( -7 );
487 }
488
489 /**
490 * @param {HTMLElement} field
491 * @return {boolean} True if the field is a SSA appointment field.
492 */
493 function isAppointmentField( field ) {
494 return hasClass( field, 'ssa_appointment_form_field_appointment_id' );
495 }
496
497 /**
498 * @param {HTMLElement} field
499 * @return {boolean} True if the field is inline datepicker field.
500 */
501 function isInlineDatepickerField( field ) {
502 return 'hidden' === field.type && '_alt' === field.id.substr( -4 ) && hasClass( field.nextElementSibling, 'frm_date_inline' );
503 }
504
505 /**
506 * @param {string|number} fileID
507 * @return {string} File input value.
508 */
509 function getFileVals( fileID ) {
510 let val = '';
511 const fileFields = document.querySelectorAll( 'input[name="file' + fileID + '"], input[name="file' + fileID + '[]"], input[name^="item_meta[' + fileID + ']"]' );
512
513 fileFields.forEach( function( field ) {
514 if ( val === '' ) {
515 val = field.value;
516 }
517 } );
518 return val;
519 }
520
521 /**
522 * @param {HTMLElement} field
523 * @param {Array} errors
524 * @return {void}
525 */
526 function checkUrlField( field, errors ) {
527 let fieldID,
528 url = field.value;
529
530 if ( url !== '' && ! /^http(s)?:\/\/(?:localhost|(?:[\da-z\.-]+\.[\da-z\.-]+))/i.test( url ) ) {
531 fieldID = getFieldId( field, true );
532 if ( ! ( fieldID in errors ) ) {
533 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
534 }
535 }
536 }
537
538 /**
539 * Checks if the confirm field should be checked.
540 *
541 * @since 6.15
542 *
543 * @param {HTMLElement} field Field input.
544 * @param {boolean} onSubmit Is `true` if the form is being submitted.
545 * @return {boolean} True if we should confirm the field.
546 */
547 function shouldCheckConfirmField( field, onSubmit ) {
548 if ( onSubmit ) {
549 // Always check on submitting.
550 return true;
551 }
552
553 if ( 0 === field.id.indexOf( 'field_conf_' ) ) {
554 // Always check if it's the confirm field.
555 return true;
556 }
557
558 return false;
559 }
560
561 /**
562 * Check the email field for errors.
563 *
564 * @since 6.15 Added `onSubmit` parameter.
565 *
566 * @param {HTMLElement} field Field input.
567 * @param {Object} errors Errors data.
568 * @param {boolean} onSubmit Is `true` if the form is being submitted.
569 */
570 function checkEmailField( field, errors, onSubmit ) {
571 const fieldID = getFieldId( field, true ),
572 pattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
573
574 // validate the current field we're editing first
575 if ( '' !== field.value && pattern.test( field.value ) === false ) {
576 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
577 }
578
579 if ( shouldCheckConfirmField( field, onSubmit ) ) {
580 confirmField( field, errors );
581 }
582 }
583
584 /**
585 * Check the password field for errors.
586 *
587 * @since 6.15 Added `onSubmit` parameter.
588 *
589 * @param {HTMLElement} field Field input.
590 * @param {Object} errors Errors data.
591 * @param {boolean} onSubmit Is `true` if the form is being submitted.
592 */
593 function checkPasswordField( field, errors, onSubmit ) {
594 if ( shouldCheckConfirmField( field, onSubmit ) ) {
595 confirmField( field, errors );
596 }
597 }
598
599 /**
600 * @param {HTMLElement} field
601 * @param {Array} errors
602 * @return {void}
603 */
604 function confirmField( field, errors ) {
605 let value, confirmValue, firstField,
606 fieldID = getFieldId( field, true ),
607 strippedId = field.id.replace( 'conf_', '' ),
608 strippedFieldID = fieldID.replace( 'conf_', '' ),
609 confirmField = document.getElementById( strippedId.replace( 'field_', 'field_conf_' ) );
610
611 if ( confirmField === null || typeof errors[ 'conf_' + strippedFieldID ] !== 'undefined' ) {
612 return;
613 }
614
615 if ( fieldID !== strippedFieldID ) {
616 firstField = document.getElementById( strippedId );
617 value = firstField.value;
618 confirmValue = confirmField.value;
619 if ( value !== confirmValue ) {
620 errors[ 'conf_' + strippedFieldID ] = getFieldValidationMessage( confirmField, 'data-confmsg' );
621 }
622 } else {
623 validateField( confirmField );
624 }
625 }
626
627 /**
628 * @param {HTMLElement} field
629 * @param {Array} errors
630 * @return {void}
631 */
632 function checkNumberField( field, errors ) {
633 let fieldID,
634 number = field.value;
635
636 if ( number !== '' && isNaN( number / 1 ) !== false ) {
637 fieldID = getFieldId( field, true );
638 if ( ! ( fieldID in errors ) ) {
639 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
640 }
641 }
642 }
643
644 /**
645 * @param {HTMLElement} field
646 * @param {Array} errors
647 * @return {void}
648 */
649 function checkPatternField( field, errors ) {
650 let fieldID,
651 text = field.value,
652 format = getFieldValidationMessage( field, 'pattern' );
653
654 if ( format !== '' && text !== '' ) {
655 fieldID = getFieldId( field, true );
656 if ( ! ( fieldID in errors ) ) {
657 if ( 'object' === typeof window.frmProForm && 'function' === typeof window.frmProForm.isIntlPhoneInput && window.frmProForm.isIntlPhoneInput( field ) ) {
658 if ( ! window.frmProForm.validateIntlPhoneInput( field ) ) {
659 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
660 }
661 } else {
662 format = new RegExp( '^' + format + '$', 'i' );
663 if ( format.test( text ) === false ) {
664 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
665 }
666 }
667 }
668 }
669 }
670
671 /**
672 * Set color for select placeholders.
673 *
674 * @since 6.5.1
675 */
676 function setSelectPlaceholderColor() {
677 let selects = document.querySelectorAll( '.form-field select' ),
678 styleElement = document.querySelector( '.with_frm_style' ),
679 textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : '',
680 changeSelectColor;
681
682 // Exit if there are no select elements or the textColorDisabled property is missing
683 if ( ! selects.length || ! textColorDisabled ) {
684 return;
685 }
686
687 // Function to change the color of a select element
688 changeSelectColor = function( select ) {
689 if ( select.options[ select.selectedIndex ] && hasClass( select.options[ select.selectedIndex ], 'frm-select-placeholder' ) ) {
690 select.style.setProperty( 'color', textColorDisabled, 'important' );
691 } else {
692 select.style.color = '';
693 }
694 };
695
696 // Use a loop to iterate through each select element
697 Array.prototype.forEach.call( selects, function( select ) {
698 // Apply the color change to each select element
699 changeSelectColor( select );
700
701 // Add an event listener for future changes
702 select.addEventListener( 'change', function() {
703 changeSelectColor( select );
704 } );
705 } );
706 }
707
708 /**
709 * @param {HTMLElement|jQuery} object
710 *
711 * @return {HTMLElement|false} Captcha element if there is an invisible recaptcha.
712 */
713 function hasInvisibleRecaptcha( object ) {
714 if ( isGoingToPrevPage( object ) ) {
715 return false;
716 }
717
718 const form = object instanceof jQuery ? object.get( 0 ) : object;
719 if ( ! form ) {
720 return false;
721 }
722
723 const recaptcha = form.querySelector( '.frm-g-recaptcha[data-size="invisible"], .g-recaptcha[data-size="invisible"]' );
724 if ( recaptcha ) {
725 const recaptchaID = recaptcha.dataset.rid;
726 const alreadyChecked = grecaptcha.getResponse( recaptchaID );
727 if ( alreadyChecked.length === 0 ) {
728 return recaptcha;
729 }
730 }
731
732 return false;
733 }
734
735 /**
736 * @param {HTMLElement} invisibleRecaptcha
737 *
738 * @return {void}
739 */
740 function executeInvisibleRecaptcha( invisibleRecaptcha ) {
741 const recaptchaID = invisibleRecaptcha.dataset.rid;
742 grecaptcha.reset( recaptchaID );
743 grecaptcha.execute( recaptchaID );
744 }
745
746 function validateRecaptcha( form, errors ) {
747 const formEl = form instanceof jQuery ? form.get( 0 ) : form;
748 if ( ! formEl ) {
749 return errors;
750 }
751
752 const recaptcha = formEl.querySelector( '.frm-g-recaptcha' );
753 if ( ! recaptcha ) {
754 return errors;
755 }
756
757 const recaptchaID = recaptcha.dataset.rid;
758 let response;
759
760 try {
761 response = grecaptcha.getResponse( recaptchaID );
762 } catch ( e ) {
763 if ( formEl.querySelector( 'input[name="recaptcha_checked"]' ) ) {
764 return errors;
765 }
766 response = '';
767 }
768
769 if ( response.length === 0 ) {
770 const fieldContainer = recaptcha.closest( '.frm_form_field' );
771 if ( fieldContainer && fieldContainer.id ) {
772 const fieldID = fieldContainer.id.replace( 'frm_field_', '' ).replace( '_container', '' );
773 errors[ fieldID ] = '';
774 }
775 }
776
777 return errors;
778 }
779
780 /**
781 * @param {HTMLElement} field
782 * @param {string} messageType
783 * @return {string} The error message to display.
784 */
785 function getFieldValidationMessage( field, messageType ) {
786 let msg = field.getAttribute( messageType );
787 if ( null === msg ) {
788 msg = '';
789 }
790
791 if ( '' !== msg && shouldWrapErrorHtmlAroundMessageType( messageType ) ) {
792 msg = wrapErrorHtml( msg, field );
793 }
794
795 return msg;
796 }
797
798 /**
799 * @param {string} msg
800 * @param {HTMLElement} field
801 * @return {string} The error HTML to use.
802 */
803 function wrapErrorHtml( msg, field ) {
804 let errorHtml = field.getAttribute( 'data-error-html' );
805 if ( null === errorHtml ) {
806 return msg;
807 }
808
809 errorHtml = errorHtml.replace( /\+/g, '%20' );
810 msg = decodeURIComponent( errorHtml ).replace( '[error]', msg );
811 const fieldId = getFieldId( field, false );
812 const split = fieldId.split( '-' );
813 const fieldIdParts = field.id.split( '_' );
814 fieldIdParts.shift(); // Drop the "field" value from the front.
815 split[ 0 ] = fieldIdParts.join( '_' );
816 const errorKey = split.join( '-' );
817 return msg.replace( '[key]', errorKey );
818 }
819
820 function shouldWrapErrorHtmlAroundMessageType( type ) {
821 return 'pattern' !== type;
822 }
823
824 /**
825 * Check if JS validation should happen.
826 *
827 * @param {HTMLElement|Object} object Form object.
828 * @return {boolean} True if validation is enabled and we are not saving a draft or going to a previous page.
829 */
830 function shouldJSValidate( object ) {
831 if ( 'function' === typeof object.get ) {
832 // Get the HTMLElement from a jQuery object.
833 object = object.get( 0 );
834 }
835 let validate = hasClass( object, 'frm_js_validate' );
836 if ( validate && typeof frmProForm !== 'undefined' && ( frmProForm.savingDraft( object ) || frmProForm.goingToPreviousPage( object ) ) ) {
837 validate = false;
838 }
839
840 return validate;
841 }
842
843 /**
844 * @param {HTMLElement} object
845 * @param {string} action
846 * @return {void}
847 */
848 function getFormErrors( object, action ) {
849 let data, success, error, shouldTriggerEvent;
850
851 const fieldsets = object.querySelectorAll( '.frm_form_field' );
852 fieldsets.forEach( field => field.classList.add( 'frm_doing_ajax' ) );
853
854 data = jQuery( object ).serialize() + '&action=frm_entries_' + action + '&nonce=' + frm_js.nonce; // eslint-disable-line camelcase
855 shouldTriggerEvent = object.classList.contains( 'frm_trigger_event_on_submit' );
856
857 const doRedirect = response => {
858 jQuery( document ).trigger( 'frmBeforeFormRedirect', [ object, response ] );
859
860 if ( ! response.openInNewTab ) {
861 // We return here because we're redirecting there is no need to update content.
862 window.location = response.redirect;
863 return;
864 }
865
866 // We don't return here because we're opening in a new tab, the old tab will still update.
867 const newTab = window.open( response.redirect, '_blank' );
868 if ( ! newTab && response.fallbackMsg && response.content ) {
869 response.content = response.content.trim().replace( /(<\/div><\/div>)$/, ' ' + response.fallbackMsg + '</div></div>' );
870 }
871 };
872
873 success = function( response ) {
874 let defaultResponse, formID, replaceContent, pageOrder, formReturned, contSubmit, delay,
875 $fieldCont, key, inCollapsedSection, frmTrigger;
876
877 defaultResponse = {
878 content: '',
879 errors: {},
880 pass: false
881 };
882
883 if ( response === null ) {
884 response = defaultResponse;
885 } else {
886 // Response is a string. Convert it to an object.
887 response = response.replace( /^\s+|\s+$/g, '' );
888 if ( response.indexOf( '{' ) === 0 ) {
889 response = JSON.parse( response );
890 } else {
891 response = defaultResponse;
892 }
893 }
894
895 if ( typeof response.redirect !== 'undefined' ) {
896 if ( shouldTriggerEvent ) {
897 triggerCustomEvent( object, 'frmSubmitEvent' );
898 return;
899 }
900
901 if ( response.delay ) {
902 setTimeout( function() {
903 doRedirect( response );
904 }, 1000 * response.delay );
905 } else {
906 doRedirect( response );
907 }
908 }
909
910 if ( 'string' === typeof response.content && response.content !== '' ) {
911 // the form or success message was returned
912
913 if ( shouldTriggerEvent ) {
914 triggerCustomEvent( object, 'frmSubmitEvent', { content: response.content } );
915 return;
916 }
917
918 removeSubmitLoading( jQuery( object ) );
919 if ( frm_js.offset != -1 ) { // eslint-disable-line camelcase
920 frmFrontForm.scrollMsg( jQuery( object ), false );
921 }
922
923 const formIdInput = object.querySelector( 'input[name="form_id"]' );
924 formID = formIdInput ? formIdInput.value : '';
925 response.content = response.content.replace( / frm_pro_form /g, ' frm_pro_form frm_no_hide ' );
926 replaceContent = jQuery( object ).closest( '.frm_forms' );
927 removeAddedScripts( replaceContent, formID );
928 delay = maybeSlideOut( replaceContent, response.content );
929
930 setTimeout(
931 function() {
932 afterFormSubmittedBeforeReplace( object, response );
933
934 replaceContent.replaceWith( response.content );
935
936 addUrlParam( response );
937
938 if ( typeof frmThemeOverride_frmAfterSubmit === 'function' ) { // eslint-disable-line camelcase
939 const pageOrderInput = document.querySelector( 'input[name="frm_page_order_' + formID + '"]' );
940 pageOrder = pageOrderInput ? pageOrderInput.value : '';
941 const tempDiv = document.createElement( 'div' );
942 tempDiv.innerHTML = response.content;
943 const formReturnedInput = tempDiv.querySelector( 'input[name="form_id"]' );
944 formReturned = formReturnedInput ? formReturnedInput.value : '';
945 frmThemeOverride_frmAfterSubmit( formReturned, pageOrder, response.content, object );
946 }
947
948 afterFormSubmitted( object, response );
949 },
950 delay
951 );
952 } else if ( Object.keys( response.errors ).length ) {
953 // errors were returned
954 removeSubmitLoading( jQuery( object ), 'enable' );
955
956 //show errors
957 contSubmit = true;
958 removeAllErrors();
959
960 $fieldCont = null;
961
962 for ( key in response.errors ) {
963 const fieldContEl = object.querySelector( '#frm_field_' + key + '_container' );
964 $fieldCont = fieldContEl ? jQuery( fieldContEl ) : jQuery();
965
966 if ( $fieldCont.length ) {
967 if ( ! $fieldCont.is( ':visible' ) ) {
968 inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' );
969 if ( inCollapsedSection.length ) {
970 frmTrigger = inCollapsedSection.prev();
971 if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) {
972 // If the frmTrigger object is the section description, check to see if the previous element is the trigger
973 frmTrigger = frmTrigger.prev( '.frm_trigger' );
974 }
975 frmTrigger.trigger( 'click' );
976 }
977 }
978
979 if ( $fieldCont.is( ':visible' ) ) {
980 addFieldError( $fieldCont, key, response.errors );
981 contSubmit = false;
982 }
983 }
984 }
985
986 object.querySelectorAll( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).forEach( function( captchaEl ) {
987 const recaptchaID = captchaEl.dataset.rid;
988
989 if ( typeof grecaptcha !== 'undefined' && grecaptcha ) {
990 if ( recaptchaID ) {
991 grecaptcha.reset( recaptchaID );
992 } else {
993 grecaptcha.reset();
994 }
995 }
996
997 if ( typeof hcaptcha !== 'undefined' && hcaptcha ) {
998 hcaptcha.reset();
999 }
1000 } );
1001
1002 if ( window.turnstile ) {
1003 object.querySelectorAll( '.frm-cf-turnstile' ).forEach(
1004 turnstileField => turnstileField.dataset.rid && turnstile.reset( turnstileField.dataset.rid )
1005 );
1006 }
1007
1008 jQuery( document ).trigger( 'frmFormErrors', [ object, response ] );
1009
1010 fieldsets.forEach( field => field.classList.remove( 'frm_doing_ajax' ) );
1011 scrollToFirstField( object );
1012
1013 if ( contSubmit ) {
1014 object.submit();
1015 } else {
1016 object.insertAdjacentHTML( 'afterbegin', response.error_message );
1017 checkForErrorsAndMaybeSetFocus();
1018 }
1019 } else {
1020 // there may have been a plugin conflict, or the form is not set to submit with ajax
1021
1022 showFileLoading( object );
1023
1024 object.submit();
1025 }
1026 };
1027
1028 error = function() {
1029 object.querySelectorAll( 'input[type="submit"], input[type="button"]' ).forEach(
1030 button => button.disabled = false
1031 );
1032 object.submit();
1033 };
1034
1035 postToAjaxUrl( object, data, success, error );
1036 }
1037
1038 function postToAjaxUrl( form, data, success, error ) {
1039 let ajaxUrl, action, ajaxParams;
1040
1041 ajaxUrl = frm_js.ajax_url; // eslint-disable-line camelcase
1042 action = form.getAttribute( 'action' );
1043
1044 if ( 'string' === typeof action && -1 !== action.indexOf( '?action=frm_forms_preview' ) ) {
1045 ajaxUrl = action.split( '?action=frm_forms_preview' )[ 0 ];
1046 }
1047
1048 ajaxParams = {
1049 type: 'POST',
1050 url: ajaxUrl,
1051 data: data,
1052 success: success
1053 };
1054
1055 if ( 'function' === typeof error ) {
1056 ajaxParams.error = error;
1057 }
1058
1059 jQuery.ajax( ajaxParams );
1060 }
1061
1062 function afterFormSubmitted( object, response ) {
1063 const tempDiv = document.createElement( 'div' );
1064 tempDiv.innerHTML = response.content;
1065 const formCompleted = tempDiv.querySelector( '.frm_message' );
1066 if ( formCompleted ) {
1067 jQuery( document ).trigger( 'frmFormComplete', [ object, response ] );
1068 } else {
1069 jQuery( document ).trigger( 'frmPageChanged', [ object, response ] );
1070 }
1071 }
1072
1073 /**
1074 * Trigger an event before the form is replaced with a success message.
1075 *
1076 * @since 6.9
1077 *
1078 * @param {HTMLElement} object The form.
1079 * @param {Object} response The response from submitting the form with AJAX.
1080 * @return {void}
1081 */
1082 function afterFormSubmittedBeforeReplace( object, response ) {
1083 const tempDiv = document.createElement( 'div' );
1084 tempDiv.innerHTML = response.content;
1085 const formCompleted = tempDiv.querySelector( '.frm_message' );
1086 if ( formCompleted ) {
1087 triggerCustomEvent( document, 'frmFormCompleteBeforeReplace', { object, response } );
1088 }
1089 }
1090
1091 function removeAddedScripts( formContainer, formID ) {
1092 const endReplace = document.querySelectorAll( '.frm_end_ajax_' + formID );
1093 if ( endReplace.length ) {
1094 formContainer.nextUntil( '.frm_end_ajax_' + formID ).remove();
1095 endReplace.forEach( el => el.remove() );
1096 }
1097 }
1098
1099 function maybeSlideOut( oldContent, newContent ) {
1100 let c,
1101 newClass = 'frm_slideout';
1102 if ( newContent.indexOf( ' frm_slide' ) !== -1 ) {
1103 c = oldContent.children();
1104 if ( newContent.indexOf( ' frm_going_back' ) !== -1 ) {
1105 newClass += ' frm_going_back';
1106 }
1107 c.removeClass( 'frm_going_back' );
1108 c.addClass( newClass );
1109 return 300;
1110 }
1111 return 0;
1112 }
1113
1114 function addUrlParam( response ) {
1115 let url;
1116 if ( history.pushState && typeof response.page !== 'undefined' ) {
1117 url = addQueryVar( 'frm_page', response.page );
1118 window.history.pushState( { html: response.html }, '', '?' + url );
1119 }
1120 }
1121
1122 function addQueryVar( key, value ) {
1123 let kvp, i, x;
1124
1125 key = encodeURI( key );
1126 value = encodeURI( value );
1127
1128 kvp = document.location.search.substr( 1 ).split( '&' );
1129
1130 i = kvp.length;
1131 while ( i-- ) {
1132 x = kvp[ i ].split( '=' );
1133
1134 if ( x[ 0 ] == key ) {
1135 x[ 1 ] = value;
1136 kvp[ i ] = x.join( '=' );
1137 break;
1138 }
1139 }
1140
1141 if ( i < 0 ) {
1142 kvp[ kvp.length ] = [ key, value ].join( '=' );
1143 }
1144
1145 return kvp.join( '&' );
1146 }
1147
1148 function addFieldError( $fieldCont, key, jsErrors ) {
1149 let id, describedBy, roleString;
1150 const container = $fieldCont instanceof jQuery ? $fieldCont.get( 0 ) : $fieldCont;
1151 if ( ! container || container.offsetParent === null ) {
1152 return;
1153 }
1154
1155 container.classList.add( 'frm_blank_field' );
1156 const input = container.querySelector( 'input, select, textarea' );
1157 id = getErrorElementId( key, input );
1158
1159 describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1160
1161 if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1162 frmThemeOverride_frmPlaceError( key, jsErrors );
1163 } else {
1164 let errorHtml;
1165 if ( -1 !== jsErrors[ key ].indexOf( '<div' ) ) {
1166 errorHtml = jsErrors[ key ];
1167 } else {
1168 roleString = frm_js.include_alert_role ? 'role="alert"' : ''; // eslint-disable-line camelcase
1169 errorHtml = '<div class="frm_error" ' + roleString + ' id="' + id + '">' + jsErrors[ key ] + '</div>';
1170 }
1171 container.insertAdjacentHTML( 'beforeend', errorHtml );
1172
1173 if ( input ) {
1174 if ( ! describedBy ) {
1175 describedBy = id;
1176 } else if ( describedBy.indexOf( id ) === -1 && describedBy.indexOf( 'frm_error_field_' ) === -1 ) {
1177 const errorFirst = input.dataset.errorFirst;
1178 if ( errorFirst === '0' ) {
1179 describedBy = describedBy + ' ' + id;
1180 } else {
1181 describedBy = id + ' ' + describedBy;
1182 }
1183 }
1184 input.setAttribute( 'aria-describedby', describedBy );
1185 }
1186 }
1187
1188 if ( input ) {
1189 if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1190 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1191 if ( group ) {
1192 group.setAttribute( 'aria-invalid', 'true' );
1193 }
1194 } else {
1195 input.setAttribute( 'aria-invalid', 'true' );
1196 }
1197 }
1198
1199 jQuery( document ).trigger( 'frmAddFieldError', [ jQuery( container ), key, jsErrors ] );
1200 }
1201
1202 /**
1203 * Get the ID to use for an error element added when submitting with AJAX.
1204 *
1205 * @param {string} key
1206 * @param {HTMLElement} input
1207 * @return {string} The ID to use for the error element.
1208 */
1209 function getErrorElementId( key, input ) {
1210 if ( isNaN( key ) || ! input || ! input.id ) {
1211 // If key isn't a number, assume it's already in the right format.
1212 return 'frm_error_field_' + key;
1213 }
1214 return 'frm_error_' + input.id;
1215 }
1216
1217 /**
1218 * Removes errors before validating with JS.
1219 * This prevents issues with stale errors that has since been fixed.
1220 *
1221 * @param {HTMLElement|jQuery} fieldCont Field container element.
1222 * @return {void}
1223 */
1224 function removeFieldError( fieldCont ) {
1225 const container = fieldCont instanceof jQuery ? fieldCont.get( 0 ) : fieldCont;
1226 if ( ! container ) {
1227 return;
1228 }
1229
1230 const errorMessage = container.querySelector( '.frm_error' );
1231 const errorId = errorMessage ? errorMessage.id : '';
1232 const input = container.querySelector( 'input, select, textarea' );
1233 let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1234
1235 container.classList.remove( 'frm_blank_field', 'has-error' );
1236
1237 if ( input ) {
1238 if ( 'true' === input.getAttribute( 'aria-invalid' ) ) {
1239 input.setAttribute( 'aria-invalid', 'false' );
1240 } else if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1241 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1242 if ( group ) {
1243 group.setAttribute( 'aria-invalid', 'false' );
1244 }
1245 }
1246 }
1247
1248 if ( errorMessage ) {
1249 errorMessage.remove();
1250 }
1251
1252 if ( input ) {
1253 input.removeAttribute( 'aria-describedby' );
1254 if ( describedBy ) {
1255 describedBy = describedBy.replace( errorId, '' ).trim();
1256 if ( describedBy ) {
1257 input.setAttribute( 'aria-describedby', describedBy );
1258 }
1259 }
1260 }
1261 }
1262
1263 function removeAllErrors() {
1264 document.querySelectorAll( '.form-field' ).forEach( field => {
1265 field.classList.remove( 'frm_blank_field', 'has-error' );
1266 } );
1267 document.querySelectorAll( '.form-field .frm_error' ).forEach( error => error.remove() );
1268 document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
1269 }
1270
1271 /**
1272 * @param {HTMLElement|Object} object Form object.
1273 * @return {void}
1274 */
1275 function scrollToFirstField( object ) {
1276 if ( 'function' === typeof object.get ) {
1277 // Get the HTMLElement from a jQuery object.
1278 object = object.get( 0 );
1279 }
1280 const field = object.querySelector( '.frm_blank_field' );
1281 if ( field ) {
1282 frmFrontForm.scrollMsg( jQuery( field ), object, true );
1283 }
1284 }
1285
1286 function showSubmitLoading( $object ) {
1287 showLoadingIndicator( $object );
1288 disableSubmitButton( $object );
1289 disableSaveDraft( $object );
1290 }
1291
1292 function showLoadingIndicator( $object ) {
1293 if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) {
1294 addLoadingClass( $object );
1295 $object.trigger( 'frmStartFormLoading' );
1296 }
1297 }
1298
1299 function addLoadingClass( $object ) {
1300 const loadingClass = isGoingToPrevPage( $object ) ? 'frm_loading_prev' : 'frm_loading_form';
1301
1302 $object.addClass( loadingClass );
1303 }
1304
1305 function isGoingToPrevPage( $object ) {
1306 return ( typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object ) );
1307 }
1308
1309 function removeSubmitLoading( _, enable, processesRunning ) {
1310 if ( processesRunning > 0 ) {
1311 return;
1312 }
1313
1314 document.querySelectorAll( '.frm_loading_form' ).forEach( function( form ) {
1315 form.classList.remove( 'frm_loading_form', 'frm_loading_prev' );
1316 jQuery( form ).trigger( 'frmEndFormLoading' );
1317
1318 if ( enable === 'enable' ) {
1319 enableSubmitButton( form );
1320 enableSaveDraft( form );
1321 }
1322 } );
1323 }
1324
1325 function showFileLoading( object ) {
1326 const loading = document.getElementById( 'frm_loading' );
1327 if ( loading === null ) {
1328 return;
1329 }
1330
1331 const fileInput = object.querySelector( 'input[type=file]' );
1332 const fileval = fileInput ? fileInput.value : '';
1333 if ( fileval !== '' ) {
1334 setTimeout( function() {
1335 jQuery( loading ).fadeIn( 'slow' );
1336 }, 2000 );
1337 }
1338 }
1339
1340 /**********************************************
1341 * General Helpers
1342 *********************************************/
1343
1344 function confirmClick() {
1345 /*jshint validthis:true */
1346 const message = this.dataset.frmconfirm;
1347 return confirm( message );
1348 }
1349
1350 /**
1351 * Check for -webkit-box-shadow css value for input:-webkit-autofill selector.
1352 * If this is a match, the User is autofilling the input on a Webkit browser.
1353 * We want to delete the Honeypot field, otherwise it will get triggered as spam on autocomplete.
1354 */
1355 function onHoneypotFieldChange() {
1356 /*jshint validthis:true */
1357 const css = window.getComputedStyle( this ).boxShadow;
1358 if ( css && css.match( /inset/ ) ) {
1359 this.parentNode.removeChild( this );
1360 }
1361 }
1362
1363 /**
1364 * Focus on the first sub field when clicking to the primary label of combo field.
1365 *
1366 * @since 4.10.02
1367 */
1368 function changeFocusWhenClickComboFieldLabel() {
1369 let label;
1370
1371 const comboInputsContainer = document.querySelectorAll( '.frm_combo_inputs_container' );
1372 comboInputsContainer.forEach( function( inputsContainer ) {
1373 if ( ! inputsContainer.closest( '.frm_form_field' ) ) {
1374 return;
1375 }
1376
1377 label = inputsContainer.closest( '.frm_form_field' ).querySelector( '.frm_primary_label' );
1378 if ( ! label ) {
1379 return;
1380 }
1381
1382 label.addEventListener( 'click', function() {
1383 inputsContainer.querySelector( '.frm_form_field:first-child input, .frm_form_field:first-child select, .frm_form_field:first-child textarea' ).focus();
1384 } );
1385 } );
1386 }
1387
1388 /**
1389 * Sets focus on a the first subfield of a combo field that has an error.
1390 *
1391 * @since 6.16.3
1392 *
1393 * @param {HTMLElement} element
1394 * @return {boolean} True if the focus was set on a combo field.
1395 */
1396 function maybeFocusOnComboSubField( element ) {
1397 if ( 'FIELDSET' !== element.nodeName ) {
1398 return false;
1399 }
1400 if ( ! element.querySelector( '.frm_combo_inputs_container' ) ) {
1401 return false;
1402 }
1403 const comboSubfield = element.querySelector( '[aria-invalid="true"]' );
1404 if ( comboSubfield ) {
1405 focusInput( comboSubfield );
1406 return true;
1407 }
1408 return false;
1409 }
1410
1411 function checkForErrorsAndMaybeSetFocus() {
1412 let errors, element, timeoutCallback;
1413
1414 if ( ! frm_js.focus_first_error ) { // eslint-disable-line camelcase
1415 return;
1416 }
1417
1418 errors = document.querySelectorAll( '.frm_form_field .frm_error' );
1419 if ( ! errors.length ) {
1420 return;
1421 }
1422
1423 element = errors[ 0 ];
1424 do {
1425 element = element.previousSibling;
1426 if ( -1 !== [ 'input', 'select', 'textarea' ].indexOf( element.nodeName.toLowerCase() ) ) {
1427 focusInput( element );
1428 break;
1429 }
1430
1431 if ( maybeFocusOnComboSubField( element ) ) {
1432 break;
1433 }
1434
1435 if ( 'undefined' !== typeof element.classList ) {
1436 if ( element.classList.contains( 'html-active' ) ) {
1437 timeoutCallback = function() {
1438 const textarea = element.querySelector( 'textarea' );
1439 if ( null !== textarea ) {
1440 textarea.focus();
1441 }
1442 };
1443 } else if ( element.classList.contains( 'tmce-active' ) ) {
1444 timeoutCallback = function() {
1445 tinyMCE.activeEditor.focus();
1446 };
1447 } else if ( element.classList.contains( 'frm_opt_container' ) ) {
1448 const firstInput = element.querySelector( 'input' );
1449 if ( firstInput ) {
1450 focusInput( firstInput );
1451 break;
1452 }
1453 }
1454
1455 if ( 'function' === typeof timeoutCallback ) {
1456 setTimeout( timeoutCallback, 0 );
1457 break;
1458 }
1459 }
1460 } while ( element.previousSibling );
1461 }
1462
1463 /**
1464 * Focus a visible input, or possibly delay the focus event until the form has faded in.
1465 *
1466 * @since 6.16.3
1467 *
1468 * @param {HTMLElement} input
1469 * @return {void}
1470 */
1471 function focusInput( input ) {
1472 if ( input.offsetParent !== null ) {
1473 input.focus();
1474 } else {
1475 triggerCustomEvent( document, 'frmMaybeDelayFocus', { input } );
1476 }
1477 }
1478
1479 /**
1480 * Does the same as jQuery( document ).on( 'event', 'selector', handler ).
1481 *
1482 * @since 5.4
1483 *
1484 * @param {string} event Event name.
1485 * @param {string} selector Selector.
1486 * @param {Function} handler Handler.
1487 * @param {boolean | Object} options Options to be added to `addEventListener()` method. Default is `false`.
1488 */
1489 function documentOn( event, selector, handler, options ) {
1490 if ( 'undefined' === typeof options ) {
1491 options = false;
1492 }
1493
1494 document.addEventListener( event, function( e ) {
1495 let target;
1496
1497 // loop parent nodes from the target to the delegation node.
1498 for ( target = e.target; target && target != this; target = target.parentNode ) {
1499 if ( target && target.matches && target.matches( selector ) ) {
1500 handler.call( target, e );
1501 break;
1502 }
1503 }
1504 }, options );
1505 }
1506
1507 function initFloatingLabels() {
1508 let checkFloatLabel, checkDropdownLabel, runOnLoad, selector, floatClass;
1509
1510 selector = '.frm-show-form .frm_inside_container input, .frm-show-form .frm_inside_container select, .frm-show-form .frm_inside_container textarea';
1511 floatClass = 'frm_label_float_top';
1512
1513 checkFloatLabel = function( input ) {
1514 let container, shouldFloatTop, firstOpt;
1515
1516 container = input.closest( '.frm_inside_container' );
1517 if ( ! container ) {
1518 return;
1519 }
1520
1521 shouldFloatTop = input.value || document.activeElement === input;
1522
1523 container.classList.toggle( floatClass, shouldFloatTop );
1524
1525 if ( 'SELECT' === input.tagName ) {
1526 firstOpt = input.querySelector( 'option:first-child' );
1527
1528 if ( shouldFloatTop ) {
1529 if ( firstOpt.hasAttribute( 'data-label' ) ) {
1530 firstOpt.textContent = firstOpt.getAttribute( 'data-label' );
1531 firstOpt.removeAttribute( 'data-label' );
1532 }
1533 } else if ( firstOpt.textContent ) {
1534 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1535 firstOpt.textContent = '';
1536 }
1537 }
1538 };
1539
1540 checkDropdownLabel = function() {
1541 document.querySelectorAll( '.frm-show-form .frm_inside_container:not(.' + floatClass + ') select' ).forEach( function( input ) {
1542 const firstOpt = input.querySelector( 'option:first-child' );
1543
1544 if ( firstOpt.textContent ) {
1545 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1546 firstOpt.textContent = '';
1547 }
1548 } );
1549 };
1550
1551 [ 'focus', 'blur', 'change' ].forEach( function( eventName ) {
1552 documentOn(
1553 eventName,
1554 selector,
1555 function( event ) {
1556 checkFloatLabel( event.target );
1557 },
1558 true
1559 );
1560 } );
1561
1562 runOnLoad = function( firstLoad ) {
1563 if ( firstLoad && document.activeElement && -1 !== [ 'INPUT', 'SELECT', 'TEXTAREA' ].indexOf( document.activeElement.tagName ) ) {
1564 checkFloatLabel( document.activeElement );
1565 } else if ( firstLoad ) {
1566 document.querySelectorAll( '.frm_inside_container' ).forEach(
1567 function( container ) {
1568 const input = container.querySelector( 'input, select, textarea' );
1569 if ( input && '' !== input.value ) {
1570 checkFloatLabel( input );
1571 }
1572 }
1573 );
1574 }
1575
1576 checkDropdownLabel();
1577 };
1578
1579 runOnLoad( true );
1580
1581 jQuery( document ).on( 'frmPageChanged', function( event ) {
1582 runOnLoad();
1583 } );
1584
1585 document.addEventListener( 'frm_after_start_over', function( event ) {
1586 runOnLoad();
1587 } );
1588 }
1589
1590 function shouldUpdateValidityMessage( target ) {
1591 if ( 'INPUT' !== target.nodeName ) {
1592 return false;
1593 }
1594
1595 if ( ! target.dataset.invmsg ) {
1596 return false;
1597 }
1598
1599 if ( 'text' !== target.getAttribute( 'type' ) ) {
1600 return false;
1601 }
1602
1603 if ( target.classList.contains( 'frm_verify' ) ) {
1604 return false;
1605 }
1606
1607 return true;
1608 }
1609
1610 function maybeClearCustomValidityMessage( event, field ) {
1611 let key,
1612 isInvalid = false;
1613
1614 if ( ! shouldUpdateValidityMessage( field ) ) {
1615 return;
1616 }
1617
1618 for ( key in field.validity ) {
1619 if ( 'customError' === key ) {
1620 continue;
1621 }
1622 if ( 'valid' !== key && field.validity[ key ] === true ) {
1623 isInvalid = true;
1624 break;
1625 }
1626 }
1627
1628 if ( ! isInvalid ) {
1629 field.setCustomValidity( '' );
1630 }
1631 }
1632
1633 function maybeShowNewTabFallbackMessage() {
1634 let messageEl;
1635
1636 if ( ! window.frmShowNewTabFallback ) {
1637 return;
1638 }
1639
1640 messageEl = document.querySelector( '#frm_form_' + frmShowNewTabFallback.formId + '_container .frm_message' );
1641 if ( ! messageEl ) {
1642 return;
1643 }
1644
1645 messageEl.insertAdjacentHTML( 'beforeend', ' ' + frmShowNewTabFallback.message );
1646 }
1647
1648 function setCustomValidityMessage() {
1649 let forms, length, index;
1650
1651 forms = document.getElementsByClassName( 'frm-show-form' );
1652 length = forms.length;
1653
1654 for ( index = 0; index < length; ++index ) {
1655 forms[ index ].addEventListener(
1656 'invalid',
1657 function( event ) {
1658 const target = event.target;
1659
1660 if ( shouldUpdateValidityMessage( target ) ) {
1661 target.setCustomValidity( target.dataset.invmsg );
1662 }
1663 },
1664 true
1665 );
1666 }
1667 }
1668
1669 function enableSubmitButtonOnBackButtonPress() {
1670 window.addEventListener( 'pageshow', function( event ) {
1671 if ( event.persisted ) {
1672 document.querySelectorAll( '.frm_loading_form' ).forEach(
1673 function( form ) {
1674 enableSubmitButton( form );
1675 }
1676 );
1677 removeSubmitLoading();
1678 }
1679 } );
1680 }
1681
1682 /**
1683 * Destroys the formidable generated global hcaptcha object since it wouldn't otherwise render.
1684 */
1685 function destroyhCaptcha() {
1686 if ( ! window.hasOwnProperty( 'hcaptcha' ) || ! document.querySelector( '.frm-show-form .h-captcha' ) ) {
1687 return;
1688 }
1689 window.hcaptcha = null;
1690 }
1691
1692 /**
1693 * @since 6.16.3
1694 *
1695 * @return {string} Unique key, used for duplicate checks.
1696 */
1697 function getUniqueKey() {
1698 const uniqueKey = Array.from( window.crypto.getRandomValues( new Uint8Array( 8 ) ) )
1699 .map( b => b.toString( 16 ).padStart( 2, '0' ) )
1700 .join( '' );
1701 const timestamp = Date.now().toString( 16 );
1702 return uniqueKey + '-' + timestamp;
1703 }
1704
1705 /**
1706 * Animates the scroll position of the document.
1707 *
1708 * @since 6.20
1709 *
1710 * @param {number} start
1711 * @param {number} end
1712 * @param {number} duration
1713 * @return {void}
1714 */
1715 function animateScroll( start, end, duration ) {
1716 if ( ! window.hasOwnProperty( 'performance' ) || ! window.hasOwnProperty( 'requestAnimationFrame' ) ) {
1717 document.documentElement.scrollTop = end;
1718 return;
1719 }
1720
1721 /* eslint-disable compat/compat */
1722 const startTime = performance.now();
1723 const step = currentTime => {
1724 const progress = Math.min( ( currentTime - startTime ) / duration, 1 );
1725 document.documentElement.scrollTop = start + ( ( end - start ) * progress );
1726 if ( progress < 1 ) {
1727 requestAnimationFrame( step );
1728 }
1729 };
1730 requestAnimationFrame( step );
1731 /* eslint-enable compat/compat */
1732 }
1733
1734 /**
1735 * Make sure that the captcha label for a reCAPTCHA or Turnstile field matches the response input ID.
1736 * This is determined dynamically, so we check for the ID after the input is rendered.
1737 * hCaptcha is handled separately, in the frmCaptcha function as it is not rendered explicitly.
1738 *
1739 * @since 6.25.1
1740 *
1741 * @param {HTMLElement} captcha
1742 * @return {void}
1743 */
1744 function maybeFixCaptchaLabel( captcha ) {
1745 const form = captcha.closest( 'form' );
1746 if ( ! form ) {
1747 return;
1748 }
1749
1750 const label = form.querySelector( 'label[for="g-recaptcha-response"], label[for="cf-turnstile-response"]' );
1751 const captchaResponse = form.querySelector( '[name="g-recaptcha-response"], [name="cf-turnstile-response"]' );
1752
1753 if ( label && captchaResponse ) {
1754 label.htmlFor = captchaResponse.id;
1755 }
1756 }
1757
1758 return {
1759 init: function() {
1760 jQuery( document ).off( 'submit.formidable', '.frm-show-form' );
1761 jQuery( document ).on( 'submit.formidable', '.frm-show-form', frmFrontForm.submitForm );
1762
1763 jQuery( document ).on( 'change', '.frm-show-form input[name^="item_meta"], .frm-show-form select[name^="item_meta"], .frm-show-form textarea[name^="item_meta"]', frmFrontForm.fieldValueChanged );
1764
1765 jQuery( document ).on( 'change', '.frm_verify[id^=field_]', onHoneypotFieldChange );
1766
1767 jQuery( document ).on( 'click', 'a[data-frmconfirm]', confirmClick );
1768
1769 checkForErrorsAndMaybeSetFocus();
1770
1771 // Focus on the first sub field when clicking to the primary label of combo field.
1772 changeFocusWhenClickComboFieldLabel();
1773
1774 initFloatingLabels();
1775 maybeShowNewTabFallbackMessage();
1776
1777 jQuery( document ).on( 'frmAfterAddRow', setCustomValidityMessage );
1778 setCustomValidityMessage();
1779 jQuery( document ).on( 'frmFieldChanged', maybeClearCustomValidityMessage );
1780
1781 setSelectPlaceholderColor();
1782
1783 // Elementor popup show event. Fix Elementor Popup && FF Captcha field conflicts
1784 jQuery( document ).on( 'elementor/popup/show', frmRecaptcha );
1785
1786 enableSubmitButtonOnBackButtonPress();
1787 jQuery( document ).on(
1788 'frmPageChanged',
1789 destroyhCaptcha
1790 );
1791 },
1792
1793 getFieldId,
1794
1795 /**
1796 * Render a captcha field.
1797 *
1798 * @param {HTMLElement} captcha
1799 * @param {string} captchaSelector
1800 * @return {void}
1801 */
1802 renderCaptcha: function( captcha, captchaSelector ) {
1803 const rendered = captcha.getAttribute( 'data-rid' ) !== null;
1804 if ( rendered ) {
1805 return;
1806 }
1807
1808 const size = captcha.getAttribute( 'data-size' );
1809 const params = {
1810 sitekey: captcha.getAttribute( 'data-sitekey' ),
1811 size: size,
1812 theme: captcha.getAttribute( 'data-theme' )
1813 };
1814
1815 if ( size === 'invisible' ) {
1816 const formID = captcha.closest( 'form' )?.querySelector( 'input[name="form_id"]' )?.value;
1817
1818 const captchaLabel = captcha.closest( '.frm_form_field' )?.querySelector( '.frm_primary_label' );
1819 if ( captchaLabel ) {
1820 captchaLabel.style.display = 'none';
1821 }
1822
1823 params.callback = function( token ) {
1824 frmFrontForm.afterRecaptcha( token, formID );
1825 };
1826 }
1827
1828 const activeCaptcha = getSelectedCaptcha( captchaSelector );
1829 const captchaContainer = typeof turnstile !== 'undefined' && turnstile === activeCaptcha ? '#' + captcha.id : captcha.id;
1830 const captchaID = activeCaptcha.render( captchaContainer, params );
1831
1832 captcha.setAttribute( 'data-rid', captchaID );
1833
1834 maybeFixCaptchaLabel( captcha );
1835 },
1836
1837 afterSingleRecaptcha: function() {
1838 const recaptcha = document.querySelector( '.frm-show-form .g-recaptcha' );
1839 const object = recaptcha ? recaptcha.closest( 'form' ) : null;
1840 frmFrontForm.submitFormNow( object );
1841 },
1842
1843 afterRecaptcha: function( _, formID ) {
1844 const object = document.querySelector( '#frm_form_' + formID + '_container form' );
1845 frmFrontForm.submitFormNow( object );
1846 },
1847
1848 submitForm: function( e ) {
1849 frmFrontForm.submitFormManual( e, this );
1850 },
1851
1852 /**
1853 * @param {Event} e
1854 * @param {HTMLElement} object The form object that is being submitted.
1855 * @return {void}
1856 */
1857 submitFormManual: function( e, object ) {
1858 if ( document.body.classList.contains( 'wp-admin' ) && ! object.closest( '.frmapi-form' ) ) {
1859 return;
1860 }
1861
1862 e.preventDefault();
1863
1864 if ( typeof frmProForm !== 'undefined' && typeof frmProForm.submitAllowed === 'function' && ! frmProForm.submitAllowed( object ) ) {
1865 return;
1866 }
1867
1868 const errors = frmFrontForm.validateFormSubmit( object );
1869 if ( Object.keys( errors ).length !== 0 ) {
1870 return;
1871 }
1872
1873 const invisibleRecaptcha = hasInvisibleRecaptcha( object );
1874
1875 if ( invisibleRecaptcha ) {
1876 showLoadingIndicator( jQuery( object ) );
1877 executeInvisibleRecaptcha( invisibleRecaptcha );
1878 } else {
1879 showSubmitLoading( jQuery( object ) );
1880
1881 frmFrontForm.submitFormNow( object );
1882 }
1883 },
1884
1885 submitFormNow: function( object ) {
1886 let hasFileFields, antispamInput,
1887 classList = object.className.trim().split( /\s+/gi );
1888
1889 if ( object.hasAttribute( 'data-token' ) && null === object.querySelector( '[name="antispam_token"]' ) ) {
1890 // include the antispam token on form submit.
1891 antispamInput = document.createElement( 'input' );
1892 antispamInput.type = 'hidden';
1893 antispamInput.name = 'antispam_token';
1894 antispamInput.value = object.getAttribute( 'data-token' );
1895 object.appendChild( antispamInput );
1896 }
1897
1898 // Add a unique ID, used for duplicate checks.
1899 const uniqueIDInput = document.createElement( 'input' );
1900 uniqueIDInput.type = 'hidden';
1901 uniqueIDInput.name = 'unique_id';
1902 uniqueIDInput.value = getUniqueKey();
1903 object.appendChild( uniqueIDInput );
1904
1905 if ( classList.indexOf( 'frm_ajax_submit' ) > -1 ) {
1906 const fileInputs = object.querySelectorAll( 'input[type="file"]' );
1907 hasFileFields = Array.from( fileInputs ).filter( input => !! input.value ).length;
1908 if ( hasFileFields < 1 ) {
1909 const actionInput = object.querySelector( 'input[name="frm_action"]' );
1910 const action = actionInput ? actionInput.value : '';
1911 frmFrontForm.checkFormErrors( object, action );
1912 } else {
1913 object.submit();
1914 }
1915 } else {
1916 object.submit();
1917 }
1918 },
1919
1920 /**
1921 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
1922 *
1923 * @return {Array} List of errors.
1924 */
1925 validateFormSubmit: function( object ) {
1926 const form = object instanceof jQuery ? object.get( 0 ) : object;
1927 if ( typeof tinyMCE !== 'undefined' && form && form.querySelector( '.wp-editor-wrap' ) ) {
1928 tinyMCE.triggerSave();
1929 }
1930
1931 jsErrors = [];
1932
1933 if ( shouldJSValidate( object ) ) {
1934 frmFrontForm.getAjaxFormErrors( object );
1935
1936 if ( Object.keys( jsErrors ).length ) {
1937 frmFrontForm.addAjaxFormErrors( object );
1938 }
1939 }
1940
1941 return jsErrors;
1942 },
1943
1944 /**
1945 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
1946 * @return {Array} List of errors.
1947 */
1948 getAjaxFormErrors: function( object ) {
1949 let customErrors, key;
1950 const form = object instanceof jQuery ? object.get( 0 ) : object;
1951
1952 jsErrors = validateForm( object );
1953 if ( typeof frmThemeOverride_jsErrors === 'function' ) { // eslint-disable-line camelcase
1954 const actionInput = form ? form.querySelector( 'input[name="frm_action"]' ) : null;
1955 const action = actionInput ? actionInput.value : '';
1956 customErrors = frmThemeOverride_jsErrors( action, object );
1957 if ( Object.keys( customErrors ).length ) {
1958 for ( key in customErrors ) {
1959 jsErrors[ key ] = customErrors[ key ];
1960 }
1961 }
1962 }
1963
1964 triggerCustomEvent( document, 'frm_get_ajax_form_errors', {
1965 formEl: object,
1966 errors: jsErrors
1967 } );
1968
1969 return jsErrors;
1970 },
1971
1972 /**
1973 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
1974 * @return {void}
1975 */
1976 addAjaxFormErrors: function( object ) {
1977 let key;
1978 const form = object instanceof jQuery ? object.get( 0 ) : object;
1979 removeAllErrors();
1980
1981 for ( key in jsErrors ) {
1982 const fieldCont = form ? form.querySelector( '#frm_field_' + key + '_container' ) : null;
1983
1984 if ( fieldCont ) {
1985 addFieldError( fieldCont, key, jsErrors );
1986 } else {
1987 // we are unable to show the error, so remove it
1988 delete jsErrors[ key ];
1989 }
1990 }
1991
1992 scrollToFirstField( object );
1993 checkForErrorsAndMaybeSetFocus();
1994 },
1995
1996 checkFormErrors: getFormErrors,
1997 checkRequiredField,
1998 showSubmitLoading,
1999 removeSubmitLoading,
2000
2001 scrollToID: function( id ) {
2002 const object = jQuery( document.getElementById( id ) );
2003 frmFrontForm.scrollMsg( object, false );
2004 },
2005
2006 scrollMsg: function( id, object, animate ) {
2007 let newPos, m, b, screenTop, screenBottom,
2008 scrollObj = '';
2009 if ( typeof object === 'undefined' ) {
2010 scrollObj = jQuery( document.getElementById( 'frm_form_' + id + '_container' ) );
2011 if ( scrollObj.length < 1 ) {
2012 return;
2013 }
2014 } else if ( typeof id === 'string' ) {
2015 const formEl = object instanceof jQuery ? object.get( 0 ) : object;
2016 const fieldEl = formEl ? formEl.querySelector( '#frm_field_' + id + '_container' ) : null;
2017 scrollObj = fieldEl ? jQuery( fieldEl ) : jQuery();
2018 } else {
2019 scrollObj = id;
2020 }
2021
2022 jQuery( scrollObj ).trigger( 'focus' );
2023 newPos = scrollObj.offset().top;
2024 if ( ! newPos || frm_js.offset === '-1' ) { // eslint-disable-line camelcase
2025 return;
2026 }
2027 newPos = newPos - frm_js.offset; // eslint-disable-line camelcase
2028
2029 m = getComputedStyle( document.documentElement ).marginTop;
2030 b = getComputedStyle( document.body ).marginTop;
2031 if ( m || b ) {
2032 newPos = newPos - parseInt( m ) - parseInt( b );
2033 }
2034
2035 if ( newPos && window.innerHeight ) {
2036 screenTop = document.documentElement.scrollTop || document.body.scrollTop;
2037 screenBottom = screenTop + window.innerHeight;
2038
2039 if ( newPos > screenBottom || newPos < screenTop ) {
2040 // Not in view
2041 if ( typeof animate === 'undefined' ) {
2042 document.documentElement.scrollTop = newPos;
2043 } else {
2044 animateScroll( screenTop, newPos, 500 );
2045 }
2046 return false;
2047 }
2048 }
2049 },
2050
2051 fieldValueChanged: function( e ) {
2052 /*jshint validthis:true */
2053
2054 const fieldId = frmFrontForm.getFieldId( this, false );
2055 if ( ! fieldId || typeof fieldId === 'undefined' ) {
2056 return;
2057 }
2058
2059 if ( e.frmTriggered && e.frmTriggered == fieldId ) {
2060 return;
2061 }
2062
2063 jQuery( document ).trigger( 'frmFieldChanged', [ this, fieldId, e ] );
2064
2065 if ( e.selfTriggered !== true ) {
2066 maybeValidateChange( this );
2067 }
2068 },
2069
2070 escapeHtml: function( text ) {
2071 console.warn( 'DEPRECATED: function frmFrontForm.escapeHtml in v6.17' );
2072 return text
2073 .replace( /&/g, '&amp;' )
2074 .replace( /</g, '&lt;' )
2075 .replace( />/g, '&gt;' )
2076 .replace( /"/g, '&quot;' )
2077 .replace( /'/g, '&#039;' );
2078 },
2079
2080 triggerCustomEvent: triggerCustomEvent,
2081 documentOn
2082 };
2083 }
2084
2085 window.frmFrontForm = frmFrontFormJS();
2086
2087 jQuery( document ).ready( function() {
2088 frmFrontForm.init();
2089 } );
2090
2091 function frmRecaptcha() {
2092 frmCaptcha( '.frm-g-recaptcha' );
2093 }
2094
2095 function frmHcaptcha() {
2096 frmCaptcha( '.h-captcha' );
2097 }
2098
2099 function frmTurnstile() {
2100 frmCaptcha( '.frm-cf-turnstile' );
2101 }
2102
2103 function frmCaptcha( captchaSelector ) {
2104 if ( '.h-captcha' === captchaSelector ) {
2105 // hCaptcha is still rendered implicitly, so we only want to handle the label and exit early.
2106 // Match the hcaptcha labels to the hcaptcha response fields.
2107 const captchaLabels = document.querySelectorAll( 'label[for="h-captcha-response"]' );
2108 if ( captchaLabels.length ) {
2109 captchaLabels.forEach( label => {
2110 const captchaResponse = label.closest( 'form' )?.querySelector( '[name="h-captcha-response"]' );
2111 if ( captchaResponse ) {
2112 label.htmlFor = captchaResponse.id;
2113 }
2114 } );
2115 }
2116 return;
2117 }
2118
2119 let c;
2120 const captchas = document.querySelectorAll( captchaSelector );
2121 const cl = captchas.length;
2122 for ( c = 0; c < cl; c++ ) {
2123 const closestForm = captchas[ c ].closest( 'form' );
2124 const formIsVisible = closestForm && closestForm.offsetParent !== null;
2125 const captcha = captchas[ c ];
2126 if ( ! formIsVisible ) {
2127 // If the form is not visible, try again later in 400ms.
2128 // This fixes issues where the form fades visible on page load.
2129 // Or whne the form is inside of a modal.
2130 const interval = setInterval(
2131 function() {
2132 if ( closestForm && closestForm.offsetParent !== null ) {
2133 frmFrontForm.renderCaptcha( captcha, captchaSelector );
2134 clearInterval( interval );
2135 }
2136 },
2137 400
2138 );
2139 continue;
2140 }
2141 frmFrontForm.renderCaptcha( captcha, captchaSelector );
2142 }
2143 }
2144
2145 function getSelectedCaptcha( captchaSelector ) {
2146 if ( captchaSelector === '.frm-g-recaptcha' ) {
2147 return grecaptcha;
2148 }
2149 if ( document.querySelector( '.frm-cf-turnstile' ) ) {
2150 return turnstile;
2151 }
2152 return hcaptcha;
2153 }
2154
2155 function frmAfterRecaptcha( token ) {
2156 frmFrontForm.afterSingleRecaptcha( token );
2157 }
2158