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

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