From 21951645948cebea491e6b1882b41576fe44e333 Mon Sep 17 00:00:00 2001 From: Kieran Brahney Date: Thu, 13 Jun 2024 14:28:14 +0100 Subject: [PATCH 01/12] Build: Updating the master version to 1.20.2-pre. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1a027aa7..dfe9a5322 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jquery-validation", "title": "jQuery Validation Plugin", "description": "Client-side form validation made easy", - "version": "1.20.1-pre", + "version": "1.20.2-pre", "homepage": "https://jqueryvalidation.org/", "license": "MIT", "author": { From 0f8400fc55c801b4201ce41290b2aae21e2b20ec Mon Sep 17 00:00:00 2001 From: Kieran Brahney Date: Thu, 13 Jun 2024 14:37:54 +0100 Subject: [PATCH 02/12] Chore: update changelog --- changelog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.md b/changelog.md index 4680381ae..65b392ff8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,12 @@ +1.20.1 / 2024-06-13 +=================== + +## Core + * Fix remote validation when input is the same as in aborted request [#2481](https://github.com/jquery-validation/jquery-validation/pull/2481) + +## Localisation + * Update Arabic translations [#2485](https://github.com/jquery-validation/jquery-validation/pull/2485) + 1.20.0 / 2023-10-10 =================== From 75f51237e422360e53c5c23ad8da307223fe0b8a Mon Sep 17 00:00:00 2001 From: Daniel Hobi Date: Fri, 28 Jun 2024 17:10:19 +0200 Subject: [PATCH 03/12] Core: Add support for Web Components (#2493) Co-authored-by: Daniel Hobi --- .jscsrc | 5 ++++- .jshintignore | 1 + Gruntfile.js | 12 +++++++++++- package.json | 2 +- src/core.js | 20 +++++++++++--------- test/custom-elements.js | 17 +++++++++++++++++ test/index.html | 4 ++++ test/test.js | 21 +++++++++++++++++++++ 8 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 test/custom-elements.js diff --git a/.jscsrc b/.jscsrc index d50b36c38..018d8ca09 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,5 +1,8 @@ { "preset": "jquery", "maximumLineLength": null, - "requireCamelCaseOrUpperCaseIdentifiers": null + "requireCamelCaseOrUpperCaseIdentifiers": null, + "excludeFiles": [ + "test/custom-elements.js" + ] } diff --git a/.jshintignore b/.jshintignore index ffdbea7be..420537ee7 100644 --- a/.jshintignore +++ b/.jshintignore @@ -4,3 +4,4 @@ test/qunit/ dist/ demo/ *.min.js +test/custom-elements.js diff --git a/Gruntfile.js b/Gruntfile.js index 18cd73e33..2f4e3ea6c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -115,7 +115,17 @@ grunt.initConfig( { } }, qunit: { - files: "test/index.html" + files: "test/index.html", + options: { + puppeteer: { + args: [ + "--headless", + "--disable-web-security", + "--allow-file-access-from-files" + ] + }, + timeout: 10000 + } }, jshint: { options: { diff --git a/package.json b/package.json index dfe9a5322..26c433102 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "grunt-contrib-concat": "1.0.1", "grunt-contrib-copy": "1.0.0", "grunt-contrib-jshint": "1.0.0", - "grunt-contrib-qunit": "1.2.0", + "grunt-contrib-qunit": "10.0.0", "grunt-contrib-uglify": "1.0.1", "grunt-contrib-watch": "1.0.0", "grunt-jscs": "2.8.0", diff --git a/src/core.js b/src/core.js index 426b2551a..1b8bf875f 100644 --- a/src/core.js +++ b/src/core.js @@ -275,6 +275,7 @@ $.extend( $.validator, { onsubmit: true, ignore: ":hidden", ignoreTitle: false, + customElements: [], onfocusin: function( element ) { this.lastActive = element; @@ -422,17 +423,17 @@ $.extend( $.validator, { settings[ eventType ].call( validator, this, event ); } } - + var focusListeners = [ ":text", "[type='password']", "[type='file']", "select", "textarea", "[type='number']", "[type='search']", + "[type='tel']", "[type='url']", "[type='email']", "[type='datetime']", "[type='date']", "[type='month']", + "[type='week']", "[type='time']", "[type='datetime-local']", "[type='range']", "[type='color']", + "[type='radio']", "[type='checkbox']", "[contenteditable]", "[type='button']" ]; + var clickListeners = [ "select", "option", "[type='radio']", "[type='checkbox']" ]; $( this.currentForm ) - .on( "focusin.validate focusout.validate keyup.validate", - ":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], " + - "[type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], " + - "[type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], " + - "[type='radio'], [type='checkbox'], [contenteditable], [type='button']", delegate ) + .on( "focusin.validate focusout.validate keyup.validate", focusListeners.concat( this.settings.customElements ).join( ", " ), delegate ) // Support: Chrome, oldIE // "select" is provided as event.target when clicking a option - .on( "click.validate", "select, option, [type='radio'], [type='checkbox']", delegate ); + .on( "click.validate", clickListeners.concat( this.settings.customElements ).join( ", " ), delegate ); if ( this.settings.invalidHandler ) { $( this.currentForm ).on( "invalid-form.validate", this.settings.invalidHandler ); @@ -629,11 +630,12 @@ $.extend( $.validator, { elements: function() { var validator = this, - rulesCache = {}; + rulesCache = {}, + selectors = [ "input", "select", "textarea", "[contenteditable]" ]; // Select all valid inputs inside the form (no submit or reset buttons) return $( this.currentForm ) - .find( "input, select, textarea, [contenteditable]" ) + .find( selectors.concat( this.settings.customElements ).join( ", " ) ) .not( ":submit, :reset, :image, :disabled" ) .not( this.settings.ignore ) .filter( function() { diff --git a/test/custom-elements.js b/test/custom-elements.js new file mode 100644 index 000000000..7a4afc327 --- /dev/null +++ b/test/custom-elements.js @@ -0,0 +1,17 @@ +class CustomTextElement extends HTMLElement { + static formAssociated = true; + static observedAttributes = ["name", "id"]; + + constructor() { + super(); + this.internals_ = this.attachInternals(); + } + get form() { + return this.internals_ != null ? this.internals_.form : null; + } + get name() { + return this.getAttribute("name"); + } +} + +window.customElements.define("custom-text", CustomTextElement); diff --git a/test/index.html b/test/index.html index 601f6a505..e23e52dfc 100644 --- a/test/index.html +++ b/test/index.html @@ -11,6 +11,7 @@ + @@ -472,6 +473,9 @@

