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

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