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

2,603 lines 72.9 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 ) {
286 return;
287 }
288
289 // Removing stale errors is universal. Adding errors only happens when JS validation is enabled.
290 validateField( field, hasClass( form, 'frm_js_validate' ) );
291 }
292
293 /**
294 * @param {HTMLElement} field
295 */
296 function maybeAddHttpsToUrl( field ) {
297 const url = field.value;
298 const matches = url.match( /^(https?|ftps?|mailto|news|feed|telnet):/ );
299 if ( field.value !== '' && matches === null ) {
300 field.value = `https://${ url }`;
301 }
302 }
303
304 /**
305 * Validate a field with JS.
306 *
307 * Removing stale errors is universal. Adding errors only happens when JS validation is enabled.
308 *
309 * @since 6.32 Added the `addErrors` parameter.
310 *
311 * @param {HTMLElement} field
312 * @param {boolean} addErrors Whether to add new errors. Defaults to `true`.
313 *
314 * @return {void}
315 */
316 function validateField( field, addErrors = true ) {
317 let errors;
318 let key;
319
320 errors = [];
321 const fieldContainer = field.closest( '.frm_form_field' );
322
323 if ( ! fieldContainer ) {
324 // Hidden fields do not have a field container and do not require JS validation.
325 return;
326 }
327
328 if ( hasClass( fieldContainer, 'frm_required_field' ) && ! hasClass( field, 'frm_optional' ) ) {
329 errors = checkRequiredField( field, errors );
330 }
331
332 if ( errors.length < 1 ) {
333 validateFieldValue( field, errors, false );
334 }
335
336 const hasErrors = Object.keys( errors ).length > 0;
337
338 if ( addErrors ) {
339 removeFieldError( fieldContainer );
340 if ( hasErrors ) {
341 for ( key in errors ) {
342 addFieldError( fieldContainer, key, errors );
343 }
344 }
345 } else if ( ! hasErrors ) {
346 // JS validation is off, so only remove existing errors once the field passes validation.
347 removeFieldError( fieldContainer );
348 }
349 }
350
351 /**
352 * Validates a field value.
353 *
354 * @since 6.15 Added `onSubmit` parameter.
355 *
356 * @param {HTMLElement} field Field input.
357 * @param {Object} errors Errors data.
358 * @param {boolean} onSubmit Is `true` if the form is being submitted.
359 */
360 function validateFieldValue( field, errors, onSubmit ) {
361 if ( field.type === 'hidden' ) {
362 // don't validate
363 } else if ( field.type === 'number' ) {
364 checkNumberField( field, errors );
365 } else if ( field.type === 'email' ) {
366 checkEmailField( field, errors, onSubmit );
367 } else if ( field.type === 'password' ) {
368 checkPasswordField( field, errors, onSubmit );
369 } else if ( field.type === 'url' ) {
370 checkUrlField( field, errors );
371 } else if ( field.pattern !== null ) {
372 checkPatternField( field, errors );
373 }
374
375 if ( 'tel' === field.type && shouldCheckConfirmField( field, onSubmit ) ) {
376 confirmField( field, errors );
377 }
378
379 /**
380 * @since 6.15 Added `onSubmit` to the data.
381 */
382 triggerCustomEvent( document, 'frm_validate_field_value', {
383 field,
384 errors,
385 onSubmit
386 } );
387 }
388
389 /**
390 * @param {HTMLElement} field
391 * @param {Array} errors
392 * @return {Array} Errors
393 */
394 function checkRequiredField( field, errors ) {
395 let tempVal;
396 let i;
397 let placeholder;
398 let val = '';
399 let fieldID = '';
400 let fileID = field.getAttribute( 'data-frmfile' );
401
402 if ( field.type === 'hidden' && fileID === null && ! isAppointmentField( field ) && ! isInlineDatepickerField( field ) ) {
403 return errors;
404 }
405
406 if ( field.type === 'checkbox' || field.type === 'radio' ) {
407 document.querySelectorAll( `input[name="${ field.name }"]` ).forEach( function( input ) {
408 const requiredField = input.closest( '.frm_required_field' );
409 if ( ! requiredField ) {
410 return;
411 }
412
413 const checkedInputs = requiredField.querySelectorAll( 'input:checked' );
414 checkedInputs.forEach( function( checkedInput ) {
415 val = checkedInput.value;
416 } );
417 } );
418 } else if ( field.type === 'file' || fileID ) {
419 if ( fileID === undefined ) {
420 fileID = getFieldId( field, true );
421 fileID = fileID.replace( 'file', '' );
422 }
423
424 if ( errors[ fileID ] === undefined ) {
425 val = getFileVals( fileID );
426 }
427 fieldID = fileID;
428 } else {
429 if ( hasClass( field, 'frm_pos_none' ) ) {
430 // skip hidden other fields
431 return errors;
432 }
433
434 val = jQuery( field ).val(); // eslint-disable-line no-jquery/no-val
435
436 if ( val === null ) {
437 val = '';
438 } else if ( typeof val !== 'string' ) {
439 tempVal = val;
440 val = '';
441 for ( i = 0; i < tempVal.length; i++ ) {
442 if ( tempVal[ i ] !== '' ) {
443 val = tempVal[ i ];
444 }
445 }
446 }
447
448 if ( hasClass( field, 'frm_other_input' ) ) {
449 fieldID = getFieldId( field, false );
450
451 if ( val === '' ) {
452 field = document.getElementById( field.id.replace( '-otext', '' ) );
453 }
454 } else {
455 fieldID = getFieldId( field, true );
456 }
457
458 // Make sure fieldID is a string.
459 // fieldID may be a number which doesn't include a .replace function.
460 if ( 'function' !== typeof fieldID.replace ) {
461 fieldID = fieldID.toString();
462 }
463
464 if ( hasClass( field, 'frm_time_select' ) ) {
465 // set id for time field
466 fieldID = fieldID.replace( '-H', '' ).replace( '-m', '' );
467 } else if ( isSignatureField( field ) ) {
468 if ( val === '' ) {
469 const fieldContainer = field.closest( '.frm_form_field' );
470 const outputField = fieldContainer ? fieldContainer.querySelector( `[name="${ field.getAttribute( 'name' ).replace( '[typed]', '[output]' ) }"]` ) : null;
471 val = outputField ? outputField.value : '';
472 }
473 fieldID = fieldID.replace( '-typed', '' );
474 }
475
476 placeholder = field.getAttribute( 'data-frmplaceholder' );
477 if ( placeholder !== null && val === placeholder ) {
478 val = '';
479 }
480 }
481
482 if ( val === '' ) {
483 if ( fieldID === '' ) {
484 fieldID = getFieldId( field, true );
485 }
486 if ( ! ( fieldID in errors ) ) {
487 errors[ fieldID ] = getFieldValidationMessage( field, 'data-reqmsg' );
488 }
489 }
490
491 return errors;
492 }
493
494 /**
495 * @param {HTMLElement} field
496 * @return {boolean} True if the input is a typed signature input.
497 */
498 function isSignatureField( field ) {
499 const name = field.getAttribute( 'name' );
500 return 'string' === typeof name && '[typed]' === name.substr( -7 );
501 }
502
503 /**
504 * @param {HTMLElement} field
505 * @return {boolean} True if the field is a SSA appointment field.
506 */
507 function isAppointmentField( field ) {
508 return hasClass( field, 'ssa_appointment_form_field_appointment_id' );
509 }
510
511 /**
512 * @param {HTMLElement} field
513 * @return {boolean} True if the field is inline datepicker field.
514 */
515 function isInlineDatepickerField( field ) {
516 return 'hidden' === field.type && '_alt' === field.id.substr( -4 ) && hasClass( field.nextElementSibling, 'frm_date_inline' );
517 }
518
519 /**
520 * @param {string|number} fileID
521 * @return {string} File input value.
522 */
523 function getFileVals( fileID ) {
524 let val = '';
525 const fileFields = document.querySelectorAll( `input[name="file${ fileID }"], input[name="file${ fileID }[]"], input[name^="item_meta[${ fileID }]"]` );
526
527 fileFields.forEach( function( field ) {
528 if ( val === '' ) {
529 val = field.value;
530 }
531 } );
532 return val;
533 }
534
535 /**
536 * @param {HTMLElement} field
537 * @param {Array} errors
538 * @return {void}
539 */
540 function checkUrlField( field, errors ) {
541 let fieldID;
542 const url = field.value;
543
544 if ( url !== '' && ! /^http(s)?:\/\/(?:localhost|(?:[\da-z\.-]+\.[\da-z\.-]+))/i.test( url ) ) {
545 fieldID = getFieldId( field, true );
546 if ( ! ( fieldID in errors ) ) {
547 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
548 }
549 }
550 }
551
552 /**
553 * Checks if the confirm field should be checked.
554 *
555 * @since 6.15
556 *
557 * @param {HTMLElement} field Field input.
558 * @param {boolean} onSubmit Is `true` if the form is being submitted.
559 * @return {boolean} True if we should confirm the field.
560 */
561 function shouldCheckConfirmField( field, onSubmit ) {
562 if ( onSubmit ) {
563 // Always check on submitting.
564 return true;
565 }
566
567 if ( 0 === field.id.indexOf( 'field_conf_' ) ) {
568 // Always check if it's the confirm field.
569 return true;
570 }
571
572 return false;
573 }
574
575 /**
576 * Check the email field for errors.
577 *
578 * @since 6.15 Added `onSubmit` parameter.
579 *
580 * @param {HTMLElement} field Field input.
581 * @param {Object} errors Errors data.
582 * @param {boolean} onSubmit Is `true` if the form is being submitted.
583 */
584 function checkEmailField( field, errors, onSubmit ) {
585 const fieldID = getFieldId( field, true );
586 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;
587
588 // validate the current field we're editing first
589 if ( '' !== field.value && pattern.test( field.value ) === false ) {
590 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
591 }
592
593 if ( shouldCheckConfirmField( field, onSubmit ) ) {
594 confirmField( field, errors );
595 }
596 }
597
598 /**
599 * Check the password field for errors.
600 *
601 * @since 6.15 Added `onSubmit` parameter.
602 *
603 * @param {HTMLElement} field Field input.
604 * @param {Object} errors Errors data.
605 * @param {boolean} onSubmit Is `true` if the form is being submitted.
606 */
607 function checkPasswordField( field, errors, onSubmit ) {
608 if ( shouldCheckConfirmField( field, onSubmit ) ) {
609 confirmField( field, errors );
610 }
611 }
612
613 /**
614 * @param {HTMLElement} field
615 * @param {Array} errors
616 * @return {void}
617 */
618 function confirmField( field, errors ) {
619 const fieldID = getFieldId( field, true );
620 const strippedId = field.id.replace( 'conf_', '' );
621 const strippedFieldID = fieldID.replace( 'conf_', '' );
622 const confirmField = document.getElementById( strippedId.replace( 'field_', 'field_conf_' ) );
623
624 if ( ! confirmField || errors[ `conf_${ strippedFieldID }` ] !== undefined ) {
625 return;
626 }
627
628 if ( fieldID !== strippedFieldID ) {
629 const firstField = document.getElementById( strippedId );
630 const { value } = firstField;
631 const confirmValue = confirmField.value;
632 if ( value !== confirmValue ) {
633 errors[ `conf_${ strippedFieldID }` ] = getFieldValidationMessage( confirmField, 'data-confmsg' );
634 }
635 } else {
636 validateField( confirmField );
637 }
638 }
639
640 /**
641 * @param {HTMLElement} field
642 * @param {Array} errors
643 * @return {void}
644 */
645 function checkNumberField( field, errors ) {
646 let fieldID;
647 const number = field.value;
648
649 if ( number !== '' && isNaN( number / 1 ) !== false ) {
650 fieldID = getFieldId( field, true );
651 if ( ! ( fieldID in errors ) ) {
652 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
653 }
654 }
655 }
656
657 /**
658 * @param {HTMLElement} field
659 * @param {Array} errors
660 * @return {void}
661 */
662 function checkPatternField( field, errors ) {
663 let fieldID;
664 const text = field.value;
665 let format = getFieldValidationMessage( field, 'pattern' );
666
667 if ( format !== '' && text !== '' ) {
668 fieldID = getFieldId( field, true );
669 if ( ! ( fieldID in errors ) ) {
670 if ( 'object' === typeof window.frmProForm && 'function' === typeof window.frmProForm.isIntlPhoneInput && window.frmProForm.isIntlPhoneInput( field ) ) {
671 if ( ! window.frmProForm.validateIntlPhoneInput( field ) ) {
672 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
673 }
674 } else {
675 format = new RegExp( `^${ format }$`, 'i' );
676 if ( format.test( text ) === false ) {
677 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
678 }
679 }
680 }
681 }
682 }
683
684 /**
685 * Set color for select placeholders.
686 *
687 * @since 6.5.1
688 */
689 function setSelectPlaceholderColor() {
690 const selects = document.querySelectorAll( '.form-field select' );
691 const styleElement = document.querySelector( '.with_frm_style' );
692 const textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : '';
693 // Exit if there are no select elements or the textColorDisabled property is missing
694 if ( ! selects.length || ! textColorDisabled ) {
695 return;
696 }
697
698 // Function to change the color of a select element
699 const changeSelectColor = function( select ) {
700 if ( select.options[ select.selectedIndex ] && hasClass( select.options[ select.selectedIndex ], 'frm-select-placeholder' ) ) {
701 select.style.setProperty( 'color', textColorDisabled, 'important' );
702 } else {
703 select.style.color = '';
704 }
705 };
706
707 // Use a loop to iterate through each select element
708 Array.prototype.forEach.call( selects, function( select ) {
709 // Apply the color change to each select element
710 changeSelectColor( select );
711
712 // Add an event listener for future changes
713 select.addEventListener( 'change', function() {
714 changeSelectColor( select );
715 } );
716 } );
717 }
718
719 /**
720 * @param {HTMLElement|jQuery} object
721 *
722 * @return {HTMLElement|false} Captcha element if there is an invisible recaptcha.
723 */
724 function hasInvisibleRecaptcha( object ) {
725 if ( isGoingToPrevPage( object ) ) {
726 return false;
727 }
728
729 const form = object instanceof jQuery ? object.get( 0 ) : object;
730 if ( ! form ) {
731 return false;
732 }
733
734 const recaptcha = form.querySelector( '.frm-g-recaptcha[data-size="invisible"], .g-recaptcha[data-size="invisible"]' );
735 if ( recaptcha ) {
736 const recaptchaID = recaptcha.dataset.rid;
737 const alreadyChecked = grecaptcha.getResponse( recaptchaID );
738 if ( alreadyChecked.length === 0 ) {
739 return recaptcha;
740 }
741 }
742
743 return false;
744 }
745
746 /**
747 * @param {HTMLElement} invisibleRecaptcha
748 *
749 * @return {void}
750 */
751 function executeInvisibleRecaptcha( invisibleRecaptcha ) {
752 const recaptchaID = invisibleRecaptcha.dataset.rid;
753 grecaptcha.reset( recaptchaID );
754 grecaptcha.execute( recaptchaID );
755 }
756
757 function validateRecaptcha( form, errors ) {
758 const formEl = form instanceof jQuery ? form.get( 0 ) : form;
759 if ( ! formEl ) {
760 return errors;
761 }
762
763 const recaptcha = formEl.querySelector( '.frm-g-recaptcha' );
764 if ( ! recaptcha ) {
765 return errors;
766 }
767
768 const recaptchaID = recaptcha.dataset.rid;
769 let response;
770
771 try {
772 response = grecaptcha.getResponse( recaptchaID );
773 } catch ( e ) {
774 if ( formEl.querySelector( 'input[name="recaptcha_checked"]' ) ) {
775 return errors;
776 }
777 response = '';
778 }
779
780 if ( response.length === 0 ) {
781 const fieldContainer = recaptcha.closest( '.frm_form_field' );
782 if ( fieldContainer?.id ) {
783 const fieldID = fieldContainer.id.replace( 'frm_field_', '' ).replace( '_container', '' );
784 errors[ fieldID ] = '';
785 }
786 }
787
788 return errors;
789 }
790
791 /**
792 * @param {HTMLElement} field
793 * @param {string} messageType
794 * @return {string} The error message to display.
795 */
796 function getFieldValidationMessage( field, messageType ) {
797 let msg = field.getAttribute( messageType );
798 if ( null === msg ) {
799 msg = '';
800 }
801
802 if ( '' !== msg && shouldWrapErrorHtmlAroundMessageType( messageType ) ) {
803 msg = wrapErrorHtml( msg, field );
804 }
805
806 return msg;
807 }
808
809 /**
810 * @param {string} msg
811 * @param {HTMLElement} field
812 * @return {string} The error HTML to use.
813 */
814 function wrapErrorHtml( msg, field ) {
815 let errorHtml = field.getAttribute( 'data-error-html' );
816 if ( null === errorHtml ) {
817 return msg;
818 }
819
820 errorHtml = errorHtml.replace( /\+/g, '%20' );
821 msg = decodeURIComponent( errorHtml ).replace( '[error]', msg );
822 const fieldId = getFieldId( field, false );
823 const split = fieldId.split( '-' );
824 const fieldIdParts = field.id.split( '_' );
825 fieldIdParts.shift(); // Drop the "field" value from the front.
826 split[ 0 ] = fieldIdParts.join( '_' );
827 const errorKey = split.join( '-' );
828 return msg.replace( '[key]', errorKey );
829 }
830
831 function shouldWrapErrorHtmlAroundMessageType( type ) {
832 return 'pattern' !== type;
833 }
834
835 /**
836 * Check if JS validation should happen.
837 *
838 * @param {HTMLElement|Object} object Form object.
839 * @return {boolean} True if validation is enabled and we are not saving a draft or going to a previous page.
840 */
841 function shouldJSValidate( object ) {
842 if ( 'function' === typeof object.get ) {
843 // Get the HTMLElement from a jQuery object.
844 object = object.get( 0 );
845 }
846 let validate = hasClass( object, 'frm_js_validate' );
847 if ( validate && typeof frmProForm !== 'undefined' && ( frmProForm.savingDraft( object ) || frmProForm.goingToPreviousPage( object ) ) ) {
848 validate = false;
849 }
850
851 return validate;
852 }
853
854 /**
855 * @param {HTMLElement} object
856 * @param {string} action
857 * @return {void}
858 */
859 function getFormErrors( object, action ) {
860 const fieldsets = object.querySelectorAll( '.frm_form_field' );
861 fieldsets.forEach( field => field.classList.add( 'frm_doing_ajax' ) );
862
863 const data = `${ jQuery( object ).serialize() }&action=frm_entries_${ action }&nonce=${ frm_js.nonce }`; // eslint-disable-line no-jquery/no-serialize
864 const shouldTriggerEvent = object.classList.contains( 'frm_trigger_event_on_submit' );
865
866 const doRedirect = response => {
867 jQuery( document ).trigger( 'frmBeforeFormRedirect', [ object, response ] );
868
869 if ( ! response.openInNewTab ) {
870 // We return here because we're redirecting there is no need to update content.
871 window.location = response.redirect;
872 return;
873 }
874
875 // We don't return here because we're opening in a new tab, the old tab will still update.
876 const newTab = window.open( response.redirect, '_blank' );
877 if ( ! newTab && response.fallbackMsg && response.content ) {
878 response.content = response.content.trim().replace( /(<\/div><\/div>)$/, ` ${ response.fallbackMsg }</div></div>` );
879 }
880 };
881
882 const success = function( response ) {
883 const defaultResponse = {
884 content: '',
885 errors: {},
886 pass: false
887 };
888
889 if ( response === null ) {
890 response = defaultResponse;
891 } else {
892 // Response is a string. Convert it to an object.
893 response = response.replace( /^\s+|\s+$/g, '' );
894 if ( response.indexOf( '{' ) === 0 ) {
895 response = JSON.parse( response );
896 } else {
897 response = defaultResponse;
898 }
899 }
900
901 let willRedirect = false;
902
903 if ( response.redirect !== undefined ) {
904 if ( shouldTriggerEvent ) {
905 triggerCustomEvent( object, 'frmSubmitEvent' );
906 return;
907 }
908
909 if ( response.delay ) {
910 setTimeout( function() {
911 doRedirect( response );
912 }, 1000 * response.delay );
913 } else {
914 doRedirect( response );
915 }
916
917 willRedirect = true;
918 }
919
920 if ( 'string' === typeof response.content && response.content !== '' ) {
921 // the form or success message was returned
922
923 if ( shouldTriggerEvent ) {
924 triggerCustomEvent( object, 'frmSubmitEvent', { content: response.content } );
925 return;
926 }
927
928 removeSubmitLoading( jQuery( object ) );
929 if ( frm_js.offset != -1 ) {
930 frmFrontForm.scrollMsg( jQuery( object ), false );
931 }
932
933 const formIdInput = object.querySelector( 'input[name="form_id"]' );
934 const formID = formIdInput ? formIdInput.value : '';
935 response.content = response.content.replace( / frm_pro_form /g, ' frm_pro_form frm_no_hide ' );
936 const replaceContent = jQuery( object ).closest( '.frm_forms' ); // eslint-disable-line no-jquery/no-closest
937 removeAddedScripts( replaceContent, formID );
938 const delay = maybeSlideOut( replaceContent, response.content );
939
940 setTimeout(
941 function() {
942 afterFormSubmittedBeforeReplace( object, response );
943
944 replaceContent.replaceWith( response.content );
945
946 addUrlParam( response );
947
948 if ( typeof frmThemeOverride_frmAfterSubmit === 'function' ) { // eslint-disable-line camelcase
949 const pageOrderInput = document.querySelector( `input[name="frm_page_order_${ formID }"]` );
950 const pageOrder = pageOrderInput ? pageOrderInput.value : '';
951 const tempDiv = document.createElement( 'div' );
952 tempDiv.innerHTML = response.content;
953 const formReturnedInput = tempDiv.querySelector( 'input[name="form_id"]' );
954 const formReturned = formReturnedInput ? formReturnedInput.value : '';
955 frmThemeOverride_frmAfterSubmit( formReturned, pageOrder, response.content, object );
956 }
957
958 afterFormSubmitted( object, response );
959 },
960 delay
961 );
962 } else if ( response.errors !== undefined && Object.keys( response.errors ).length ) {
963 // errors were returned
964 removeSubmitLoading( jQuery( object ), 'enable' );
965
966 //show errors
967 let contSubmit = true;
968 removeAllErrors();
969
970 let $fieldCont = null;
971
972 for ( const key in response.errors ) {
973 const fieldContEl = object.querySelector( `#frm_field_${ key }_container` );
974 $fieldCont = fieldContEl ? jQuery( fieldContEl ) : jQuery();
975
976 if ( $fieldCont.length ) {
977 if ( ! $fieldCont.is( ':visible' ) ) { // eslint-disable-line no-jquery/no-is
978 const inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' ); // eslint-disable-line no-jquery/no-closest, formidable/no-jquery-variable-methods
979 if ( inCollapsedSection.length ) {
980 let frmTrigger = inCollapsedSection.prev();
981 if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) { // eslint-disable-line formidable/no-jquery-variable-methods
982 // If the frmTrigger object is the section description, check to see if the previous element is the trigger
983 frmTrigger = frmTrigger.prev( '.frm_trigger' );
984 }
985 frmTrigger.trigger( 'click' );
986 }
987 }
988
989 if ( $fieldCont.is( ':visible' ) ) { // eslint-disable-line no-jquery/no-is
990 addFieldError( $fieldCont, key, response.errors );
991 contSubmit = false;
992 }
993 }
994 }
995
996 object.querySelectorAll( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).forEach( function( captchaEl ) {
997 const recaptchaID = captchaEl.dataset.rid;
998
999 if ( typeof grecaptcha !== 'undefined' && grecaptcha ) {
1000 if ( recaptchaID ) {
1001 grecaptcha.reset( recaptchaID );
1002 } else {
1003 grecaptcha.reset();
1004 }
1005 }
1006
1007 if ( typeof hcaptcha !== 'undefined' && hcaptcha ) {
1008 hcaptcha.reset();
1009 }
1010 } );
1011
1012 if ( window.turnstile ) {
1013 object.querySelectorAll( '.frm-cf-turnstile' ).forEach(
1014 turnstileField => turnstileField.dataset.rid && turnstile.reset( turnstileField.dataset.rid )
1015 );
1016 }
1017
1018 jQuery( document ).trigger( 'frmFormErrors', [ object, response ] );
1019
1020 fieldsets.forEach( field => field.classList.remove( 'frm_doing_ajax' ) );
1021 scrollToFirstField( object );
1022
1023 if ( contSubmit ) {
1024 object.submit();
1025 } else {
1026 object.insertAdjacentHTML( 'afterbegin', response.error_message );
1027 checkForErrorsAndMaybeSetFocus();
1028 }
1029 } else if ( ! willRedirect ) { // Avoid double submission if redirecting to a page.
1030 // There may have been a plugin conflict, or the form is not set to submit with ajax.
1031
1032 showFileLoading( object );
1033
1034 object.submit();
1035 }
1036 };
1037
1038 const error = function() {
1039 object.querySelectorAll( 'input[type="submit"], input[type="button"]' ).forEach(
1040 button => button.disabled = false
1041 );
1042 object.submit();
1043 };
1044
1045 postToAjaxUrl( object, data, success, error );
1046 }
1047
1048 function postToAjaxUrl( form, data, success, error ) {
1049 let ajaxUrl = frm_js.ajax_url;
1050 const action = form.getAttribute( 'action' );
1051
1052 if ( 'string' === typeof action && action.includes( '?action=frm_forms_preview' ) ) {
1053 ajaxUrl = action.split( '?action=frm_forms_preview' )[ 0 ];
1054 }
1055
1056 const ajaxParams = {
1057 type: 'POST',
1058 url: ajaxUrl,
1059 data,
1060 success
1061 };
1062
1063 if ( 'function' === typeof error ) {
1064 ajaxParams.error = error;
1065 }
1066
1067 jQuery.ajax( ajaxParams ); // eslint-disable-line no-jquery/no-ajax
1068 }
1069
1070 function afterFormSubmitted( object, response ) {
1071 const tempDiv = document.createElement( 'div' );
1072 tempDiv.innerHTML = response.content;
1073 const formCompleted = tempDiv.querySelector( '.frm_message' );
1074 if ( formCompleted ) {
1075 jQuery( document ).trigger( 'frmFormComplete', [ object, response ] );
1076 } else {
1077 jQuery( document ).trigger( 'frmPageChanged', [ object, response ] );
1078 }
1079 }
1080
1081 /**
1082 * Trigger an event before the form is replaced with a success message.
1083 *
1084 * @since 6.9
1085 *
1086 * @param {HTMLElement} object The form.
1087 * @param {Object} response The response from submitting the form with AJAX.
1088 * @return {void}
1089 */
1090 function afterFormSubmittedBeforeReplace( object, response ) {
1091 const tempDiv = document.createElement( 'div' );
1092 tempDiv.innerHTML = response.content;
1093 const formCompleted = tempDiv.querySelector( '.frm_message' );
1094 if ( formCompleted ) {
1095 triggerCustomEvent( document, 'frmFormCompleteBeforeReplace', { object, response } );
1096 }
1097 }
1098
1099 function removeAddedScripts( formContainer, formID ) {
1100 const endReplace = document.querySelectorAll( `.frm_end_ajax_${ formID }` );
1101 if ( endReplace.length ) {
1102 formContainer.nextUntil( `.frm_end_ajax_${ formID }` ).remove();
1103 endReplace.forEach( el => el.remove() );
1104 }
1105 }
1106
1107 function maybeSlideOut( oldContent, newContent ) {
1108 let c;
1109 let newClass = 'frm_slideout';
1110 if ( newContent.includes( ' frm_slide' ) ) {
1111 c = oldContent.children();
1112 if ( newContent.includes( ' frm_going_back' ) ) {
1113 newClass += ' frm_going_back';
1114 }
1115 c.removeClass( 'frm_going_back' );
1116 c.addClass( newClass );
1117 return 300;
1118 }
1119 return 0;
1120 }
1121
1122 function addUrlParam( response ) {
1123 let url;
1124 if ( history.pushState && response.page !== undefined ) {
1125 url = addQueryVar( 'frm_page', response.page );
1126 window.history.pushState( { html: response.html }, '', `?${ url }` );
1127 }
1128 }
1129
1130 function addQueryVar( key, value ) {
1131 key = encodeURI( key );
1132 value = encodeURI( value );
1133
1134 const kvp = document.location.search.substr( 1 ).split( '&' );
1135
1136 let i = kvp.length;
1137 while ( i-- ) {
1138 const x = kvp[ i ].split( '=' );
1139
1140 if ( x[ 0 ] == key ) {
1141 x[ 1 ] = value;
1142 kvp[ i ] = x.join( '=' );
1143 break;
1144 }
1145 }
1146
1147 if ( i < 0 ) {
1148 kvp[ kvp.length ] = [ key, value ].join( '=' );
1149 }
1150
1151 return kvp.join( '&' );
1152 }
1153
1154 function addFieldError( $fieldCont, key, jsErrors ) {
1155 const container = $fieldCont instanceof jQuery ? $fieldCont.get( 0 ) : $fieldCont;
1156
1157 if ( ! container || container.offsetParent === null ) {
1158 return;
1159 }
1160
1161 container.classList.add( 'frm_blank_field' );
1162 const inputs = container.querySelectorAll( 'input, select, textarea' );
1163 const id = getErrorElementId( key, inputs[ 0 ] );
1164
1165 let describedBy;
1166
1167 if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1168 frmThemeOverride_frmPlaceError( key, jsErrors );
1169 } else {
1170 let errorHtml;
1171 if ( jsErrors[ key ].includes( '<div' ) ) {
1172 errorHtml = jsErrors[ key ];
1173 } else {
1174 const roleString = frm_js.include_alert_role ? 'role="alert"' : '';
1175 errorHtml = `<div class="frm_error" ${ roleString } id="${ id }">${ jsErrors[ key ] }</div>`;
1176 }
1177 container.insertAdjacentHTML( 'beforeend', errorHtml );
1178 inputs.forEach( input => {
1179 describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1180 if ( ! describedBy ) {
1181 describedBy = id;
1182 } else if ( ! describedBy.includes( id ) && ! describedBy.includes( 'frm_error_field_' ) ) {
1183 const { errorFirst } = input.dataset;
1184 if ( errorFirst === '0' ) {
1185 describedBy = `${ describedBy } ${ id }`;
1186 } else {
1187 describedBy = `${ id } ${ describedBy }`;
1188 }
1189 }
1190 input.setAttribute( 'aria-describedby', describedBy );
1191 } );
1192 }
1193
1194 inputs.forEach( input => {
1195 if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1196 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1197 if ( group ) {
1198 group.setAttribute( 'aria-invalid', 'true' );
1199 }
1200 } else {
1201 input.setAttribute( 'aria-invalid', 'true' );
1202 }
1203 } );
1204
1205 jQuery( document ).trigger( 'frmAddFieldError', [ jQuery( container ), key, jsErrors ] );
1206 }
1207
1208 /**
1209 * Get the ID to use for an error element added when submitting with AJAX.
1210 *
1211 * @param {string} key
1212 * @param {HTMLElement} input
1213 * @return {string} The ID to use for the error element.
1214 */
1215 function getErrorElementId( key, input ) {
1216 if ( isNaN( key ) || ! input || ! input.id ) {
1217 // If key isn't a number, assume it's already in the right format.
1218 return `frm_error_field_${ key }`;
1219 }
1220 return `frm_error_${ input.id }`;
1221 }
1222
1223 /**
1224 * Removes errors before validating with JS.
1225 * This prevents issues with stale errors that has since been fixed.
1226 *
1227 * @param {HTMLElement|jQuery} fieldCont Field container element.
1228 * @return {void}
1229 */
1230 function removeFieldError( fieldCont ) {
1231 const container = fieldCont instanceof jQuery ? fieldCont.get( 0 ) : fieldCont;
1232 if ( ! container ) {
1233 return;
1234 }
1235
1236 const errorMessage = container.querySelector( '.frm_error' );
1237 const input = container.querySelector( 'input, select, textarea' );
1238
1239 container.classList.remove( 'frm_blank_field', 'has-error' );
1240
1241 if ( input ) {
1242 if ( 'true' === input.getAttribute( 'aria-invalid' ) ) {
1243 input.setAttribute( 'aria-invalid', 'false' );
1244 } else if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1245 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1246 if ( group ) {
1247 group.setAttribute( 'aria-invalid', 'false' );
1248 }
1249 }
1250 }
1251
1252 if ( errorMessage ) {
1253 removeElementFromInputDescribedBy( errorMessage );
1254 errorMessage.remove();
1255 }
1256 }
1257
1258 /**
1259 * Updates the aria-describedby attribute, removing the target element ID.
1260 *
1261 * @since 6.32
1262 *
1263 * @param {HTMLElement} el The target element that is removed from the aria-describedby data.
1264 * @return {void}
1265 */
1266 function removeElementFromInputDescribedBy( el ) {
1267 document.querySelectorAll( `[aria-describedby*="${ el.id }"]` ).forEach( input => {
1268 let ariaDescribedBy = input.getAttribute( 'aria-describedby' ).split( ' ' );
1269 ariaDescribedBy = ariaDescribedBy.filter( value => {
1270 const trimmedValue = value.trim();
1271 return trimmedValue && trimmedValue !== el.id;
1272 } );
1273
1274 if ( ariaDescribedBy.length ) {
1275 input.setAttribute( 'aria-describedby', ariaDescribedBy.join( ' ' ) );
1276 return;
1277 }
1278 input.removeAttribute( 'aria-describedby' );
1279 } );
1280 }
1281
1282 function removeAllErrors() {
1283 document.querySelectorAll( '.form-field' ).forEach( field => {
1284 field.classList.remove( 'frm_blank_field', 'has-error' );
1285 } );
1286 document.querySelectorAll( '.form-field .frm_error' ).forEach( el => {
1287 removeElementFromInputDescribedBy( el );
1288 el.remove();
1289 } );
1290 document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
1291 }
1292
1293 /**
1294 * @param {HTMLElement|Object} object Form object.
1295 * @return {void}
1296 */
1297 function scrollToFirstField( object ) {
1298 if ( 'function' === typeof object.get ) {
1299 // Get the HTMLElement from a jQuery object.
1300 object = object.get( 0 );
1301 }
1302 const field = object.querySelector( '.frm_blank_field' );
1303 if ( field ) {
1304 frmFrontForm.scrollMsg( jQuery( field ), object, true );
1305 }
1306 }
1307
1308 function showSubmitLoading( $object ) {
1309 showLoadingIndicator( $object );
1310 disableSubmitButton( $object );
1311 disableSaveDraft( $object );
1312 }
1313
1314 function showLoadingIndicator( $object ) {
1315 if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) { // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1316 addLoadingClass( $object );
1317 $object.trigger( 'frmStartFormLoading' );
1318 }
1319 }
1320
1321 function addLoadingClass( $object ) {
1322 const loadingClass = isGoingToPrevPage( $object ) ? 'frm_loading_prev' : 'frm_loading_form';
1323
1324 $object.addClass( loadingClass ); // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1325 }
1326
1327 function isGoingToPrevPage( $object ) {
1328 return typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object );
1329 }
1330
1331 function removeSubmitLoading( _, enable, processesRunning ) {
1332 if ( processesRunning > 0 ) {
1333 return;
1334 }
1335
1336 document.querySelectorAll( '.frm_loading_form' ).forEach( function( form ) {
1337 form.classList.remove( 'frm_loading_form', 'frm_loading_prev' );
1338 jQuery( form ).trigger( 'frmEndFormLoading' );
1339
1340 if ( enable === 'enable' ) {
1341 enableSubmitButton( form );
1342 enableSaveDraft( form );
1343 }
1344 } );
1345 }
1346
1347 function showFileLoading( object ) {
1348 const loading = document.getElementById( 'frm_loading' );
1349 if ( ! loading ) {
1350 return;
1351 }
1352
1353 const fileInput = object.querySelector( 'input[type=file]' );
1354 const fileval = fileInput ? fileInput.value : '';
1355 if ( fileval !== '' ) {
1356 setTimeout( function() {
1357 jQuery( loading ).fadeIn( 'slow' ); // eslint-disable-line no-jquery/no-fade
1358 }, 2000 );
1359 }
1360 }
1361
1362 /**********************************************
1363 * General Helpers
1364 *********************************************/
1365
1366 function confirmClick() {
1367 /*jshint validthis:true */
1368 const message = this.dataset.frmconfirm;
1369 return confirm( message );
1370 }
1371
1372 /**
1373 * Check for -webkit-box-shadow css value for input:-webkit-autofill selector.
1374 * If this is a match, the User is autofilling the input on a Webkit browser.
1375 * We want to delete the Honeypot field, otherwise it will get triggered as spam on autocomplete.
1376 */
1377 function onHoneypotFieldChange() {
1378 /*jshint validthis:true */
1379 const css = window.getComputedStyle( this ).boxShadow;
1380 if ( css?.match( /inset/ ) ) {
1381 this.remove();
1382 }
1383 }
1384
1385 /**
1386 * Focus on the first sub field when clicking to the primary label of combo field.
1387 *
1388 * @since 4.10.02
1389 */
1390 function changeFocusWhenClickComboFieldLabel() {
1391 let label;
1392
1393 const comboInputsContainer = document.querySelectorAll( '.frm_combo_inputs_container' );
1394 comboInputsContainer.forEach( function( inputsContainer ) {
1395 if ( ! inputsContainer.closest( '.frm_form_field' ) ) {
1396 return;
1397 }
1398
1399 label = inputsContainer.closest( '.frm_form_field' ).querySelector( '.frm_primary_label' );
1400 if ( ! label ) {
1401 return;
1402 }
1403
1404 label.addEventListener( 'click', function() {
1405 inputsContainer.querySelector( '.frm_form_field:first-child input, .frm_form_field:first-child select, .frm_form_field:first-child textarea' ).focus();
1406 } );
1407 } );
1408 }
1409
1410 /**
1411 * Sets focus on a the first subfield of a combo field that has an error.
1412 *
1413 * @since 6.16.3
1414 *
1415 * @param {HTMLElement} element
1416 * @return {boolean} True if the focus was set on a combo field.
1417 */
1418 function maybeFocusOnComboSubField( element ) {
1419 if ( 'FIELDSET' !== element.nodeName ) {
1420 return false;
1421 }
1422 if ( ! element.querySelector( '.frm_combo_inputs_container' ) ) {
1423 return false;
1424 }
1425 const comboSubfield = element.querySelector( '[aria-invalid="true"]' );
1426 if ( comboSubfield ) {
1427 focusInput( comboSubfield );
1428 return true;
1429 }
1430 return false;
1431 }
1432
1433 function checkForErrorsAndMaybeSetFocus() {
1434 if ( ! frm_js.focus_first_error ) {
1435 return;
1436 }
1437
1438 const errors = document.querySelectorAll( '.frm_form_field .frm_error' );
1439 if ( ! errors.length ) {
1440 return;
1441 }
1442
1443 let element = errors[ 0 ];
1444 let timeoutCallback;
1445 do {
1446 element = element.previousSibling;
1447 if ( [ 'input', 'select', 'textarea' ].includes( element.nodeName.toLowerCase() ) ) {
1448 focusInput( element );
1449 break;
1450 }
1451
1452 if ( maybeFocusOnComboSubField( element ) ) {
1453 break;
1454 }
1455
1456 if ( element.classList !== undefined ) {
1457 if ( element.classList.contains( 'html-active' ) ) {
1458 timeoutCallback = function() {
1459 const textarea = element.querySelector( 'textarea' );
1460 if ( null !== textarea ) {
1461 textarea.focus();
1462 }
1463 };
1464 } else if ( element.classList.contains( 'tmce-active' ) ) {
1465 timeoutCallback = function() {
1466 tinyMCE.activeEditor.focus();
1467 };
1468 } else if ( element.classList.contains( 'frm_opt_container' ) ) {
1469 const firstInput = element.querySelector( 'input' );
1470 if ( firstInput ) {
1471 focusInput( firstInput );
1472 break;
1473 }
1474 }
1475
1476 if ( 'function' === typeof timeoutCallback ) {
1477 setTimeout( timeoutCallback, 0 );
1478 break;
1479 }
1480 }
1481 } while ( element.previousSibling );
1482 }
1483
1484 /**
1485 * Focus a visible input, or possibly delay the focus event until the form has faded in.
1486 *
1487 * @since 6.16.3
1488 *
1489 * @param {HTMLElement} input
1490 * @return {void}
1491 */
1492 function focusInput( input ) {
1493 if ( input.offsetParent !== null ) {
1494 input.focus();
1495 } else {
1496 triggerCustomEvent( document, 'frmMaybeDelayFocus', { input } );
1497 }
1498 }
1499
1500 /**
1501 * Does the same as jQuery( document ).on( 'event', 'selector', handler ).
1502 *
1503 * @since 5.4
1504 *
1505 * @param {string} event Event name.
1506 * @param {string} selector Selector.
1507 * @param {Function} handler Handler.
1508 * @param {boolean | Object} options Options to be added to `addEventListener()` method. Default is `false`.
1509 */
1510 function documentOn( event, selector, handler, options ) {
1511 if ( options === undefined ) {
1512 options = false;
1513 }
1514
1515 document.addEventListener( event, function( e ) {
1516 let target;
1517
1518 // loop parent nodes from the target to the delegation node.
1519 for ( target = e.target; target && target != this; target = target.parentNode ) {
1520 if ( target.matches && target.matches( selector ) ) {
1521 handler.call( target, e );
1522 break;
1523 }
1524 }
1525 }, options );
1526 }
1527
1528 function initFloatingLabels() {
1529 const selector = '.frm-show-form .frm_inside_container input, .frm-show-form .frm_inside_container select, .frm-show-form .frm_inside_container textarea';
1530 const floatClass = 'frm_label_float_top';
1531
1532 const checkFloatLabel = function( input ) {
1533 const container = input.closest( '.frm_inside_container' );
1534 if ( ! container ) {
1535 return;
1536 }
1537
1538 const shouldFloatTop = input.value || document.activeElement === input;
1539
1540 container.classList.toggle( floatClass, shouldFloatTop );
1541
1542 if ( 'SELECT' === input.tagName ) {
1543 const firstOpt = input.querySelector( 'option:first-child' );
1544
1545 if ( shouldFloatTop ) {
1546 if ( firstOpt.hasAttribute( 'data-label' ) ) {
1547 firstOpt.textContent = firstOpt.getAttribute( 'data-label' );
1548 firstOpt.removeAttribute( 'data-label' );
1549 }
1550 } else if ( firstOpt.textContent ) {
1551 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1552 firstOpt.textContent = '';
1553 }
1554 }
1555 };
1556
1557 const checkDropdownLabel = function() {
1558 document.querySelectorAll( `.frm-show-form .frm_inside_container:not(.${ floatClass }) select` ).forEach( function( input ) {
1559 const firstOpt = input.querySelector( 'option:first-child' );
1560
1561 if ( firstOpt.textContent ) {
1562 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
1563 firstOpt.textContent = '';
1564 }
1565 } );
1566 };
1567
1568 [ 'focus', 'blur', 'change' ].forEach( function( eventName ) {
1569 documentOn(
1570 eventName,
1571 selector,
1572 function( event ) {
1573 checkFloatLabel( event.target );
1574 },
1575 true
1576 );
1577 } );
1578
1579 const runOnLoad = function( firstLoad ) {
1580 if ( firstLoad && document.activeElement && [ 'INPUT', 'SELECT', 'TEXTAREA' ].includes( document.activeElement.tagName ) ) {
1581 checkFloatLabel( document.activeElement );
1582 } else if ( firstLoad ) {
1583 document.querySelectorAll( '.frm_inside_container' ).forEach(
1584 function( container ) {
1585 const input = container.querySelector( 'input, select, textarea' );
1586 if ( input && '' !== input.value ) {
1587 checkFloatLabel( input );
1588 }
1589 }
1590 );
1591 }
1592
1593 checkDropdownLabel();
1594 calcProductsTotal();
1595 };
1596
1597 runOnLoad( true );
1598
1599 jQuery( document ).on( 'frmPageChanged', function( event ) {
1600 runOnLoad();
1601 } );
1602
1603 document.addEventListener( 'frm_after_start_over', function( event ) {
1604 runOnLoad();
1605 } );
1606 }
1607
1608 function shouldUpdateValidityMessage( target ) {
1609 if ( 'INPUT' !== target.nodeName ) {
1610 return false;
1611 }
1612
1613 if ( ! target.dataset.invmsg ) {
1614 return false;
1615 }
1616
1617 if ( 'text' !== target.getAttribute( 'type' ) ) {
1618 return false;
1619 }
1620
1621 if ( target.classList.contains( 'frm_verify' ) ) {
1622 return false;
1623 }
1624
1625 return true;
1626 }
1627
1628 function maybeClearCustomValidityMessage( event, field ) {
1629 let key;
1630 let isInvalid = false;
1631
1632 if ( ! shouldUpdateValidityMessage( field ) ) {
1633 return;
1634 }
1635
1636 for ( key in field.validity ) {
1637 if ( 'customError' === key ) {
1638 continue;
1639 }
1640 if ( 'valid' !== key && field.validity[ key ] === true ) {
1641 isInvalid = true;
1642 break;
1643 }
1644 }
1645
1646 if ( ! isInvalid ) {
1647 field.setCustomValidity( '' );
1648 }
1649 }
1650
1651 function maybeShowNewTabFallbackMessage() {
1652 if ( ! window.frmShowNewTabFallback ) {
1653 return;
1654 }
1655
1656 const messageEl = document.querySelector( `#frm_form_${ frmShowNewTabFallback.formId }_container .frm_message` );
1657 if ( ! messageEl ) {
1658 return;
1659 }
1660
1661 messageEl.insertAdjacentHTML( 'beforeend', ` ${ frmShowNewTabFallback.message }` );
1662 }
1663
1664 function setCustomValidityMessage() {
1665 const forms = document.getElementsByClassName( 'frm-show-form' );
1666 const { length } = forms;
1667
1668 for ( let index = 0; index < length; ++index ) {
1669 forms[ index ].addEventListener(
1670 'invalid',
1671 function( event ) {
1672 const { target } = event;
1673
1674 if ( shouldUpdateValidityMessage( target ) ) {
1675 target.setCustomValidity( target.dataset.invmsg );
1676 }
1677 },
1678 true
1679 );
1680 }
1681 }
1682
1683 function enableSubmitButtonOnBackButtonPress() {
1684 window.addEventListener( 'pageshow', function( event ) {
1685 if ( event.persisted ) {
1686 document.querySelectorAll( '.frm_loading_form' ).forEach(
1687 function( form ) {
1688 enableSubmitButton( form );
1689 }
1690 );
1691 removeSubmitLoading();
1692 }
1693 } );
1694 }
1695
1696 /**
1697 * Destroys the formidable generated global hcaptcha object since it wouldn't otherwise render.
1698 */
1699 function destroyhCaptcha() {
1700 if ( ! window.hasOwnProperty( 'hcaptcha' ) || ! document.querySelector( '.frm-show-form .h-captcha' ) ) {
1701 return;
1702 }
1703 window.hcaptcha = null;
1704 }
1705
1706 /**
1707 * @since 6.16.3
1708 *
1709 * @return {string} Unique key, used for duplicate checks.
1710 */
1711 function getUniqueKey() {
1712 const uniqueKey = Array.from( window.crypto.getRandomValues( new Uint8Array( 8 ) ) )
1713 .map( b => b.toString( 16 ).padStart( 2, '0' ) )
1714 .join( '' );
1715 const timestamp = Date.now().toString( 16 );
1716 return `${ uniqueKey }-${ timestamp }`;
1717 }
1718
1719 /**
1720 * Animates the scroll position of the document.
1721 *
1722 * @since 6.20
1723 *
1724 * @param {number} start
1725 * @param {number} end
1726 * @param {number} duration
1727 * @return {void}
1728 */
1729 function animateScroll( start, end, duration ) {
1730 if ( ! window.hasOwnProperty( 'performance' ) || ! window.hasOwnProperty( 'requestAnimationFrame' ) ) {
1731 document.documentElement.scrollTop = end;
1732 return;
1733 }
1734
1735 const startTime = performance.now();
1736 const step = currentTime => {
1737 const progress = Math.min( ( currentTime - startTime ) / duration, 1 );
1738 document.documentElement.scrollTop = start + ( ( end - start ) * progress );
1739 if ( progress < 1 ) {
1740 requestAnimationFrame( step );
1741 }
1742 };
1743 requestAnimationFrame( step );
1744 }
1745
1746 /**
1747 * Make sure that the captcha label for a reCAPTCHA or Turnstile field matches the response input ID.
1748 * This is determined dynamically, so we check for the ID after the input is rendered.
1749 * hCaptcha is handled separately, in the frmCaptcha function as it is not rendered explicitly.
1750 *
1751 * @since 6.25.1
1752 *
1753 * @param {HTMLElement} captcha
1754 * @return {void}
1755 */
1756 function maybeFixCaptchaLabel( captcha ) {
1757 const form = captcha.closest( 'form' );
1758 if ( ! form ) {
1759 return;
1760 }
1761
1762 const label = form.querySelector( 'label[for="g-recaptcha-response"], label[for="cf-turnstile-response"]' );
1763 const captchaResponse = form.querySelector( '[name="g-recaptcha-response"], [name="cf-turnstile-response"]' );
1764
1765 if ( label && captchaResponse ) {
1766 label.htmlFor = captchaResponse.id;
1767 }
1768 }
1769
1770 /**
1771 * Check to make sure the quantity field value is within the min and max values.
1772 *
1773 * @param {HTMLElement} input
1774 * @return {number} The quantity value.
1775 */
1776 function checkQuantityFieldMinMax( input ) {
1777 if ( '' === input.value ) {
1778 // Leave the value if it is empty.
1779 return 0;
1780 }
1781
1782 const val = parseFloat( input.value ? input.value.trim() : 0 );
1783 if ( isNaN( val ) ) {
1784 return 0;
1785 }
1786
1787 let max = input.hasAttribute( 'max' ) ? parseFloat( input.getAttribute( 'max' ) ) : 0;
1788 let min = input.hasAttribute( 'min' ) ? parseFloat( input.getAttribute( 'min' ) ) : 0;
1789
1790 max = isNaN( max ) ? 0 : max;
1791 min = isNaN( min ) ? 0 : Math.max( 0, min );
1792
1793 if ( val < min ) {
1794 input.value = min;
1795 return min;
1796 }
1797
1798 if ( 0 !== max && val > max ) {
1799 input.value = max;
1800 return max;
1801 }
1802
1803 return val;
1804 }
1805
1806 function triggerChange( input, fieldKey ) {
1807 if ( fieldKey === undefined ) {
1808 fieldKey = 'dependent';
1809 }
1810
1811 jQuery( input ).trigger( { type: 'change', selfTriggered: true, frmTriggered: fieldKey } );
1812 }
1813
1814 /**
1815 * Calculates the total price.
1816 *
1817 * @param {Event|undefined} e The event object.
1818 * @return {void}
1819 */
1820 function calcProductsTotal( e ) {
1821 if ( 'object' === typeof frmProForm ) {
1822 // Pro is installed, use the Pro JS.
1823 return;
1824 }
1825
1826 if ( typeof __FRMCURR === 'undefined' ) {
1827 return;
1828 }
1829
1830 const totalFields = document.querySelectorAll( '[data-frmtotal]' );
1831 if ( ! totalFields.length ) {
1832 return;
1833 }
1834
1835 const formTotals = [];
1836
1837 totalFields.forEach( totalField => {
1838 let total = 0;
1839 const form = totalField.closest( 'form' );
1840
1841 if ( ! form ) {
1842 return;
1843 }
1844
1845 const formId = form.querySelector( 'input[name="form_id"]' ).value;
1846 const currency = getCurrency( formId );
1847
1848 if ( undefined !== formTotals[ formId ] ) {
1849 total = formTotals[ formId ];
1850 } else {
1851 form.querySelectorAll( 'input[data-frmprice],select:has([data-frmprice])' ).forEach( function( input ) {
1852 let quantity = 0;
1853 let price = 0;
1854 const isSingle = 'hidden' === input.type;
1855
1856 if ( input.tagName === 'SELECT' ) {
1857 if ( input.selectedIndex !== -1 ) {
1858 price = input.options[ input.selectedIndex ].getAttribute( 'data-frmprice' );
1859 }
1860 } else {
1861 if ( ! isSingle && ! input.matches( ':checked' ) ) {
1862 return;
1863 }
1864 price = input.getAttribute( 'data-frmprice' );
1865 }
1866
1867 if ( ! price ) {
1868 price = 0;
1869 } else {
1870 price = preparePrice( price, currency );
1871 quantity = getQuantity( input );
1872 price = parseFloat( quantity ) * parseFloat( price );
1873 }
1874
1875 if ( 'true' === input.getAttribute( 'data-frmdiscount' ) ) {
1876 price = price * -1;
1877 }
1878
1879 total += price;
1880 } );
1881
1882 formTotals[ formId ] = total;
1883 }
1884
1885 total = isNaN( total ) ? 0 : total;
1886
1887 // Set a decimal separator for currency if no default for it
1888 currency.decimal_separator = currency.decimal_separator.trim(); // first remove unnecessary space(s)
1889 if ( ! currency.decimal_separator.length ) {
1890 currency.decimal_separator = '.';
1891 }
1892
1893 totalField.value = roundTotal( total, currency );
1894 total = normalizeTotal( total, currency );
1895
1896 // because of e.g. fields that might be using this field for calculations
1897 triggerChange( totalField );
1898
1899 total = formatCurrency( total, currency );
1900 const formatted = totalField.previousElementSibling;
1901 if ( formatted?.matches( '.frm_total_formatted' ) ) {
1902 // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1903 formatted.innerHTML = total;
1904 return;
1905 }
1906
1907 const formattedEls = totalField.closest( '.frm_form_field' ).querySelectorAll( '.frm_total_formatted' );
1908 formattedEls.forEach( formattedEl => {
1909 // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1910 formattedEl.innerHTML = total;
1911 } );
1912 } );
1913 }
1914
1915 /**
1916 * Round total and maybe add trailing zeros so formatCurrency has a proper format to work with.
1917 *
1918 * @param {number} total The total amount to normalize.
1919 * @param {Object} currency The currency object containing decimal information.
1920 * @return {string} The normalized total amount.
1921 */
1922 function normalizeTotal( total, currency ) {
1923 const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1924
1925 total = roundTotal( total, currency );
1926
1927 return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal );
1928 }
1929
1930 function roundTotal( total, currency ) {
1931 const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1932
1933 if ( ! isLargeTotal ) {
1934 const { decimals } = currency;
1935 total = decimals > 0 ? round10( total, decimals ) : Math.ceil( total );
1936 }
1937
1938 return total;
1939 }
1940
1941 function round10( value, decimals ) {
1942 return Number( `${ Math.round( `${ value }e${ decimals }` ) }e-${ decimals }` );
1943 }
1944
1945 /**
1946 * Format a numeric value according to the specified currency format settings.
1947 *
1948 * @param {string} total The numeric value to format.
1949 * @param {Object} currency The currency formatting configuration.
1950 * @return {string} The formatted currency string.
1951 */
1952 function formatCurrency( total, currency ) {
1953 total = maybeAddTrailingZeroToPrice( total, currency );
1954 if ( total.length && ( total[ total.length - 1 ] === '.' || total[ total.length - 1 ] === currency.decimal_separator ) ) {
1955 total = total.substr( 0, total.length - 1 );
1956 }
1957
1958 total = maybeRemoveTrailingZerosFromPrice( total, currency );
1959 total = addThousands( total, currency );
1960
1961 const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : '';
1962 const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : '';
1963
1964 return `${ leftSymbol }${ total }${ rightSymbol }`;
1965 }
1966
1967 /**
1968 * Gets currency from form id.
1969 *
1970 * @param {number} formId Form ID.
1971 * @return {Object} Currency object.
1972 */
1973 function getCurrency( formId ) {
1974 if ( undefined !== window.__FRMCURR && undefined !== window.__FRMCURR[ formId ] ) {
1975 return window.__FRMCURR[ formId ];
1976 }
1977
1978 return {
1979 symbol_left: '$',
1980 symbol_right: '',
1981 symbol_padding: '',
1982 thousand_separator: ',',
1983 decimal_separator: '.',
1984 decimals: 2,
1985 };
1986 }
1987
1988 /**
1989 * Gets quantity.
1990 *
1991 * @param {HTMLElement} field The field element.
1992 * @return {number} The quantity.
1993 */
1994 function getQuantity( field ) {
1995 const fieldID = frmFrontForm.getFieldId( field, false );
1996 if ( ! fieldID ) {
1997 return 0;
1998 }
1999
2000 const quantityField = getQuantityField( field, fieldID );
2001 if ( ! quantityField ) {
2002 // If there is no quantity field, assume 1.
2003 return 1;
2004 }
2005
2006 return checkQuantityFieldMinMax( quantityField );
2007 }
2008
2009 /**
2010 * Gets quantity field.
2011 *
2012 * @param {HTMLElement} element The element.
2013 * @param {number} fieldID The field ID.
2014 * @return {HTMLElement|null} The quantity field.
2015 */
2016 function getQuantityField( element, fieldID ) {
2017 const quantityFields = element.closest( 'form' ).querySelectorAll( '[data-frmproduct]' );
2018 if ( ! quantityFields.length ) {
2019 return null;
2020 }
2021
2022 fieldID = fieldID.toString();
2023
2024 return Array.from( quantityFields ).find( element => {
2025 let ids;
2026
2027 ids = JSON.parse( element.getAttribute( 'data-frmproduct' ).trim() );
2028 if ( '' === ids ) {
2029 return false;
2030 }
2031
2032 // Convert to array if necessary because of existing fields that are already using single product fields.
2033 ids = 'string' === typeof ids ? [ ids ] : ids;
2034 return ids.includes( fieldID );
2035 } );
2036 }
2037
2038 /**
2039 * Prepare a price for calculation.
2040 *
2041 * @param {number|string} price The price to prepare.
2042 * @param {Object} currency The currency object containing decimal information.
2043 * @return {string} The prepared price.
2044 */
2045 function preparePrice( price, currency ) {
2046 if ( ! price ) {
2047 return 0;
2048 }
2049 price = `${ price }`; // convert to string just to be sure
2050
2051 const regex = getRegexForPrice( currency );
2052
2053 const matches = price.match( regex );
2054 if ( null === matches ) {
2055 return 0;
2056 }
2057
2058 price = matches.length ? matches[ matches.length - 1 ] : 0;
2059 price = price.trim();
2060
2061 // Fix issues with parsing Fr.15.00. The regex catches .15.00.
2062 // This checks for the leading decimal and removes it.
2063 if ( currency.decimal_separator === '.' && 3 === price.split( '.' ).length && price[ 0 ] === '.' ) {
2064 price = price.substr( 1 );
2065 }
2066
2067 if ( price ) {
2068 price = maybeUseDecimal( price, currency );
2069 price = price.split( currency.thousand_separator ).join( '' ).replace( currency.decimal_separator, '.' );
2070 }
2071
2072 return price;
2073 }
2074
2075 /**
2076 * @param {Object} currency The currency object.
2077 * @return {RegExp} The regular expression object.
2078 */
2079 function getRegexForPrice( currency ) {
2080 let regexString = '[0-9,.';
2081
2082 if ( currency.thousand_separator !== '.' && currency.thousand_separator !== ',' ) {
2083 regexString += currency.thousand_separator;
2084 }
2085 if ( currency.decimal_separator !== '.' && currency.decimal_separator !== ',' ) {
2086 regexString += currency.decimal_separator;
2087 }
2088
2089 regexString += ']*\\.?\\,?[0-9]+';
2090
2091 return new RegExp( regexString, 'g' );
2092 }
2093
2094 /**
2095 * Maybe replace the decimal separator with the currency's decimal separator.
2096 *
2097 * @param {string} price The price string.
2098 * @param {Object} currency The currency object.
2099 * @return {string} The modified price string.
2100 */
2101 function maybeUseDecimal( price, currency ) {
2102 let usedForDecimal;
2103 let priceParts;
2104 if ( '.' === currency.thousand_separator ) {
2105 priceParts = price.split( '.' );
2106 usedForDecimal = 2 === priceParts.length && 2 === priceParts[ 1 ].length;
2107 if ( usedForDecimal ) {
2108 price = price.replace( '.', currency.decimal_separator );
2109 }
2110 }
2111 return price;
2112 }
2113
2114 /**
2115 * Add trailing zeros to a price if necessary and replace the decimal separator.
2116 *
2117 * @param {number|string} price The price to format.
2118 * @param {Object} currency The currency object containing the decimal separator.
2119 * @param {boolean} [force=false] Whether to force processing even if the price is not a number.
2120 * @return {string} The formatted price string.
2121 */
2122 function maybeAddTrailingZeroToPrice( price, currency, force = false ) {
2123 if ( 'number' !== typeof price && ! force ) {
2124 return price;
2125 }
2126
2127 price = String( price ); // first convert to string
2128 const pos = price.indexOf( '.' );
2129
2130 if ( pos === -1 ) {
2131 price = `${ price }.`;
2132
2133 for ( let n = 0; n < currency.decimals; ++n ) {
2134 price += '0';
2135 }
2136 } else {
2137 const decimalsString = price.substring( pos + 1 );
2138 if ( decimalsString.length < currency.decimals ) {
2139 if ( decimalsString.length < 2 ) {
2140 price += '0';
2141 }
2142
2143 for ( let n = 2; n < currency.decimals; ++n ) {
2144 price += '0';
2145 }
2146 }
2147 }
2148
2149 return price.replace( '.', currency.decimal_separator );
2150 }
2151
2152 /**
2153 * Format a numeric string by adding thousand separators.
2154 *
2155 * @param {string|number} price The numeric value to format.
2156 * @param {Object} options Formatting options.
2157 * @param {string} options.decimal_separator Character used as decimal separator.
2158 * @param {string} options.thousand_separator Character used as thousand separator.
2159 *
2160 * @return {string} The price string with thousand separators.
2161 */
2162 function addThousands( price, options ) {
2163 const split = options.decimal_separator === ''
2164 ? [ price.toString() ]
2165 : price.split( options.decimal_separator );
2166
2167 if ( options.thousand_separator ) {
2168 split[ 0 ] = split[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, options.thousand_separator );
2169 }
2170
2171 return split.join( options.decimal_separator );
2172 }
2173
2174 /**
2175 * Maybe remove trailing zeros from a price string.
2176 *
2177 * @param {string} price The price string.
2178 * @param {Object} currency The currency data.
2179 *
2180 * @return {string} The price string with trailing zeros removed.
2181 */
2182 function maybeRemoveTrailingZerosFromPrice( price, currency ) {
2183 const split = price.split( currency.decimal_separator );
2184 if ( 2 !== split.length || split[ 1 ].length <= currency.decimals ) {
2185 return price;
2186 }
2187 if ( 0 === currency.decimals ) {
2188 return split[ 0 ];
2189 }
2190 return `${ split[ 0 ] }${ currency.decimal_separator }${ split[ 1 ].substr( 0, currency.decimals ) }`;
2191 }
2192
2193 return {
2194 init() {
2195 jQuery( document ).off( 'submit.formidable', '.frm-show-form' );
2196 jQuery( document ).on( 'submit.formidable', '.frm-show-form', frmFrontForm.submitForm );
2197
2198 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 );
2199
2200 jQuery( document ).on( 'change', '.frm_verify[id^=field_]', onHoneypotFieldChange );
2201
2202 jQuery( document ).on( 'click', 'a[data-frmconfirm]', confirmClick );
2203
2204 checkForErrorsAndMaybeSetFocus();
2205
2206 // Focus on the first sub field when clicking to the primary label of combo field.
2207 changeFocusWhenClickComboFieldLabel();
2208
2209 initFloatingLabels();
2210 maybeShowNewTabFallbackMessage();
2211
2212 jQuery( document ).on( 'frmAfterAddRow', setCustomValidityMessage );
2213 setCustomValidityMessage();
2214 jQuery( document ).on( 'frmFieldChanged', maybeClearCustomValidityMessage );
2215
2216 setSelectPlaceholderColor();
2217
2218 // Elementor popup show event. Fix Elementor Popup && FF Captcha field conflicts
2219 jQuery( document ).on( 'elementor/popup/show', frmRecaptcha );
2220
2221 enableSubmitButtonOnBackButtonPress();
2222 jQuery( document ).on(
2223 'frmPageChanged',
2224 destroyhCaptcha
2225 );
2226
2227 jQuery( document ).on( 'frmAfterAddRow frmAfterRemoveRow', calcProductsTotal );
2228 jQuery( document ).on( 'change', '[type="checkbox"][data-frmprice],[type="radio"][data-frmprice],[type="hidden"][data-frmprice],select:has([data-frmprice])', calcProductsTotal );
2229 jQuery( document ).on( 'keyup change', '[data-frmproduct],[type="text"][data-frmprice]', calcProductsTotal );
2230 calcProductsTotal();
2231 },
2232
2233 getFieldId,
2234
2235 /**
2236 * Render a captcha field.
2237 *
2238 * @param {HTMLElement} captcha
2239 * @param {string} captchaSelector
2240 * @return {void}
2241 */
2242 renderCaptcha( captcha, captchaSelector ) {
2243 const rendered = captcha.getAttribute( 'data-rid' ) !== null;
2244 if ( rendered ) {
2245 return;
2246 }
2247
2248 const size = captcha.getAttribute( 'data-size' );
2249 const params = {
2250 sitekey: captcha.getAttribute( 'data-sitekey' ),
2251 size,
2252 theme: captcha.getAttribute( 'data-theme' )
2253 };
2254
2255 if ( size === 'invisible' ) {
2256 const formID = captcha.closest( 'form' )?.querySelector( 'input[name="form_id"]' )?.value;
2257
2258 const captchaLabel = captcha.closest( '.frm_form_field' )?.querySelector( '.frm_primary_label' );
2259 if ( captchaLabel ) {
2260 captchaLabel.style.display = 'none';
2261 }
2262
2263 params.callback = function( token ) {
2264 frmFrontForm.afterRecaptcha( token, formID );
2265 };
2266 }
2267
2268 const activeCaptcha = getSelectedCaptcha( captchaSelector );
2269 const captchaContainer = typeof turnstile !== 'undefined' && turnstile === activeCaptcha ? `#${ captcha.id }` : captcha.id;
2270 const captchaID = activeCaptcha.render( captchaContainer, params );
2271
2272 captcha.setAttribute( 'data-rid', captchaID );
2273
2274 maybeFixCaptchaLabel( captcha );
2275 },
2276
2277 afterSingleRecaptcha() {
2278 const recaptcha = document.querySelector( '.frm-show-form .g-recaptcha' );
2279 const object = recaptcha ? recaptcha.closest( 'form' ) : null;
2280 frmFrontForm.submitFormNow( object );
2281 },
2282
2283 afterRecaptcha( _, formID ) {
2284 const object = document.querySelector( `#frm_form_${ formID }_container form` );
2285 frmFrontForm.submitFormNow( object );
2286 },
2287
2288 submitForm( e ) {
2289 frmFrontForm.submitFormManual( e, this );
2290 },
2291
2292 /**
2293 * @param {Event} e
2294 * @param {HTMLElement} object The form object that is being submitted.
2295 * @return {void}
2296 */
2297 submitFormManual( e, object ) {
2298 if ( document.body.classList.contains( 'wp-admin' ) && ! object.closest( '.frmapi-form' ) ) {
2299 return;
2300 }
2301
2302 e.preventDefault();
2303
2304 if ( typeof frmProForm !== 'undefined' && typeof frmProForm.submitAllowed === 'function' && ! frmProForm.submitAllowed( object ) ) {
2305 return;
2306 }
2307
2308 const errors = frmFrontForm.validateFormSubmit( object );
2309 if ( Object.keys( errors ).length !== 0 ) {
2310 return;
2311 }
2312
2313 const invisibleRecaptcha = hasInvisibleRecaptcha( object );
2314
2315 if ( invisibleRecaptcha ) {
2316 showLoadingIndicator( jQuery( object ) );
2317 executeInvisibleRecaptcha( invisibleRecaptcha );
2318 } else {
2319 showSubmitLoading( jQuery( object ) );
2320
2321 frmFrontForm.submitFormNow( object );
2322 }
2323 },
2324
2325 submitFormNow( object ) {
2326 let hasFileFields;
2327 let antispamInput;
2328 const classList = object.className.trim().split( /\s+/gi );
2329
2330 if ( object.hasAttribute( 'data-token' ) && null === object.querySelector( '[name="antispam_token"]' ) ) {
2331 // include the antispam token on form submit.
2332 antispamInput = document.createElement( 'input' );
2333 antispamInput.type = 'hidden';
2334 antispamInput.name = 'antispam_token';
2335 antispamInput.value = object.getAttribute( 'data-token' );
2336 object.append( antispamInput );
2337 }
2338
2339 // Add a unique ID, used for duplicate checks.
2340 const uniqueIDInput = document.createElement( 'input' );
2341 uniqueIDInput.type = 'hidden';
2342 uniqueIDInput.name = 'unique_id';
2343 uniqueIDInput.value = getUniqueKey();
2344 object.append( uniqueIDInput );
2345
2346 if ( classList.includes( 'frm_ajax_submit' ) ) {
2347 const fileInputs = object.querySelectorAll( 'input[type="file"]' );
2348 hasFileFields = Array.from( fileInputs ).filter( input => !! input.value ).length;
2349 if ( hasFileFields < 1 ) {
2350 const actionInput = object.querySelector( 'input[name="frm_action"]' );
2351 const action = actionInput ? actionInput.value : '';
2352 frmFrontForm.checkFormErrors( object, action );
2353 } else {
2354 object.submit();
2355 }
2356 } else {
2357 object.submit();
2358 }
2359 },
2360
2361 /**
2362 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
2363 *
2364 * @return {Array} List of errors.
2365 */
2366 validateFormSubmit( object ) {
2367 const form = object instanceof jQuery ? object.get( 0 ) : object;
2368 if ( typeof tinyMCE !== 'undefined' && form?.querySelector( '.wp-editor-wrap' ) ) {
2369 tinyMCE.triggerSave();
2370 }
2371
2372 jsErrors = [];
2373
2374 if ( shouldJSValidate( object ) ) {
2375 frmFrontForm.getAjaxFormErrors( object );
2376
2377 if ( Object.keys( jsErrors ).length ) {
2378 frmFrontForm.addAjaxFormErrors( object );
2379 }
2380 }
2381
2382 return jsErrors;
2383 },
2384
2385 /**
2386 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
2387 * @return {Array} List of errors.
2388 */
2389 getAjaxFormErrors( object ) {
2390 let customErrors;
2391 let key;
2392 const form = object instanceof jQuery ? object.get( 0 ) : object;
2393
2394 jsErrors = validateForm( object );
2395 if ( typeof frmThemeOverride_jsErrors === 'function' ) { // eslint-disable-line camelcase
2396 const actionInput = form ? form.querySelector( 'input[name="frm_action"]' ) : null;
2397 const action = actionInput ? actionInput.value : '';
2398 customErrors = frmThemeOverride_jsErrors( action, object );
2399 if ( Object.keys( customErrors ).length ) {
2400 for ( key in customErrors ) {
2401 jsErrors[ key ] = customErrors[ key ];
2402 }
2403 }
2404 }
2405
2406 triggerCustomEvent( document, 'frm_get_ajax_form_errors', {
2407 formEl: object,
2408 errors: jsErrors
2409 } );
2410
2411 return jsErrors;
2412 },
2413
2414 /**
2415 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
2416 * @return {void}
2417 */
2418 addAjaxFormErrors( object ) {
2419 let key;
2420 const form = object instanceof jQuery ? object.get( 0 ) : object;
2421 removeAllErrors();
2422
2423 for ( key in jsErrors ) {
2424 const fieldCont = form ? form.querySelector( `#frm_field_${ key }_container` ) : null;
2425
2426 if ( fieldCont ) {
2427 addFieldError( fieldCont, key, jsErrors );
2428 } else {
2429 // we are unable to show the error, so remove it
2430 delete jsErrors[ key ];
2431 }
2432 }
2433
2434 scrollToFirstField( object );
2435 checkForErrorsAndMaybeSetFocus();
2436 },
2437
2438 checkFormErrors: getFormErrors,
2439 checkRequiredField,
2440 showSubmitLoading,
2441 removeSubmitLoading,
2442
2443 scrollToID( id ) {
2444 const object = jQuery( document.getElementById( id ) );
2445 frmFrontForm.scrollMsg( object, false );
2446 },
2447
2448 scrollMsg( id, object, animate ) {
2449 let newPos;
2450 let screenTop;
2451 let screenBottom;
2452 let scrollObj = '';
2453
2454 if ( object === undefined ) {
2455 scrollObj = jQuery( document.getElementById( `frm_form_${ id }_container` ) );
2456 if ( scrollObj.length < 1 ) {
2457 return;
2458 }
2459 } else if ( typeof id === 'string' ) {
2460 const formEl = object instanceof jQuery ? object.get( 0 ) : object;
2461 const fieldEl = formEl ? formEl.querySelector( `#frm_field_${ id }_container` ) : null;
2462 scrollObj = fieldEl ? jQuery( fieldEl ) : jQuery();
2463 } else {
2464 scrollObj = id;
2465 }
2466
2467 jQuery( scrollObj ).trigger( 'focus' );
2468 newPos = scrollObj.offset().top;
2469 if ( ! newPos || frm_js.offset === '-1' ) {
2470 return;
2471 }
2472 newPos = newPos - frm_js.offset;
2473
2474 const docMarginTop = getComputedStyle( document.documentElement ).marginTop;
2475 const bodyMarginTop = getComputedStyle( document.body ).marginTop;
2476 if ( docMarginTop || bodyMarginTop ) {
2477 newPos = newPos - parseInt( docMarginTop ) - parseInt( bodyMarginTop );
2478 }
2479
2480 if ( newPos && window.innerHeight ) {
2481 screenTop = document.documentElement.scrollTop || document.body.scrollTop;
2482 screenBottom = screenTop + window.innerHeight;
2483
2484 if ( newPos > screenBottom || newPos < screenTop ) {
2485 // Not in view
2486 if ( animate === undefined ) {
2487 document.documentElement.scrollTop = newPos;
2488 } else {
2489 animateScroll( screenTop, newPos, 500 );
2490 }
2491 return false;
2492 }
2493 }
2494 },
2495
2496 fieldValueChanged( e ) {
2497 /*jshint validthis:true */
2498
2499 const fieldId = frmFrontForm.getFieldId( this, false );
2500 if ( ! fieldId ) {
2501 return;
2502 }
2503
2504 if ( e.frmTriggered && e.frmTriggered == fieldId ) {
2505 return;
2506 }
2507
2508 jQuery( document ).trigger( 'frmFieldChanged', [ this, fieldId, e ] );
2509
2510 if ( e.selfTriggered !== true ) {
2511 maybeValidateChange( this );
2512 }
2513 },
2514
2515 escapeHtml( text ) {
2516 console.warn( 'DEPRECATED: function frmFrontForm.escapeHtml in v6.17' );
2517 return text
2518 .replace( /&/g, '&amp;' )
2519 .replace( /</g, '&lt;' )
2520 .replace( />/g, '&gt;' )
2521 .replace( /"/g, '&quot;' )
2522 .replace( /'/g, '&#039;' );
2523 },
2524
2525 triggerCustomEvent,
2526 documentOn
2527 };
2528 }
2529
2530 window.frmFrontForm = frmFrontFormJS();
2531
2532 jQuery( document ).ready( function() {
2533 frmFrontForm.init();
2534 } );
2535
2536 function frmRecaptcha() {
2537 frmCaptcha( '.frm-g-recaptcha' );
2538 }
2539
2540 function frmHcaptcha() {
2541 frmCaptcha( '.h-captcha' );
2542 }
2543
2544 function frmTurnstile() {
2545 frmCaptcha( '.frm-cf-turnstile' );
2546 }
2547
2548 function frmCaptcha( captchaSelector ) {
2549 if ( '.h-captcha' === captchaSelector ) {
2550 // hCaptcha is still rendered implicitly, so we only want to handle the label and exit early.
2551 // Match the hcaptcha labels to the hcaptcha response fields.
2552 const captchaLabels = document.querySelectorAll( 'label[for="h-captcha-response"]' );
2553 if ( captchaLabels.length ) {
2554 captchaLabels.forEach( label => {
2555 const captchaResponse = label.closest( 'form' )?.querySelector( '[name="h-captcha-response"]' );
2556 if ( captchaResponse ) {
2557 label.htmlFor = captchaResponse.id;
2558 }
2559 } );
2560 }
2561 return;
2562 }
2563
2564 let c;
2565 const captchas = document.querySelectorAll( captchaSelector );
2566 const cl = captchas.length;
2567 for ( c = 0; c < cl; c++ ) {
2568 const closestForm = captchas[ c ].closest( 'form' );
2569 const formIsVisible = closestForm && closestForm.offsetParent !== null;
2570 const captcha = captchas[ c ];
2571 if ( ! formIsVisible ) {
2572 // If the form is not visible, try again later in 400ms.
2573 // This fixes issues where the form fades visible on page load.
2574 // Or when the form is inside of a modal.
2575 const interval = setInterval(
2576 function() {
2577 if ( closestForm && closestForm.offsetParent !== null ) {
2578 frmFrontForm.renderCaptcha( captcha, captchaSelector );
2579 clearInterval( interval );
2580 }
2581 },
2582 400
2583 );
2584 continue;
2585 }
2586 frmFrontForm.renderCaptcha( captcha, captchaSelector );
2587 }
2588 }
2589
2590 function getSelectedCaptcha( captchaSelector ) {
2591 if ( captchaSelector === '.frm-g-recaptcha' ) {
2592 return grecaptcha;
2593 }
2594 if ( document.querySelector( '.frm-cf-turnstile' ) ) {
2595 return turnstile;
2596 }
2597 return hcaptcha;
2598 }
2599
2600 function frmAfterRecaptcha( token ) {
2601 frmFrontForm.afterSingleRecaptcha( token );
2602 }
2603