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