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

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