+
+
+ diff --git a/test/test.js b/test/test.js index c6f123f69..f9fbd4bdc 100644 --- a/test/test.js +++ b/test/test.js @@ -2786,3 +2786,24 @@ QUnit.test( "stopRequest() should submit the form once pendingRequests === 0", f // Submit the form $( button ).click(); } ); + +QUnit.test( "Assign rules to customElement via .validate() method", function( assert ) { + var form = $( "#customElementsForm" ); + var v = form.validate( { + customElements: [ "custom-text" ], + rules: { + customTextElement: { + required: true + } + } + } ); + var customTextElementRules = $( "#customTextElement", form ).rules(); + var expectedRules = { required: true }; + + assert.deepEqual( + customTextElementRules, expectedRules, "The rules should be the same" + ); + + v.form(); + assert.equal( v.numberOfInvalids(), 1, "The form has one error" ); +} ); From e837cc49beec2b17f7e18eaa3e1e9676e14c3133 Mon Sep 17 00:00:00 2001 From: Christopher Stieg Date: Sat, 29 Jun 2024 06:11:15 -0400 Subject: [PATCH 04/12] Core: Allow negative decimal with no 0 (#2483) * Core: Allow negative decimal with no 0 * Update regex --------- Co-authored-by: METALCOMPINC\cstieg Co-authored-by: Kieran --- src/core.js | 2 +- test/methods.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index 1b8bf875f..240e2737b 100644 --- a/src/core.js +++ b/src/core.js @@ -1485,7 +1485,7 @@ $.extend( $.validator, { // https://jqueryvalidation.org/number-method/ number: function( value, element ) { - return this.optional( element ) || /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test( value ); + return this.optional( element ) || /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:-?\.\d+)?$/.test( value ); }, // https://jqueryvalidation.org/digits-method/ diff --git a/test/methods.js b/test/methods.js index b2644f59d..e0d02eef2 100644 --- a/test/methods.js +++ b/test/methods.js @@ -177,6 +177,7 @@ QUnit.test( "number", function( assert ) { assert.ok( method( "123,000.00" ), "Valid decimal" ); assert.ok( method( "-123,000.00" ), "Valid decimal" ); assert.ok( method( ".100" ), "Valid decimal" ); + assert.ok( method( "-.100" ), "Valid decimal" ); assert.ok( !method( "1230,000.00" ), "Invalid decimal" ); assert.ok( !method( "123.0.0,0" ), "Invalid decimal" ); assert.ok( !method( "x123" ), "Invalid decimal" ); From 09f67cbf770d2b83982859950565b4b5eb377389 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 17 Jul 2024 10:18:39 +0100 Subject: [PATCH 05/12] Update package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 26c433102..e4ee566df 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jquery-validation", "title": "jQuery Validation Plugin", "description": "Client-side form validation made easy", - "version": "1.20.2-pre", + "version": "1.21.0-pre", "homepage": "https://jqueryvalidation.org/", "license": "MIT", "author": { From bd54405de2c0c820d7903f625a54b157e707f75e Mon Sep 17 00:00:00 2001 From: Kieran Brahney Date: Wed, 17 Jul 2024 10:45:25 +0100 Subject: [PATCH 06/12] Build: Updating the master version to 1.21.1-pre. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e4ee566df..7fd85ca91 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jquery-validation", "title": "jQuery Validation Plugin", "description": "Client-side form validation made easy", - "version": "1.21.0-pre", + "version": "1.21.1-pre", "homepage": "https://jqueryvalidation.org/", "license": "MIT", "author": { From 6cd68f68e395b1c3a2588e6e7d64f561410c2dc0 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 16 Dec 2024 19:10:59 +0000 Subject: [PATCH 07/12] Update stale.yml --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3e3c347e7..6332ce773 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -35,5 +35,5 @@ jobs: stale-issue-label: 'stale' stale-pr-label: 'stale' exempt-all-milestones: true - exempt-issue-labels: 'Status: Verified' + exempt-issue-labels: 'Status: Verified, Type: Feature' exempt-pr-labels: 'MERGE ME, NEEDS REVIEW' From 07d2594c8201863ea03d901551fb0f17cf11b3c7 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 12 Jun 2025 11:20:12 +0100 Subject: [PATCH 08/12] Chore: downgrade ubuntu (#2518) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb9551a9c..7b3883f6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: linux_tests: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 From 707623a5cede70bdce8a911c2925f7786911e97d Mon Sep 17 00:00:00 2001 From: appel <61596+appel@users.noreply.github.com> Date: Thu, 12 Jun 2025 06:22:47 -0400 Subject: [PATCH 09/12] Localization: Additional string translations for messages_nl.js (#2517) * Update messages_nl.js Additional string translations (on par with DE). * Fixes styling issue. Added missing spaces inside the parentheses of $.validator.format() calls. --- src/localization/messages_nl.js | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/localization/messages_nl.js b/src/localization/messages_nl.js index 2d6a6970e..abee59f9e 100644 --- a/src/localization/messages_nl.js +++ b/src/localization/messages_nl.js @@ -30,5 +30,45 @@ $.extend( $.validator.messages, { postalcodeNL: "Vul hier een geldige postcode in.", bankaccountNL: "Vul hier een geldig bankrekeningnummer in.", giroaccountNL: "Vul hier een geldig gironummer in.", - bankorgiroaccountNL: "Vul hier een geldig bank- of gironummer in." + bankorgiroaccountNL: "Vul hier een geldig bank- of gironummer in.", + + maxWords: $.validator.format( "Vul hier maximaal {0} woorden in." ), + minWords: $.validator.format( "Vul hier minimaal {0} woorden in." ), + rangeWords: $.validator.format( "Vul hier tussen {0} en {1} woorden in." ), + accept: "Vul hier een waarde in met een geldig MIME-type.", + alphanumeric: "Vul hier alleen letters, cijfers of underscores in.", + bic: "Vul hier een geldige BIC-code in.", + creditcardtypes: "Vul hier een geldig creditcardnummer in.", + currency: "Vul hier een geldige valuta in.", + integer: "Vul hier een geheel getal in.", + ipv4: "Vul hier een geldig IPv4-adres in.", + ipv6: "Vul hier een geldig IPv6-adres in.", + lettersonly: "Vul hier alleen letters in.", + letterswithbasicpunc: "Vul hier alleen letters of leestekens in.", + netmask: "Vul hier een geldig netmasker in.", + notEqualTo: "Vul hier een andere waarde in. De waarden mogen niet gelijk zijn.", + nowhitespace: "Spaties zijn niet toegestaan.", + pattern: "Ongeldig formaat.", + require_from_group: $.validator.format( "Vul minimaal {0} van deze velden in." ), + skip_or_fill_minimum: $.validator.format( "Sla deze velden over of vul er minimaal {0} van in." ), + strippedminlength: $.validator.format( "Vul hier minimaal {0} tekens in." ), + time: "Vul hier een geldige tijd in tussen 00:00 en 23:59.", + time12h: "Vul hier een geldige tijd in (12-uursnotatie).", + cifES: "Vul hier een geldig CIF-nummer in.", + cpfBR: "Vul hier een geldig CPF-nummer in.", + mobileUK: "Vul hier een geldig Brits mobiel nummer in.", + nieES: "Vul hier een geldig NIE-nummer in.", + nifES: "Vul hier een geldig NIF-nummer in.", + nipPL: "Vul hier een geldig NIP-nummer in.", + phonesUK: "Vul hier een geldig Brits telefoonnummer in.", + phoneUK: "Vul hier een geldig Brits telefoonnummer in.", + phoneUS: "Vul hier een geldig Amerikaans telefoonnummer in.", + postalcodeBR: "Vul hier een geldige Braziliaanse postcode in.", + postalCodeCA: "Vul hier een geldige Canadese postcode in.", + postalcodeIT: "Vul hier een geldige Italiaanse postcode in.", + postcodeUK: "Vul hier een geldige Britse postcode in.", + stateUS: "Vul hier een geldige Amerikaanse staat in.", + vinUS: "Het opgegeven voertuigidentificatienummer (VIN) is ongeldig.", + zipcodeUS: "De opgegeven Amerikaanse postcode is ongeldig.", + ziprange: "Uw postcode moet binnen het bereik 902xx-xxxx tot 905xx-xxxx liggen." } ); From 6eb2df0da1bf65791941122e2ad77b85a308754f Mon Sep 17 00:00:00 2001 From: eden-jh <55098579+eden-jh@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:46:18 -0400 Subject: [PATCH 10/12] Core: Unnecessary aria-describedby (#2410) * Core: progress on removing aria-describedby from valid fields * core: aria-describedby * Core: remove aria-describedby when hiding error * Core: fix syntax issues, Test: add test for new setting * Core: Fix bugs in aria-describedby behavior and related tests * Core: bugfix for labels without id * Core: don't create a new error element if one exists * Core: progress on test for group of fields with ariaDescribedbyCleanup * Core: Groups aria-describedby * Core: fix aria-describedby not being removed from grouped fields Ensure that aria-describedby is removed from all members of a group when all the known errors are resolved * Core: Update capitalization * Demo: Add page for ariaDescribedByCleanup * Core: add setting to remove aria-describedby from valid fields Includes additional unit tests and a demo page * Core: Fix camel case inconsistency, remove stray comment * Update demo/css/cmxform.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Kieran Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- demo/aria-describedby-cleanup.html | 110 +++++++++++++++++++++ demo/css/cmxform.css | 26 ++++- demo/css/cmxformTemplate.css | 22 +++-- demo/index.html | 2 + src/core.js | 116 ++++++++++++++++------ test/error-placement.js | 148 +++++++++++++++++++++++++++++ test/index.html | 32 ++++++- 7 files changed, 414 insertions(+), 42 deletions(-) create mode 100644 demo/aria-describedby-cleanup.html diff --git a/demo/aria-describedby-cleanup.html b/demo/aria-describedby-cleanup.html new file mode 100644 index 000000000..907517cf0 --- /dev/null +++ b/demo/aria-describedby-cleanup.html @@ -0,0 +1,110 @@ + + + + + jQuery Validation Plugin Demo - ariaDescribedByCleanup set to true + + + + + + + +

jQuery Validation Plugin Demo - ariaDescribedByCleanup set to true

+
+ +
+ +
+

Example with group

+

Fields marked with * are required

+ +
+
+ Name* +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ +
+ + 300 characters maximum +
+ +
+
+ + + +
+
+ +
+

Back to main page

+ +
+ diff --git a/demo/css/cmxform.css b/demo/css/cmxform.css index 120f5a473..7424c6dd4 100644 --- a/demo/css/cmxform.css +++ b/demo/css/cmxform.css @@ -17,18 +17,36 @@ form.cmxform legend, form.cmxform label { color: #333; } -form.cmxform fieldset { +form.cmxform fieldset, form.cmxform .box { border: none; border-top: 1px solid #C9DCA6; background: url(../images/cmxform-fieldset.gif) left bottom repeat-x; background-color: #F8FDEF; } +form.cmxform fieldset .col label { + margin-left: 0; +} +form.cmxform fieldset .col { + margin-right: .5rem; +} +form.cmxform fieldset .row .col:last-child { + margin-right: 0; +} +form.cmxform fieldset .row { + display: flex; -form.cmxform fieldset fieldset { + box-sizing: border-box; +} +form.cmxform .box, +form.cmxform fieldset .row, +form.cmxform .box fieldset { + width: 100%; +} +form.cmxform fieldset fieldset, form.cmxform .box fieldset { background: none; } -form.cmxform fieldset p, form.cmxform fieldset fieldset { +form.cmxform .box > .row, form.cmxform fieldset p, form.cmxform fieldset fieldset, form.cmxform .box fieldset { padding: 5px 10px 7px; background: url(../images/cmxform-divider.gif) left bottom repeat-x; } @@ -43,4 +61,4 @@ input { border: 1px solid black; } input.checkbox { border: none } input:focus { border: 1px dotted black; } input.error { border: 1px dotted red; } -form.cmxform .gray * { color: gray; } \ No newline at end of file +form.cmxform .gray * { color: gray; } diff --git a/demo/css/cmxformTemplate.css b/demo/css/cmxformTemplate.css index ac52f71b4..f5cdf04d6 100644 --- a/demo/css/cmxformTemplate.css +++ b/demo/css/cmxformTemplate.css @@ -7,7 +7,7 @@ form.cmxform fieldset { margin-bottom: 10px; } -form.cmxform legend { +form.cmxform legend, form.cmxform .box .title { padding: 0 2px; font-weight: bold; _margin: 0 -7px; /* IE Win */ @@ -20,29 +20,35 @@ form.cmxform label { cursor: hand; } -form.cmxform fieldset p { +form.cmxform fieldset p, +form.cmxform .box p { list-style: none; padding: 5px; margin: 0; } -form.cmxform fieldset fieldset { +form.cmxform fieldset fieldset, +form.cmxform .box fieldset { border: none; margin: 3px 0 0; } -form.cmxform fieldset fieldset legend { +form.cmxform fieldset fieldset legend, form.cmxform .box fieldset legend { padding: 0 0 5px; font-weight: normal; } -form.cmxform fieldset fieldset label { + +form.cmxform label { width: 100px; } /* Width of labels */ +form.cmxform fieldset fieldset label, +form.cmxform .box fieldset label { display: block; width: auto; + /* Width plus 3 (html space) */ + margin-left: 103px; } -form.cmxform label { width: 100px; } /* Width of labels */ -form.cmxform fieldset fieldset label { margin-left: 103px; } /* Width plus 3 (html space) */ + form.cmxform label.error { margin-left: 103px; width: 220px; @@ -52,4 +58,4 @@ form.cmxform input.submit { margin-left: 103px; } -/*\*//*/ form.cmxform legend { display: inline-block; } /* IE Mac legend fix */ \ No newline at end of file +/*\*//*/ form.cmxform legend { display: inline-block; } /* IE Mac legend fix */ diff --git a/demo/index.html b/demo/index.html index 2a472d561..3be0c4035 100644 --- a/demo/index.html +++ b/demo/index.html @@ -226,6 +226,8 @@

Synthetic examples

  • Using with Semantic-UI
  • +
  • ariaDescribedByCleanup set to true +
  • Real-world examples

      diff --git a/src/core.js b/src/core.js index 240e2737b..c1a9bc8bc 100644 --- a/src/core.js +++ b/src/core.js @@ -270,6 +270,7 @@ $.extend( $.validator, { errorElement: "label", focusCleanup: false, focusInvalid: true, + ariaDescribedByCleanup: false, errorContainer: $( [] ), errorLabelContainer: $( [] ), onsubmit: true, @@ -481,6 +482,8 @@ $.extend( $.validator, { $.each( this.groups, function( name, testgroup ) { if ( testgroup === group && name !== checkElement.name ) { cleanElement = v.validationTargetFor( v.clean( v.findByName( name ) ) ); + + // Don't want to check fields if a user hasn't gotten to them yet if ( cleanElement && cleanElement.name in v.invalid ) { v.currentElements.push( cleanElement ); result = v.check( cleanElement ) && result; @@ -591,10 +594,76 @@ $.extend( $.validator, { hideErrors: function() { this.hideThese( this.toHide ); }, + addErrorAriaDescribedBy: function( element, error, updateGroupMembers ) { + updateGroupMembers = ( updateGroupMembers === undefined ) ? false : updateGroupMembers; + + var errorID, v, group, + describedBy = $( element ).attr( "aria-describedby" ); + errorID = error.attr( "id" ); + + // Respect existing non-error aria-describedby + if ( !describedBy ) { + describedBy = errorID; + } else if ( !describedBy.match( new RegExp( "\\b" + this.escapeCssMeta( errorID ) + "\\b" ) ) ) { + + // Add to end of list if not already present + describedBy += " " + errorID; + } + + $( element ).attr( "aria-describedby", describedBy ); + + if ( updateGroupMembers ) { + + // If this element is grouped, then assign to all elements in the same group + group = this.groups[ element.name ]; + if ( group ) { + v = this; + $.each( v.groups, function( name, testgroup ) { + if ( testgroup === group ) { + v.addErrorAriaDescribedBy( $( "[name='" + v.escapeCssMeta( name ) + "']", v.currentForm ), error, false ); + } + } ); + } + } + }, + + removeErrorAriaDescribedBy: function( element, error ) { + + var describedBy = $( element ).attr( "aria-describedby" ), + describedByIds = describedBy.split( " " ), + errorID = error.attr( "id" ), + ind = describedByIds.indexOf( errorID ); + + if ( ind > -1 ) { + describedByIds.splice( ind, 1 ); + } + + if ( describedByIds.length ) { + $( element ).attr( "aria-describedby", describedByIds.join( " " ) ); + } else { + $( element ).removeAttr( "aria-describedby" ); + } + + }, hideThese: function( errors ) { - errors.not( this.containers ).text( "" ); - this.addWrapper( errors ).hide(); + + for ( var i = 0; errors[ i ]; i++ ) { + var error = $( errors[ i ] ), + errorID = error.attr( "id" ) ? this.escapeCssMeta( error.attr( "id" ) ) : undefined, + element = ( errorID ) ? this.elements().filter( '[aria-describedby~="' + errorID + '"]' ) : []; + + if ( this.settings.ariaDescribedByCleanup && element.length ) { + this.removeErrorAriaDescribedBy( element, error ); + } + + if ( !error.is( this.containers ) ) { + error.text( "" ); + } + + this.addWrapper( error ).hide(); + } + }, valid: function() { @@ -900,6 +969,7 @@ $.extend( $.validator, { defaultShowErrors: function() { var i, elements, error; + for ( i = 0; this.errorList[ i ]; i++ ) { error = this.errorList[ i ]; if ( this.settings.highlight ) { @@ -907,19 +977,23 @@ $.extend( $.validator, { } this.showLabel( error.element, error.message ); } + if ( this.errorList.length ) { this.toShow = this.toShow.add( this.containers ); } + if ( this.settings.success ) { for ( i = 0; this.successList[ i ]; i++ ) { this.showLabel( this.successList[ i ] ); } } + if ( this.settings.unhighlight ) { for ( i = 0, elements = this.validElements(); elements[ i ]; i++ ) { this.settings.unhighlight.call( this, elements[ i ], this.settings.errorClass, this.settings.validClass ); } } + this.toHide = this.toHide.not( this.toShow ); this.hideErrors(); this.addWrapper( this.toShow ).show(); @@ -936,13 +1010,18 @@ $.extend( $.validator, { }, showLabel: function( element, message ) { - var place, group, errorID, v, + var place, error = this.errorsFor( element ), elementID = this.idOrName( element ), describedBy = $( element ).attr( "aria-describedby" ); if ( error.length ) { + // Non-label error exists but is not currently associated with element via aria-describedby + if ( error.closest( "label[for='" + this.escapeCssMeta( elementID ) + "']" ).length === 0 && ( describedBy === undefined || describedBy.split( " " ).indexOf( error.attr( "id" ) ) === -1 ) ) { + this.addErrorAriaDescribedBy( element, error, true ); + } + // Refresh error/success class error.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); @@ -987,32 +1066,10 @@ $.extend( $.validator, { // If the error is a label, then associate using 'for' error.attr( "for", elementID ); - // If the element is not a child of an associated label, then it's necessary - // to explicitly apply aria-describedby + // If the element is not a child of an associated label, then it's necessary + // to explicitly apply aria-describedby } else if ( error.parents( "label[for='" + this.escapeCssMeta( elementID ) + "']" ).length === 0 ) { - errorID = error.attr( "id" ); - - // Respect existing non-error aria-describedby - if ( !describedBy ) { - describedBy = errorID; - } else if ( !describedBy.match( new RegExp( "\\b" + this.escapeCssMeta( errorID ) + "\\b" ) ) ) { - - // Add to end of list if not already present - describedBy += " " + errorID; - } - $( element ).attr( "aria-describedby", describedBy ); - - // If this element is grouped, then assign to all elements in the same group - group = this.groups[ element.name ]; - if ( group ) { - v = this; - $.each( v.groups, function( name, testgroup ) { - if ( testgroup === group ) { - $( "[name='" + v.escapeCssMeta( name ) + "']", v.currentForm ) - .attr( "aria-describedby", error.attr( "id" ) ); - } - } ); - } + this.addErrorAriaDescribedBy( element, error, true ); } } if ( !message && this.settings.success ) { @@ -1037,6 +1094,9 @@ $.extend( $.validator, { .replace( /\s+/g, ", #" ); } + // There may be hidden error elements not currently associated via aria-describedby (if ariaDescribedByCleanup is true) + selector = selector + ", #" + name + "-error"; + return this .errors() .filter( selector ); diff --git a/test/error-placement.js b/test/error-placement.js index 05e5c565f..9296bd587 100644 --- a/test/error-placement.js +++ b/test/error-placement.js @@ -347,7 +347,155 @@ QUnit.test( "test existing non-error aria-describedby", function( assert ) { assert.strictEqual( $( "#testForm17text-description" ).text(), "This is where you enter your data" ); assert.strictEqual( $( "#testForm17text-error" ).text(), "", "Error label is empty for valid field" ); } ); +QUnit.test( "test aria-describedby cleanup with existing non-error aria-describedby", function( assert ) { + assert.expect( 13 ); + var form = $( "#ariaDescribedByCleanupWithExistingNonError" ), + field = $( "#testCleanupExistingNonErrortext" ), + errorID = "testCleanupExistingNonErrortext-error", + descriptionID = "testCleanupExistingNonErrortext-description"; + + assert.equal( field.attr( "aria-describedby" ), descriptionID ); + + // First test an invalid value + form.validate( { errorElement: "span", ariaDescribedByCleanup: true } ); + assert.ok( !field.valid() ); + assert.equal( ( field.attr( "aria-describedby" ).split( " " ).indexOf( errorID ) > -1 && field.attr( "aria-describedby" ).split( " " ).indexOf( descriptionID ) > -1 ), true ); + assert.hasError( field, "required" ); + var errorElement = form.validate().errorsFor( field[ 0 ] ); + assert.equal( errorElement.attr( "id" ), errorID ); + + // Then make it valid again to ensure that the aria-describedby relationship is restored + field.val( "foo" ); + assert.ok( field.valid() ); + assert.noErrorFor( field ); + assert.equal( field.attr( "aria-describedby" ), descriptionID ); + assert.strictEqual( true, errorElement.is( ":hidden" ) ); + + // Then make it invalid again + field.val( "" ).trigger( "keyup" ); + assert.ok( !field.valid() ); + assert.hasError( field, "required" ); + + // Make sure there's not more than one error + assert.equal( $( "[id=" + errorID + "]" ).length, 1 ); + assert.equal( ( field.attr( "aria-describedby" ).split( " " ).indexOf( errorID ) > -1 && field.attr( "aria-describedby" ).split( " " ).indexOf( descriptionID ) > -1 ), true ); +} ); +QUnit.test( "test aria-describedby cleanup when field becomes valid", function( assert ) { + assert.expect( 16 ); + var form = $( "#ariaDescribedByCleanup" ), + field = $( "#ariaDescribedByCleanupText" ), + errorID = "ariaDescribedByCleanupText-error"; + + // First test an invalid value + form.validate( { errorElement: "span", ariaDescribedByCleanup: true } ); + assert.ok( !field.valid() ); + assert.equal( field.attr( "aria-describedby" ), "ariaDescribedByCleanupText-error" ); + assert.hasError( field, "required" ); + var errorElement = form.validate().errorsFor( field[ 0 ] ); + assert.equal( field.attr( "aria-describedby" ), errorID ); + assert.equal( errorElement.attr( "id" ), errorID ); + + // Then make it valid again to ensure that the aria-describedby relationship is restored + field.val( "foo" ); + + assert.ok( field.valid() ); + assert.noErrorFor( field ); + assert.notOk( field.attr( "aria-describedby" ) ); + assert.strictEqual( true, errorElement.is( ":hidden" ) ); + + // Then make it invalid again + field.val( "" ).trigger( "keyup" ); + assert.ok( !field.valid() ); + assert.equal( field.attr( "aria-describedby" ), "ariaDescribedByCleanupText-error" ); + assert.hasError( field, "required" ); + errorElement = form.validate().errorsFor( field[ 0 ] ); + assert.ok( errorElement ); + + // Make sure there's not more than one error + assert.equal( $( "[id=" + errorID + "]" ).length, 1 ); + assert.ok( field.attr( "aria-describedby" ) ); + assert.equal( field.attr( "aria-describedby" ), errorID ); +} ); +QUnit.test( "test aria-describedby cleanup on group", function( assert ) { + assert.expect( 34 ); + var form = $( "#ariaDescribedByCleanupGroup" ), + firstID = "ariaDescribedByCleanupGroupFirst", + first = $( "#" + firstID ), + middleID = "ariaDescribedByCleanupGroupMiddle", + middle = $( "#" + middleID ), + lastID = "ariaDescribedByCleanupGroupLast", + last = $( "#" + lastID ), + emailID = "ariaDescribedByCleanupGroupEmail", + email = $( "#" + emailID ), + groupName = "ariaDescribedByCleanupGroupName", + groupOptions = { }; + groupOptions[ groupName ] = firstID + " " + middleID + " " + lastID; + + // First test an invalid value + form.validate( { errorElement: "span", ariaDescribedByCleanup: true, groups: groupOptions } ); + form.trigger( "submit" ); + assert.equal( first.attr( "aria-describedby" ), groupName + "-error" ); + assert.hasError( first, "required" ); + assert.hasError( middle, "required" ); + assert.hasError( last, "required" ); + var errorElement = form.validate().errorsFor( first[ 0 ] ); + + // Previous behavior was for error to apply to all group members. It still does that, but now it removes aria-describedby from each individual field (or at least I think that's what's happening because when first name has an error, middle and last are valid and don't have aria-describedby) + assert.equal( errorElement.attr( "id" ), groupName + "-error" ); + assert.equal( middle.attr( "aria-describedby" ), groupName + "-error" ); + assert.equal( last.attr( "aria-describedby" ), groupName + "-error" ); + + // Check email field + assert.hasError( email, "required" ); + assert.equal( email.attr( "aria-describedby" ), emailID + "-error" ); + + // Then make it valid again to ensure that the aria-describedby relationship is restored + first.val( "Person" ); + middle.val( "Syntax" ); + last.val( "Personname" ); + + email.val( "aa" ); + + form.trigger( "submit" ); + + assert.hasError( email, "email" ); + assert.equal( email.attr( "aria-describedby" ), emailID + "-error" ); + var emailError = form.validate().errorsFor( email[ 0 ] ); + assert.equal( emailError.attr( "id" ), email.attr( "aria-describedby" ) ); + + assert.ok( first.valid() ); + assert.noErrorFor( first ); + assert.notOk( first.attr( "aria-describedby" ) ); + assert.noErrorFor( middle ); + assert.notOk( middle.attr( "aria-describedby" ) ); + assert.noErrorFor( last ); + assert.notOk( last.attr( "aria-describedby" ) ); + assert.strictEqual( true, errorElement.is( ":hidden" ) ); + + // Then make it invalid again + first.val( "" ).trigger( "keyup" ); + assert.hasError( first, "required" ); + assert.equal( first.attr( "aria-describedby" ), groupName + "-error" ); + assert.equal( errorElement.attr( "id" ), groupName + "-error" ); + assert.equal( middle.attr( "aria-describedby" ), groupName + "-error" ); + assert.equal( last.attr( "aria-describedby" ), groupName + "-error" ); + assert.ok( !first.valid() ); + + // Make sure there's not more than one error + assert.equal( $( "[id=" + groupName + "-error]" ).length, 1 ); + assert.equal( $( "[id=" + emailID + "-error]" ).length, 1 ); + + email.val( "test@test.com" ).trigger( "keyup" ); + first.val( "Person" ).trigger( "keyup" ); + + assert.noErrorFor( first ); + assert.noErrorFor( email ); + assert.notOk( email.attr( "aria-describedby" ) ); + assert.notOk( first.attr( "aria-describedby" ) ); + assert.notOk( middle.attr( "aria-describedby" ) ); + assert.notOk( last.attr( "aria-describedby" ) ); +} ); QUnit.test( "test pre-assigned non-error aria-describedby", function( assert ) { assert.expect( 7 ); var form = $( "#testForm17" ), diff --git a/test/index.html b/test/index.html index e23e52dfc..b07f08fa3 100644 --- a/test/index.html +++ b/test/index.html @@ -188,6 +188,34 @@

      This is where you enter your data +
      + + + + This is where you enter your data +
      +
      + + + +
      +
      + +
      + + Name + + + + + + + +
      + + + +
      @@ -454,12 +482,12 @@

      - +
      - +

      From 94418e4283a454fb630620a340a4e5b8cbeb8c2e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:37:45 +0100 Subject: [PATCH 11/12] Additional: Replace deprecated substr() with slice() and charAt() in cifES.js (#2520) * Initial plan * Initial plan: Replace deprecated substr with slice Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> * Additional: Replace deprecated substr with slice and charAt in cifES * Build: Remove package-lock.json and add to gitignore Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> * Build: Revert .gitignore changes Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> --- src/additional/cifES.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/additional/cifES.js b/src/additional/cifES.js index cfef967b5..15e3e32d3 100644 --- a/src/additional/cifES.js +++ b/src/additional/cifES.js @@ -95,9 +95,9 @@ $.validator.addMethod( "cifES", function( value, element ) { } all_sum = even_sum + odd_sum; - control_digit = ( 10 - ( all_sum ).toString().substr( -1 ) ).toString(); + control_digit = ( 10 - ( all_sum ).toString().slice( -1 ) ).toString(); control_digit = parseInt( control_digit, 10 ) > 9 ? "0" : control_digit; - control_letter = "JABCDEFGHI".substr( control_digit, 1 ).toString(); + control_letter = "JABCDEFGHI".charAt( control_digit ).toString(); // Control must be a digit if ( letter.match( /[ABEH]/ ) ) { From 4a25a8ff62965aec8221016b8f47873c1833596e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:16:27 +0000 Subject: [PATCH 12/12] Core: Add support for HTML5 form attribute on elements outside form (#2521) * Initial plan * Core: Add support for form attribute on elements outside form Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> * Core: Also update findByName to support form attribute Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> * Build: Remove package-lock.json and add to gitignore Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> * Tests: Add assertion to check form exists before validating Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> * Revert: "Tests: Add assertion to check form exists before validating" This reverts commit e3c28f79121e9e394e35c7a6f1655cd23c8d725b. * Revert: "Build: Remove package-lock.json and add to gitignore" This reverts commit dff7ce864054ded48bac8fab0c5e11714cc39e9a. * Fix: dont use self closing tag on custom-text element * Chore: remove package-lock.json --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bytestream <1788397+bytestream@users.noreply.github.com> Co-authored-by: bytestream --- src/core.js | 37 ++++++++++++++++++++++++++++++++----- test/index.html | 14 +++++++++++--- test/test.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/core.js b/src/core.js index c1a9bc8bc..75eeefbbe 100644 --- a/src/core.js +++ b/src/core.js @@ -700,14 +700,28 @@ $.extend( $.validator, { elements: function() { var validator = this, rulesCache = {}, - selectors = [ "input", "select", "textarea", "[contenteditable]" ]; + selectors = [ "input", "select", "textarea", "[contenteditable]" ], + formId = this.currentForm.id, + elements; // Select all valid inputs inside the form (no submit or reset buttons) - return $( this.currentForm ) + elements = $( this.currentForm ) .find( selectors.concat( this.settings.customElements ).join( ", " ) ) .not( ":submit, :reset, :image, :disabled" ) - .not( this.settings.ignore ) - .filter( function() { + .not( this.settings.ignore ); + + // If the form has an ID, also include elements outside the form that have + // a form attribute pointing to this form + if ( formId ) { + elements = elements.add( + $( selectors.concat( this.settings.customElements ).join( ", " ) ) + .filter( "[form='" + validator.escapeCssMeta( formId ) + "']" ) + .not( ":submit, :reset, :image, :disabled" ) + .not( this.settings.ignore ) + ); + } + + return elements.filter( function() { var name = this.name || $( this ).attr( "name" ); // For contenteditable var isContentEditable = typeof $( this ).attr( "contenteditable" ) !== "undefined" && $( this ).attr( "contenteditable" ) !== "false"; @@ -1133,7 +1147,20 @@ $.extend( $.validator, { }, findByName: function( name ) { - return $( this.currentForm ).find( "[name='" + this.escapeCssMeta( name ) + "']" ); + var formId = this.currentForm.id, + selector = "[name='" + this.escapeCssMeta( name ) + "']", + elements = $( this.currentForm ).find( selector ); + + // If the form has an ID, also include elements outside the form that have + // a form attribute pointing to this form + if ( formId ) { + elements = elements.add( + $( selector ) + .filter( "[form='" + this.escapeCssMeta( formId ) + "']" ) + ); + } + + return elements; }, getLength: function( value, element ) { diff --git a/test/index.html b/test/index.html index b07f08fa3..511c643f2 100644 --- a/test/index.html +++ b/test/index.html @@ -498,13 +498,21 @@

      -
      +
      - - + + +
      + +
      + +
      + +
      + diff --git a/test/test.js b/test/test.js index f9fbd4bdc..dbc7958b0 100644 --- a/test/test.js +++ b/test/test.js @@ -369,6 +369,36 @@ QUnit.test( "Ignore elements that have form attribute set to other forms", funct $( "#testForm28-other" ).remove(); } ); +QUnit.test( "Validate elements outside form with form attribute", function( assert ) { + assert.expect( 3 ); + + var form = $( "#testForm29" ); + var v = form.validate(); + + // The form has one input inside and one input outside with form attribute + assert.equal( v.elements().length, 2, "Both elements should be included" ); + + // Validate the form - both fields are required and empty + var result = v.form(); + assert.ok( !result, "Form validation should fail when both inputs are empty" ); + assert.equal( v.numberOfInvalids(), 2, "Should have 2 invalid elements" ); +} ); + +QUnit.test( "Validate checkboxes outside form with form attribute", function( assert ) { + assert.expect( 3 ); + + var form = $( "#testForm30" ); + var v = form.validate(); + + // The form has one checkbox inside and one checkbox outside with form attribute + assert.equal( v.elements().length, 2, "Both checkboxes should be included" ); + + // Validate the form - both checkboxes are required and unchecked + var result = v.form(); + assert.ok( !result, "Form validation should fail when both checkboxes are unchecked" ); + assert.equal( v.numberOfInvalids(), 2, "Should have 2 invalid elements" ); +} ); + QUnit.test( "addMethod", function( assert ) { assert.expect( 3 ); $.validator.addMethod( "hi", function( value ) {