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