diff --git a/src/jquery/core.js b/src/jquery/core.js index 3d4e0fb3..1956e282 100644 --- a/src/jquery/core.js +++ b/src/jquery/core.js @@ -102,12 +102,6 @@ migratePatchAndWarnFunc( jQuery, "holdReady", jQuery.holdReady, migratePatchAndWarnFunc( jQuery, "unique", jQuery.uniqueSort, "unique", "jQuery.unique is deprecated; use jQuery.uniqueSort" ); -// Now jQuery.expr.pseudos is the standard incantation -migrateWarnProp( jQuery.expr, "filters", jQuery.expr.pseudos, "expr-pre-pseudos", - "jQuery.expr.filters is deprecated; use jQuery.expr.pseudos" ); -migrateWarnProp( jQuery.expr, ":", jQuery.expr.pseudos, "expr-pre-pseudos", - "jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos" ); - // Prior to jQuery 3.1.1 there were internal refs so we don't warn there if ( jQueryVersionSince( "3.1.1" ) ) { migratePatchAndWarnFunc( jQuery, "trim", function( text ) { diff --git a/src/jquery/selector.js b/src/jquery/selector.js new file mode 100644 index 00000000..b1395186 --- /dev/null +++ b/src/jquery/selector.js @@ -0,0 +1,69 @@ +import { migratePatchFunc, migrateWarnProp, migrateWarn } from "../main.js"; + +// Now jQuery.expr.pseudos is the standard incantation +migrateWarnProp( jQuery.expr, "filters", jQuery.expr.pseudos, "expr-pre-pseudos", + "jQuery.expr.filters is deprecated; use jQuery.expr.pseudos" ); +migrateWarnProp( jQuery.expr, ":", jQuery.expr.pseudos, "expr-pre-pseudos", + "jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos" ); + +function markFunction( fn ) { + fn[ jQuery.expando ] = true; + return fn; +} + +migratePatchFunc( jQuery.expr.filter, "PSEUDO", function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // https://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = jQuery.expr.pseudos[ pseudo ] || + jQuery.expr.setFilters[ pseudo.toLowerCase() ] || + jQuery.error( "Syntax error, unrecognized expression: unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as jQuery does + if ( fn[ jQuery.expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + migrateWarn( "legacy-custom-pseudos", + "Pseudos with multiple arguments are deprecated; " + + "use jQuery.expr.createPseudo()" ); + args = [ pseudo, pseudo, "", argument ]; + return jQuery.expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = Array.prototype.indexOf.call( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; +}, "legacy-custom-pseudos" ); + +if ( typeof Proxy !== "undefined" ) { + jQuery.each( [ "pseudos", "setFilters" ], function( _, api ) { + jQuery.expr[ api ] = new Proxy( jQuery.expr[ api ], { + set: function( _target, _prop, fn ) { + if ( typeof fn === "function" && !fn[ jQuery.expando ] && fn.length > 1 ) { + migrateWarn( "legacy-custom-pseudos", + "Pseudos with multiple arguments are deprecated; " + + "use jQuery.expr.createPseudo()" ); + } + return Reflect.set.apply( this, arguments ); + } + } ); + } ); +} diff --git a/src/migrate.js b/src/migrate.js index ade36a55..d60dba01 100644 --- a/src/migrate.js +++ b/src/migrate.js @@ -2,6 +2,7 @@ import "./version.js"; import "./compareVersions.js"; import "./main.js"; import "./jquery/core.js"; +import "./jquery/selector.js"; import "./jquery/ajax.js"; import "./jquery/attributes.js"; import "./jquery/css.js"; diff --git a/test/data/testinit.js b/test/data/testinit.js index 019fffd9..f44b99ea 100644 --- a/test/data/testinit.js +++ b/test/data/testinit.js @@ -64,13 +64,14 @@ "unit/jquery/attributes.js", "unit/jquery/css.js", "unit/jquery/data.js", + "unit/jquery/deferred.js", "unit/jquery/effects.js", "unit/jquery/event.js", "unit/jquery/manipulation.js", "unit/jquery/offset.js", + "unit/jquery/selector.js", "unit/jquery/serialize.js", - "unit/jquery/traversing.js", - "unit/jquery/deferred.js" + "unit/jquery/traversing.js" ]; testFiles.forEach( function( testFile ) { diff --git a/test/runner/flags/modules.js b/test/runner/flags/modules.js index 94e75851..594702fd 100644 --- a/test/runner/flags/modules.js +++ b/test/runner/flags/modules.js @@ -5,11 +5,12 @@ export const modules = [ "attributes", "css", "data", + "deferred", "effects", "event", "manipulation", "offset", + "selector", "serialize", - "traversing", - "deferred" + "traversing" ]; diff --git a/test/unit/jquery/core.js b/test/unit/jquery/core.js index ce0e17d8..634f3815 100644 --- a/test/unit/jquery/core.js +++ b/test/unit/jquery/core.js @@ -315,35 +315,6 @@ QUnit.test( "jQuery.unique", function( assert ) { } ); } ); -QUnit.test( "jQuery.expr.pseudos aliases", function( assert ) { - assert.expect( 7 ); - - expectWarning( assert, "jQuery.expr.filters", function() { - jQuery.expr.filters.mazda = function( elem ) { - return elem.style.zoom === "3"; - }; - } ); - - expectWarning( assert, "jQuery.expr[':']", function() { - jQuery.expr[ ":" ].marginal = function( elem ) { - return parseInt( elem.style.marginLeftWidth ) > 20; - }; - } ); - - expectNoWarning( assert, "jQuery.expr.pseudos", function() { - var fixture = jQuery( "#qunit-fixture" ).prepend( "
hello
" ); - - assert.ok( jQuery.expr.pseudos.mazda, "filters assigned" ); - assert.ok( jQuery.expr.pseudos.marginal, "[':'] assigned" ); - fixture.find( "p" ).first().css( "marginLeftWidth", "40px" ); - assert.equal( fixture.find( "p:marginal" ).length, 1, "One marginal element" ); - assert.equal( fixture.find( "div:mazda" ).length, 0, "No mazda elements" ); - delete jQuery.expr.pseudos.mazda; - delete jQuery.expr.pseudos.marginal; - } ); - -} ); - QUnit.test( "jQuery.holdReady (warn only)", function( assert ) { assert.expect( 1 ); diff --git a/test/unit/jquery/selector.js b/test/unit/jquery/selector.js new file mode 100644 index 00000000..64c75017 --- /dev/null +++ b/test/unit/jquery/selector.js @@ -0,0 +1,245 @@ +"use strict"; + +QUnit.module( "selector", { + beforeEach: function() { + + /* eslint-disable max-len */ + var template = "" + + "See this blog entry for more information.
" + + "\n" + + "" +
+ " Here are some [links] in a normal paragraph: Google," +
+ " Google Groups (Link)." +
+ " This link has class='blog'
:" +
+ " mozilla" +
+ "\n" +
+ "
Everything inside the red border is inside a div with id='foo'
.
This is a normal link: Yahoo
" + + "This link has class='blog'
: Timmy Willison's Weblog
hello
" ); + + assert.ok( jQuery.expr.pseudos.mazda, "filters assigned" ); + assert.ok( jQuery.expr.pseudos.marginal, "[':'] assigned" ); + fixture.find( "p" ).first().css( "marginLeftWidth", "40px" ); + assert.equal( fixture.find( "p:marginal" ).length, 1, "One marginal element" ); + assert.equal( fixture.find( "div:mazda" ).length, 0, "No mazda elements" ); + delete jQuery.expr.pseudos.mazda; + delete jQuery.expr.pseudos.marginal; + } ); +} ); + +QUnit.test( "custom pseudos", function( assert ) { + assert.expect( 7 ); + + expectNoWarning( assert, "custom pseudos", function() { + try { + jQuery.expr.pseudos.foundation = jQuery.expr.pseudos.root; + assert.deepEqual( + jQuery.find( ":foundation" ), + [ document.documentElement ], + "Copy element filter with new name" + ); + } finally { + delete jQuery.expr.pseudos.foundation; + } + + try { + jQuery.expr.setFilters.primary = jQuery.expr.setFilters.first; + testSelector( + assert, + "Copy set filter with new name", + "div#qunit-fixture :primary", + [ "firstp" ] + ); + } finally { + delete jQuery.expr.setFilters.primary; + } + + try { + jQuery.expr.pseudos.aristotlean = jQuery.expr.createPseudo( function() { + return function( elem ) { + return !!elem.id; + }; + } ); + testSelector( + assert, + "Custom element filter", + "#foo :aristotlean", + [ "sndp", "en", "yahoo", "sap", "anchor2", "timmy" ] + ); + } finally { + delete jQuery.expr.pseudos.aristotlean; + } + + try { + jQuery.expr.pseudos.endswith = jQuery.expr.createPseudo( function( text ) { + return function( elem ) { + return jQuery.text( elem ).slice( -text.length ) === text; + }; + } ); + testSelector( + assert, + "Custom element filter with argument", + "a:endswith(ogle)", + [ "google" ] + ); + } finally { + delete jQuery.expr.pseudos.endswith; + } + + try { + jQuery.expr.setFilters.second = jQuery.expr.createPseudo( function() { + return jQuery.expr.createPseudo( function( seed, matches ) { + if ( seed[ 1 ] ) { + matches[ 1 ] = seed[ 1 ]; + seed[ 1 ] = false; + } + } ); + } ); + testSelector( assert, + "Custom set filter", + "#qunit-fixture p:second", + [ "ap" ] + ); + } finally { + delete jQuery.expr.pseudos.second; + } + + try { + jQuery.expr.setFilters.slice = jQuery.expr.createPseudo( function( argument ) { + var bounds = argument.split( ":" ); + return jQuery.expr.createPseudo( function( seed, matches ) { + var i = bounds[ 1 ]; + + // Match elements found at the specified indexes + while ( --i >= bounds[ 0 ] ) { + if ( seed[ i ] ) { + matches[ i ] = seed[ i ]; + seed[ i ] = false; + } + } + } ); + } ); + testSelector( + assert, + "Custom set filter with argument", + "#qunit-fixture p:slice(1:3)", + [ "ap", "sndp" ] + ); + } finally { + delete jQuery.expr.pseudos.slice; + } + } ); +} ); + +QUnit.test( "backwards-compatible custom pseudos", function( assert ) { + assert.expect( 7 ); + + try { + expectWarning( assert, "Custom element filter with argument - setter", function() { + jQuery.expr.pseudos.icontains = function( elem, i, match ) { + return jQuery + .text( elem ) + .toLowerCase() + .indexOf( ( match[ 3 ] || "" ).toLowerCase() ) > -1; + }; + } ); + expectWarning( assert, "Custom element filter with argument - getter", function() { + testSelector( + assert, + "Custom element filter with argument", + "a:icontains(THIS BLOG ENTRY)", + [ "john1" ] + ); + } ); + } finally { + delete jQuery.expr.pseudos.icontains; + } + + try { + expectWarning( assert, "Custom setFilter pseudo - setter", function() { + jQuery.expr.setFilters.podium = function( elements, argument ) { + var count = argument == null || argument === "" ? 3 : +argument; + return elements.slice( 0, count ); + }; + } ); + expectWarning( assert, "Custom setFilter pseudo - getter", function() { + + // Using TAG as the first token here forces this setMatcher into a fail state + // Where the descendent combinator was lost + testSelector( + assert, + "Custom setFilter", + "form#form :PODIUM", + [ "label-for", "text1", "text2" ] + ); + testSelector( + assert, + "Custom setFilter with argument", + "#form input:Podium(1)", + [ "text1" ] + ); + } ); + } finally { + delete jQuery.expr.setFilters.podium; + } +} ); + +} )();