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

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