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

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

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