PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.29
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.29
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.29, at js/formidable.js

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