PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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
← All changes | js/formidable.js +909 -397 6.25 → trunk View file →
@@ -1,13 +1,9 @@
1 1 /* exported frmRecaptcha, frmAfterRecaptcha */
2 -/* eslint-disable prefer-const */
3 2
4 3 function frmFrontFormJS() {
5 4 'use strict';
6 5
7 - /*global jQuery:false, frm_js, grecaptcha, hcaptcha, turnstile, frmProForm, tinyMCE */
8 - /*global frmThemeOverride_jsErrors, frmThemeOverride_frmPlaceError, frmThemeOverride_frmAfterSubmit */
9 -
10 6 let jsErrors = [];
11 7
12 8 /**
13 9 * Triggers custom JS event.
@@ -36,29 +32,27 @@
36 32 * @param {boolean} fullID
37 33 * @return {string|number} Field ID.
38 34 */
39 35 function getFieldId( field, fullID ) {
40 - let nameParts, fieldId,
41 - isRepeating = false,
42 - fieldName = '';
36 + let nameParts;
37 + let fieldId;
38 + let isRepeating = false;
39 + let fieldName = '';
40 +
43 41 if ( field instanceof jQuery ) {
44 - fieldName = field.attr( 'name' );
45 - } else {
46 - fieldName = field.name;
42 + field = field.get( 0 );
47 43 }
48 44
49 - if ( typeof fieldName === 'undefined' ) {
45 + fieldName = field.name;
46 +
47 + if ( fieldName === undefined ) {
50 48 fieldName = '';
51 49 }
52 50
53 51 if ( fieldName === '' ) {
54 - if ( field instanceof jQuery ) {
55 - fieldName = field.data( 'name' );
56 - } else {
57 - fieldName = field.getAttribute( 'data-name' );
58 - }
52 + fieldName = field.getAttribute( 'data-name' );
59 53
60 - if ( typeof fieldName === 'undefined' ) {
54 + if ( fieldName === undefined ) {
61 55 fieldName = '';
62 56 }
63 57
64 58 if ( fieldName !== '' && fieldName ) {
@@ -86,9 +80,9 @@
86 80 return 0;
87 81 }
88 82
89 83 // Check if 'this' is in a repeating section
90 - if ( document.querySelector( 'input[name="item_meta[' + fieldId + '][form]"]' ) ) {
84 + if ( document.querySelector( `input[name="item_meta[${ fieldId }][form]"]` ) ) {
91 85 // this is a repeatable section with name: item_meta[repeating-section-id][row-id][field-id]
92 86 fieldId = nameParts[ 2 ].replace( '[', '' );
93 87 isRepeating = true;
94 88 }
@@ -106,11 +100,11 @@
106 100
107 101 if ( fullID === true ) {
108 102 // For use in the container div id
109 103 if ( fieldId === nameParts[ 0 ] ) {
110 - fieldId = fieldId + '-' + nameParts[ 1 ].replace( '[', '' );
104 + fieldId = `${ fieldId }-${ nameParts[ 1 ].replace( '[', '' ) }`;
111 105 } else {
112 - fieldId = fieldId + '-' + nameParts[ 0 ] + '-' + nameParts[ 1 ].replace( '[', '' );
106 + fieldId = `${ fieldId }-${ nameParts[ 0 ] }-${ nameParts[ 1 ].replace( '[', '' ) }`;
113 107 }
114 108 }
115 109
116 110 return fieldId;
@@ -123,9 +117,15 @@
123 117 *
124 118 * @param {Object} $form
125 119 */
126 120 function disableSubmitButton( $form ) {
127 - $form.find( 'input[type="submit"], input[type="button"], button[type="submit"], button.frm_save_draft' ).attr( 'disabled', 'disabled' );
121 + const form = $form instanceof jQuery ? $form.get( 0 ) : $form;
122 + if ( ! form ) {
123 + return;
124 + }
125 + form.querySelectorAll( 'input[type="submit"], input[type="button"], button[type="submit"], button.frm_save_draft' ).forEach(
126 + button => button.disabled = true
127 + );
128 128 }
129 129
130 130 /**
131 131 * Enable the submit button for a given jQuery form object
@@ -131,12 +131,16 @@
131 131 * Enable the submit button for a given jQuery form object
132 132 *
133 133 * @since 2.03.02
134 134 *
135 - * @param {Object} $form
135 + * @param {HTMLElement} form
136 + *
137 + * @return {void}
136 138 */
137 - function enableSubmitButton( $form ) {
138 - $form.find( 'input[type="submit"], input[type="button"], button[type="submit"]' ).prop( 'disabled', false );
139 + function enableSubmitButton( form ) {
140 + form.querySelectorAll( 'input[type="submit"], input[type="button"], button[type="submit"]' ).forEach(
141 + button => button.disabled = false
142 + );
139 143 }
140 144
141 145 /**
142 146 * Disable the save draft link for a given jQuery form object
@@ -145,23 +149,30 @@
145 149 *
146 150 * @param {Object} $form
147 151 */
148 152 function disableSaveDraft( $form ) {
149 - $form.find( 'a.frm_save_draft' ).css( 'pointer-events', 'none' );
153 + const form = $form instanceof jQuery ? $form.get( 0 ) : $form;
154 + if ( ! form ) {
155 + return;
156 + }
157 + form.querySelectorAll( 'a.frm_save_draft' ).forEach(
158 + link => link.style.pointerEvents = 'none'
159 + );
150 160 }
151 161
152 162 /**
153 - * Enable the save draft link for a given jQuery form object
163 + * Enable the save draft link for a given form object.
154 164 *
155 165 * @since 4.04.03
156 166 *
157 - * @param {jQuery} $form
167 + * @param {jQuery|HTMLElement} $form
158 168 */
159 169 function enableSaveDraft( $form ) {
160 - if ( ! $form.length ) {
170 + const form = $form instanceof jQuery ? $form.get( 0 ) : $form;
171 + if ( ! form ) {
161 172 return;
162 173 }
163 - $form[ 0 ].querySelectorAll( '.frm_save_draft' ).forEach( saveDraftButton => {
174 + form.querySelectorAll( '.frm_save_draft' ).forEach( saveDraftButton => {
164 175 saveDraftButton.disabled = false;
165 176 saveDraftButton.style.pointerEvents = '';
166 177 } );
167 178 }
@@ -236,15 +247,14 @@
236 247 * @param {Array} errors
237 248 * @return {void}
238 249 */
239 250 function checkValidity( field, errors ) {
240 - let fieldID;
241 251 if ( 'object' !== typeof field.validity || false !== field.validity.valid ) {
242 252 return;
243 253 }
244 254
245 - fieldID = getFieldId( field, true );
246 - if ( 'undefined' === typeof errors[ fieldID ] ) {
255 + const fieldID = getFieldId( field, true );
256 + if ( errors[ fieldID ] === undefined ) {
247 257 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
248 258 }
249 259
250 260 if ( 'function' === typeof field.reportValidity ) {
@@ -271,11 +281,14 @@
271 281 if ( field.type === 'url' ) {
272 282 maybeAddHttpsToUrl( field );
273 283 }
274 284 const form = field.closest( 'form' );
275 - if ( form && hasClass( form, 'frm_js_validate' ) ) {
276 - validateField( field );
285 + if ( ! form ) {
286 + return;
277 287 }
288 +
289 + // Removing stale errors is universal. Adding errors only happens when JS validation is enabled.
290 + validateField( field, hasClass( form, 'frm_js_validate' ) );
278 291 }
279 292
280 293 /**
281 294 * @param {HTMLElement} field
@@ -283,9 +296,9 @@
283 296 function maybeAddHttpsToUrl( field ) {
284 297 const url = field.value;
285 298 const matches = url.match( /^(https?|ftps?|mailto|news|feed|telnet):/ );
286 299 if ( field.value !== '' && matches === null ) {
287 - field.value = 'https://' + url;
300 + field.value = `https://${ url }`;
288 301 }
289 302 }
290 303
291 304 /**
@@ -290,14 +303,20 @@
290 303
291 304 /**
292 305 * Validate a field with JS.
293 306 *
307 + * Removing stale errors is universal. Adding errors only happens when JS validation is enabled.
308 + *
309 + * @since 6.32 Added the `addErrors` parameter.
310 + *
294 311 * @param {HTMLElement} field
312 + * @param {boolean} addErrors Whether to add new errors. Defaults to `true`.
295 313 *
296 314 * @return {void}
297 315 */
298 - function validateField( field ) {
299 - let errors, key;
316 + function validateField( field, addErrors = true ) {
317 + let errors;
318 + let key;
300 319
301 320 errors = [];
302 321 const fieldContainer = field.closest( '.frm_form_field' );
303 322
@@ -313,14 +332,20 @@
313 332 if ( errors.length < 1 ) {
314 333 validateFieldValue( field, errors, false );
315 334 }
316 335
317 - const $fieldCont = jQuery( fieldContainer );
318 - removeFieldError( $fieldCont );
319 - if ( Object.keys( errors ).length > 0 ) {
320 - for ( key in errors ) {
321 - addFieldError( $fieldCont, key, errors );
336 + const hasErrors = Object.keys( errors ).length > 0;
337 +
338 + if ( addErrors ) {
339 + removeFieldError( fieldContainer );
340 + if ( hasErrors ) {
341 + for ( key in errors ) {
342 + addFieldError( fieldContainer, key, errors );
343 + }
322 344 }
345 + } else if ( ! hasErrors ) {
346 + // JS validation is off, so only remove existing errors once the field passes validation.
347 + removeFieldError( fieldContainer );
323 348 }
324 349 }
325 350
326 351 /**
@@ -346,15 +371,19 @@
346 371 } else if ( field.pattern !== null ) {
347 372 checkPatternField( field, errors );
348 373 }
349 374
375 + if ( 'tel' === field.type && shouldCheckConfirmField( field, onSubmit ) ) {
376 + confirmField( field, errors );
377 + }
378 +
350 379 /**
351 380 * @since 6.15 Added `onSubmit` to the data.
352 381 */
353 382 triggerCustomEvent( document, 'frm_validate_field_value', {
354 - field: field,
355 - errors: errors,
356 - onSubmit: onSubmit
383 + field,
384 + errors,
385 + onSubmit
357 386 } );
358 387 }
359 388
360 389 /**
@@ -362,12 +391,14 @@
362 391 * @param {Array} errors
363 392 * @return {Array} Errors
364 393 */
365 394 function checkRequiredField( field, errors ) {
366 - let tempVal, i, placeholder,
367 - val = '',
368 - fieldID = '',
369 - fileID = field.getAttribute( 'data-frmfile' );
395 + let tempVal;
396 + let i;
397 + let placeholder;
398 + let val = '';
399 + let fieldID = '';
400 + let fileID = field.getAttribute( 'data-frmfile' );
370 401
371 402 if ( field.type === 'hidden' && fileID === null && ! isAppointmentField( field ) && ! isInlineDatepickerField( field ) ) {
372 403 return errors;
373 404 }
@@ -372,9 +403,9 @@
372 403 return errors;
373 404 }
374 405
375 406 if ( field.type === 'checkbox' || field.type === 'radio' ) {
376 - document.querySelectorAll( 'input[name="' + field.name + '"]' ).forEach( function( input ) {
407 + document.querySelectorAll( `input[name="${ field.name }"]` ).forEach( function( input ) {
377 408 const requiredField = input.closest( '.frm_required_field' );
378 409 if ( ! requiredField ) {
379 410 return;
380 411 }
@@ -384,14 +415,14 @@
384 415 val = checkedInput.value;
385 416 } );
386 417 } );
387 418 } else if ( field.type === 'file' || fileID ) {
388 - if ( typeof fileID === 'undefined' ) {
419 + if ( fileID === undefined ) {
389 420 fileID = getFieldId( field, true );
390 421 fileID = fileID.replace( 'file', '' );
391 422 }
392 423
393 - if ( typeof errors[ fileID ] === 'undefined' ) {
424 + if ( errors[ fileID ] === undefined ) {
394 425 val = getFileVals( fileID );
395 426 }
396 427 fieldID = fileID;
397 428 } else {
@@ -399,9 +430,10 @@
399 430 // skip hidden other fields
400 431 return errors;
401 432 }
402 433
403 - val = jQuery( field ).val();
434 + val = jQuery( field ).val(); // eslint-disable-line no-jquery/no-val
435 +
404 436 if ( val === null ) {
405 437 val = '';
406 438 } else if ( typeof val !== 'string' ) {
407 439 tempVal = val;
@@ -433,9 +465,11 @@
433 465 // set id for time field
434 466 fieldID = fieldID.replace( '-H', '' ).replace( '-m', '' );
435 467 } else if ( isSignatureField( field ) ) {
436 468 if ( val === '' ) {
437 - val = jQuery( field ).closest( '.frm_form_field' ).find( '[name="' + field.getAttribute( 'name' ).replace( '[typed]', '[output]' ) + '"]' ).val();
469 + const fieldContainer = field.closest( '.frm_form_field' );
470 + const outputField = fieldContainer ? fieldContainer.querySelector( `[name="${ field.getAttribute( 'name' ).replace( '[typed]', '[output]' ) }"]` ) : null;
471 + val = outputField ? outputField.value : '';
438 472 }
439 473 fieldID = fieldID.replace( '-typed', '' );
440 474 }
441 475
@@ -486,14 +520,14 @@
486 520 * @param {string|number} fileID
487 521 * @return {string} File input value.
488 522 */
489 523 function getFileVals( fileID ) {
490 - let val = '',
491 - fileFields = jQuery( 'input[name="file' + fileID + '"], input[name="file' + fileID + '[]"], input[name^="item_meta[' + fileID + ']"]' );
524 + let val = '';
525 + const fileFields = document.querySelectorAll( `input[name="file${ fileID }"], input[name="file${ fileID }[]"], input[name^="item_meta[${ fileID }]"]` );
492 526
493 - fileFields.each( function() {
527 + fileFields.forEach( function( field ) {
494 528 if ( val === '' ) {
495 - val = this.value;
529 + val = field.value;
496 530 }
497 531 } );
498 532 return val;
499 533 }
@@ -503,10 +537,10 @@
503 537 * @param {Array} errors
504 538 * @return {void}
505 539 */
506 540 function checkUrlField( field, errors ) {
507 - let fieldID,
508 - url = field.value;
541 + let fieldID;
542 + const url = field.value;
509 543
510 544 if ( url !== '' && ! /^http(s)?:\/\/(?:localhost|(?:[\da-z\.-]+\.[\da-z\.-]+))/i.test( url ) ) {
511 545 fieldID = getFieldId( field, true );
512 546 if ( ! ( fieldID in errors ) ) {
@@ -547,10 +581,10 @@
547 581 * @param {Object} errors Errors data.
548 582 * @param {boolean} onSubmit Is `true` if the form is being submitted.
549 583 */
550 584 function checkEmailField( field, errors, onSubmit ) {
551 - const fieldID = getFieldId( field, true ),
552 - pattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
585 + const fieldID = getFieldId( field, true );
586 + const pattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;
553 587
554 588 // validate the current field we're editing first
555 589 if ( '' !== field.value && pattern.test( field.value ) === false ) {
556 590 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
@@ -581,24 +615,23 @@
581 615 * @param {Array} errors
582 616 * @return {void}
583 617 */
584 618 function confirmField( field, errors ) {
585 - let value, confirmValue, firstField,
586 - fieldID = getFieldId( field, true ),
587 - strippedId = field.id.replace( 'conf_', '' ),
588 - strippedFieldID = fieldID.replace( 'conf_', '' ),
589 - confirmField = document.getElementById( strippedId.replace( 'field_', 'field_conf_' ) );
619 + const fieldID = getFieldId( field, true );
620 + const strippedId = field.id.replace( 'conf_', '' );
621 + const strippedFieldID = fieldID.replace( 'conf_', '' );
622 + const confirmField = document.getElementById( strippedId.replace( 'field_', 'field_conf_' ) );
590 623
591 - if ( confirmField === null || typeof errors[ 'conf_' + strippedFieldID ] !== 'undefined' ) {
624 + if ( ! confirmField || errors[ `conf_${ strippedFieldID }` ] !== undefined ) {
592 625 return;
593 626 }
594 627
595 628 if ( fieldID !== strippedFieldID ) {
596 - firstField = document.getElementById( strippedId );
597 - value = firstField.value;
598 - confirmValue = confirmField.value;
629 + const firstField = document.getElementById( strippedId );
630 + const { value } = firstField;
631 + const confirmValue = confirmField.value;
599 632 if ( value !== confirmValue ) {
600 - errors[ 'conf_' + strippedFieldID ] = getFieldValidationMessage( confirmField, 'data-confmsg' );
633 + errors[ `conf_${ strippedFieldID }` ] = getFieldValidationMessage( confirmField, 'data-confmsg' );
601 634 }
602 635 } else {
603 636 validateField( confirmField );
604 637 }
@@ -609,10 +642,10 @@
609 642 * @param {Array} errors
610 643 * @return {void}
611 644 */
612 645 function checkNumberField( field, errors ) {
613 - let fieldID,
614 - number = field.value;
646 + let fieldID;
647 + const number = field.value;
615 648
616 649 if ( number !== '' && isNaN( number / 1 ) !== false ) {
617 650 fieldID = getFieldId( field, true );
618 651 if ( ! ( fieldID in errors ) ) {
@@ -626,11 +659,11 @@
626 659 * @param {Array} errors
627 660 * @return {void}
628 661 */
629 662 function checkPatternField( field, errors ) {
630 - let fieldID,
631 - text = field.value,
632 - format = getFieldValidationMessage( field, 'pattern' );
663 + let fieldID;
664 + const text = field.value;
665 + let format = getFieldValidationMessage( field, 'pattern' );
633 666
634 667 if ( format !== '' && text !== '' ) {
635 668 fieldID = getFieldId( field, true );
636 669 if ( ! ( fieldID in errors ) ) {
@@ -638,9 +671,9 @@
638 671 if ( ! window.frmProForm.validateIntlPhoneInput( field ) ) {
639 672 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
640 673 }
641 674 } else {
642 - format = new RegExp( '^' + format + '$', 'i' );
675 + format = new RegExp( `^${ format }$`, 'i' );
643 676 if ( format.test( text ) === false ) {
644 677 errors[ fieldID ] = getFieldValidationMessage( field, 'data-invmsg' );
645 678 }
646 679 }
@@ -653,13 +686,11 @@
653 686 *
654 687 * @since 6.5.1
655 688 */
656 689 function setSelectPlaceholderColor() {
657 - let selects = document.querySelectorAll( '.form-field select' ),
658 - styleElement = document.querySelector( '.with_frm_style' ),
659 - textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : '',
660 - changeSelectColor;
661 -
690 + const selects = document.querySelectorAll( '.form-field select' );
691 + const styleElement = document.querySelector( '.with_frm_style' );
692 + const textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : '';
662 693 // Exit if there are no select elements or the textColorDisabled property is missing
663 694 if ( ! selects.length || ! textColorDisabled ) {
664 695 return;
665 696 }
@@ -664,9 +695,9 @@
664 695 return;
665 696 }
666 697
667 698 // Function to change the color of a select element
668 - changeSelectColor = function( select ) {
699 + const changeSelectColor = function( select ) {
669 700 if ( select.options[ select.selectedIndex ] && hasClass( select.options[ select.selectedIndex ], 'frm-select-placeholder' ) ) {
670 701 select.style.setProperty( 'color', textColorDisabled, 'important' );
671 702 } else {
672 703 select.style.color = '';
@@ -686,51 +717,62 @@
686 717 }
687 718
688 719 /**
689 720 * @param {HTMLElement|jQuery} object
690 - * @return {boolean} True if there is an invisible recaptcha.
721 + *
722 + * @return {HTMLElement|false} Captcha element if there is an invisible recaptcha.
691 723 */
692 724 function hasInvisibleRecaptcha( object ) {
693 - let recaptcha, recaptchaID, alreadyChecked;
725 + if ( isGoingToPrevPage( object ) ) {
726 + return false;
727 + }
694 728
695 - if ( isGoingToPrevPage( object ) ) {
729 + const form = object instanceof jQuery ? object.get( 0 ) : object;
730 + if ( ! form ) {
696 731 return false;
697 732 }
698 733
699 - recaptcha = jQuery( object ).find( '.frm-g-recaptcha[data-size="invisible"], .g-recaptcha[data-size="invisible"]' );
700 - if ( recaptcha.length ) {
701 - recaptchaID = recaptcha.data( 'rid' );
702 - alreadyChecked = grecaptcha.getResponse( recaptchaID );
734 + const recaptcha = form.querySelector( '.frm-g-recaptcha[data-size="invisible"], .g-recaptcha[data-size="invisible"]' );
735 + if ( recaptcha ) {
736 + const recaptchaID = recaptcha.dataset.rid;
737 + const alreadyChecked = grecaptcha.getResponse( recaptchaID );
703 738 if ( alreadyChecked.length === 0 ) {
704 739 return recaptcha;
705 740 }
706 741 }
742 +
707 743 return false;
708 744 }
709 745
710 746 /**
711 - * @param {jQuery} invisibleRecaptcha
747 + * @param {HTMLElement} invisibleRecaptcha
748 + *
749 + * @return {void}
712 750 */
713 751 function executeInvisibleRecaptcha( invisibleRecaptcha ) {
714 - const recaptchaID = invisibleRecaptcha.data( 'rid' );
752 + const recaptchaID = invisibleRecaptcha.dataset.rid;
715 753 grecaptcha.reset( recaptchaID );
716 754 grecaptcha.execute( recaptchaID );
717 755 }
718 756
719 757 function validateRecaptcha( form, errors ) {
720 - let response;
758 + const formEl = form instanceof jQuery ? form.get( 0 ) : form;
759 + if ( ! formEl ) {
760 + return errors;
761 + }
721 762
722 - const $recaptcha = jQuery( form ).find( '.frm-g-recaptcha' );
723 - if ( ! $recaptcha.length ) {
763 + const recaptcha = formEl.querySelector( '.frm-g-recaptcha' );
764 + if ( ! recaptcha ) {
724 765 return errors;
725 766 }
726 767
727 - const recaptchaID = $recaptcha.data( 'rid' );
768 + const recaptchaID = recaptcha.dataset.rid;
769 + let response;
728 770
729 771 try {
730 772 response = grecaptcha.getResponse( recaptchaID );
731 773 } catch ( e ) {
732 - if ( jQuery( form ).find( 'input[name="recaptcha_checked"]' ).length ) {
774 + if ( formEl.querySelector( 'input[name="recaptcha_checked"]' ) ) {
733 775 return errors;
734 776 }
735 777 response = '';
736 778 }
@@ -735,11 +777,13 @@
735 777 response = '';
736 778 }
737 779
738 780 if ( response.length === 0 ) {
739 - const fieldContainer = $recaptcha.closest( '.frm_form_field' );
740 - const fieldID = fieldContainer.attr( 'id' ).replace( 'frm_field_', '' ).replace( '_container', '' );
741 - errors[ fieldID ] = '';
781 + const fieldContainer = recaptcha.closest( '.frm_form_field' );
782 + if ( fieldContainer?.id ) {
783 + const fieldID = fieldContainer.id.replace( 'frm_field_', '' ).replace( '_container', '' );
784 + errors[ fieldID ] = '';
785 + }
742 786 }
743 787
744 788 return errors;
745 789 }
@@ -812,16 +856,14 @@
812 856 * @param {string} action
813 857 * @return {void}
814 858 */
815 859 function getFormErrors( object, action ) {
816 - let fieldset, data, success, error, shouldTriggerEvent;
860 + const fieldsets = object.querySelectorAll( '.frm_form_field' );
861 + fieldsets.forEach( field => field.classList.add( 'frm_doing_ajax' ) );
817 862
818 - fieldset = jQuery( object ).find( '.frm_form_field' );
819 - fieldset.addClass( 'frm_doing_ajax' );
863 + const data = `${ jQuery( object ).serialize() }&action=frm_entries_${ action }&nonce=${ frm_js.nonce }`; // eslint-disable-line no-jquery/no-serialize
864 + const shouldTriggerEvent = object.classList.contains( 'frm_trigger_event_on_submit' );
820 865
821 - data = jQuery( object ).serialize() + '&action=frm_entries_' + action + '&nonce=' + frm_js.nonce; // eslint-disable-line camelcase
822 - shouldTriggerEvent = object.classList.contains( 'frm_trigger_event_on_submit' );
823 -
824 866 const doRedirect = response => {
825 867 jQuery( document ).trigger( 'frmBeforeFormRedirect', [ object, response ] );
826 868
827 869 if ( ! response.openInNewTab ) {
@@ -832,17 +874,14 @@
832 874
833 875 // We don't return here because we're opening in a new tab, the old tab will still update.
834 876 const newTab = window.open( response.redirect, '_blank' );
835 877 if ( ! newTab && response.fallbackMsg && response.content ) {
836 - response.content = response.content.trim().replace( /(<\/div><\/div>)$/, ' ' + response.fallbackMsg + '</div></div>' );
878 + response.content = response.content.trim().replace( /(<\/div><\/div>)$/, ` ${ response.fallbackMsg }</div></div>` );
837 879 }
838 880 };
839 881
840 - success = function( response ) {
841 - let defaultResponse, formID, replaceContent, pageOrder, formReturned, contSubmit, delay,
842 - $fieldCont, key, inCollapsedSection, frmTrigger;
843 -
844 - defaultResponse = {
882 + const success = function( response ) {
883 + const defaultResponse = {
845 884 content: '',
846 885 errors: {},
847 886 pass: false
848 887 };
@@ -858,9 +897,11 @@
858 897 response = defaultResponse;
859 898 }
860 899 }
861 900
862 - if ( typeof response.redirect !== 'undefined' ) {
901 + let willRedirect = false;
902 +
903 + if ( response.redirect !== undefined ) {
863 904 if ( shouldTriggerEvent ) {
864 905 triggerCustomEvent( object, 'frmSubmitEvent' );
865 906 return;
866 907 }
@@ -871,8 +912,10 @@
871 912 }, 1000 * response.delay );
872 913 } else {
873 914 doRedirect( response );
874 915 }
916 +
917 + willRedirect = true;
875 918 }
876 919
877 920 if ( 'string' === typeof response.content && response.content !== '' ) {
878 921 // the form or success message was returned
@@ -882,22 +925,21 @@
882 925 return;
883 926 }
884 927
885 928 removeSubmitLoading( jQuery( object ) );
886 - if ( frm_js.offset != -1 ) { // eslint-disable-line camelcase
929 + if ( frm_js.offset != -1 ) {
887 930 frmFrontForm.scrollMsg( jQuery( object ), false );
888 931 }
889 932
890 - formID = jQuery( object ).find( 'input[name="form_id"]' ).val();
933 + const formIdInput = object.querySelector( 'input[name="form_id"]' );
934 + const formID = formIdInput ? formIdInput.value : '';
891 935 response.content = response.content.replace( / frm_pro_form /g, ' frm_pro_form frm_no_hide ' );
892 - replaceContent = jQuery( object ).closest( '.frm_forms' );
936 + const replaceContent = jQuery( object ).closest( '.frm_forms' ); // eslint-disable-line no-jquery/no-closest
893 937 removeAddedScripts( replaceContent, formID );
894 - delay = maybeSlideOut( replaceContent, response.content );
938 + const delay = maybeSlideOut( replaceContent, response.content );
895 939
896 940 setTimeout(
897 941 function() {
898 - let container, input, previousInput;
899 -
900 942 afterFormSubmittedBeforeReplace( object, response );
901 943
902 944 replaceContent.replaceWith( response.content );
903 945
@@ -903,48 +945,41 @@
903 945
904 946 addUrlParam( response );
905 947
906 948 if ( typeof frmThemeOverride_frmAfterSubmit === 'function' ) { // eslint-disable-line camelcase
907 - pageOrder = jQuery( 'input[name="frm_page_order_' + formID + '"]' ).val();
908 - formReturned = jQuery( response.content ).find( 'input[name="form_id"]' ).val();
949 + const pageOrderInput = document.querySelector( `input[name="frm_page_order_${ formID }"]` );
950 + const pageOrder = pageOrderInput ? pageOrderInput.value : '';
951 + const tempDiv = document.createElement( 'div' );
952 + tempDiv.innerHTML = response.content;
953 + const formReturnedInput = tempDiv.querySelector( 'input[name="form_id"]' );
954 + const formReturned = formReturnedInput ? formReturnedInput.value : '';
909 955 frmThemeOverride_frmAfterSubmit( formReturned, pageOrder, response.content, object );
910 956 }
911 957
912 - if ( typeof response.recaptcha !== 'undefined' ) {
913 - container = jQuery( '#frm_form_' + formID + '_container' ).find( '.frm_fields_container' );
914 - input = '<input type="hidden" name="recaptcha_checked" value="' + response.recaptcha + '">';
915 - previousInput = container.find( 'input[name="recaptcha_checked"]' );
916 -
917 - if ( previousInput.length ) {
918 - previousInput.replaceWith( input );
919 - } else {
920 - container.append( input );
921 - }
922 - }
923 -
924 958 afterFormSubmitted( object, response );
925 959 },
926 960 delay
927 961 );
928 - } else if ( Object.keys( response.errors ).length ) {
962 + } else if ( response.errors !== undefined && Object.keys( response.errors ).length ) {
929 963 // errors were returned
930 964 removeSubmitLoading( jQuery( object ), 'enable' );
931 965
932 966 //show errors
933 - contSubmit = true;
967 + let contSubmit = true;
934 968 removeAllErrors();
935 969
936 - $fieldCont = null;
970 + let $fieldCont = null;
937 971
938 - for ( key in response.errors ) {
939 - $fieldCont = jQuery( object ).find( '#frm_field_' + key + '_container' );
972 + for ( const key in response.errors ) {
973 + const fieldContEl = object.querySelector( `#frm_field_${ key }_container` );
974 + $fieldCont = fieldContEl ? jQuery( fieldContEl ) : jQuery();
940 975
941 976 if ( $fieldCont.length ) {
942 - if ( ! $fieldCont.is( ':visible' ) ) {
943 - inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' );
977 + if ( ! $fieldCont.is( ':visible' ) ) { // eslint-disable-line no-jquery/no-is
978 + const inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' ); // eslint-disable-line no-jquery/no-closest, formidable/no-jquery-variable-methods
944 979 if ( inCollapsedSection.length ) {
945 - frmTrigger = inCollapsedSection.prev();
946 - if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) {
980 + let frmTrigger = inCollapsedSection.prev();
981 + if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) { // eslint-disable-line formidable/no-jquery-variable-methods
947 982 // If the frmTrigger object is the section description, check to see if the previous element is the trigger
948 983 frmTrigger = frmTrigger.prev( '.frm_trigger' );
949 984 }
950 985 frmTrigger.trigger( 'click' );
@@ -950,9 +985,9 @@
950 985 frmTrigger.trigger( 'click' );
951 986 }
952 987 }
953 988
954 - if ( $fieldCont.is( ':visible' ) ) {
989 + if ( $fieldCont.is( ':visible' ) ) { // eslint-disable-line no-jquery/no-is
955 990 addFieldError( $fieldCont, key, response.errors );
956 991 contSubmit = false;
957 992 }
958 993 }
@@ -957,11 +992,10 @@
957 992 }
958 993 }
959 994 }
960 995
961 - jQuery( object ).find( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).each( function() {
962 - const $recaptcha = jQuery( this ),
963 - recaptchaID = $recaptcha.data( 'rid' );
996 + object.querySelectorAll( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).forEach( function( captchaEl ) {
997 + const recaptchaID = captchaEl.dataset.rid;
964 998
965 999 if ( typeof grecaptcha !== 'undefined' && grecaptcha ) {
966 1000 if ( recaptchaID ) {
967 1001 grecaptcha.reset( recaptchaID );
@@ -968,8 +1002,9 @@
968 1002 } else {
969 1003 grecaptcha.reset();
970 1004 }
971 1005 }
1006 +
972 1007 if ( typeof hcaptcha !== 'undefined' && hcaptcha ) {
973 1008 hcaptcha.reset();
974 1009 }
975 1010 } );
@@ -974,9 +1009,9 @@
974 1009 }
975 1010 } );
976 1011
977 1012 if ( window.turnstile ) {
978 - object.querySelectorAll( '.cf-turnstile' ).forEach(
1013 + object.querySelectorAll( '.frm-cf-turnstile' ).forEach(
979 1014 turnstileField => turnstileField.dataset.rid && turnstile.reset( turnstileField.dataset.rid )
980 1015 );
981 1016 }
982 1017
@@ -981,9 +1016,9 @@
981 1016 }
982 1017
983 1018 jQuery( document ).trigger( 'frmFormErrors', [ object, response ] );
984 1019
985 - fieldset.removeClass( 'frm_doing_ajax' );
1020 + fieldsets.forEach( field => field.classList.remove( 'frm_doing_ajax' ) );
986 1021 scrollToFirstField( object );
987 1022
988 1023 if ( contSubmit ) {
989 1024 object.submit();
@@ -990,10 +1025,10 @@
990 1025 } else {
991 1026 object.insertAdjacentHTML( 'afterbegin', response.error_message );
992 1027 checkForErrorsAndMaybeSetFocus();
993 1028 }
994 - } else {
995 - // there may have been a plugin conflict, or the form is not set to submit with ajax
1029 + } else if ( ! willRedirect ) { // Avoid double submission if redirecting to a page.
1030 + // There may have been a plugin conflict, or the form is not set to submit with ajax.
996 1031
997 1032 showFileLoading( object );
998 1033
999 1034 object.submit();
@@ -999,10 +1034,12 @@
999 1034 object.submit();
1000 1035 }
1001 1036 };
1002 1037
1003 - error = function() {
1004 - jQuery( object ).find( 'input[type="submit"], input[type="button"]' ).prop( 'disabled', false );
1038 + const error = function() {
1039 + object.querySelectorAll( 'input[type="submit"], input[type="button"]' ).forEach(
1040 + button => button.disabled = false
1041 + );
1005 1042 object.submit();
1006 1043 };
1007 1044
1008 1045 postToAjaxUrl( object, data, success, error );
@@ -1008,22 +1045,20 @@
1008 1045 postToAjaxUrl( object, data, success, error );
1009 1046 }
1010 1047
1011 1048 function postToAjaxUrl( form, data, success, error ) {
1012 - let ajaxUrl, action, ajaxParams;
1049 + let ajaxUrl = frm_js.ajax_url;
1050 + const action = form.getAttribute( 'action' );
1013 1051
1014 - ajaxUrl = frm_js.ajax_url; // eslint-disable-line camelcase
1015 - action = form.getAttribute( 'action' );
1016 -
1017 - if ( 'string' === typeof action && -1 !== action.indexOf( '?action=frm_forms_preview' ) ) {
1052 + if ( 'string' === typeof action && action.includes( '?action=frm_forms_preview' ) ) {
1018 1053 ajaxUrl = action.split( '?action=frm_forms_preview' )[ 0 ];
1019 1054 }
1020 1055
1021 - ajaxParams = {
1056 + const ajaxParams = {
1022 1057 type: 'POST',
1023 1058 url: ajaxUrl,
1024 - data: data,
1025 - success: success
1059 + data,
1060 + success
1026 1061 };
1027 1062
1028 1063 if ( 'function' === typeof error ) {
1029 1064 ajaxParams.error = error;
@@ -1028,14 +1063,16 @@
1028 1063 if ( 'function' === typeof error ) {
1029 1064 ajaxParams.error = error;
1030 1065 }
1031 1066
1032 - jQuery.ajax( ajaxParams );
1067 + jQuery.ajax( ajaxParams ); // eslint-disable-line no-jquery/no-ajax
1033 1068 }
1034 1069
1035 1070 function afterFormSubmitted( object, response ) {
1036 - const formCompleted = jQuery( response.content ).find( '.frm_message' );
1037 - if ( formCompleted.length ) {
1071 + const tempDiv = document.createElement( 'div' );
1072 + tempDiv.innerHTML = response.content;
1073 + const formCompleted = tempDiv.querySelector( '.frm_message' );
1074 + if ( formCompleted ) {
1038 1075 jQuery( document ).trigger( 'frmFormComplete', [ object, response ] );
1039 1076 } else {
1040 1077 jQuery( document ).trigger( 'frmPageChanged', [ object, response ] );
1041 1078 }
@@ -1050,28 +1087,30 @@
1050 1087 * @param {Object} response The response from submitting the form with AJAX.
1051 1088 * @return {void}
1052 1089 */
1053 1090 function afterFormSubmittedBeforeReplace( object, response ) {
1054 - const formCompleted = jQuery( response.content ).find( '.frm_message' );
1055 - if ( formCompleted.length ) {
1091 + const tempDiv = document.createElement( 'div' );
1092 + tempDiv.innerHTML = response.content;
1093 + const formCompleted = tempDiv.querySelector( '.frm_message' );
1094 + if ( formCompleted ) {
1056 1095 triggerCustomEvent( document, 'frmFormCompleteBeforeReplace', { object, response } );
1057 1096 }
1058 1097 }
1059 1098
1060 1099 function removeAddedScripts( formContainer, formID ) {
1061 - const endReplace = jQuery( '.frm_end_ajax_' + formID );
1100 + const endReplace = document.querySelectorAll( `.frm_end_ajax_${ formID }` );
1062 1101 if ( endReplace.length ) {
1063 - formContainer.nextUntil( '.frm_end_ajax_' + formID ).remove();
1064 - endReplace.remove();
1102 + formContainer.nextUntil( `.frm_end_ajax_${ formID }` ).remove();
1103 + endReplace.forEach( el => el.remove() );
1065 1104 }
1066 1105 }
1067 1106
1068 1107 function maybeSlideOut( oldContent, newContent ) {
1069 - let c,
1070 - newClass = 'frm_slideout';
1071 - if ( newContent.indexOf( ' frm_slide' ) !== -1 ) {
1108 + let c;
1109 + let newClass = 'frm_slideout';
1110 + if ( newContent.includes( ' frm_slide' ) ) {
1072 1111 c = oldContent.children();
1073 - if ( newContent.indexOf( ' frm_going_back' ) !== -1 ) {
1112 + if ( newContent.includes( ' frm_going_back' ) ) {
1074 1113 newClass += ' frm_going_back';
1075 1114 }
1076 1115 c.removeClass( 'frm_going_back' );
1077 1116 c.addClass( newClass );
@@ -1081,25 +1120,23 @@
1081 1120 }
1082 1121
1083 1122 function addUrlParam( response ) {
1084 1123 let url;
1085 - if ( history.pushState && typeof response.page !== 'undefined' ) {
1124 + if ( history.pushState && response.page !== undefined ) {
1086 1125 url = addQueryVar( 'frm_page', response.page );
1087 - window.history.pushState( { html: response.html }, '', '?' + url );
1126 + window.history.pushState( { html: response.html }, '', `?${ url }` );
1088 1127 }
1089 1128 }
1090 1129
1091 1130 function addQueryVar( key, value ) {
1092 - let kvp, i, x;
1093 -
1094 1131 key = encodeURI( key );
1095 1132 value = encodeURI( value );
1096 1133
1097 - kvp = document.location.search.substr( 1 ).split( '&' );
1134 + const kvp = document.location.search.substr( 1 ).split( '&' );
1098 1135
1099 - i = kvp.length;
1136 + let i = kvp.length;
1100 1137 while ( i-- ) {
1101 - x = kvp[ i ].split( '=' );
1138 + const x = kvp[ i ].split( '=' );
1102 1139
1103 1140 if ( x[ 0 ] == key ) {
1104 1141 x[ 1 ] = value;
1105 1142 kvp[ i ] = x.join( '=' );
@@ -1114,49 +1151,59 @@
1114 1151 return kvp.join( '&' );
1115 1152 }
1116 1153
1117 1154 function addFieldError( $fieldCont, key, jsErrors ) {
1118 - let input, id, describedBy, roleString;
1119 - if ( $fieldCont.length && $fieldCont.is( ':visible' ) ) {
1120 - $fieldCont.addClass( 'frm_blank_field' );
1121 - input = $fieldCont.find( 'input, select, textarea' );
1122 - id = getErrorElementId( key, input.get( 0 ) );
1155 + const container = $fieldCont instanceof jQuery ? $fieldCont.get( 0 ) : $fieldCont;
1123 1156
1124 - describedBy = input.attr( 'aria-describedby' );
1157 + if ( ! container || container.offsetParent === null ) {
1158 + return;
1159 + }
1125 1160
1126 - if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1127 - frmThemeOverride_frmPlaceError( key, jsErrors );
1161 + container.classList.add( 'frm_blank_field' );
1162 + const inputs = container.querySelectorAll( 'input, select, textarea' );
1163 + const id = getErrorElementId( key, inputs[ 0 ] );
1164 +
1165 + let describedBy;
1166 +
1167 + if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1168 + frmThemeOverride_frmPlaceError( key, jsErrors );
1169 + } else {
1170 + let errorHtml;
1171 + if ( jsErrors[ key ].includes( '<div' ) ) {
1172 + errorHtml = jsErrors[ key ];
1128 1173 } else {
1129 - if ( -1 !== jsErrors[ key ].indexOf( '<div' ) ) {
1130 - $fieldCont.append(
1131 - jsErrors[ key ]
1132 - );
1133 - } else {
1134 - roleString = frm_js.include_alert_role ? 'role="alert"' : ''; // eslint-disable-line camelcase
1135 - $fieldCont.append( '<div class="frm_error" ' + roleString + ' id="' + id + '">' + jsErrors[ key ] + '</div>' );
1136 - }
1137 -
1138 - if ( typeof describedBy === 'undefined' ) {
1174 + const roleString = frm_js.include_alert_role ? 'role="alert"' : '';
1175 + errorHtml = `<div class="frm_error" ${ roleString } id="${ id }">${ jsErrors[ key ] }</div>`;
1176 + }
1177 + container.insertAdjacentHTML( 'beforeend', errorHtml );
1178 + inputs.forEach( input => {
1179 + describedBy = input.getAttribute( 'aria-describedby' );
1180 + if ( ! describedBy ) {
1139 1181 describedBy = id;
1140 - } else if ( describedBy.indexOf( id ) === -1 && describedBy.indexOf( 'frm_error_field_' ) === -1 ) {
1141 - if ( input.data( 'error-first' ) === 0 ) {
1142 - describedBy = describedBy + ' ' + id;
1182 + } else if ( ! describedBy.includes( id ) && ! describedBy.includes( 'frm_error_field_' ) ) {
1183 + const { errorFirst } = input.dataset;
1184 + if ( errorFirst === '0' ) {
1185 + describedBy = `${ describedBy } ${ id }`;
1143 1186 } else {
1144 - describedBy = id + ' ' + describedBy;
1187 + describedBy = `${ id } ${ describedBy }`;
1145 1188 }
1146 1189 }
1190 + input.setAttribute( 'aria-describedby', describedBy );
1191 + } );
1192 + }
1147 1193
1148 - input.attr( 'aria-describedby', describedBy );
1149 - }
1150 -
1151 - if ( [ 'radio', 'checkbox' ].includes( input.attr( 'type' ) ) ) {
1152 - input.closest( '[role="radiogroup"], [role="group"]' ).attr( 'aria-invalid', true );
1194 + inputs.forEach( input => {
1195 + if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1196 + const group = input.closest( '[role="radiogroup"], [role="group"]' );
1197 + if ( group ) {
1198 + group.setAttribute( 'aria-invalid', 'true' );
1199 + }
1153 1200 } else {
1154 - input.attr( 'aria-invalid', true );
1201 + input.setAttribute( 'aria-invalid', 'true' );
1155 1202 }
1203 + } );
1156 1204
1157 - jQuery( document ).trigger( 'frmAddFieldError', [ $fieldCont, key, jsErrors ] );
1158 - }
1205 + jQuery( document ).trigger( 'frmAddFieldError', [ jQuery( container ), key, jsErrors ] );
1159 1206 }
1160 1207
1161 1208 /**
1162 1209 * Get the ID to use for an error element added when submitting with AJAX.
@@ -1167,11 +1214,11 @@
1167 1214 */
1168 1215 function getErrorElementId( key, input ) {
1169 1216 if ( isNaN( key ) || ! input || ! input.id ) {
1170 1217 // If key isn't a number, assume it's already in the right format.
1171 - return 'frm_error_field_' + key;
1218 + return `frm_error_field_${ key }`;
1172 1219 }
1173 - return 'frm_error_' + input.id;
1220 + return `frm_error_${ input.id }`;
1174 1221 }
1175 1222
1176 1223 /**
1177 1224 * Removes errors before validating with JS.
@@ -1176,41 +1223,72 @@
1176 1223 /**
1177 1224 * Removes errors before validating with JS.
1178 1225 * This prevents issues with stale errors that has since been fixed.
1179 1226 *
1180 - * @param {Object} $fieldCont jQuery object.
1227 + * @param {HTMLElement|jQuery} fieldCont Field container element.
1181 1228 * @return {void}
1182 1229 */
1183 - function removeFieldError( $fieldCont ) {
1184 - const errorMessage = $fieldCont.find( '.frm_error' );
1185 - const errorId = errorMessage.attr( 'id' );
1186 - const input = $fieldCont.find( 'input, select, textarea' );
1187 - let describedBy = input.attr( 'aria-describedby' );
1230 + function removeFieldError( fieldCont ) {
1231 + const container = fieldCont instanceof jQuery ? fieldCont.get( 0 ) : fieldCont;
1232 + if ( ! container ) {
1233 + return;
1234 + }
1188 1235
1189 - const fieldContainer = $fieldCont.get( 0 );
1190 - if ( fieldContainer && fieldContainer.classList ) {
1191 - fieldContainer.classList.remove( 'frm_blank_field', 'has-error' );
1236 + const errorMessage = container.querySelector( '.frm_error' );
1237 + const input = container.querySelector( 'input, select, textarea' );
1238 +
1239 + container.classList.remove( 'frm_blank_field', 'has-error' );
1240 +
1241 + if ( input ) {
1242 + if ( 'true' === input.getAttribute( 'aria-invalid' ) ) {
1243 + input.setAttribute( 'aria-invalid', 'false' );
1244 + } else if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1245 + const group = input.closest( '[role="radiogroup"], [role="group"]' );
1246 + if ( group ) {
1247 + group.setAttribute( 'aria-invalid', 'false' );
1248 + }
1249 + }
1192 1250 }
1193 1251
1194 - if ( 'true' === input.attr( 'aria-invalid' ) ) {
1195 - input.attr( 'aria-invalid', false );
1196 - } else if ( [ 'radio', 'checkbox' ].includes( input.attr( 'type' ) ) ) {
1197 - input.closest( '[role="radiogroup"], [role="group"]' ).attr( 'aria-invalid', false );
1252 + if ( errorMessage ) {
1253 + removeElementFromInputDescribedBy( errorMessage );
1254 + errorMessage.remove();
1198 1255 }
1256 + }
1199 1257
1200 - errorMessage.remove();
1201 - input.removeAttr( 'aria-describedby' );
1258 + /**
1259 + * Updates the aria-describedby attribute, removing the target element ID.
1260 + *
1261 + * @since 6.32
1262 + *
1263 + * @param {HTMLElement} el The target element that is removed from the aria-describedby data.
1264 + * @return {void}
1265 + */
1266 + function removeElementFromInputDescribedBy( el ) {
1267 + document.querySelectorAll( `[aria-describedby*="${ el.id }"]` ).forEach( input => {
1268 + let ariaDescribedBy = input.getAttribute( 'aria-describedby' ).split( ' ' );
1269 + ariaDescribedBy = ariaDescribedBy.filter( value => {
1270 + const trimmedValue = value.trim();
1271 + return trimmedValue && trimmedValue !== el.id;
1272 + } );
1202 1273
1203 - if ( typeof describedBy !== 'undefined' ) {
1204 - describedBy = describedBy.replace( errorId, '' );
1205 - input.attr( 'aria-describedby', describedBy );
1206 - }
1274 + if ( ariaDescribedBy.length ) {
1275 + input.setAttribute( 'aria-describedby', ariaDescribedBy.join( ' ' ) );
1276 + return;
1277 + }
1278 + input.removeAttribute( 'aria-describedby' );
1279 + } );
1207 1280 }
1208 1281
1209 1282 function removeAllErrors() {
1210 - jQuery( '.form-field' ).removeClass( 'frm_blank_field has-error' );
1211 - jQuery( '.form-field .frm_error' ).replaceWith( '' );
1212 - jQuery( '.frm_error_style' ).remove();
1283 + document.querySelectorAll( '.form-field' ).forEach( field => {
1284 + field.classList.remove( 'frm_blank_field', 'has-error' );
1285 + } );
1286 + document.querySelectorAll( '.form-field .frm_error' ).forEach( el => {
1287 + removeElementFromInputDescribedBy( el );
1288 + el.remove();
1289 + } );
1290 + document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
1213 1291 }
1214 1292
1215 1293 /**
1216 1294 * @param {HTMLElement|Object} object Form object.
@@ -1233,9 +1311,9 @@
1233 1311 disableSaveDraft( $object );
1234 1312 }
1235 1313
1236 1314 function showLoadingIndicator( $object ) {
1237 - if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) {
1315 + if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) { // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1238 1316 addLoadingClass( $object );
1239 1317 $object.trigger( 'frmStartFormLoading' );
1240 1318 }
1241 1319 }
@@ -1242,44 +1320,43 @@
1242 1320
1243 1321 function addLoadingClass( $object ) {
1244 1322 const loadingClass = isGoingToPrevPage( $object ) ? 'frm_loading_prev' : 'frm_loading_form';
1245 1323
1246 - $object.addClass( loadingClass );
1324 + $object.addClass( loadingClass ); // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1247 1325 }
1248 1326
1249 1327 function isGoingToPrevPage( $object ) {
1250 - return ( typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object ) );
1328 + return typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object );
1251 1329 }
1252 1330
1253 1331 function removeSubmitLoading( _, enable, processesRunning ) {
1254 - let loadingForm;
1255 -
1256 1332 if ( processesRunning > 0 ) {
1257 1333 return;
1258 1334 }
1259 1335
1260 - loadingForm = jQuery( '.frm_loading_form' );
1261 - loadingForm.removeClass( 'frm_loading_form' );
1262 - loadingForm.removeClass( 'frm_loading_prev' );
1336 + document.querySelectorAll( '.frm_loading_form' ).forEach( function( form ) {
1337 + form.classList.remove( 'frm_loading_form', 'frm_loading_prev' );
1338 + jQuery( form ).trigger( 'frmEndFormLoading' );
1263 1339
1264 - loadingForm.trigger( 'frmEndFormLoading' );
1340 + if ( enable === 'enable' ) {
1341 + enableSubmitButton( form );
1342 + enableSaveDraft( form );
1343 + }
1344 + } );
1345 + }
1265 1346
1266 - if ( enable === 'enable' ) {
1267 - enableSubmitButton( loadingForm );
1268 - enableSaveDraft( loadingForm );
1347 + function showFileLoading( object ) {
1348 + const loading = document.getElementById( 'frm_loading' );
1349 + if ( ! loading ) {
1350 + return;
1269 1351 }
1270 - }
1271 1352
1272 - function showFileLoading( object ) {
1273 - let fileval,
1274 - loading = document.getElementById( 'frm_loading' );
1275 - if ( loading !== null ) {
1276 - fileval = jQuery( object ).find( 'input[type=file]' ).val();
1277 - if ( typeof fileval !== 'undefined' && fileval !== '' ) {
1278 - setTimeout( function() {
1279 - jQuery( loading ).fadeIn( 'slow' );
1280 - }, 2000 );
1281 - }
1353 + const fileInput = object.querySelector( 'input[type=file]' );
1354 + const fileval = fileInput ? fileInput.value : '';
1355 + if ( fileval !== '' ) {
1356 + setTimeout( function() {
1357 + jQuery( loading ).fadeIn( 'slow' ); // eslint-disable-line no-jquery/no-fade
1358 + }, 2000 );
1282 1359 }
1283 1360 }
1284 1361
1285 1362 /**********************************************
@@ -1287,9 +1364,9 @@
1287 1364 *********************************************/
1288 1365
1289 1366 function confirmClick() {
1290 1367 /*jshint validthis:true */
1291 - const message = jQuery( this ).data( 'frmconfirm' );
1368 + const message = this.dataset.frmconfirm;
1292 1369 return confirm( message );
1293 1370 }
1294 1371
1295 1372 /**
@@ -1297,11 +1374,12 @@
1297 1374 * If this is a match, the User is autofilling the input on a Webkit browser.
1298 1375 * We want to delete the Honeypot field, otherwise it will get triggered as spam on autocomplete.
1299 1376 */
1300 1377 function onHoneypotFieldChange() {
1301 - const css = jQuery( this ).css( 'box-shadow' );
1302 - if ( css.match( /inset/ ) ) {
1303 - this.parentNode.removeChild( this );
1378 + /*jshint validthis:true */
1379 + const css = window.getComputedStyle( this ).boxShadow;
1380 + if ( css?.match( /inset/ ) ) {
1381 + this.remove();
1304 1382 }
1305 1383 }
1306 1384
1307 1385 /**
@@ -1352,23 +1430,22 @@
1352 1430 return false;
1353 1431 }
1354 1432
1355 1433 function checkForErrorsAndMaybeSetFocus() {
1356 - let errors, element, timeoutCallback;
1357 -
1358 - if ( ! frm_js.focus_first_error ) { // eslint-disable-line camelcase
1434 + if ( ! frm_js.focus_first_error ) {
1359 1435 return;
1360 1436 }
1361 1437
1362 - errors = document.querySelectorAll( '.frm_form_field .frm_error' );
1438 + const errors = document.querySelectorAll( '.frm_form_field .frm_error' );
1363 1439 if ( ! errors.length ) {
1364 1440 return;
1365 1441 }
1366 1442
1367 - element = errors[ 0 ];
1443 + let element = errors[ 0 ];
1444 + let timeoutCallback;
1368 1445 do {
1369 1446 element = element.previousSibling;
1370 - if ( -1 !== [ 'input', 'select', 'textarea' ].indexOf( element.nodeName.toLowerCase() ) ) {
1447 + if ( [ 'input', 'select', 'textarea' ].includes( element.nodeName.toLowerCase() ) ) {
1371 1448 focusInput( element );
1372 1449 break;
1373 1450 }
1374 1451
@@ -1375,9 +1452,9 @@
1375 1452 if ( maybeFocusOnComboSubField( element ) ) {
1376 1453 break;
1377 1454 }
1378 1455
1379 - if ( 'undefined' !== typeof element.classList ) {
1456 + if ( element.classList !== undefined ) {
1380 1457 if ( element.classList.contains( 'html-active' ) ) {
1381 1458 timeoutCallback = function() {
1382 1459 const textarea = element.querySelector( 'textarea' );
1383 1460 if ( null !== textarea ) {
@@ -1430,9 +1507,9 @@
1430 1507 * @param {Function} handler Handler.
1431 1508 * @param {boolean | Object} options Options to be added to `addEventListener()` method. Default is `false`.
1432 1509 */
1433 1510 function documentOn( event, selector, handler, options ) {
1434 - if ( 'undefined' === typeof options ) {
1511 + if ( options === undefined ) {
1435 1512 options = false;
1436 1513 }
1437 1514
1438 1515 document.addEventListener( event, function( e ) {
@@ -1439,9 +1516,9 @@
1439 1516 let target;
1440 1517
1441 1518 // loop parent nodes from the target to the delegation node.
1442 1519 for ( target = e.target; target && target != this; target = target.parentNode ) {
1443 - if ( target && target.matches && target.matches( selector ) ) {
1520 + if ( target.matches && target.matches( selector ) ) {
1444 1521 handler.call( target, e );
1445 1522 break;
1446 1523 }
1447 1524 }
@@ -1448,27 +1525,23 @@
1448 1525 }, options );
1449 1526 }
1450 1527
1451 1528 function initFloatingLabels() {
1452 - let checkFloatLabel, checkDropdownLabel, runOnLoad, selector, floatClass;
1529 + const selector = '.frm-show-form .frm_inside_container input, .frm-show-form .frm_inside_container select, .frm-show-form .frm_inside_container textarea';
1530 + const floatClass = 'frm_label_float_top';
1453 1531
1454 - selector = '.frm-show-form .frm_inside_container input, .frm-show-form .frm_inside_container select, .frm-show-form .frm_inside_container textarea';
1455 - floatClass = 'frm_label_float_top';
1456 -
1457 - checkFloatLabel = function( input ) {
1458 - let container, shouldFloatTop, firstOpt;
1459 -
1460 - container = input.closest( '.frm_inside_container' );
1532 + const checkFloatLabel = function( input ) {
1533 + const container = input.closest( '.frm_inside_container' );
1461 1534 if ( ! container ) {
1462 1535 return;
1463 1536 }
1464 1537
1465 - shouldFloatTop = input.value || document.activeElement === input;
1538 + const shouldFloatTop = input.value || document.activeElement === input;
1466 1539
1467 1540 container.classList.toggle( floatClass, shouldFloatTop );
1468 1541
1469 1542 if ( 'SELECT' === input.tagName ) {
1470 - firstOpt = input.querySelector( 'option:first-child' );
1543 + const firstOpt = input.querySelector( 'option:first-child' );
1471 1544
1472 1545 if ( shouldFloatTop ) {
1473 1546 if ( firstOpt.hasAttribute( 'data-label' ) ) {
1474 1547 firstOpt.textContent = firstOpt.getAttribute( 'data-label' );
@@ -1480,10 +1553,10 @@
1480 1553 }
1481 1554 }
1482 1555 };
1483 1556
1484 - checkDropdownLabel = function() {
1485 - document.querySelectorAll( '.frm-show-form .frm_inside_container:not(.' + floatClass + ') select' ).forEach( function( input ) {
1557 + const checkDropdownLabel = function() {
1558 + document.querySelectorAll( `.frm-show-form .frm_inside_container:not(.${ floatClass }) select` ).forEach( function( input ) {
1486 1559 const firstOpt = input.querySelector( 'option:first-child' );
1487 1560
1488 1561 if ( firstOpt.textContent ) {
1489 1562 firstOpt.setAttribute( 'data-label', firstOpt.textContent );
@@ -1502,14 +1575,10 @@
1502 1575 true
1503 1576 );
1504 1577 } );
1505 1578
1506 - jQuery( document ).on( 'change', selector, function( event ) {
1507 - checkFloatLabel( event.target );
1508 - } );
1509 -
1510 - runOnLoad = function( firstLoad ) {
1511 - if ( firstLoad && document.activeElement && -1 !== [ 'INPUT', 'SELECT', 'TEXTAREA' ].indexOf( document.activeElement.tagName ) ) {
1579 + const runOnLoad = function( firstLoad ) {
1580 + if ( firstLoad && document.activeElement && [ 'INPUT', 'SELECT', 'TEXTAREA' ].includes( document.activeElement.tagName ) ) {
1512 1581 checkFloatLabel( document.activeElement );
1513 1582 } else if ( firstLoad ) {
1514 1583 document.querySelectorAll( '.frm_inside_container' ).forEach(
1515 1584 function( container ) {
@@ -1521,8 +1590,9 @@
1521 1590 );
1522 1591 }
1523 1592
1524 1593 checkDropdownLabel();
1594 + calcProductsTotal();
1525 1595 };
1526 1596
1527 1597 runOnLoad( true );
1528 1598
@@ -1555,10 +1625,10 @@
1555 1625 return true;
1556 1626 }
1557 1627
1558 1628 function maybeClearCustomValidityMessage( event, field ) {
1559 - let key,
1560 - isInvalid = false;
1629 + let key;
1630 + let isInvalid = false;
1561 1631
1562 1632 if ( ! shouldUpdateValidityMessage( field ) ) {
1563 1633 return;
1564 1634 }
@@ -1578,33 +1648,29 @@
1578 1648 }
1579 1649 }
1580 1650
1581 1651 function maybeShowNewTabFallbackMessage() {
1582 - let messageEl;
1583 -
1584 1652 if ( ! window.frmShowNewTabFallback ) {
1585 1653 return;
1586 1654 }
1587 1655
1588 - messageEl = document.querySelector( '#frm_form_' + frmShowNewTabFallback.formId + '_container .frm_message' );
1656 + const messageEl = document.querySelector( `#frm_form_${ frmShowNewTabFallback.formId }_container .frm_message` );
1589 1657 if ( ! messageEl ) {
1590 1658 return;
1591 1659 }
1592 1660
1593 - messageEl.insertAdjacentHTML( 'beforeend', ' ' + frmShowNewTabFallback.message );
1661 + messageEl.insertAdjacentHTML( 'beforeend', ` ${ frmShowNewTabFallback.message }` );
1594 1662 }
1595 1663
1596 1664 function setCustomValidityMessage() {
1597 - let forms, length, index;
1665 + const forms = document.getElementsByClassName( 'frm-show-form' );
1666 + const { length } = forms;
1598 1667
1599 - forms = document.getElementsByClassName( 'frm-show-form' );
1600 - length = forms.length;
1601 -
1602 - for ( index = 0; index < length; ++index ) {
1668 + for ( let index = 0; index < length; ++index ) {
1603 1669 forms[ index ].addEventListener(
1604 1670 'invalid',
1605 1671 function( event ) {
1606 - const target = event.target;
1672 + const { target } = event;
1607 1673
1608 1674 if ( shouldUpdateValidityMessage( target ) ) {
1609 1675 target.setCustomValidity( target.dataset.invmsg );
1610 1676 }
@@ -1618,9 +1684,9 @@
1618 1684 window.addEventListener( 'pageshow', function( event ) {
1619 1685 if ( event.persisted ) {
1620 1686 document.querySelectorAll( '.frm_loading_form' ).forEach(
1621 1687 function( form ) {
1622 - enableSubmitButton( jQuery( form ) );
1688 + enableSubmitButton( form );
1623 1689 }
1624 1690 );
1625 1691 removeSubmitLoading();
1626 1692 }
@@ -1646,9 +1712,9 @@
1646 1712 const uniqueKey = Array.from( window.crypto.getRandomValues( new Uint8Array( 8 ) ) )
1647 1713 .map( b => b.toString( 16 ).padStart( 2, '0' ) )
1648 1714 .join( '' );
1649 1715 const timestamp = Date.now().toString( 16 );
1650 - return uniqueKey + '-' + timestamp;
1716 + return `${ uniqueKey }-${ timestamp }`;
1651 1717 }
1652 1718
1653 1719 /**
1654 1720 * Animates the scroll position of the document.
@@ -1665,9 +1731,8 @@
1665 1731 document.documentElement.scrollTop = end;
1666 1732 return;
1667 1733 }
1668 1734
1669 - /* eslint-disable compat/compat */
1670 1735 const startTime = performance.now();
1671 1736 const step = currentTime => {
1672 1737 const progress = Math.min( ( currentTime - startTime ) / duration, 1 );
1673 1738 document.documentElement.scrollTop = start + ( ( end - start ) * progress );
@@ -1675,22 +1740,462 @@
1675 1740 requestAnimationFrame( step );
1676 1741 }
1677 1742 };
1678 1743 requestAnimationFrame( step );
1679 - /* eslint-enable compat/compat */
1680 1744 }
1681 1745
1746 + /**
1747 + * Make sure that the captcha label for a reCAPTCHA or Turnstile field matches the response input ID.
1748 + * This is determined dynamically, so we check for the ID after the input is rendered.
1749 + * hCaptcha is handled separately, in the frmCaptcha function as it is not rendered explicitly.
1750 + *
1751 + * @since 6.25.1
1752 + *
1753 + * @param {HTMLElement} captcha
1754 + * @return {void}
1755 + */
1756 + function maybeFixCaptchaLabel( captcha ) {
1757 + const form = captcha.closest( 'form' );
1758 + if ( ! form ) {
1759 + return;
1760 + }
1761 +
1762 + const label = form.querySelector( 'label[for="g-recaptcha-response"], label[for="cf-turnstile-response"]' );
1763 + const captchaResponse = form.querySelector( '[name="g-recaptcha-response"], [name="cf-turnstile-response"]' );
1764 +
1765 + if ( label && captchaResponse ) {
1766 + label.htmlFor = captchaResponse.id;
1767 + }
1768 + }
1769 +
1770 + /**
1771 + * Check to make sure the quantity field value is within the min and max values.
1772 + *
1773 + * @param {HTMLElement} input
1774 + * @return {number} The quantity value.
1775 + */
1776 + function checkQuantityFieldMinMax( input ) {
1777 + if ( '' === input.value ) {
1778 + // Leave the value if it is empty.
1779 + return 0;
1780 + }
1781 +
1782 + const val = parseFloat( input.value ? input.value.trim() : 0 );
1783 + if ( isNaN( val ) ) {
1784 + return 0;
1785 + }
1786 +
1787 + let max = input.hasAttribute( 'max' ) ? parseFloat( input.getAttribute( 'max' ) ) : 0;
1788 + let min = input.hasAttribute( 'min' ) ? parseFloat( input.getAttribute( 'min' ) ) : 0;
1789 +
1790 + max = isNaN( max ) ? 0 : max;
1791 + min = isNaN( min ) ? 0 : Math.max( 0, min );
1792 +
1793 + if ( val < min ) {
1794 + input.value = min;
1795 + return min;
1796 + }
1797 +
1798 + if ( 0 !== max && val > max ) {
1799 + input.value = max;
1800 + return max;
1801 + }
1802 +
1803 + return val;
1804 + }
1805 +
1806 + function triggerChange( input, fieldKey ) {
1807 + if ( fieldKey === undefined ) {
1808 + fieldKey = 'dependent';
1809 + }
1810 +
1811 + jQuery( input ).trigger( { type: 'change', selfTriggered: true, frmTriggered: fieldKey } );
1812 + }
1813 +
1814 + /**
1815 + * Calculates the total price.
1816 + *
1817 + * @param {Event|undefined} e The event object.
1818 + * @return {void}
1819 + */
1820 + function calcProductsTotal( e ) {
1821 + if ( 'object' === typeof frmProForm ) {
1822 + // Pro is installed, use the Pro JS.
1823 + return;
1824 + }
1825 +
1826 + if ( typeof __FRMCURR === 'undefined' ) {
1827 + return;
1828 + }
1829 +
1830 + const totalFields = document.querySelectorAll( '[data-frmtotal]' );
1831 + if ( ! totalFields.length ) {
1832 + return;
1833 + }
1834 +
1835 + const formTotals = [];
1836 +
1837 + totalFields.forEach( totalField => {
1838 + let total = 0;
1839 + const form = totalField.closest( 'form' );
1840 +
1841 + if ( ! form ) {
1842 + return;
1843 + }
1844 +
1845 + const formId = form.querySelector( 'input[name="form_id"]' ).value;
1846 + const currency = getCurrency( formId );
1847 +
1848 + if ( undefined !== formTotals[ formId ] ) {
1849 + total = formTotals[ formId ];
1850 + } else {
1851 + form.querySelectorAll( 'input[data-frmprice],select:has([data-frmprice])' ).forEach( function( input ) {
1852 + let quantity = 0;
1853 + let price = 0;
1854 + const isSingle = 'hidden' === input.type;
1855 +
1856 + if ( input.tagName === 'SELECT' ) {
1857 + if ( input.selectedIndex !== -1 ) {
1858 + price = input.options[ input.selectedIndex ].getAttribute( 'data-frmprice' );
1859 + }
1860 + } else {
1861 + if ( ! isSingle && ! input.matches( ':checked' ) ) {
1862 + return;
1863 + }
1864 + price = input.getAttribute( 'data-frmprice' );
1865 + }
1866 +
1867 + if ( ! price ) {
1868 + price = 0;
1869 + } else {
1870 + price = preparePrice( price, currency );
1871 + quantity = getQuantity( input );
1872 + price = parseFloat( quantity ) * parseFloat( price );
1873 + }
1874 +
1875 + if ( 'true' === input.getAttribute( 'data-frmdiscount' ) ) {
1876 + price = price * -1;
1877 + }
1878 +
1879 + total += price;
1880 + } );
1881 +
1882 + formTotals[ formId ] = total;
1883 + }
1884 +
1885 + total = isNaN( total ) ? 0 : total;
1886 +
1887 + // Set a decimal separator for currency if no default for it
1888 + currency.decimal_separator = currency.decimal_separator.trim(); // first remove unnecessary space(s)
1889 + if ( ! currency.decimal_separator.length ) {
1890 + currency.decimal_separator = '.';
1891 + }
1892 +
1893 + totalField.value = roundTotal( total, currency );
1894 + total = normalizeTotal( total, currency );
1895 +
1896 + // because of e.g. fields that might be using this field for calculations
1897 + triggerChange( totalField );
1898 +
1899 + total = formatCurrency( total, currency );
1900 + const formatted = totalField.previousElementSibling;
1901 + if ( formatted?.matches( '.frm_total_formatted' ) ) {
1902 + // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1903 + formatted.innerHTML = total;
1904 + return;
1905 + }
1906 +
1907 + const formattedEls = totalField.closest( '.frm_form_field' ).querySelectorAll( '.frm_total_formatted' );
1908 + formattedEls.forEach( formattedEl => {
1909 + // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1910 + formattedEl.innerHTML = total;
1911 + } );
1912 + } );
1913 + }
1914 +
1915 + /**
1916 + * Round total and maybe add trailing zeros so formatCurrency has a proper format to work with.
1917 + *
1918 + * @param {number} total The total amount to normalize.
1919 + * @param {Object} currency The currency object containing decimal information.
1920 + * @return {string} The normalized total amount.
1921 + */
1922 + function normalizeTotal( total, currency ) {
1923 + const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1924 +
1925 + total = roundTotal( total, currency );
1926 +
1927 + return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal );
1928 + }
1929 +
1930 + function roundTotal( total, currency ) {
1931 + const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1932 +
1933 + if ( ! isLargeTotal ) {
1934 + const { decimals } = currency;
1935 + total = decimals > 0 ? round10( total, decimals ) : Math.ceil( total );
1936 + }
1937 +
1938 + return total;
1939 + }
1940 +
1941 + function round10( value, decimals ) {
1942 + return Number( `${ Math.round( `${ value }e${ decimals }` ) }e-${ decimals }` );
1943 + }
1944 +
1945 + /**
1946 + * Format a numeric value according to the specified currency format settings.
1947 + *
1948 + * @param {string} total The numeric value to format.
1949 + * @param {Object} currency The currency formatting configuration.
1950 + * @return {string} The formatted currency string.
1951 + */
1952 + function formatCurrency( total, currency ) {
1953 + total = maybeAddTrailingZeroToPrice( total, currency );
1954 + if ( total.length && ( total[ total.length - 1 ] === '.' || total[ total.length - 1 ] === currency.decimal_separator ) ) {
1955 + total = total.substr( 0, total.length - 1 );
1956 + }
1957 +
1958 + total = maybeRemoveTrailingZerosFromPrice( total, currency );
1959 + total = addThousands( total, currency );
1960 +
1961 + const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : '';
1962 + const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : '';
1963 +
1964 + return `${ leftSymbol }${ total }${ rightSymbol }`;
1965 + }
1966 +
1967 + /**
1968 + * Gets currency from form id.
1969 + *
1970 + * @param {number} formId Form ID.
1971 + * @return {Object} Currency object.
1972 + */
1973 + function getCurrency( formId ) {
1974 + if ( undefined !== window.__FRMCURR && undefined !== window.__FRMCURR[ formId ] ) {
1975 + return window.__FRMCURR[ formId ];
1976 + }
1977 +
1978 + return {
1979 + symbol_left: '$',
1980 + symbol_right: '',
1981 + symbol_padding: '',
1982 + thousand_separator: ',',
1983 + decimal_separator: '.',
1984 + decimals: 2,
1985 + };
1986 + }
1987 +
1988 + /**
1989 + * Gets quantity.
1990 + *
1991 + * @param {HTMLElement} field The field element.
1992 + * @return {number} The quantity.
1993 + */
1994 + function getQuantity( field ) {
1995 + const fieldID = frmFrontForm.getFieldId( field, false );
1996 + if ( ! fieldID ) {
1997 + return 0;
1998 + }
1999 +
2000 + const quantityField = getQuantityField( field, fieldID );
2001 + if ( ! quantityField ) {
2002 + // If there is no quantity field, assume 1.
2003 + return 1;
2004 + }
2005 +
2006 + return checkQuantityFieldMinMax( quantityField );
2007 + }
2008 +
2009 + /**
2010 + * Gets quantity field.
2011 + *
2012 + * @param {HTMLElement} element The element.
2013 + * @param {number} fieldID The field ID.
2014 + * @return {HTMLElement|null} The quantity field.
2015 + */
2016 + function getQuantityField( element, fieldID ) {
2017 + const quantityFields = element.closest( 'form' ).querySelectorAll( '[data-frmproduct]' );
2018 + if ( ! quantityFields.length ) {
2019 + return null;
2020 + }
2021 +
2022 + fieldID = fieldID.toString();
2023 +
2024 + return Array.from( quantityFields ).find( element => {
2025 + let ids;
2026 +
2027 + ids = JSON.parse( element.getAttribute( 'data-frmproduct' ).trim() );
2028 + if ( '' === ids ) {
2029 + return false;
2030 + }
2031 +
2032 + // Convert to array if necessary because of existing fields that are already using single product fields.
2033 + ids = 'string' === typeof ids ? [ ids ] : ids;
2034 + return ids.includes( fieldID );
2035 + } );
2036 + }
2037 +
2038 + /**
2039 + * Prepare a price for calculation.
2040 + *
2041 + * @param {number|string} price The price to prepare.
2042 + * @param {Object} currency The currency object containing decimal information.
2043 + * @return {string} The prepared price.
2044 + */
2045 + function preparePrice( price, currency ) {
2046 + if ( ! price ) {
2047 + return 0;
2048 + }
2049 + price = `${ price }`; // convert to string just to be sure
2050 +
2051 + const regex = getRegexForPrice( currency );
2052 +
2053 + const matches = price.match( regex );
2054 + if ( null === matches ) {
2055 + return 0;
2056 + }
2057 +
2058 + price = matches.length ? matches[ matches.length - 1 ] : 0;
2059 + price = price.trim();
2060 +
2061 + // Fix issues with parsing Fr.15.00. The regex catches .15.00.
2062 + // This checks for the leading decimal and removes it.
2063 + if ( currency.decimal_separator === '.' && 3 === price.split( '.' ).length && price[ 0 ] === '.' ) {
2064 + price = price.substr( 1 );
2065 + }
2066 +
2067 + if ( price ) {
2068 + price = maybeUseDecimal( price, currency );
2069 + price = price.split( currency.thousand_separator ).join( '' ).replace( currency.decimal_separator, '.' );
2070 + }
2071 +
2072 + return price;
2073 + }
2074 +
2075 + /**
2076 + * @param {Object} currency The currency object.
2077 + * @return {RegExp} The regular expression object.
2078 + */
2079 + function getRegexForPrice( currency ) {
2080 + let regexString = '[0-9,.';
2081 +
2082 + if ( currency.thousand_separator !== '.' && currency.thousand_separator !== ',' ) {
2083 + regexString += currency.thousand_separator;
2084 + }
2085 + if ( currency.decimal_separator !== '.' && currency.decimal_separator !== ',' ) {
2086 + regexString += currency.decimal_separator;
2087 + }
2088 +
2089 + regexString += ']*\\.?\\,?[0-9]+';
2090 +
2091 + return new RegExp( regexString, 'g' );
2092 + }
2093 +
2094 + /**
2095 + * Maybe replace the decimal separator with the currency's decimal separator.
2096 + *
2097 + * @param {string} price The price string.
2098 + * @param {Object} currency The currency object.
2099 + * @return {string} The modified price string.
2100 + */
2101 + function maybeUseDecimal( price, currency ) {
2102 + let usedForDecimal;
2103 + let priceParts;
2104 + if ( '.' === currency.thousand_separator ) {
2105 + priceParts = price.split( '.' );
2106 + usedForDecimal = 2 === priceParts.length && 2 === priceParts[ 1 ].length;
2107 + if ( usedForDecimal ) {
2108 + price = price.replace( '.', currency.decimal_separator );
2109 + }
2110 + }
2111 + return price;
2112 + }
2113 +
2114 + /**
2115 + * Add trailing zeros to a price if necessary and replace the decimal separator.
2116 + *
2117 + * @param {number|string} price The price to format.
2118 + * @param {Object} currency The currency object containing the decimal separator.
2119 + * @param {boolean} [force=false] Whether to force processing even if the price is not a number.
2120 + * @return {string} The formatted price string.
2121 + */
2122 + function maybeAddTrailingZeroToPrice( price, currency, force = false ) {
2123 + if ( 'number' !== typeof price && ! force ) {
2124 + return price;
2125 + }
2126 +
2127 + price = String( price ); // first convert to string
2128 + const pos = price.indexOf( '.' );
2129 +
2130 + if ( pos === -1 ) {
2131 + price = `${ price }.`;
2132 +
2133 + for ( let n = 0; n < currency.decimals; ++n ) {
2134 + price += '0';
2135 + }
2136 + } else {
2137 + const decimalsString = price.substring( pos + 1 );
2138 + if ( decimalsString.length < currency.decimals ) {
2139 + if ( decimalsString.length < 2 ) {
2140 + price += '0';
2141 + }
2142 +
2143 + for ( let n = 2; n < currency.decimals; ++n ) {
2144 + price += '0';
2145 + }
2146 + }
2147 + }
2148 +
2149 + return price.replace( '.', currency.decimal_separator );
2150 + }
2151 +
2152 + /**
2153 + * Format a numeric string by adding thousand separators.
2154 + *
2155 + * @param {string|number} price The numeric value to format.
2156 + * @param {Object} options Formatting options.
2157 + * @param {string} options.decimal_separator Character used as decimal separator.
2158 + * @param {string} options.thousand_separator Character used as thousand separator.
2159 + *
2160 + * @return {string} The price string with thousand separators.
2161 + */
2162 + function addThousands( price, options ) {
2163 + const split = options.decimal_separator === ''
2164 + ? [ price.toString() ]
2165 + : price.split( options.decimal_separator );
2166 +
2167 + if ( options.thousand_separator ) {
2168 + split[ 0 ] = split[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, options.thousand_separator );
2169 + }
2170 +
2171 + return split.join( options.decimal_separator );
2172 + }
2173 +
2174 + /**
2175 + * Maybe remove trailing zeros from a price string.
2176 + *
2177 + * @param {string} price The price string.
2178 + * @param {Object} currency The currency data.
2179 + *
2180 + * @return {string} The price string with trailing zeros removed.
2181 + */
2182 + function maybeRemoveTrailingZerosFromPrice( price, currency ) {
2183 + const split = price.split( currency.decimal_separator );
2184 + if ( 2 !== split.length || split[ 1 ].length <= currency.decimals ) {
2185 + return price;
2186 + }
2187 + if ( 0 === currency.decimals ) {
2188 + return split[ 0 ];
2189 + }
2190 + return `${ split[ 0 ] }${ currency.decimal_separator }${ split[ 1 ].substr( 0, currency.decimals ) }`;
2191 + }
2192 +
1682 2193 return {
1683 - init: function() {
2194 + init() {
1684 2195 jQuery( document ).off( 'submit.formidable', '.frm-show-form' );
1685 2196 jQuery( document ).on( 'submit.formidable', '.frm-show-form', frmFrontForm.submitForm );
1686 2197
1687 - jQuery( '.frm-show-form input[onblur], .frm-show-form textarea[onblur]' ).each( function() {
1688 - if ( jQuery( this ).val() === '' ) {
1689 - jQuery( this ).trigger( 'blur' );
1690 - }
1691 - } );
1692 -
1693 2198 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 );
1694 2199
1695 2200 jQuery( document ).on( 'change', '.frm_verify[id^=field_]', onHoneypotFieldChange );
1696 2201
@@ -1717,8 +2222,13 @@
1717 2222 jQuery( document ).on(
1718 2223 'frmPageChanged',
1719 2224 destroyhCaptcha
1720 2225 );
2226 +
2227 + jQuery( document ).on( 'frmAfterAddRow frmAfterRemoveRow', calcProductsTotal );
2228 + jQuery( document ).on( 'change', '[type="checkbox"][data-frmprice],[type="radio"][data-frmprice],[type="hidden"][data-frmprice],select:has([data-frmprice])', calcProductsTotal );
2229 + jQuery( document ).on( 'keyup change', '[data-frmproduct],[type="text"][data-frmprice]', calcProductsTotal );
2230 + calcProductsTotal();
1721 2231 },
1722 2232
1723 2233 getFieldId,
1724 2234
@@ -1728,9 +2238,9 @@
1728 2238 * @param {HTMLElement} captcha
1729 2239 * @param {string} captchaSelector
1730 2240 * @return {void}
1731 2241 */
1732 - renderCaptcha: function( captcha, captchaSelector ) {
2242 + renderCaptcha( captcha, captchaSelector ) {
1733 2243 const rendered = captcha.getAttribute( 'data-rid' ) !== null;
1734 2244 if ( rendered ) {
1735 2245 return;
1736 2246 }
@@ -1737,9 +2247,9 @@
1737 2247
1738 2248 const size = captcha.getAttribute( 'data-size' );
1739 2249 const params = {
1740 2250 sitekey: captcha.getAttribute( 'data-sitekey' ),
1741 - size: size,
2251 + size,
1742 2252 theme: captcha.getAttribute( 'data-theme' )
1743 2253 };
1744 2254
1745 2255 if ( size === 'invisible' ) {
@@ -1755,25 +2265,28 @@
1755 2265 };
1756 2266 }
1757 2267
1758 2268 const activeCaptcha = getSelectedCaptcha( captchaSelector );
1759 - const captchaContainer = typeof turnstile !== 'undefined' && turnstile === activeCaptcha ? '#' + captcha.id : captcha.id;
2269 + const captchaContainer = typeof turnstile !== 'undefined' && turnstile === activeCaptcha ? `#${ captcha.id }` : captcha.id;
1760 2270 const captchaID = activeCaptcha.render( captchaContainer, params );
1761 2271
1762 2272 captcha.setAttribute( 'data-rid', captchaID );
2273 +
2274 + maybeFixCaptchaLabel( captcha );
1763 2275 },
1764 2276
1765 - afterSingleRecaptcha: function() {
1766 - const object = jQuery( '.frm-show-form .g-recaptcha' ).closest( 'form' )[ 0 ];
2277 + afterSingleRecaptcha() {
2278 + const recaptcha = document.querySelector( '.frm-show-form .g-recaptcha' );
2279 + const object = recaptcha ? recaptcha.closest( 'form' ) : null;
1767 2280 frmFrontForm.submitFormNow( object );
1768 2281 },
1769 2282
1770 - afterRecaptcha: function( _, formID ) {
1771 - const object = jQuery( '#frm_form_' + formID + '_container form' )[ 0 ];
2283 + afterRecaptcha( _, formID ) {
2284 + const object = document.querySelector( `#frm_form_${ formID }_container form` );
1772 2285 frmFrontForm.submitFormNow( object );
1773 2286 },
1774 2287
1775 - submitForm: function( e ) {
2288 + submitForm( e ) {
1776 2289 frmFrontForm.submitFormManual( e, this );
1777 2290 },
1778 2291
1779 2292 /**
@@ -1780,21 +2293,10 @@
1780 2293 * @param {Event} e
1781 2294 * @param {HTMLElement} object The form object that is being submitted.
1782 2295 * @return {void}
1783 2296 */
1784 - submitFormManual: function( e, object ) {
1785 - let isPro, errors,
1786 - invisibleRecaptcha = hasInvisibleRecaptcha( object ),
1787 - classList = object.className.trim().split( /\s+/gi );
1788 -
1789 - if ( classList && invisibleRecaptcha.length < 1 ) {
1790 - isPro = classList.indexOf( 'frm_pro_form' ) > -1;
1791 - if ( ! isPro ) {
1792 - return;
1793 - }
1794 - }
1795 -
1796 - if ( jQuery( 'body' ).hasClass( 'wp-admin' ) && jQuery( object ).closest( '.frmapi-form' ).length < 1 ) {
2297 + submitFormManual( e, object ) {
2298 + if ( document.body.classList.contains( 'wp-admin' ) && ! object.closest( '.frmapi-form' ) ) {
1797 2299 return;
1798 2300 }
1799 2301
1800 2302 e.preventDefault();
@@ -1802,14 +2304,16 @@
1802 2304 if ( typeof frmProForm !== 'undefined' && typeof frmProForm.submitAllowed === 'function' && ! frmProForm.submitAllowed( object ) ) {
1803 2305 return;
1804 2306 }
1805 2307
1806 - errors = frmFrontForm.validateFormSubmit( object );
2308 + const errors = frmFrontForm.validateFormSubmit( object );
1807 2309 if ( Object.keys( errors ).length !== 0 ) {
1808 2310 return;
1809 2311 }
1810 2312
1811 - if ( invisibleRecaptcha.length ) {
2313 + const invisibleRecaptcha = hasInvisibleRecaptcha( object );
2314 +
2315 + if ( invisibleRecaptcha ) {
1812 2316 showLoadingIndicator( jQuery( object ) );
1813 2317 executeInvisibleRecaptcha( invisibleRecaptcha );
1814 2318 } else {
1815 2319 showSubmitLoading( jQuery( object ) );
@@ -1817,11 +2321,12 @@
1817 2321 frmFrontForm.submitFormNow( object );
1818 2322 }
1819 2323 },
1820 2324
1821 - submitFormNow: function( object ) {
1822 - let hasFileFields, antispamInput,
1823 - classList = object.className.trim().split( /\s+/gi );
2325 + submitFormNow( object ) {
2326 + let hasFileFields;
2327 + let antispamInput;
2328 + const classList = object.className.trim().split( /\s+/gi );
1824 2329
1825 2330 if ( object.hasAttribute( 'data-token' ) && null === object.querySelector( '[name="antispam_token"]' ) ) {
1826 2331 // include the antispam token on form submit.
1827 2332 antispamInput = document.createElement( 'input' );
@@ -1827,9 +2332,9 @@
1827 2332 antispamInput = document.createElement( 'input' );
1828 2333 antispamInput.type = 'hidden';
1829 2334 antispamInput.name = 'antispam_token';
1830 2335 antispamInput.value = object.getAttribute( 'data-token' );
1831 - object.appendChild( antispamInput );
2336 + object.append( antispamInput );
1832 2337 }
1833 2338
1834 2339 // Add a unique ID, used for duplicate checks.
1835 2340 const uniqueIDInput = document.createElement( 'input' );
@@ -1835,16 +2340,16 @@
1835 2340 const uniqueIDInput = document.createElement( 'input' );
1836 2341 uniqueIDInput.type = 'hidden';
1837 2342 uniqueIDInput.name = 'unique_id';
1838 2343 uniqueIDInput.value = getUniqueKey();
1839 - object.appendChild( uniqueIDInput );
2344 + object.append( uniqueIDInput );
1840 2345
1841 - if ( classList.indexOf( 'frm_ajax_submit' ) > -1 ) {
1842 - hasFileFields = jQuery( object ).find( 'input[type="file"]' ).filter( function() {
1843 - return !! this.value;
1844 - } ).length;
2346 + if ( classList.includes( 'frm_ajax_submit' ) ) {
2347 + const fileInputs = object.querySelectorAll( 'input[type="file"]' );
2348 + hasFileFields = Array.from( fileInputs ).filter( input => !! input.value ).length;
1845 2349 if ( hasFileFields < 1 ) {
1846 - const action = jQuery( object ).find( 'input[name="frm_action"]' ).val();
2350 + const actionInput = object.querySelector( 'input[name="frm_action"]' );
2351 + const action = actionInput ? actionInput.value : '';
1847 2352 frmFrontForm.checkFormErrors( object, action );
1848 2353 } else {
1849 2354 object.submit();
1850 2355 }
@@ -1857,10 +2362,11 @@
1857 2362 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
1858 2363 *
1859 2364 * @return {Array} List of errors.
1860 2365 */
1861 - validateFormSubmit: function( object ) {
1862 - if ( typeof tinyMCE !== 'undefined' && jQuery( object ).find( '.wp-editor-wrap' ).length ) {
2366 + validateFormSubmit( object ) {
2367 + const form = object instanceof jQuery ? object.get( 0 ) : object;
2368 + if ( typeof tinyMCE !== 'undefined' && form?.querySelector( '.wp-editor-wrap' ) ) {
1863 2369 tinyMCE.triggerSave();
1864 2370 }
1865 2371
1866 2372 jsErrors = [];
@@ -1879,14 +2385,17 @@
1879 2385 /**
1880 2386 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
1881 2387 * @return {Array} List of errors.
1882 2388 */
1883 - getAjaxFormErrors: function( object ) {
1884 - let customErrors, key;
2389 + getAjaxFormErrors( object ) {
2390 + let customErrors;
2391 + let key;
2392 + const form = object instanceof jQuery ? object.get( 0 ) : object;
1885 2393
1886 2394 jsErrors = validateForm( object );
1887 2395 if ( typeof frmThemeOverride_jsErrors === 'function' ) { // eslint-disable-line camelcase
1888 - const action = jQuery( object ).find( 'input[name="frm_action"]' ).val();
2396 + const actionInput = form ? form.querySelector( 'input[name="frm_action"]' ) : null;
2397 + const action = actionInput ? actionInput.value : '';
1889 2398 customErrors = frmThemeOverride_jsErrors( action, object );
1890 2399 if ( Object.keys( customErrors ).length ) {
1891 2400 for ( key in customErrors ) {
1892 2401 jsErrors[ key ] = customErrors[ key ];
@@ -1905,17 +2414,18 @@
1905 2414 /**
1906 2415 * @param {HTMLElement|Object} object Form object. This might be a jQuery object.
1907 2416 * @return {void}
1908 2417 */
1909 - addAjaxFormErrors: function( object ) {
1910 - let key, $fieldCont;
2418 + addAjaxFormErrors( object ) {
2419 + let key;
2420 + const form = object instanceof jQuery ? object.get( 0 ) : object;
1911 2421 removeAllErrors();
1912 2422
1913 2423 for ( key in jsErrors ) {
1914 - $fieldCont = jQuery( object ).find( '#frm_field_' + key + '_container' );
2424 + const fieldCont = form ? form.querySelector( `#frm_field_${ key }_container` ) : null;
1915 2425
1916 - if ( $fieldCont.length ) {
1917 - addFieldError( $fieldCont, key, jsErrors );
2426 + if ( fieldCont ) {
2427 + addFieldError( fieldCont, key, jsErrors );
1918 2428 } else {
1919 2429 // we are unable to show the error, so remove it
1920 2430 delete jsErrors[ key ];
1921 2431 }
@@ -1929,23 +2439,28 @@
1929 2439 checkRequiredField,
1930 2440 showSubmitLoading,
1931 2441 removeSubmitLoading,
1932 2442
1933 - scrollToID: function( id ) {
2443 + scrollToID( id ) {
1934 2444 const object = jQuery( document.getElementById( id ) );
1935 2445 frmFrontForm.scrollMsg( object, false );
1936 2446 },
1937 2447
1938 - scrollMsg: function( id, object, animate ) {
1939 - let newPos, m, b, screenTop, screenBottom,
1940 - scrollObj = '';
1941 - if ( typeof object === 'undefined' ) {
1942 - scrollObj = jQuery( document.getElementById( 'frm_form_' + id + '_container' ) );
2448 + scrollMsg( id, object, animate ) {
2449 + let newPos;
2450 + let screenTop;
2451 + let screenBottom;
2452 + let scrollObj = '';
2453 +
2454 + if ( object === undefined ) {
2455 + scrollObj = jQuery( document.getElementById( `frm_form_${ id }_container` ) );
1943 2456 if ( scrollObj.length < 1 ) {
1944 2457 return;
1945 2458 }
1946 2459 } else if ( typeof id === 'string' ) {
1947 - scrollObj = jQuery( object ).find( '#frm_field_' + id + '_container' );
2460 + const formEl = object instanceof jQuery ? object.get( 0 ) : object;
2461 + const fieldEl = formEl ? formEl.querySelector( `#frm_field_${ id }_container` ) : null;
2462 + scrollObj = fieldEl ? jQuery( fieldEl ) : jQuery();
1948 2463 } else {
1949 2464 scrollObj = id;
1950 2465 }
1951 2466
@@ -1950,17 +2465,17 @@
1950 2465 }
1951 2466
1952 2467 jQuery( scrollObj ).trigger( 'focus' );
1953 2468 newPos = scrollObj.offset().top;
1954 - if ( ! newPos || frm_js.offset === '-1' ) { // eslint-disable-line camelcase
2469 + if ( ! newPos || frm_js.offset === '-1' ) {
1955 2470 return;
1956 2471 }
1957 - newPos = newPos - frm_js.offset; // eslint-disable-line camelcase
2472 + newPos = newPos - frm_js.offset;
1958 2473
1959 - m = jQuery( 'html' ).css( 'margin-top' );
1960 - b = jQuery( 'body' ).css( 'margin-top' );
1961 - if ( m || b ) {
1962 - newPos = newPos - parseInt( m ) - parseInt( b );
2474 + const docMarginTop = getComputedStyle( document.documentElement ).marginTop;
2475 + const bodyMarginTop = getComputedStyle( document.body ).marginTop;
2476 + if ( docMarginTop || bodyMarginTop ) {
2477 + newPos = newPos - parseInt( docMarginTop ) - parseInt( bodyMarginTop );
1963 2478 }
1964 2479
1965 2480 if ( newPos && window.innerHeight ) {
1966 2481 screenTop = document.documentElement.scrollTop || document.body.scrollTop;
@@ -1967,9 +2482,9 @@
1967 2482 screenBottom = screenTop + window.innerHeight;
1968 2483
1969 2484 if ( newPos > screenBottom || newPos < screenTop ) {
1970 2485 // Not in view
1971 - if ( typeof animate === 'undefined' ) {
2486 + if ( animate === undefined ) {
1972 2487 document.documentElement.scrollTop = newPos;
1973 2488 } else {
1974 2489 animateScroll( screenTop, newPos, 500 );
1975 2490 }
@@ -1977,13 +2492,13 @@
1977 2492 }
1978 2493 }
1979 2494 },
1980 2495
1981 - fieldValueChanged: function( e ) {
2496 + fieldValueChanged( e ) {
1982 2497 /*jshint validthis:true */
1983 2498
1984 2499 const fieldId = frmFrontForm.getFieldId( this, false );
1985 - if ( ! fieldId || typeof fieldId === 'undefined' ) {
2500 + if ( ! fieldId ) {
1986 2501 return;
1987 2502 }
1988 2503
1989 2504 if ( e.frmTriggered && e.frmTriggered == fieldId ) {
@@ -1996,9 +2511,9 @@
1996 2511 maybeValidateChange( this );
1997 2512 }
1998 2513 },
1999 2514
2000 - escapeHtml: function( text ) {
2515 + escapeHtml( text ) {
2001 2516 console.warn( 'DEPRECATED: function frmFrontForm.escapeHtml in v6.17' );
2002 2517 return text
2003 2518 .replace( /&/g, '&amp;' )
2004 2519 .replace( /</g, '&lt;' )
@@ -2006,31 +2521,9 @@
2006 2521 .replace( /"/g, '&quot;' )
2007 2522 .replace( /'/g, '&#039;' );
2008 2523 },
2009 2524
2010 - /**
2011 - * This function was used in old back end code in v2.0.
2012 - *
2013 - * @param {string} classes
2014 - * @return {void}
2015 - */
2016 - invisible: function( classes ) {
2017 - console.warn( 'DEPRECATED: function frmFrontForm.invisible in v6.16.3' );
2018 - jQuery( classes ).css( 'visibility', 'hidden' );
2019 - },
2020 -
2021 - /**
2022 - * This function was used in old back end code in v2.0.
2023 - *
2024 - * @param {string} classes
2025 - * @return {void}
2026 - */
2027 - visible: function( classes ) {
2028 - console.warn( 'DEPRECATED: function frmFrontForm.visible in v6.16.3' );
2029 - jQuery( classes ).css( 'visibility', 'visible' );
2030 - },
2031 -
2032 - triggerCustomEvent: triggerCustomEvent,
2525 + triggerCustomEvent,
2033 2526 documentOn
2034 2527 };
2035 2528 }
2036 2529
@@ -2043,13 +2536,32 @@
2043 2536 function frmRecaptcha() {
2044 2537 frmCaptcha( '.frm-g-recaptcha' );
2045 2538 }
2046 2539
2540 +function frmHcaptcha() {
2541 + frmCaptcha( '.h-captcha' );
2542 +}
2543 +
2047 2544 function frmTurnstile() {
2048 - frmCaptcha( '.cf-turnstile' );
2545 + frmCaptcha( '.frm-cf-turnstile' );
2049 2546 }
2050 2547
2051 2548 function frmCaptcha( captchaSelector ) {
2549 + if ( '.h-captcha' === captchaSelector ) {
2550 + // hCaptcha is still rendered implicitly, so we only want to handle the label and exit early.
2551 + // Match the hcaptcha labels to the hcaptcha response fields.
2552 + const captchaLabels = document.querySelectorAll( 'label[for="h-captcha-response"]' );
2553 + if ( captchaLabels.length ) {
2554 + captchaLabels.forEach( label => {
2555 + const captchaResponse = label.closest( 'form' )?.querySelector( '[name="h-captcha-response"]' );
2556 + if ( captchaResponse ) {
2557 + label.htmlFor = captchaResponse.id;
2558 + }
2559 + } );
2560 + }
2561 + return;
2562 + }
2563 +
2052 2564 let c;
2053 2565 const captchas = document.querySelectorAll( captchaSelector );
2054 2566 const cl = captchas.length;
2055 2567 for ( c = 0; c < cl; c++ ) {
@@ -2058,9 +2570,9 @@
2058 2570 const captcha = captchas[ c ];
2059 2571 if ( ! formIsVisible ) {
2060 2572 // If the form is not visible, try again later in 400ms.
2061 2573 // This fixes issues where the form fades visible on page load.
2062 - // Or whne the form is inside of a modal.
2574 + // Or when the form is inside of a modal.
2063 2575 const interval = setInterval(
2064 2576 function() {
2065 2577 if ( closestForm && closestForm.offsetParent !== null ) {
2066 2578 frmFrontForm.renderCaptcha( captcha, captchaSelector );
@@ -2078,9 +2590,9 @@
2078 2590 function getSelectedCaptcha( captchaSelector ) {
2079 2591 if ( captchaSelector === '.frm-g-recaptcha' ) {
2080 2592 return grecaptcha;
2081 2593 }
2082 - if ( document.querySelector( '.cf-turnstile' ) ) {
2594 + if ( document.querySelector( '.frm-cf-turnstile' ) ) {
2083 2595 return turnstile;
2084 2596 }
2085 2597 return hcaptcha;
2086 2598 }