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

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