| @@ -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 }` ); |