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 +57 -26 6.31 → 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 /**
@@ -1143,12 +1158,12 @@
1143 1158 return;
1144 1159 }
1145 1160
1146 1161 container.classList.add( 'frm_blank_field' );
1147 - const input = container.querySelector( 'input, select, textarea' );
1148 - const id = getErrorElementId( key, input );
1162 + const inputs = container.querySelectorAll( 'input, select, textarea' );
1163 + const id = getErrorElementId( key, inputs[ 0 ] );
1149 1164
1150 - let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1165 + let describedBy;
1151 1166
1152 1167 if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1153 1168 frmThemeOverride_frmPlaceError( key, jsErrors );
1154 1169 } else {
@@ -1159,10 +1174,10 @@
1159 1174 const roleString = frm_js.include_alert_role ? 'role="alert"' : '';
1160 1175 errorHtml = `<div class="frm_error" ${ roleString } id="${ id }">${ jsErrors[ key ] }</div>`;
1161 1176 }
1162 1177 container.insertAdjacentHTML( 'beforeend', errorHtml );
1163 -
1164 - if ( input ) {
1178 + inputs.forEach( input => {
1179 + describedBy = input.getAttribute( 'aria-describedby' );
1165 1180 if ( ! describedBy ) {
1166 1181 describedBy = id;
1167 1182 } else if ( ! describedBy.includes( id ) && ! describedBy.includes( 'frm_error_field_' ) ) {
1168 1183 const { errorFirst } = input.dataset;
@@ -1172,12 +1187,12 @@
1172 1187 describedBy = `${ id } ${ describedBy }`;
1173 1188 }
1174 1189 }
1175 1190 input.setAttribute( 'aria-describedby', describedBy );
1176 - }
1191 + } );
1177 1192 }
1178 1193
1179 - if ( input ) {
1194 + inputs.forEach( input => {
1180 1195 if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1181 1196 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1182 1197 if ( group ) {
1183 1198 group.setAttribute( 'aria-invalid', 'true' );
@@ -1184,9 +1199,9 @@
1184 1199 }
1185 1200 } else {
1186 1201 input.setAttribute( 'aria-invalid', 'true' );
1187 1202 }
1188 - }
1203 + } );
1189 1204
1190 1205 jQuery( document ).trigger( 'frmAddFieldError', [ jQuery( container ), key, jsErrors ] );
1191 1206 }
1192 1207
@@ -1218,11 +1233,9 @@
1218 1233 return;
1219 1234 }
1220 1235
1221 1236 const errorMessage = container.querySelector( '.frm_error' );
1222 - const errorId = errorMessage ? errorMessage.id : '';
1223 1237 const input = container.querySelector( 'input, select, textarea' );
1224 - let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1225 1238
1226 1239 container.classList.remove( 'frm_blank_field', 'has-error' );
1227 1240
1228 1241 if ( input ) {
@@ -1236,20 +1249,35 @@
1236 1249 }
1237 1250 }
1238 1251
1239 1252 if ( errorMessage ) {
1253 + removeElementFromInputDescribedBy( errorMessage );
1240 1254 errorMessage.remove();
1241 1255 }
1256 + }
1242 1257
1243 - 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 + }
1244 1278 input.removeAttribute( 'aria-describedby' );
1245 - if ( describedBy ) {
1246 - describedBy = describedBy.replace( errorId, '' ).trim();
1247 - if ( describedBy ) {
1248 - input.setAttribute( 'aria-describedby', describedBy );
1249 - }
1250 - }
1251 - }
1279 + } );
1252 1280 }
1253 1281
1254 1282 function removeAllErrors() {
1255 1283 document.querySelectorAll( '.form-field' ).forEach( field => {
@@ -1254,9 +1282,12 @@
1254 1282 function removeAllErrors() {
1255 1283 document.querySelectorAll( '.form-field' ).forEach( field => {
1256 1284 field.classList.remove( 'frm_blank_field', 'has-error' );
1257 1285 } );
1258 - 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 + } );
1259 1290 document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
1260 1291 }
1261 1292
1262 1293 /**