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

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