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

2,572 lines 71.7 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 || 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?.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 let willRedirect = false;
887
888 if ( response.redirect !== undefined ) {
889 if ( shouldTriggerEvent ) {
890 triggerCustomEvent( object, 'frmSubmitEvent' );
891 return;
892 }
893
894 if ( response.delay ) {
895 setTimeout( function() {
896 doRedirect( response );
897 }, 1000 * response.delay );
898 } else {
899 doRedirect( response );
900 }
901
902 willRedirect = true;
903 }
904
905 if ( 'string' === typeof response.content && response.content !== '' ) {
906 // the form or success message was returned
907
908 if ( shouldTriggerEvent ) {
909 triggerCustomEvent( object, 'frmSubmitEvent', { content: response.content } );
910 return;
911 }
912
913 removeSubmitLoading( jQuery( object ) );
914 if ( frm_js.offset != -1 ) {
915 frmFrontForm.scrollMsg( jQuery( object ), false );
916 }
917
918 const formIdInput = object.querySelector( 'input[name="form_id"]' );
919 const formID = formIdInput ? formIdInput.value : '';
920 response.content = response.content.replace( / frm_pro_form /g, ' frm_pro_form frm_no_hide ' );
921 const replaceContent = jQuery( object ).closest( '.frm_forms' ); // eslint-disable-line no-jquery/no-closest
922 removeAddedScripts( replaceContent, formID );
923 const delay = maybeSlideOut( replaceContent, response.content );
924
925 setTimeout(
926 function() {
927 afterFormSubmittedBeforeReplace( object, response );
928
929 replaceContent.replaceWith( response.content );
930
931 addUrlParam( response );
932
933 if ( typeof frmThemeOverride_frmAfterSubmit === 'function' ) { // eslint-disable-line camelcase
934 const pageOrderInput = document.querySelector( `input[name="frm_page_order_${ formID }"]` );
935 const pageOrder = pageOrderInput ? pageOrderInput.value : '';
936 const tempDiv = document.createElement( 'div' );
937 tempDiv.innerHTML = response.content;
938 const formReturnedInput = tempDiv.querySelector( 'input[name="form_id"]' );
939 const formReturned = formReturnedInput ? formReturnedInput.value : '';
940 frmThemeOverride_frmAfterSubmit( formReturned, pageOrder, response.content, object );
941 }
942
943 afterFormSubmitted( object, response );
944 },
945 delay
946 );
947 } else if ( response.errors !== undefined && Object.keys( response.errors ).length ) {
948 // errors were returned
949 removeSubmitLoading( jQuery( object ), 'enable' );
950
951 //show errors
952 let contSubmit = true;
953 removeAllErrors();
954
955 let $fieldCont = null;
956
957 for ( const key in response.errors ) {
958 const fieldContEl = object.querySelector( `#frm_field_${ key }_container` );
959 $fieldCont = fieldContEl ? jQuery( fieldContEl ) : jQuery();
960
961 if ( $fieldCont.length ) {
962 if ( ! $fieldCont.is( ':visible' ) ) { // eslint-disable-line no-jquery/no-is
963 const inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' ); // eslint-disable-line no-jquery/no-closest, formidable/no-jquery-variable-methods
964 if ( inCollapsedSection.length ) {
965 let frmTrigger = inCollapsedSection.prev();
966 if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) { // eslint-disable-line formidable/no-jquery-variable-methods
967 // If the frmTrigger object is the section description, check to see if the previous element is the trigger
968 frmTrigger = frmTrigger.prev( '.frm_trigger' );
969 }
970 frmTrigger.trigger( 'click' );
971 }
972 }
973
974 if ( $fieldCont.is( ':visible' ) ) { // eslint-disable-line no-jquery/no-is
975 addFieldError( $fieldCont, key, response.errors );
976 contSubmit = false;
977 }
978 }
979 }
980
981 object.querySelectorAll( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).forEach( function( captchaEl ) {
982 const recaptchaID = captchaEl.dataset.rid;
983
984 if ( typeof grecaptcha !== 'undefined' && grecaptcha ) {
985 if ( recaptchaID ) {
986 grecaptcha.reset( recaptchaID );
987 } else {
988 grecaptcha.reset();
989 }
990 }
991
992 if ( typeof hcaptcha !== 'undefined' && hcaptcha ) {
993 hcaptcha.reset();
994 }
995 } );
996
997 if ( window.turnstile ) {
998 object.querySelectorAll( '.frm-cf-turnstile' ).forEach(
999 turnstileField => turnstileField.dataset.rid && turnstile.reset( turnstileField.dataset.rid )
1000 );
1001 }
1002
1003 jQuery( document ).trigger( 'frmFormErrors', [ object, response ] );
1004
1005 fieldsets.forEach( field => field.classList.remove( 'frm_doing_ajax' ) );
1006 scrollToFirstField( object );
1007
1008 if ( contSubmit ) {
1009 object.submit();
1010 } else {
1011 object.insertAdjacentHTML( 'afterbegin', response.error_message );
1012 checkForErrorsAndMaybeSetFocus();
1013 }
1014 } else if ( ! willRedirect ) { // Avoid double submission if redirecting to a page.
1015 // There may have been a plugin conflict, or the form is not set to submit with ajax.
1016
1017 showFileLoading( object );
1018
1019 object.submit();
1020 }
1021 };
1022
1023 const error = function() {
1024 object.querySelectorAll( 'input[type="submit"], input[type="button"]' ).forEach(
1025 button => button.disabled = false
1026 );
1027 object.submit();
1028 };
1029
1030 postToAjaxUrl( object, data, success, error );
1031 }
1032
1033 function postToAjaxUrl( form, data, success, error ) {
1034 let ajaxUrl = frm_js.ajax_url;
1035 const action = form.getAttribute( 'action' );
1036
1037 if ( 'string' === typeof action && action.includes( '?action=frm_forms_preview' ) ) {
1038 ajaxUrl = action.split( '?action=frm_forms_preview' )[ 0 ];
1039 }
1040
1041 const ajaxParams = {
1042 type: 'POST',
1043 url: ajaxUrl,
1044 data,
1045 success
1046 };
1047
1048 if ( 'function' === typeof error ) {
1049 ajaxParams.error = error;
1050 }
1051
1052 jQuery.ajax( ajaxParams ); // eslint-disable-line no-jquery/no-ajax
1053 }
1054
1055 function afterFormSubmitted( object, response ) {
1056 const tempDiv = document.createElement( 'div' );
1057 tempDiv.innerHTML = response.content;
1058 const formCompleted = tempDiv.querySelector( '.frm_message' );
1059 if ( formCompleted ) {
1060 jQuery( document ).trigger( 'frmFormComplete', [ object, response ] );
1061 } else {
1062 jQuery( document ).trigger( 'frmPageChanged', [ object, response ] );
1063 }
1064 }
1065
1066 /**
1067 * Trigger an event before the form is replaced with a success message.
1068 *
1069 * @since 6.9
1070 *
1071 * @param {HTMLElement} object The form.
1072 * @param {Object} response The response from submitting the form with AJAX.
1073 * @return {void}
1074 */
1075 function afterFormSubmittedBeforeReplace( object, response ) {
1076 const tempDiv = document.createElement( 'div' );
1077 tempDiv.innerHTML = response.content;
1078 const formCompleted = tempDiv.querySelector( '.frm_message' );
1079 if ( formCompleted ) {
1080 triggerCustomEvent( document, 'frmFormCompleteBeforeReplace', { object, response } );
1081 }
1082 }
1083
1084 function removeAddedScripts( formContainer, formID ) {
1085 const endReplace = document.querySelectorAll( `.frm_end_ajax_${ formID }` );
1086 if ( endReplace.length ) {
1087 formContainer.nextUntil( `.frm_end_ajax_${ formID }` ).remove();
1088 endReplace.forEach( el => el.remove() );
1089 }
1090 }
1091
1092 function maybeSlideOut( oldContent, newContent ) {
1093 let c;
1094 let newClass = 'frm_slideout';
1095 if ( newContent.includes( ' frm_slide' ) ) {
1096 c = oldContent.children();
1097 if ( newContent.includes( ' frm_going_back' ) ) {
1098 newClass += ' frm_going_back';
1099 }
1100 c.removeClass( 'frm_going_back' );
1101 c.addClass( newClass );
1102 return 300;
1103 }
1104 return 0;
1105 }
1106
1107 function addUrlParam( response ) {
1108 let url;
1109 if ( history.pushState && response.page !== undefined ) {
1110 url = addQueryVar( 'frm_page', response.page );
1111 window.history.pushState( { html: response.html }, '', `?${ url }` );
1112 }
1113 }
1114
1115 function addQueryVar( key, value ) {
1116 key = encodeURI( key );
1117 value = encodeURI( value );
1118
1119 const kvp = document.location.search.substr( 1 ).split( '&' );
1120
1121 let i = kvp.length;
1122 while ( i-- ) {
1123 const x = kvp[ i ].split( '=' );
1124
1125 if ( x[ 0 ] == key ) {
1126 x[ 1 ] = value;
1127 kvp[ i ] = x.join( '=' );
1128 break;
1129 }
1130 }
1131
1132 if ( i < 0 ) {
1133 kvp[ kvp.length ] = [ key, value ].join( '=' );
1134 }
1135
1136 return kvp.join( '&' );
1137 }
1138
1139 function addFieldError( $fieldCont, key, jsErrors ) {
1140 const container = $fieldCont instanceof jQuery ? $fieldCont.get( 0 ) : $fieldCont;
1141
1142 if ( ! container || container.offsetParent === null ) {
1143 return;
1144 }
1145
1146 container.classList.add( 'frm_blank_field' );
1147 const input = container.querySelector( 'input, select, textarea' );
1148 const id = getErrorElementId( key, input );
1149
1150 let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1151
1152 if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1153 frmThemeOverride_frmPlaceError( key, jsErrors );
1154 } else {
1155 let errorHtml;
1156 if ( jsErrors[ key ].includes( '<div' ) ) {
1157 errorHtml = jsErrors[ key ];
1158 } else {
1159 const roleString = frm_js.include_alert_role ? 'role="alert"' : '';
1160 errorHtml = `<div class="frm_error" ${ roleString } id="${ id }">${ jsErrors[ key ] }</div>`;
1161 }
1162 container.insertAdjacentHTML( 'beforeend', errorHtml );
1163
1164 if ( input ) {
1165 if ( ! describedBy ) {
1166 describedBy = id;
1167 } else if ( ! describedBy.includes( id ) && ! describedBy.includes( 'frm_error_field_' ) ) {
1168 const { errorFirst } = input.dataset;
1169 if ( errorFirst === '0' ) {
1170 describedBy = `${ describedBy } ${ id }`;
1171 } else {
1172 describedBy = `${ id } ${ describedBy }`;
1173 }
1174 }
1175 input.setAttribute( 'aria-describedby', describedBy );
1176 }
1177 }
1178
1179 if ( input ) {
1180 if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1181 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1182 if ( group ) {
1183 group.setAttribute( 'aria-invalid', 'true' );
1184 }
1185 } else {
1186 input.setAttribute( 'aria-invalid', 'true' );
1187 }
1188 }
1189
1190 jQuery( document ).trigger( 'frmAddFieldError', [ jQuery( container ), key, jsErrors ] );
1191 }
1192
1193 /**
1194 * Get the ID to use for an error element added when submitting with AJAX.
1195 *
1196 * @param {string} key
1197 * @param {HTMLElement} input
1198 * @return {string} The ID to use for the error element.
1199 */
1200 function getErrorElementId( key, input ) {
1201 if ( isNaN( key ) || ! input || ! input.id ) {
1202 // If key isn't a number, assume it's already in the right format.
1203 return `frm_error_field_${ key }`;
1204 }
1205 return `frm_error_${ input.id }`;
1206 }
1207
1208 /**
1209 * Removes errors before validating with JS.
1210 * This prevents issues with stale errors that has since been fixed.
1211 *
1212 * @param {HTMLElement|jQuery} fieldCont Field container element.
1213 * @return {void}
1214 */
1215 function removeFieldError( fieldCont ) {
1216 const container = fieldCont instanceof jQuery ? fieldCont.get( 0 ) : fieldCont;
1217 if ( ! container ) {
1218 return;
1219 }
1220
1221 const errorMessage = container.querySelector( '.frm_error' );
1222 const errorId = errorMessage ? errorMessage.id : '';
1223 const input = container.querySelector( 'input, select, textarea' );
1224 let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1225
1226 container.classList.remove( 'frm_blank_field', 'has-error' );
1227
1228 if ( input ) {
1229 if ( 'true' === input.getAttribute( 'aria-invalid' ) ) {
1230 input.setAttribute( 'aria-invalid', 'false' );
1231 } else if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1232 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1233 if ( group ) {
1234 group.setAttribute( 'aria-invalid', 'false' );
1235 }
1236 }
1237 }
1238
1239 if ( errorMessage ) {
1240 errorMessage.remove();
1241 }
1242
1243 if ( input ) {
1244 input.removeAttribute( 'aria-describedby' );
1245 if ( describedBy ) {
1246 describedBy = describedBy.replace( errorId, '' ).trim();
1247 if ( describedBy ) {
1248 input.setAttribute( 'aria-describedby', describedBy );
1249 }
1250 }
1251 }
1252 }
1253
1254 function removeAllErrors() {
1255 document.querySelectorAll( '.form-field' ).forEach( field => {
1256 field.classList.remove( 'frm_blank_field', 'has-error' );
1257 } );
1258 document.querySelectorAll( '.form-field .frm_error' ).forEach( error => error.remove() );
1259 document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
1260 }
1261
1262 /**
1263 * @param {HTMLElement|Object} object Form object.
1264 * @return {void}
1265 */
1266 function scrollToFirstField( object ) {
1267 if ( 'function' === typeof object.get ) {
1268 // Get the HTMLElement from a jQuery object.
1269 object = object.get( 0 );
1270 }
1271 const field = object.querySelector( '.frm_blank_field' );
1272 if ( field ) {
1273 frmFrontForm.scrollMsg( jQuery( field ), object, true );
1274 }
1275 }
1276
1277 function showSubmitLoading( $object ) {
1278 showLoadingIndicator( $object );
1279 disableSubmitButton( $object );
1280 disableSaveDraft( $object );
1281 }
1282
1283 function showLoadingIndicator( $object ) {
1284 if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) { // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1285 addLoadingClass( $object );
1286 $object.trigger( 'frmStartFormLoading' );
1287 }
1288 }
1289
1290 function addLoadingClass( $object ) {
1291 const loadingClass = isGoingToPrevPage( $object ) ? 'frm_loading_prev' : 'frm_loading_form';
1292
1293 $object.addClass( loadingClass ); // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1294 }
1295
1296 function isGoingToPrevPage( $object ) {
1297 return typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object );
1298 }
1299
1300 function removeSubmitLoading( _, enable, processesRunning ) {
1301 if ( processesRunning > 0 ) {
1302 return;
1303 }
1304
1305 document.querySelectorAll( '.frm_loading_form' ).forEach( function( form ) {
1306 form.classList.remove( 'frm_loading_form', 'frm_loading_prev' );
1307 jQuery( form ).trigger( 'frmEndFormLoading' );
1308
1309 if ( enable === 'enable' ) {
1310 enableSubmitButton( form );
1311 enableSaveDraft( form );
1312 }
1313 } );
1314 }
1315
1316 function showFileLoading( object ) {
1317 const loading = document.getElementById( 'frm_loading' );
1318 if ( ! loading ) {
1319 return;
1320 }
1321
1322 const fileInput = object.querySelector( 'input[type=file]' );
1323 const fileval = fileInput ? fileInput.value : '';
1324 if ( fileval !== '' ) {
1325 setTimeout( function() {
1326 jQuery( loading ).fadeIn( 'slow' ); // eslint-disable-line no-jquery/no-fade
1327 }, 2000 );
1328 }
1329 }
1330
1331 /**********************************************
1332 * General Helpers
1333 *********************************************/
1334
1335 function confirmClick() {
1336 /*jshint validthis:true */
1337 const message = this.dataset.frmconfirm;
1338 return confirm( message );
1339 }
1340
1341 /**
1342 * Check for -webkit-box-shadow css value for input:-webkit-autofill selector.
1343 * If this is a match, the User is autofilling the input on a Webkit browser.
1344 * We want to delete the Honeypot field, otherwise it will get triggered as spam on autocomplete.
1345 */
1346 function onHoneypotFieldChange() {
1347 /*jshint validthis:true */
1348 const css = window.getComputedStyle( this ).boxShadow;
1349 if ( css?.match( /inset/ ) ) {
1350 this.remove();
1351 }
1352 }
1353
1354 /**
1355 * Focus on the first sub field when clicking to the primary label of combo field.
1356 *
1357 * @since 4.10.02
1358 */
1359 function changeFocusWhenClickComboFieldLabel() {
1360 let label;
1361
1362 const comboInputsContainer = document.querySelectorAll( '.frm_combo_inputs_container' );
1363 comboInputsContainer.forEach( function( inputsContainer ) {
1364 if ( ! inputsContainer.closest( '.frm_form_field' ) ) {
1365 return;
1366 }
1367
1368 label = inputsContainer.closest( '.frm_form_field' ).querySelector( '.frm_primary_label' );
1369 if ( ! label ) {
1370 return;
1371 }
1372
1373 label.addEventListener( 'click', function() {
1374 inputsContainer.querySelector( '.frm_form_field:first-child input, .frm_form_field:first-child select, .frm_form_field:first-child textarea' ).focus();
1375 } );
1376 } );
1377 }
1378
1379 /**
1380 * Sets focus on a the first subfield of a combo field that has an error.
1381 *
1382 * @since 6.16.3
1383 *
1384 * @param {HTMLElement} element
1385 * @return {boolean} True if the focus was set on a combo field.
1386 */
1387 function maybeFocusOnComboSubField( element ) {
1388 if ( 'FIELDSET' !== element.nodeName ) {
1389 return false;
1390 }
1391 if ( ! element.querySelector( '.frm_combo_inputs_container' ) ) {
1392 return false;
1393 }
1394 const comboSubfield = element.querySelector( '[aria-invalid="true"]' );
1395 if ( comboSubfield ) {
1396 focusInput( comboSubfield );
1397 return true;
1398 }
1399 return false;
1400 }
1401
1402 function checkForErrorsAndMaybeSetFocus() {
1403 if ( ! frm_js.focus_first_error ) {
1404 return;
1405 }
1406
1407 const errors = document.querySelectorAll( '.frm_form_field .frm_error' );
1408 if ( ! errors.length ) {
1409 return;
1410 }
1411
1412 let element = errors[ 0 ];
1413 let timeoutCallback;
1414 do {
1415 element = element.previousSibling;
1416 if ( [ 'input', 'select', 'textarea' ].includes( element.nodeName.toLowerCase() ) ) {
1417 focusInput( element );
1418 break;
1419 }
1420
1421 if ( maybeFocusOnComboSubField( element ) ) {
1422 break;
1423 }
1424
1425 if ( element.classList !== undefined ) {
1426 if ( element.classList.contains( 'html-active' ) ) {
1427 timeoutCallback = function() {
1428 const textarea = element.querySelector( 'textarea' );
1429 if ( null !== textarea ) {
1430 textarea.focus();
1431 }
1432 };
1433 } else if ( element.classList.contains( 'tmce-active' ) ) {
1434 timeoutCallback = function() {
1435 tinyMCE.activeEditor.focus();
1436 };
1437 } else if ( element.classList.contains( 'frm_opt_container' ) ) {
1438 const firstInput = element.querySelector( 'input' );
1439 if ( firstInput ) {
1440 focusInput( firstInput );
1441 break;
1442 }
1443 }
1444
1445 if ( 'function' === typeof timeoutCallback ) {
1446 setTimeout( timeoutCallback, 0 );
1447 break;
1448 }
1449 }
1450 } while ( element.previousSibling );
1451 }
1452
1453 /**
1454 * Focus a visible input, or possibly delay the focus event until the form has faded in.
1455 *
1456 * @since 6.16.3
1457 *
1458 * @param {HTMLElement} input
1459 * @return {void}
1460 */
1461 function focusInput( input ) {
1462 if ( input.offsetParent !== null ) {
1463 input.focus();
1464 } else {
1465 triggerCustomEvent( document, 'frmMaybeDelayFocus', { input } );
1466 }
1467 }
1468
1469 /**
1470 * Does the same as jQuery( document ).on( 'event', 'selector', handler ).
1471 *
1472 * @since 5.4
1473 *
1474 * @param {string} event Event name.
1475 * @param {string} selector Selector.
1476 * @param {Function} handler Handler.
1477 * @param {boolean | Object} options Options to be added to `addEventListener()` method. Default is `false`.
1478 */
1479 function documentOn( event, selector, handler, options ) {
1480 if ( options === undefined ) {
1481 options = false;
1482 }
1483
1484 document.addEventListener( event, function( e ) {
1485 let target;
1486
1487 // loop parent nodes from the target to the delegation node.
1488 for ( target = e.target; target && target != this; target = target.parentNode ) {
1489 if ( target.matches && target.matches( selector ) ) {
1490 handler.call( target, e );
1491 break;
1492 }
1493 }
1494 }, options );
1495 }
1496
1497 function initFloatingLabels() {
1498 const selector = '.frm-show-form .frm_inside_container input, .frm-show-form .frm_inside_container select, .frm-show-form .frm_inside_container textarea';
1499 const floatClass = 'frm_label_float_top';
1500
1501 const checkFloatLabel = function( input ) {
1502 const container = input.closest( '.frm_inside_container' );
1503 if ( ! container ) {
1504 return;
1505 }
1506
1507 const shouldFloatTop = input.value || document.activeElement === input;
1508
1509 container.classList.toggle( floatClass, shouldFloatTop );
1510
1511 if ( 'SELECT' === input.tagName ) {
1512 const firstOpt = input.querySelector( 'option:first-child' );
1513
1514 if ( shouldFloatTop ) {
1515 if ( firstOpt.hasAttribute( 'data-label' ) ) {
1516 firstOpt.textContent = firstOpt.getAttribute( 'data-label' );
1517 firstOpt.removeAttribute( 'data-label' );
1518 }
1519 } else if ( firstOpt.textContent ) {
1520 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1521 firstOpt.textContent = '';
1522 }
1523 }
1524 };
1525
1526 const checkDropdownLabel = function() {
1527 document.querySelectorAll( `.frm-show-form .frm_inside_container:not(.${ floatClass }) select` ).forEach( function( input ) {
1528 const firstOpt = input.querySelector( 'option:first-child' );
1529
1530 if ( firstOpt.textContent ) {
1531 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1532 firstOpt.textContent = '';
1533 }
1534 } );
1535 };
1536
1537 [ 'focus', 'blur', 'change' ].forEach( function( eventName ) {
1538 documentOn(
1539 eventName,
1540 selector,
1541 function( event ) {
1542 checkFloatLabel( event.target );
1543 },
1544 true
1545 );
1546 } );
1547
1548 const runOnLoad = function( firstLoad ) {
1549 if ( firstLoad && document.activeElement && [ 'INPUT', 'SELECT', 'TEXTAREA' ].includes( document.activeElement.tagName ) ) {
1550 checkFloatLabel( document.activeElement );
1551 } else if ( firstLoad ) {
1552 document.querySelectorAll( '.frm_inside_container' ).forEach(
1553 function( container ) {
1554 const input = container.querySelector( 'input, select, textarea' );
1555 if ( input && '' !== input.value ) {
1556 checkFloatLabel( input );
1557 }
1558 }
1559 );
1560 }
1561
1562 checkDropdownLabel();
1563 calcProductsTotal();
1564 };
1565
1566 runOnLoad( true );
1567
1568 jQuery( document ).on( 'frmPageChanged', function( event ) {
1569 runOnLoad();
1570 } );
1571
1572 document.addEventListener( 'frm_after_start_over', function( event ) {
1573 runOnLoad();
1574 } );
1575 }
1576
1577 function shouldUpdateValidityMessage( target ) {
1578 if ( 'INPUT' !== target.nodeName ) {
1579 return false;
1580 }
1581
1582 if ( ! target.dataset.invmsg ) {
1583 return false;
1584 }
1585
1586 if ( 'text' !== target.getAttribute( 'type' ) ) {
1587 return false;
1588 }
1589
1590 if ( target.classList.contains( 'frm_verify' ) ) {
1591 return false;
1592 }
1593
1594 return true;
1595 }
1596
1597 function maybeClearCustomValidityMessage( event, field ) {
1598 let key;
1599 let isInvalid = false;
1600
1601 if ( ! shouldUpdateValidityMessage( field ) ) {
1602 return;
1603 }
1604
1605 for ( key in field.validity ) {
1606 if ( 'customError' === key ) {
1607 continue;
1608 }
1609 if ( 'valid' !== key && field.validity[ key ] === true ) {
1610 isInvalid = true;
1611 break;
1612 }
1613 }
1614
1615 if ( ! isInvalid ) {
1616 field.setCustomValidity( '' );
1617 }
1618 }
1619
1620 function maybeShowNewTabFallbackMessage() {
1621 if ( ! window.frmShowNewTabFallback ) {
1622 return;
1623 }
1624
1625 const messageEl = document.querySelector( `#frm_form_${ frmShowNewTabFallback.formId }_container .frm_message` );
1626 if ( ! messageEl ) {
1627 return;
1628 }
1629
1630 messageEl.insertAdjacentHTML( 'beforeend', ` ${ frmShowNewTabFallback.message }` );
1631 }
1632
1633 function setCustomValidityMessage() {
1634 const forms = document.getElementsByClassName( 'frm-show-form' );
1635 const { length } = forms;
1636
1637 for ( let index = 0; index < length; ++index ) {
1638 forms[ index ].addEventListener(
1639 'invalid',
1640 function( event ) {
1641 const { target } = event;
1642
1643 if ( shouldUpdateValidityMessage( target ) ) {
1644 target.setCustomValidity( target.dataset.invmsg );
1645 }
1646 },
1647 true
1648 );
1649 }
1650 }
1651
1652 function enableSubmitButtonOnBackButtonPress() {
1653 window.addEventListener( 'pageshow', function( event ) {
1654 if ( event.persisted ) {
1655 document.querySelectorAll( '.frm_loading_form' ).forEach(
1656 function( form ) {
1657 enableSubmitButton( form );
1658 }
1659 );
1660 removeSubmitLoading();
1661 }
1662 } );
1663 }
1664
1665 /**
1666 * Destroys the formidable generated global hcaptcha object since it wouldn't otherwise render.
1667 */
1668 function destroyhCaptcha() {
1669 if ( ! window.hasOwnProperty( 'hcaptcha' ) || ! document.querySelector( '.frm-show-form .h-captcha' ) ) {
1670 return;
1671 }
1672 window.hcaptcha = null;
1673 }
1674
1675 /**
1676 * @since 6.16.3
1677 *
1678 * @return {string} Unique key, used for duplicate checks.
1679 */
1680 function getUniqueKey() {
1681 const uniqueKey = Array.from( window.crypto.getRandomValues( new Uint8Array( 8 ) ) )
1682 .map( b => b.toString( 16 ).padStart( 2, '0' ) )
1683 .join( '' );
1684 const timestamp = Date.now().toString( 16 );
1685 return `${ uniqueKey }-${ timestamp }`;
1686 }
1687
1688 /**
1689 * Animates the scroll position of the document.
1690 *
1691 * @since 6.20
1692 *
1693 * @param {number} start
1694 * @param {number} end
1695 * @param {number} duration
1696 * @return {void}
1697 */
1698 function animateScroll( start, end, duration ) {
1699 if ( ! window.hasOwnProperty( 'performance' ) || ! window.hasOwnProperty( 'requestAnimationFrame' ) ) {
1700 document.documentElement.scrollTop = end;
1701 return;
1702 }
1703
1704 const startTime = performance.now();
1705 const step = currentTime => {
1706 const progress = Math.min( ( currentTime - startTime ) / duration, 1 );
1707 document.documentElement.scrollTop = start + ( ( end - start ) * progress );
1708 if ( progress < 1 ) {
1709 requestAnimationFrame( step );
1710 }
1711 };
1712 requestAnimationFrame( step );
1713 }
1714
1715 /**
1716 * Make sure that the captcha label for a reCAPTCHA or Turnstile field matches the response input ID.
1717 * This is determined dynamically, so we check for the ID after the input is rendered.
1718 * hCaptcha is handled separately, in the frmCaptcha function as it is not rendered explicitly.
1719 *
1720 * @since 6.25.1
1721 *
1722 * @param {HTMLElement} captcha
1723 * @return {void}
1724 */
1725 function maybeFixCaptchaLabel( captcha ) {
1726 const form = captcha.closest( 'form' );
1727 if ( ! form ) {
1728 return;
1729 }
1730
1731 const label = form.querySelector( 'label[for="g-recaptcha-response"], label[for="cf-turnstile-response"]' );
1732 const captchaResponse = form.querySelector( '[name="g-recaptcha-response"], [name="cf-turnstile-response"]' );
1733
1734 if ( label && captchaResponse ) {
1735 label.htmlFor = captchaResponse.id;
1736 }
1737 }
1738
1739 /**
1740 * Check to make sure the quantity field value is within the min and max values.
1741 *
1742 * @param {HTMLElement} input
1743 * @return {number} The quantity value.
1744 */
1745 function checkQuantityFieldMinMax( input ) {
1746 if ( '' === input.value ) {
1747 // Leave the value if it is empty.
1748 return 0;
1749 }
1750
1751 const val = parseFloat( input.value ? input.value.trim() : 0 );
1752 if ( isNaN( val ) ) {
1753 return 0;
1754 }
1755
1756 let max = input.hasAttribute( 'max' ) ? parseFloat( input.getAttribute( 'max' ) ) : 0;
1757 let min = input.hasAttribute( 'min' ) ? parseFloat( input.getAttribute( 'min' ) ) : 0;
1758
1759 max = isNaN( max ) ? 0 : max;
1760 min = isNaN( min ) ? 0 : Math.max( 0, min );
1761
1762 if ( val < min ) {
1763 input.value = min;
1764 return min;
1765 }
1766
1767 if ( 0 !== max && val > max ) {
1768 input.value = max;
1769 return max;
1770 }
1771
1772 return val;
1773 }
1774
1775 function triggerChange( input, fieldKey ) {
1776 if ( fieldKey === undefined ) {
1777 fieldKey = 'dependent';
1778 }
1779
1780 jQuery( input ).trigger( { type: 'change', selfTriggered: true, frmTriggered: fieldKey } );
1781 }
1782
1783 /**
1784 * Calculates the total price.
1785 *
1786 * @param {Event|undefined} e The event object.
1787 * @return {void}
1788 */
1789 function calcProductsTotal( e ) {
1790 if ( 'object' === typeof frmProForm ) {
1791 // Pro is installed, use the Pro JS.
1792 return;
1793 }
1794
1795 if ( typeof __FRMCURR === 'undefined' ) {
1796 return;
1797 }
1798
1799 const totalFields = document.querySelectorAll( '[data-frmtotal]' );
1800 if ( ! totalFields.length ) {
1801 return;
1802 }
1803
1804 const formTotals = [];
1805
1806 totalFields.forEach( totalField => {
1807 let total = 0;
1808 const form = totalField.closest( 'form' );
1809
1810 if ( ! form ) {
1811 return;
1812 }
1813
1814 const formId = form.querySelector( 'input[name="form_id"]' ).value;
1815 const currency = getCurrency( formId );
1816
1817 if ( undefined !== formTotals[ formId ] ) {
1818 total = formTotals[ formId ];
1819 } else {
1820 form.querySelectorAll( 'input[data-frmprice],select:has([data-frmprice])' ).forEach( function( input ) {
1821 let quantity = 0;
1822 let price = 0;
1823 const isSingle = 'hidden' === input.type;
1824
1825 if ( input.tagName === 'SELECT' ) {
1826 if ( input.selectedIndex !== -1 ) {
1827 price = input.options[ input.selectedIndex ].getAttribute( 'data-frmprice' );
1828 }
1829 } else {
1830 if ( ! isSingle && ! input.matches( ':checked' ) ) {
1831 return;
1832 }
1833 price = input.getAttribute( 'data-frmprice' );
1834 }
1835
1836 if ( ! price ) {
1837 price = 0;
1838 } else {
1839 price = preparePrice( price, currency );
1840 quantity = getQuantity( input );
1841 price = parseFloat( quantity ) * parseFloat( price );
1842 }
1843
1844 if ( 'true' === input.getAttribute( 'data-frmdiscount' ) ) {
1845 price = price * -1;
1846 }
1847
1848 total += price;
1849 } );
1850
1851 formTotals[ formId ] = total;
1852 }
1853
1854 total = isNaN( total ) ? 0 : total;
1855
1856 // Set a decimal separator for currency if no default for it
1857 currency.decimal_separator = currency.decimal_separator.trim(); // first remove unnecessary space(s)
1858 if ( ! currency.decimal_separator.length ) {
1859 currency.decimal_separator = '.';
1860 }
1861
1862 totalField.value = roundTotal( total, currency );
1863 total = normalizeTotal( total, currency );
1864
1865 // because of e.g. fields that might be using this field for calculations
1866 triggerChange( totalField );
1867
1868 total = formatCurrency( total, currency );
1869 const formatted = totalField.previousElementSibling;
1870 if ( formatted?.matches( '.frm_total_formatted' ) ) {
1871 // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1872 formatted.innerHTML = total;
1873 return;
1874 }
1875
1876 const formattedEls = totalField.closest( '.frm_form_field' ).querySelectorAll( '.frm_total_formatted' );
1877 formattedEls.forEach( formattedEl => {
1878 // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1879 formattedEl.innerHTML = total;
1880 } );
1881 } );
1882 }
1883
1884 /**
1885 * Round total and maybe add trailing zeros so formatCurrency has a proper format to work with.
1886 *
1887 * @param {number} total The total amount to normalize.
1888 * @param {Object} currency The currency object containing decimal information.
1889 * @return {string} The normalized total amount.
1890 */
1891 function normalizeTotal( total, currency ) {
1892 const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1893
1894 total = roundTotal( total, currency );
1895
1896 return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal );
1897 }
1898
1899 function roundTotal( total, currency ) {
1900 const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1901
1902 if ( ! isLargeTotal ) {
1903 const { decimals } = currency;
1904 total = decimals > 0 ? round10( total, decimals ) : Math.ceil( total );
1905 }
1906
1907 return total;
1908 }
1909
1910 function round10( value, decimals ) {
1911 return Number( `${ Math.round( `${ value }e${ decimals }` ) }e-${ decimals }` );
1912 }
1913
1914 /**
1915 * Format a numeric value according to the specified currency format settings.
1916 *
1917 * @param {string} total The numeric value to format.
1918 * @param {Object} currency The currency formatting configuration.
1919 * @return {string} The formatted currency string.
1920 */
1921 function formatCurrency( total, currency ) {
1922 total = maybeAddTrailingZeroToPrice( total, currency );
1923 if ( total.length && ( total[ total.length - 1 ] === '.' || total[ total.length - 1 ] === currency.decimal_separator ) ) {
1924 total = total.substr( 0, total.length - 1 );
1925 }
1926
1927 total = maybeRemoveTrailingZerosFromPrice( total, currency );
1928 total = addThousands( total, currency );
1929
1930 const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : '';
1931 const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : '';
1932
1933 return `${ leftSymbol }${ total }${ rightSymbol }`;
1934 }
1935
1936 /**
1937 * Gets currency from form id.
1938 *
1939 * @param {number} formId Form ID.
1940 * @return {Object} Currency object.
1941 */
1942 function getCurrency( formId ) {
1943 if ( undefined !== window.__FRMCURR && undefined !== window.__FRMCURR[ formId ] ) {
1944 return window.__FRMCURR[ formId ];
1945 }
1946
1947 return {
1948 symbol_left: '$',
1949 symbol_right: '',
1950 symbol_padding: '',
1951 thousand_separator: ',',
1952 decimal_separator: '.',
1953 decimals: 2,
1954 };
1955 }
1956
1957 /**
1958 * Gets quantity.
1959 *
1960 * @param {HTMLElement} field The field element.
1961 * @return {number} The quantity.
1962 */
1963 function getQuantity( field ) {
1964 const fieldID = frmFrontForm.getFieldId( field, false );
1965 if ( ! fieldID ) {
1966 return 0;
1967 }
1968
1969 const quantityField = getQuantityField( field, fieldID );
1970 if ( ! quantityField ) {
1971 // If there is no quantity field, assume 1.
1972 return 1;
1973 }
1974
1975 return checkQuantityFieldMinMax( quantityField );
1976 }
1977
1978 /**
1979 * Gets quantity field.
1980 *
1981 * @param {HTMLElement} element The element.
1982 * @param {number} fieldID The field ID.
1983 * @return {HTMLElement|null} The quantity field.
1984 */
1985 function getQuantityField( element, fieldID ) {
1986 const quantityFields = element.closest( 'form' ).querySelectorAll( '[data-frmproduct]' );
1987 if ( ! quantityFields.length ) {
1988 return null;
1989 }
1990
1991 fieldID = fieldID.toString();
1992
1993 return Array.from( quantityFields ).find( element => {
1994 let ids;
1995
1996 ids = JSON.parse( element.getAttribute( 'data-frmproduct' ).trim() );
1997 if ( '' === ids ) {
1998 return false;
1999 }
2000
2001 // Convert to array if necessary because of existing fields that are already using single product fields.
2002 ids = 'string' === typeof ids ? [ ids ] : ids;
2003 return ids.includes( fieldID );
2004 } );
2005 }
2006
2007 /**
2008 * Prepare a price for calculation.
2009 *
2010 * @param {number|string} price The price to prepare.
2011 * @param {Object} currency The currency object containing decimal information.
2012 * @return {string} The prepared price.
2013 */
2014 function preparePrice( price, currency ) {
2015 if ( ! price ) {
2016 return 0;
2017 }
2018 price = `${ price }`; // convert to string just to be sure
2019
2020 const regex = getRegexForPrice( currency );
2021
2022 const matches = price.match( regex );
2023 if ( null === matches ) {
2024 return 0;
2025 }
2026
2027 price = matches.length ? matches[ matches.length - 1 ] : 0;
2028 price = price.trim();
2029
2030 // Fix issues with parsing Fr.15.00. The regex catches .15.00.
2031 // This checks for the leading decimal and removes it.
2032 if ( currency.decimal_separator === '.' && 3 === price.split( '.' ).length && price[ 0 ] === '.' ) {
2033 price = price.substr( 1 );
2034 }
2035
2036 if ( price ) {
2037 price = maybeUseDecimal( price, currency );
2038 price = price.split( currency.thousand_separator ).join( '' ).replace( currency.decimal_separator, '.' );
2039 }
2040
2041 return price;
2042 }
2043
2044 /**
2045 * @param {Object} currency The currency object.
2046 * @return {RegExp} The regular expression object.
2047 */
2048 function getRegexForPrice( currency ) {
2049 let regexString = '[0-9,.';
2050
2051 if ( currency.thousand_separator !== '.' && currency.thousand_separator !== ',' ) {
2052 regexString += currency.thousand_separator;
2053 }
2054 if ( currency.decimal_separator !== '.' && currency.decimal_separator !== ',' ) {
2055 regexString += currency.decimal_separator;
2056 }
2057
2058 regexString += ']*\\.?\\,?[0-9]+';
2059
2060 return new RegExp( regexString, 'g' );
2061 }
2062
2063 /**
2064 * Maybe replace the decimal separator with the currency's decimal separator.
2065 *
2066 * @param {string} price The price string.
2067 * @param {Object} currency The currency object.
2068 * @return {string} The modified price string.
2069 */
2070 function maybeUseDecimal( price, currency ) {
2071 let usedForDecimal;
2072 let priceParts;
2073 if ( '.' === currency.thousand_separator ) {
2074 priceParts = price.split( '.' );
2075 usedForDecimal = 2 === priceParts.length && 2 === priceParts[ 1 ].length;
2076 if ( usedForDecimal ) {
2077 price = price.replace( '.', currency.decimal_separator );
2078 }
2079 }
2080 return price;
2081 }
2082
2083 /**
2084 * Add trailing zeros to a price if necessary and replace the decimal separator.
2085 *
2086 * @param {number|string} price The price to format.
2087 * @param {Object} currency The currency object containing the decimal separator.
2088 * @param {boolean} [force=false] Whether to force processing even if the price is not a number.
2089 * @return {string} The formatted price string.
2090 */
2091 function maybeAddTrailingZeroToPrice( price, currency, force = false ) {
2092 if ( 'number' !== typeof price && ! force ) {
2093 return price;
2094 }
2095
2096 price = String( price ); // first convert to string
2097 const pos = price.indexOf( '.' );
2098
2099 if ( pos === -1 ) {
2100 price = `${ price }.`;
2101
2102 for ( let n = 0; n < currency.decimals; ++n ) {
2103 price += '0';
2104 }
2105 } else {
2106 const decimalsString = price.substring( pos + 1 );
2107 if ( decimalsString.length < currency.decimals ) {
2108 if ( decimalsString.length < 2 ) {
2109 price += '0';
2110 }
2111
2112 for ( let n = 2; n < currency.decimals; ++n ) {
2113 price += '0';
2114 }
2115 }
2116 }
2117
2118 return price.replace( '.', currency.decimal_separator );
2119 }
2120
2121 /**
2122 * Format a numeric string by adding thousand separators.
2123 *
2124 * @param {string|number} price The numeric value to format.
2125 * @param {Object} options Formatting options.
2126 * @param {string} options.decimal_separator Character used as decimal separator.
2127 * @param {string} options.thousand_separator Character used as thousand separator.
2128 *
2129 * @return {string} The price string with thousand separators.
2130 */
2131 function addThousands( price, options ) {
2132 const split = options.decimal_separator === ''
2133 ? [ price.toString() ]
2134 : price.split( options.decimal_separator );
2135
2136 if ( options.thousand_separator ) {
2137 split[ 0 ] = split[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, options.thousand_separator );
2138 }
2139
2140 return split.join( options.decimal_separator );
2141 }
2142
2143 /**
2144 * Maybe remove trailing zeros from a price string.
2145 *
2146 * @param {string} price The price string.
2147 * @param {Object} currency The currency data.
2148 *
2149 * @return {string} The price string with trailing zeros removed.
2150 */
2151 function maybeRemoveTrailingZerosFromPrice( price, currency ) {
2152 const split = price.split( currency.decimal_separator );
2153 if ( 2 !== split.length || split[ 1 ].length <= currency.decimals ) {
2154 return price;
2155 }
2156 if ( 0 === currency.decimals ) {
2157 return split[ 0 ];
2158 }
2159 return `${ split[ 0 ] }${ currency.decimal_separator }${ split[ 1 ].substr( 0, currency.decimals ) }`;
2160 }
2161
2162 return {
2163 init() {
2164 jQuery( document ).off( 'submit.formidable', '.frm-show-form' );
2165 jQuery( document ).on( 'submit.formidable', '.frm-show-form', frmFrontForm.submitForm );
2166
2167 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 );
2168
2169 jQuery( document ).on( 'change', '.frm_verify[id^=field_]', onHoneypotFieldChange );
2170
2171 jQuery( document ).on( 'click', 'a[data-frmconfirm]', confirmClick );
2172
2173 checkForErrorsAndMaybeSetFocus();
2174
2175 // Focus on the first sub field when clicking to the primary label of combo field.
2176 changeFocusWhenClickComboFieldLabel();
2177
2178 initFloatingLabels();
2179 maybeShowNewTabFallbackMessage();
2180
2181 jQuery( document ).on( 'frmAfterAddRow', setCustomValidityMessage );
2182 setCustomValidityMessage();
2183 jQuery( document ).on( 'frmFieldChanged', maybeClearCustomValidityMessage );
2184
2185 setSelectPlaceholderColor();
2186
2187 // Elementor popup show event. Fix Elementor Popup && FF Captcha field conflicts
2188 jQuery( document ).on( 'elementor/popup/show', frmRecaptcha );
2189
2190 enableSubmitButtonOnBackButtonPress();
2191 jQuery( document ).on(
2192 'frmPageChanged',
2193 destroyhCaptcha
2194 );
2195
2196 jQuery( document ).on( 'frmAfterAddRow frmAfterRemoveRow', calcProductsTotal );
2197 jQuery( document ).on( 'change', '[type="checkbox"][data-frmprice],[type="radio"][data-frmprice],[type="hidden"][data-frmprice],select:has([data-frmprice])', calcProductsTotal );
2198 jQuery( document ).on( 'keyup change', '[data-frmproduct],[type="text"][data-frmprice]', calcProductsTotal );
2199 calcProductsTotal();
2200 },
2201
2202 getFieldId,
2203
2204 /**
2205 * Render a captcha field.
2206 *
2207 * @param {HTMLElement} captcha
2208 * @param {string} captchaSelector
2209 * @return {void}
2210 */
2211 renderCaptcha( captcha, captchaSelector ) {
2212 const rendered = captcha.getAttribute( 'data-rid' ) !== null;
2213 if ( rendered ) {
2214 return;
2215 }
2216
2217 const size = captcha.getAttribute( 'data-size' );
2218 const params = {
2219 sitekey: captcha.getAttribute( 'data-sitekey' ),
2220 size,
2221 theme: captcha.getAttribute( 'data-theme' )
2222 };
2223
2224 if ( size === 'invisible' ) {
2225 const formID = captcha.closest( 'form' )?.querySelector( 'input[name="form_id"]' )?.value;
2226
2227 const captchaLabel = captcha.closest( '.frm_form_field' )?.querySelector( '.frm_primary_label' );
2228 if ( captchaLabel ) {
2229 captchaLabel.style.display = 'none';
2230 }
2231
2232 params.callback = function( token ) {
2233 frmFrontForm.afterRecaptcha( token, formID );
2234 };
2235 }
2236
2237 const activeCaptcha = getSelectedCaptcha( captchaSelector );
2238 const captchaContainer = typeof turnstile !== 'undefined' && turnstile === activeCaptcha ? `#${ captcha.id }` : captcha.id;
2239 const captchaID = activeCaptcha.render( captchaContainer, params );
2240
2241 captcha.setAttribute( 'data-rid', captchaID );
2242
2243 maybeFixCaptchaLabel( captcha );
2244 },
2245
2246 afterSingleRecaptcha() {
2247 const recaptcha = document.querySelector( '.frm-show-form .g-recaptcha' );
2248 const object = recaptcha ? recaptcha.closest( 'form' ) : null;
2249 frmFrontForm.submitFormNow( object );
2250 },
2251
2252 afterRecaptcha( _, formID ) {
2253 const object = document.querySelector( `#frm_form_${ formID }_container form` );
2254 frmFrontForm.submitFormNow( object );
2255 },
2256
2257 submitForm( e ) {
2258 frmFrontForm.submitFormManual( e, this );
2259 },
2260
2261 /**
2262 * @param {Event} e
2263 * @param {HTMLElement} object The form object that is being submitted.
2264 * @return {void}
2265 */
2266 submitFormManual( e, object ) {
2267 if ( document.body.classList.contains( 'wp-admin' ) && ! object.closest( '.frmapi-form' ) ) {
2268 return;
2269 }
2270
2271 e.preventDefault();
2272
2273 if ( typeof frmProForm !== 'undefined' && typeof frmProForm.submitAllowed === 'function' && ! frmProForm.submitAllowed( object ) ) {
2274 return;
2275 }
2276
2277 const errors = frmFrontForm.validateFormSubmit( object );
2278 if ( Object.keys( errors ).length !== 0 ) {
2279 return;
2280 }
2281
2282 const invisibleRecaptcha = hasInvisibleRecaptcha( object );
2283
2284 if ( invisibleRecaptcha ) {
2285 showLoadingIndicator( jQuery( object ) );
2286 executeInvisibleRecaptcha( invisibleRecaptcha );
2287 } else {
2288 showSubmitLoading( jQuery( object ) );
2289
2290 frmFrontForm.submitFormNow( object );
2291 }
2292 },
2293
2294 submitFormNow( object ) {
2295 let hasFileFields;
2296 let antispamInput;
2297 const classList = object.className.trim().split( /\s+/gi );
2298
2299 if ( object.hasAttribute( 'data-token' ) && null === object.querySelector( '[name="antispam_token"]' ) ) {
2300 // include the antispam token on form submit.
2301 antispamInput = document.createElement( 'input' );
2302 antispamInput.type = 'hidden';
2303 antispamInput.name = 'antispam_token';
2304 antispamInput.value = object.getAttribute( 'data-token' );
2305 object.append( antispamInput );
2306 }
2307
2308 // Add a unique ID, used for duplicate checks.
2309 const uniqueIDInput = document.createElement( 'input' );
2310 uniqueIDInput.type = 'hidden';
2311 uniqueIDInput.name = 'unique_id';
2312 uniqueIDInput.value = getUniqueKey();
2313 object.append( uniqueIDInput );
2314
2315 if ( classList.includes( 'frm_ajax_submit' ) ) {
2316 const fileInputs = object.querySelectorAll( 'input[type="file"]' );
2317 hasFileFields = Array.from( fileInputs ).filter( input => !! input.value ).length;
2318 if ( hasFileFields < 1 ) {
2319 const actionInput = object.querySelector( 'input[name="frm_action"]' );
2320 const action = actionInput ? actionInput.value : '';
2321 frmFrontForm.checkFormErrors( object, action );
2322 } else {
2323 object.submit();
2324 }
2325 } else {
2326 object.submit();
2327 }
2328 },
2329
2330 /**
2331 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
2332 *
2333 * @return {Array} List of errors.
2334 */
2335 validateFormSubmit( object ) {
2336 const form = object instanceof jQuery ? object.get( 0 ) : object;
2337 if ( typeof tinyMCE !== 'undefined' && form?.querySelector( '.wp-editor-wrap' ) ) {
2338 tinyMCE.triggerSave();
2339 }
2340
2341 jsErrors = [];
2342
2343 if ( shouldJSValidate( object ) ) {
2344 frmFrontForm.getAjaxFormErrors( object );
2345
2346 if ( Object.keys( jsErrors ).length ) {
2347 frmFrontForm.addAjaxFormErrors( object );
2348 }
2349 }
2350
2351 return jsErrors;
2352 },
2353
2354 /**
2355 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
2356 * @return {Array} List of errors.
2357 */
2358 getAjaxFormErrors( object ) {
2359 let customErrors;
2360 let key;
2361 const form = object instanceof jQuery ? object.get( 0 ) : object;
2362
2363 jsErrors = validateForm( object );
2364 if ( typeof frmThemeOverride_jsErrors === 'function' ) { // eslint-disable-line camelcase
2365 const actionInput = form ? form.querySelector( 'input[name="frm_action"]' ) : null;
2366 const action = actionInput ? actionInput.value : '';
2367 customErrors = frmThemeOverride_jsErrors( action, object );
2368 if ( Object.keys( customErrors ).length ) {
2369 for ( key in customErrors ) {
2370 jsErrors[ key ] = customErrors[ key ];
2371 }
2372 }
2373 }
2374
2375 triggerCustomEvent( document, 'frm_get_ajax_form_errors', {
2376 formEl: object,
2377 errors: jsErrors
2378 } );
2379
2380 return jsErrors;
2381 },
2382
2383 /**
2384 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
2385 * @return {void}
2386 */
2387 addAjaxFormErrors( object ) {
2388 let key;
2389 const form = object instanceof jQuery ? object.get( 0 ) : object;
2390 removeAllErrors();
2391
2392 for ( key in jsErrors ) {
2393 const fieldCont = form ? form.querySelector( `#frm_field_${ key }_container` ) : null;
2394
2395 if ( fieldCont ) {
2396 addFieldError( fieldCont, key, jsErrors );
2397 } else {
2398 // we are unable to show the error, so remove it
2399 delete jsErrors[ key ];
2400 }
2401 }
2402
2403 scrollToFirstField( object );
2404 checkForErrorsAndMaybeSetFocus();
2405 },
2406
2407 checkFormErrors: getFormErrors,
2408 checkRequiredField,
2409 showSubmitLoading,
2410 removeSubmitLoading,
2411
2412 scrollToID( id ) {
2413 const object = jQuery( document.getElementById( id ) );
2414 frmFrontForm.scrollMsg( object, false );
2415 },
2416
2417 scrollMsg( id, object, animate ) {
2418 let newPos;
2419 let screenTop;
2420 let screenBottom;
2421 let scrollObj = '';
2422
2423 if ( object === undefined ) {
2424 scrollObj = jQuery( document.getElementById( `frm_form_${ id }_container` ) );
2425 if ( scrollObj.length < 1 ) {
2426 return;
2427 }
2428 } else if ( typeof id === 'string' ) {
2429 const formEl = object instanceof jQuery ? object.get( 0 ) : object;
2430 const fieldEl = formEl ? formEl.querySelector( `#frm_field_${ id }_container` ) : null;
2431 scrollObj = fieldEl ? jQuery( fieldEl ) : jQuery();
2432 } else {
2433 scrollObj = id;
2434 }
2435
2436 jQuery( scrollObj ).trigger( 'focus' );
2437 newPos = scrollObj.offset().top;
2438 if ( ! newPos || frm_js.offset === '-1' ) {
2439 return;
2440 }
2441 newPos = newPos - frm_js.offset;
2442
2443 const docMarginTop = getComputedStyle( document.documentElement ).marginTop;
2444 const bodyMarginTop = getComputedStyle( document.body ).marginTop;
2445 if ( docMarginTop || bodyMarginTop ) {
2446 newPos = newPos - parseInt( docMarginTop ) - parseInt( bodyMarginTop );
2447 }
2448
2449 if ( newPos && window.innerHeight ) {
2450 screenTop = document.documentElement.scrollTop || document.body.scrollTop;
2451 screenBottom = screenTop + window.innerHeight;
2452
2453 if ( newPos > screenBottom || newPos < screenTop ) {
2454 // Not in view
2455 if ( animate === undefined ) {
2456 document.documentElement.scrollTop = newPos;
2457 } else {
2458 animateScroll( screenTop, newPos, 500 );
2459 }
2460 return false;
2461 }
2462 }
2463 },
2464
2465 fieldValueChanged( e ) {
2466 /*jshint validthis:true */
2467
2468 const fieldId = frmFrontForm.getFieldId( this, false );
2469 if ( ! fieldId ) {
2470 return;
2471 }
2472
2473 if ( e.frmTriggered && e.frmTriggered == fieldId ) {
2474 return;
2475 }
2476
2477 jQuery( document ).trigger( 'frmFieldChanged', [ this, fieldId, e ] );
2478
2479 if ( e.selfTriggered !== true ) {
2480 maybeValidateChange( this );
2481 }
2482 },
2483
2484 escapeHtml( text ) {
2485 console.warn( 'DEPRECATED: function frmFrontForm.escapeHtml in v6.17' );
2486 return text
2487 .replace( /&/g, '&amp;' )
2488 .replace( /</g, '&lt;' )
2489 .replace( />/g, '&gt;' )
2490 .replace( /"/g, '&quot;' )
2491 .replace( /'/g, '&#039;' );
2492 },
2493
2494 triggerCustomEvent,
2495 documentOn
2496 };
2497 }
2498
2499 window.frmFrontForm = frmFrontFormJS();
2500
2501 jQuery( document ).ready( function() {
2502 frmFrontForm.init();
2503 } );
2504
2505 function frmRecaptcha() {
2506 frmCaptcha( '.frm-g-recaptcha' );
2507 }
2508
2509 function frmHcaptcha() {
2510 frmCaptcha( '.h-captcha' );
2511 }
2512
2513 function frmTurnstile() {
2514 frmCaptcha( '.frm-cf-turnstile' );
2515 }
2516
2517 function frmCaptcha( captchaSelector ) {
2518 if ( '.h-captcha' === captchaSelector ) {
2519 // hCaptcha is still rendered implicitly, so we only want to handle the label and exit early.
2520 // Match the hcaptcha labels to the hcaptcha response fields.
2521 const captchaLabels = document.querySelectorAll( 'label[for="h-captcha-response"]' );
2522 if ( captchaLabels.length ) {
2523 captchaLabels.forEach( label => {
2524 const captchaResponse = label.closest( 'form' )?.querySelector( '[name="h-captcha-response"]' );
2525 if ( captchaResponse ) {
2526 label.htmlFor = captchaResponse.id;
2527 }
2528 } );
2529 }
2530 return;
2531 }
2532
2533 let c;
2534 const captchas = document.querySelectorAll( captchaSelector );
2535 const cl = captchas.length;
2536 for ( c = 0; c < cl; c++ ) {
2537 const closestForm = captchas[ c ].closest( 'form' );
2538 const formIsVisible = closestForm && closestForm.offsetParent !== null;
2539 const captcha = captchas[ c ];
2540 if ( ! formIsVisible ) {
2541 // If the form is not visible, try again later in 400ms.
2542 // This fixes issues where the form fades visible on page load.
2543 // Or when the form is inside of a modal.
2544 const interval = setInterval(
2545 function() {
2546 if ( closestForm && closestForm.offsetParent !== null ) {
2547 frmFrontForm.renderCaptcha( captcha, captchaSelector );
2548 clearInterval( interval );
2549 }
2550 },
2551 400
2552 );
2553 continue;
2554 }
2555 frmFrontForm.renderCaptcha( captcha, captchaSelector );
2556 }
2557 }
2558
2559 function getSelectedCaptcha( captchaSelector ) {
2560 if ( captchaSelector === '.frm-g-recaptcha' ) {
2561 return grecaptcha;
2562 }
2563 if ( document.querySelector( '.frm-cf-turnstile' ) ) {
2564 return turnstile;
2565 }
2566 return hcaptcha;
2567 }
2568
2569 function frmAfterRecaptcha( token ) {
2570 frmFrontForm.afterSingleRecaptcha( token );
2571 }
2572