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

2,063 lines 56.2 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 }
846
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 if ( typeof response.redirect !== 'undefined' ) {
855 if ( shouldTriggerEvent ) {
856 triggerCustomEvent( object, 'frmSubmitEvent' );
857 return;
858 }
859
860 if ( response.delay ) {
861 setTimeout( function() {
862 doRedirect( response );
863 }, 1000 * response.delay );
864 } else {
865 doRedirect( response );
866 }
867 }
868
869 if ( response.content !== '' ) {
870 // the form or success message was returned
871
872 if ( shouldTriggerEvent ) {
873 triggerCustomEvent( object, 'frmSubmitEvent', { content: response.content });
874 return;
875 }
876
877 removeSubmitLoading( jQuery( object ) );
878 if ( frm_js.offset != -1 ) { // eslint-disable-line camelcase
879 frmFrontForm.scrollMsg( jQuery( object ), false );
880 }
881
882 formID = jQuery( object ).find( 'input[name="form_id"]' ).val();
883 response.content = response.content.replace( / frm_pro_form /g, ' frm_pro_form frm_no_hide ' );
884 replaceContent = jQuery( object ).closest( '.frm_forms' );
885 removeAddedScripts( replaceContent, formID );
886 delay = maybeSlideOut( replaceContent, response.content );
887
888 setTimeout(
889 function() {
890 let container, input, previousInput;
891
892 afterFormSubmittedBeforeReplace( object, response );
893
894 replaceContent.replaceWith( response.content );
895
896 addUrlParam( response );
897
898 if ( typeof frmThemeOverride_frmAfterSubmit === 'function' ) { // eslint-disable-line camelcase
899 pageOrder = jQuery( 'input[name="frm_page_order_' + formID + '"]' ).val();
900 formReturned = jQuery( response.content ).find( 'input[name="form_id"]' ).val();
901 frmThemeOverride_frmAfterSubmit( formReturned, pageOrder, response.content, object );
902 }
903
904 if ( typeof response.recaptcha !== 'undefined' ) {
905 container = jQuery( '#frm_form_' + formID + '_container' ).find( '.frm_fields_container' );
906 input = '<input type="hidden" name="recaptcha_checked" value="' + response.recaptcha + '">';
907 previousInput = container.find( 'input[name="recaptcha_checked"]' );
908
909 if ( previousInput.length ) {
910 previousInput.replaceWith( input );
911 } else {
912 container.append( input );
913 }
914 }
915
916 afterFormSubmitted( object, response );
917 },
918 delay
919 );
920 } else if ( Object.keys( response.errors ).length ) {
921 // errors were returned
922 removeSubmitLoading( jQuery( object ), 'enable' );
923
924 //show errors
925 contSubmit = true;
926 removeAllErrors();
927
928 $fieldCont = null;
929
930 for ( key in response.errors ) {
931 $fieldCont = jQuery( object ).find( '#frm_field_' + key + '_container' );
932
933 if ( $fieldCont.length ) {
934 if ( ! $fieldCont.is( ':visible' ) ) {
935 inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' );
936 if ( inCollapsedSection.length ) {
937 frmTrigger = inCollapsedSection.prev();
938 if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) {
939 // If the frmTrigger object is the section description, check to see if the previous element is the trigger
940 frmTrigger = frmTrigger.prev( '.frm_trigger' );
941 }
942 frmTrigger.trigger( 'click' );
943 }
944 }
945
946 if ( $fieldCont.is( ':visible' ) ) {
947 addFieldError( $fieldCont, key, response.errors );
948 contSubmit = false;
949 }
950 }
951 }
952
953 jQuery( object ).find( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).each( function() {
954 const $recaptcha = jQuery( this ),
955 recaptchaID = $recaptcha.data( 'rid' );
956
957 if ( typeof grecaptcha !== 'undefined' && grecaptcha ) {
958 if ( recaptchaID ) {
959 grecaptcha.reset( recaptchaID );
960 } else {
961 grecaptcha.reset();
962 }
963 }
964 if ( typeof hcaptcha !== 'undefined' && hcaptcha ) {
965 hcaptcha.reset();
966 }
967 });
968
969 jQuery( document ).trigger( 'frmFormErrors', [ object, response ]);
970
971 fieldset.removeClass( 'frm_doing_ajax' );
972 scrollToFirstField( object );
973
974 if ( contSubmit ) {
975 object.submit();
976 } else {
977 object.insertAdjacentHTML( 'afterbegin', response.error_message );
978 checkForErrorsAndMaybeSetFocus();
979 }
980 } else {
981 // there may have been a plugin conflict, or the form is not set to submit with ajax
982
983 showFileLoading( object );
984
985 object.submit();
986 }
987 };
988
989 error = function() {
990 jQuery( object ).find( 'input[type="submit"], input[type="button"]' ).prop( 'disabled', false );
991 object.submit();
992 };
993
994 postToAjaxUrl( object, data, success, error );
995 }
996
997 function postToAjaxUrl( form, data, success, error ) {
998 let ajaxUrl, action, ajaxParams;
999
1000 ajaxUrl = frm_js.ajax_url; // eslint-disable-line camelcase
1001 action = form.getAttribute( 'action' );
1002
1003 if ( 'string' === typeof action && -1 !== action.indexOf( '?action=frm_forms_preview' ) ) {
1004 ajaxUrl = action.split( '?action=frm_forms_preview' )[0];
1005 }
1006
1007 ajaxParams = {
1008 type: 'POST',
1009 url: ajaxUrl,
1010 data: data,
1011 success: success
1012 };
1013
1014 if ( 'function' === typeof error ) {
1015 ajaxParams.error = error;
1016 }
1017
1018 jQuery.ajax( ajaxParams );
1019 }
1020
1021 function afterFormSubmitted( object, response ) {
1022 const formCompleted = jQuery( response.content ).find( '.frm_message' );
1023 if ( formCompleted.length ) {
1024 jQuery( document ).trigger( 'frmFormComplete', [ object, response ]);
1025 } else {
1026 jQuery( document ).trigger( 'frmPageChanged', [ object, response ]);
1027 }
1028 }
1029
1030 /**
1031 * Trigger an event before the form is replaced with a success message.
1032 *
1033 * @since 6.9
1034 *
1035 * @param {HTMLElement} object The form.
1036 * @param {Object} response The response from submitting the form with AJAX.
1037 * @return {void}
1038 */
1039 function afterFormSubmittedBeforeReplace( object, response ) {
1040 const formCompleted = jQuery( response.content ).find( '.frm_message' );
1041 if ( formCompleted.length ) {
1042 triggerCustomEvent( document, 'frmFormCompleteBeforeReplace', { object, response });
1043 }
1044 }
1045
1046 function removeAddedScripts( formContainer, formID ) {
1047 const endReplace = jQuery( '.frm_end_ajax_' + formID );
1048 if ( endReplace.length ) {
1049 formContainer.nextUntil( '.frm_end_ajax_' + formID ).remove();
1050 endReplace.remove();
1051 }
1052 }
1053
1054 function maybeSlideOut( oldContent, newContent ) {
1055 let c,
1056 newClass = 'frm_slideout';
1057 if ( newContent.indexOf( ' frm_slide' ) !== -1 ) {
1058 c = oldContent.children();
1059 if ( newContent.indexOf( ' frm_going_back' ) !== -1 ) {
1060 newClass += ' frm_going_back';
1061 }
1062 c.removeClass( 'frm_going_back' );
1063 c.addClass( newClass );
1064 return 300;
1065 }
1066 return 0;
1067 }
1068
1069 function addUrlParam( response ) {
1070 let url;
1071 if ( history.pushState && typeof response.page !== 'undefined' ) {
1072 url = addQueryVar( 'frm_page', response.page );
1073 window.history.pushState({ 'html': response.html }, '', '?' + url );
1074 }
1075 }
1076
1077 function addQueryVar( key, value ) {
1078 let kvp, i, x;
1079
1080 key = encodeURI( key );
1081 value = encodeURI( value );
1082
1083 kvp = document.location.search.substr( 1 ).split( '&' );
1084
1085 i = kvp.length;
1086 while ( i-- ) {
1087 x = kvp[i].split( '=' );
1088
1089 if ( x[0] == key ) {
1090 x[1] = value;
1091 kvp[i] = x.join( '=' );
1092 break;
1093 }
1094 }
1095
1096 if ( i < 0 ) {
1097 kvp[ kvp.length ] = [ key, value ].join( '=' );
1098 }
1099
1100 return kvp.join( '&' );
1101 }
1102
1103 function addFieldError( $fieldCont, key, jsErrors ) {
1104 let input, id, describedBy, roleString;
1105 if ( $fieldCont.length && $fieldCont.is( ':visible' ) ) {
1106 $fieldCont.addClass( 'frm_blank_field' );
1107 input = $fieldCont.find( 'input, select, textarea' );
1108 id = getErrorElementId( key, input.get( 0 ) );
1109
1110 describedBy = input.attr( 'aria-describedby' );
1111
1112 if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1113 frmThemeOverride_frmPlaceError( key, jsErrors );
1114 } else {
1115 if ( -1 !== jsErrors[key].indexOf( '<div' ) ) {
1116 $fieldCont.append(
1117 jsErrors[key]
1118 );
1119 } else {
1120 roleString = frm_js.include_alert_role ? 'role="alert"' : ''; // eslint-disable-line camelcase
1121 $fieldCont.append( '<div class="frm_error" ' + roleString + ' id="' + id + '">' + jsErrors[key] + '</div>' );
1122 }
1123
1124 if ( typeof describedBy === 'undefined' ) {
1125 describedBy = id;
1126 } else if ( describedBy.indexOf( id ) === -1 && describedBy.indexOf( 'frm_error_field_' ) === -1 ) {
1127 if ( input.data( 'error-first' ) === 0 ) {
1128 describedBy = describedBy + ' ' + id;
1129 } else {
1130 describedBy = id + ' ' + describedBy;
1131 }
1132 }
1133
1134 input.attr( 'aria-describedby', describedBy );
1135 }
1136 input.attr( 'aria-invalid', true );
1137
1138 jQuery( document ).trigger( 'frmAddFieldError', [ $fieldCont, key, jsErrors ]);
1139 }
1140 }
1141
1142 /**
1143 * Get the ID to use for an error element added when submitting with AJAX.
1144 *
1145 * @param {string} key
1146 * @param {HTMLElement} input
1147 * @return {string} The ID to use for the error element.
1148 */
1149 function getErrorElementId( key, input ) {
1150 if ( isNaN( key ) || ! input || ! input.id ) {
1151 // If key isn't a number, assume it's already in the right format.
1152 return 'frm_error_field_' + key;
1153 }
1154 return 'frm_error_' + input.id;
1155 }
1156
1157 /**
1158 * Removes errors before validating with JS.
1159 * This prevents issues with stale errors that has since been fixed.
1160 *
1161 * @param {Object} $fieldCont jQuery object.
1162 * @return {void}
1163 */
1164 function removeFieldError( $fieldCont ) {
1165 const errorMessage = $fieldCont.find( '.frm_error' );
1166 const errorId = errorMessage.attr( 'id' );
1167 const input = $fieldCont.find( 'input, select, textarea' );
1168 let describedBy = input.attr( 'aria-describedby' );
1169
1170 const fieldContainer = $fieldCont.get( 0 );
1171 if ( fieldContainer && fieldContainer.classList ) {
1172 fieldContainer.classList.remove( 'frm_blank_field', 'has-error' );
1173 }
1174
1175 errorMessage.remove();
1176 input.attr( 'aria-invalid', false );
1177 input.removeAttr( 'aria-describedby' );
1178
1179 if ( typeof describedBy !== 'undefined' ) {
1180 describedBy = describedBy.replace( errorId, '' );
1181 input.attr( 'aria-describedby', describedBy );
1182 }
1183 }
1184
1185 function removeAllErrors() {
1186 jQuery( '.form-field' ).removeClass( 'frm_blank_field has-error' );
1187 jQuery( '.form-field .frm_error' ).replaceWith( '' );
1188 jQuery( '.frm_error_style' ).remove();
1189 }
1190
1191 /**
1192 * @param {HTMLElement|Object} object Form object.
1193 * @return {void}
1194 */
1195 function scrollToFirstField( object ) {
1196 if ( 'function' === typeof object.get ) {
1197 // Get the HTMLElement from a jQuery object.
1198 object = object.get( 0 );
1199 }
1200 const field = object.querySelector( '.frm_blank_field' );
1201 if ( field ) {
1202 frmFrontForm.scrollMsg( jQuery( field ), object, true );
1203 }
1204 }
1205
1206 function showSubmitLoading( $object ) {
1207 showLoadingIndicator( $object );
1208 disableSubmitButton( $object );
1209 disableSaveDraft( $object );
1210 }
1211
1212 function showLoadingIndicator( $object ) {
1213 if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) {
1214 addLoadingClass( $object );
1215 $object.trigger( 'frmStartFormLoading' );
1216 }
1217 }
1218
1219 function addLoadingClass( $object ) {
1220 const loadingClass = isGoingToPrevPage( $object ) ? 'frm_loading_prev' : 'frm_loading_form';
1221
1222 $object.addClass( loadingClass );
1223 }
1224
1225 function isGoingToPrevPage( $object ) {
1226 return ( typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object ) );
1227 }
1228
1229 function removeSubmitLoading( _, enable, processesRunning ) {
1230 let loadingForm;
1231
1232 if ( processesRunning > 0 ) {
1233 return;
1234 }
1235
1236 loadingForm = jQuery( '.frm_loading_form' );
1237 loadingForm.removeClass( 'frm_loading_form' );
1238 loadingForm.removeClass( 'frm_loading_prev' );
1239
1240 loadingForm.trigger( 'frmEndFormLoading' );
1241
1242 if ( enable === 'enable' ) {
1243 enableSubmitButton( loadingForm );
1244 enableSaveDraft( loadingForm );
1245 }
1246 }
1247
1248 function showFileLoading( object ) {
1249 let fileval,
1250 loading = document.getElementById( 'frm_loading' );
1251 if ( loading !== null ) {
1252 fileval = jQuery( object ).find( 'input[type=file]' ).val();
1253 if ( typeof fileval !== 'undefined' && fileval !== '' ) {
1254 setTimeout( function() {
1255 jQuery( loading ).fadeIn( 'slow' );
1256 }, 2000 );
1257 }
1258 }
1259 }
1260
1261 /**********************************************
1262 * General Helpers
1263 *********************************************/
1264
1265 function confirmClick() {
1266 /*jshint validthis:true */
1267 const message = jQuery( this ).data( 'frmconfirm' );
1268 return confirm( message );
1269 }
1270
1271 /**
1272 * Check for -webkit-box-shadow css value for input:-webkit-autofill selector.
1273 * If this is a match, the User is autofilling the input on a Webkit browser.
1274 * We want to delete the Honeypot field, otherwise it will get triggered as spam on autocomplete.
1275 */
1276 function onHoneypotFieldChange() {
1277 const css = jQuery( this ).css( 'box-shadow' );
1278 if ( css.match( /inset/ ) ) {
1279 this.parentNode.removeChild( this );
1280 }
1281 }
1282
1283 function maybeMakeHoneypotFieldsUntabbable() {
1284 document.addEventListener( 'keydown', handleKeyUp );
1285
1286 function handleKeyUp( event ) {
1287 let code;
1288
1289 if ( 'undefined' !== typeof event.key ) {
1290 code = event.key;
1291 } else if ( 'undefined' !== typeof event.keyCode && 9 === event.keyCode ) {
1292 code = 'Tab';
1293 }
1294
1295 if ( 'Tab' === code ) {
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