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 +74 -31 6.30 → trunk View file →
@@ -281,11 +281,14 @@
281 281 if ( field.type === 'url' ) {
282 282 maybeAddHttpsToUrl( field );
283 283 }
284 284 const form = field.closest( 'form' );
285 - if ( form && hasClass( form, 'frm_js_validate' ) ) {
286 - validateField( field );
285 + if ( ! form ) {
286 + return;
287 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' ) );
288 291 }
289 292
290 293 /**
291 294 * @param {HTMLElement} field
@@ -300,13 +303,18 @@
300 303
301 304 /**
302 305 * Validate a field with JS.
303 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 + *
304 311 * @param {HTMLElement} field
312 + * @param {boolean} addErrors Whether to add new errors. Defaults to `true`.
305 313 *
306 314 * @return {void}
307 315 */
308 - function validateField( field ) {
316 + function validateField( field, addErrors = true ) {
309 317 let errors;
310 318 let key;
311 319
312 320 errors = [];
@@ -324,13 +332,20 @@
324 332 if ( errors.length < 1 ) {
325 333 validateFieldValue( field, errors, false );
326 334 }
327 335
328 - removeFieldError( fieldContainer );
329 - if ( Object.keys( errors ).length > 0 ) {
330 - for ( key in errors ) {
331 - addFieldError( fieldContainer, 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 + }
332 344 }
345 + } else if ( ! hasErrors ) {
346 + // JS validation is off, so only remove existing errors once the field passes validation.
347 + removeFieldError( fieldContainer );
333 348 }
334 349 }
335 350
336 351 /**
@@ -882,8 +897,10 @@
882 897 response = defaultResponse;
883 898 }
884 899 }
885 900
901 + let willRedirect = false;
902 +
886 903 if ( response.redirect !== undefined ) {
887 904 if ( shouldTriggerEvent ) {
888 905 triggerCustomEvent( object, 'frmSubmitEvent' );
889 906 return;
@@ -895,8 +912,10 @@
895 912 }, 1000 * response.delay );
896 913 } else {
897 914 doRedirect( response );
898 915 }
916 +
917 + willRedirect = true;
899 918 }
900 919
901 920 if ( 'string' === typeof response.content && response.content !== '' ) {
902 921 // the form or success message was returned
@@ -939,9 +958,9 @@
939 958 afterFormSubmitted( object, response );
940 959 },
941 960 delay
942 961 );
943 - } else if ( Object.keys( response.errors ).length ) {
962 + } else if ( response.errors !== undefined && Object.keys( response.errors ).length ) {
944 963 // errors were returned
945 964 removeSubmitLoading( jQuery( object ), 'enable' );
946 965
947 966 //show errors
@@ -1006,10 +1025,10 @@
1006 1025 } else {
1007 1026 object.insertAdjacentHTML( 'afterbegin', response.error_message );
1008 1027 checkForErrorsAndMaybeSetFocus();
1009 1028 }
1010 - } else {
1011 - // 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.
1012 1031
1013 1032 showFileLoading( object );
1014 1033
1015 1034 object.submit();
@@ -1139,12 +1158,12 @@
1139 1158 return;
1140 1159 }
1141 1160
1142 1161 container.classList.add( 'frm_blank_field' );
1143 - const input = container.querySelector( 'input, select, textarea' );
1144 - const id = getErrorElementId( key, input );
1162 + const inputs = container.querySelectorAll( 'input, select, textarea' );
1163 + const id = getErrorElementId( key, inputs[ 0 ] );
1145 1164
1146 - let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1165 + let describedBy;
1147 1166
1148 1167 if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1149 1168 frmThemeOverride_frmPlaceError( key, jsErrors );
1150 1169 } else {
@@ -1155,10 +1174,10 @@
1155 1174 const roleString = frm_js.include_alert_role ? 'role="alert"' : '';
1156 1175 errorHtml = `<div class="frm_error" ${ roleString } id="${ id }">${ jsErrors[ key ] }</div>`;
1157 1176 }
1158 1177 container.insertAdjacentHTML( 'beforeend', errorHtml );
1159 -
1160 - if ( input ) {
1178 + inputs.forEach( input => {
1179 + describedBy = input.getAttribute( 'aria-describedby' );
1161 1180 if ( ! describedBy ) {
1162 1181 describedBy = id;
1163 1182 } else if ( ! describedBy.includes( id ) && ! describedBy.includes( 'frm_error_field_' ) ) {
1164 1183 const { errorFirst } = input.dataset;
@@ -1168,12 +1187,12 @@
1168 1187 describedBy = `${ id } ${ describedBy }`;
1169 1188 }
1170 1189 }
1171 1190 input.setAttribute( 'aria-describedby', describedBy );
1172 - }
1191 + } );
1173 1192 }
1174 1193
1175 - if ( input ) {
1194 + inputs.forEach( input => {
1176 1195 if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1177 1196 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1178 1197 if ( group ) {
1179 1198 group.setAttribute( 'aria-invalid', 'true' );
@@ -1180,9 +1199,9 @@
1180 1199 }
1181 1200 } else {
1182 1201 input.setAttribute( 'aria-invalid', 'true' );
1183 1202 }
1184 - }
1203 + } );
1185 1204
1186 1205 jQuery( document ).trigger( 'frmAddFieldError', [ jQuery( container ), key, jsErrors ] );
1187 1206 }
1188 1207
@@ -1214,11 +1233,9 @@
1214 1233 return;
1215 1234 }
1216 1235
1217 1236 const errorMessage = container.querySelector( '.frm_error' );
1218 - const errorId = errorMessage ? errorMessage.id : '';
1219 1237 const input = container.querySelector( 'input, select, textarea' );
1220 - let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1221 1238
1222 1239 container.classList.remove( 'frm_blank_field', 'has-error' );
1223 1240
1224 1241 if ( input ) {
@@ -1232,20 +1249,35 @@
1232 1249 }
1233 1250 }
1234 1251
1235 1252 if ( errorMessage ) {
1253 + removeElementFromInputDescribedBy( errorMessage );
1236 1254 errorMessage.remove();
1237 1255 }
1256 + }
1238 1257
1239 - if ( input ) {
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 + }
1240 1278 input.removeAttribute( 'aria-describedby' );
1241 - if ( describedBy ) {
1242 - describedBy = describedBy.replace( errorId, '' ).trim();
1243 - if ( describedBy ) {
1244 - input.setAttribute( 'aria-describedby', describedBy );
1245 - }
1246 - }
1247 - }
1279 + } );
1248 1280 }
1249 1281
1250 1282 function removeAllErrors() {
1251 1283 document.querySelectorAll( '.form-field' ).forEach( field => {
@@ -1250,9 +1282,12 @@
1250 1282 function removeAllErrors() {
1251 1283 document.querySelectorAll( '.form-field' ).forEach( field => {
1252 1284 field.classList.remove( 'frm_blank_field', 'has-error' );
1253 1285 } );
1254 - document.querySelectorAll( '.form-field .frm_error' ).forEach( error => error.remove() );
1286 + document.querySelectorAll( '.form-field .frm_error' ).forEach( el => {
1287 + removeElementFromInputDescribedBy( el );
1288 + el.remove();
1289 + } );
1255 1290 document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
1256 1291 }
1257 1292
1258 1293 /**
@@ -1854,9 +1889,9 @@
1854 1889 if ( ! currency.decimal_separator.length ) {
1855 1890 currency.decimal_separator = '.';
1856 1891 }
1857 1892
1858 - totalField.value = total;
1893 + totalField.value = roundTotal( total, currency );
1859 1894 total = normalizeTotal( total, currency );
1860 1895
1861 1896 // because of e.g. fields that might be using this field for calculations
1862 1897 triggerChange( totalField );
@@ -1886,14 +1921,22 @@
1886 1921 */
1887 1922 function normalizeTotal( total, currency ) {
1888 1923 const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1889 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 +
1890 1933 if ( ! isLargeTotal ) {
1891 1934 const { decimals } = currency;
1892 1935 total = decimals > 0 ? round10( total, decimals ) : Math.ceil( total );
1893 1936 }
1894 1937
1895 - return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal );
1938 + return total;
1896 1939 }
1897 1940
1898 1941 function round10( value, decimals ) {
1899 1942 return Number( `${ Math.round( `${ value }e${ decimals }` ) }e-${ decimals }` );