Skip to content

Commit 4c1171f

Browse files
authored
Selector: Re-introduce selector-native.js
Re-introduce the `selector-native` similar to the one on the `3.x-stable` branch. One difference is since the `main` branch inlined Sizzle, some selector utils can be shared between the main `selector` module and `selector-native`. The main `selector` module can be disabled in favor of `selector-native` via: grunt custom:-selector Other changes: * Tests: Fix Safari detection - Chrome Headless has a different user agent than Safari and a browser check in selector tests didn't take that into account. * Tests: Run selector-native tests in `npm test` * Selector: Fix querying on document fragments Ref gh-4395 Closes gh-5085
1 parent f62d8e2 commit 4c1171f

File tree

9 files changed

+161
-27
lines changed

9 files changed

+161
-27
lines changed

.github/workflows/node.js.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ jobs:
2525
NODE_VERSION: "16.x"
2626
NPM_SCRIPT: "test:no-deprecated"
2727
BROWSERS: "ChromeHeadless"
28+
- NAME: "Browser tests: selector-native build, Chrome stable"
29+
NODE_VERSION: "16.x"
30+
NPM_SCRIPT: "test:selector-native"
31+
BROWSERS: "ChromeHeadless"
2832
- NAME: "Browser tests: ES modules build, Chrome stable"
2933
NODE_VERSION: "16.x"
3034
NPM_SCRIPT: "test:esmodules"

Gruntfile.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ module.exports = function( grunt ) {
6161
all: {
6262
dest: "dist/jquery.js",
6363
minimum: [
64-
"core",
65-
"selector"
64+
"core"
6665
],
6766

6867
// Exclude specified modules if the module matching the key is removed
@@ -75,7 +74,8 @@ module.exports = function( grunt ) {
7574
remove: [ "ajax", "effects", "queue", "core/ready" ],
7675
include: [ "core/ready-no-deferred" ]
7776
},
78-
event: [ "deprecated/ajax-event-alias", "deprecated/event" ]
77+
event: [ "deprecated/ajax-event-alias", "deprecated/event" ],
78+
selector: [ "css/hiddenVisibleSelectors", "effects/animatedSelector" ]
7979
}
8080
}
8181
},

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ Some example modules that can be excluded are:
9999
- **exports/global**: Exclude the attachment of global jQuery variables ($ and jQuery) to the window.
100100
- **exports/amd**: Exclude the AMD definition.
101101

102+
As a special case, you may also replace the full jQuery `selector` module by using a special flag `grunt custom:-selector`.
103+
104+
- **selector**: The full jQuery selector engine. When this module is excluded, it is replaced by a rudimentary selector engine based on the browser's `querySelectorAll` method that does not support jQuery selector extensions or enhanced semantics. See the [selector-native.js](https://github.com/jquery/jquery/blob/main/src/selector-native.js) file for details.
105+
106+
*Note*: Excluding the full `selector` module will also exclude all jQuery selector extensions (such as `effects/animatedSelector` and `css/hiddenVisibleSelectors`).
107+
102108
The build process shows a message for each dependent module it excludes or includes.
103109

104110
##### AMD name

build/tasks/build.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,11 @@ module.exports = function( grunt ) {
128128
* Adds the specified module to the excluded or included list, depending on the flag
129129
* @param {String} flag A module path relative to
130130
* the src directory starting with + or - to indicate
131-
* whether it should included or excluded
131+
* whether it should be included or excluded
132132
*/
133133
const excluder = flag => {
134134
let additional;
135-
const m = /^(\+|\-|)([\w\/-]+)$/.exec( flag );
135+
const m = /^(\+|-|)([\w\/-]+)$/.exec( flag );
136136
const exclude = m[ 1 ] === "-";
137137
const module = m[ 2 ];
138138

@@ -150,10 +150,16 @@ module.exports = function( grunt ) {
150150
// These are the removable dependencies
151151
// It's fine if the directory is not there
152152
try {
153-
excludeList(
154-
fs.readdirSync( `${ srcFolder }/${ module }` ),
155-
module
156-
);
153+
154+
// `selector` is a special case as we don't just remove
155+
// the module, but we replace it with `selector-native`
156+
// which re-uses parts of the `src/selector` folder.
157+
if ( module !== "selector" ) {
158+
excludeList(
159+
fs.readdirSync( `${ srcFolder }/${ module }` ),
160+
module
161+
);
162+
}
157163
} catch ( e ) {
158164
grunt.verbose.writeln( e );
159165
}
@@ -232,14 +238,14 @@ module.exports = function( grunt ) {
232238
// Remove the comma for anonymous defines
233239
setOverride( `${ srcFolder }/exports/amd.js`,
234240
read( "exports/amd.js" )
235-
.replace( /(\s*)"jquery"(\,\s*)/,
241+
.replace( /(\s*)"jquery"(,\s*)/,
236242
amdName ? "$1\"" + amdName + "\"$2" : "" ) );
237243
}
238244

239245
grunt.verbose.writeflags( excluded, "Excluded" );
240246
grunt.verbose.writeflags( included, "Included" );
241247

242-
// Indicate a Slim build without listing all of the exclusions
248+
// Indicate a Slim build without listing all the exclusions
243249
// to save space.
244250
if ( isPureSlim ) {
245251
version += " slim";
@@ -260,7 +266,13 @@ module.exports = function( grunt ) {
260266

261267
// Replace excluded modules with empty sources.
262268
for ( const module of excluded ) {
263-
setOverride( `${ srcFolder }/${ module }.js`, "" );
269+
setOverride(
270+
`${ srcFolder }/${ module }.js`,
271+
272+
// The `selector` module is not removed, but replaced
273+
// with `selector-native`.
274+
module === "selector" ? read( "selector-native.js" ) : ""
275+
);
264276
}
265277
}
266278

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@
7777
"test:esmodules": "grunt && grunt karma:esmodules",
7878
"test:amd": "grunt && grunt karma:amd",
7979
"test:no-deprecated": "grunt test:prepare && grunt custom:-deprecated && grunt karma:main",
80+
"test:selector-native": "grunt test:prepare && grunt custom:-selector && grunt karma:main",
8081
"test:slim": "grunt test:prepare && grunt custom:slim && grunt karma:main",
81-
"test": "npm run test:slim && npm run test:no-deprecated && grunt && grunt test:slow && grunt karma:main && grunt karma:esmodules && grunt karma:amd",
82+
"test": "npm run test:slim && npm run test:no-deprecated && npm run test:selector-native && grunt && grunt test:slow && grunt karma:main && grunt karma:esmodules && grunt karma:amd",
8283
"jenkins": "npm run test:browserless"
8384
},
8485
"commitplease": {

src/selector-native.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Optional limited selector module for custom builds.
3+
*
4+
* Note that this DOES NOT SUPPORT many documented jQuery
5+
* features in exchange for its smaller size:
6+
*
7+
* * Attribute not equal selector (!=)
8+
* * Positional selectors (:first; :eq(n); :odd; etc.)
9+
* * Type selectors (:input; :checkbox; :button; etc.)
10+
* * State-based selectors (:animated; :visible; :hidden; etc.)
11+
* * :has(selector)
12+
* * :not(complex selector)
13+
* * custom selectors via jQuery extensions
14+
* * Leading combinators (e.g., $collection.find("> *"))
15+
* * Reliable functionality on XML fragments
16+
* * Requiring all parts of a selector to match elements under context
17+
* (e.g., $div.find("div > *") now matches children of $div)
18+
* * Matching against non-elements
19+
* * Reliable sorting of disconnected nodes
20+
* * querySelectorAll bug fixes (e.g., unreliable :focus on WebKit)
21+
*
22+
* If any of these are unacceptable tradeoffs, either use the full
23+
* selector engine or customize this stub for the project's specific
24+
* needs.
25+
*/
26+
27+
import jQuery from "./core.js";
28+
import document from "./var/document.js";
29+
import documentElement from "./var/documentElement.js";
30+
import whitespace from "./var/whitespace.js";
31+
32+
// The following utils are attached directly to the jQuery object.
33+
import "./selector/contains.js";
34+
import "./selector/escapeSelector.js";
35+
import "./selector/uniqueSort.js";
36+
37+
// Support: IE 9 - 11+
38+
// IE requires a prefix.
39+
var matches = documentElement.matches || documentElement.msMatchesSelector;
40+
41+
jQuery.extend( {
42+
find: function( selector, context, results, seed ) {
43+
var elem, nodeType,
44+
i = 0;
45+
46+
results = results || [];
47+
context = context || document;
48+
49+
// Same basic safeguard as in the full selector module
50+
if ( !selector || typeof selector !== "string" ) {
51+
return results;
52+
}
53+
54+
// Early return if context is not an element, document or document fragment
55+
if ( ( nodeType = context.nodeType ) !== 1 && nodeType !== 9 && nodeType !== 11 ) {
56+
return [];
57+
}
58+
59+
if ( seed ) {
60+
while ( ( elem = seed[ i++ ] ) ) {
61+
if ( jQuery.find.matchesSelector( elem, selector ) ) {
62+
results.push( elem );
63+
}
64+
}
65+
} else {
66+
jQuery.merge( results, context.querySelectorAll( selector ) );
67+
}
68+
69+
return results;
70+
},
71+
expr: {
72+
attrHandle: {},
73+
match: {
74+
bool: new RegExp( "^(?:checked|selected|async|autofocus|autoplay|controls|defer" +
75+
"|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$", "i" ),
76+
needsContext: new RegExp( "^" + whitespace + "*[>+~]" )
77+
}
78+
}
79+
} );
80+
81+
jQuery.extend( jQuery.find, {
82+
matches: function( expr, elements ) {
83+
return jQuery.find( expr, null, null, elements );
84+
},
85+
matchesSelector: function( elem, expr ) {
86+
return matches.call( elem, expr );
87+
}
88+
} );

test/data/testinit.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -301,17 +301,6 @@ if ( !window.__karma__ ) {
301301
QUnit.isSwarm = ( QUnit.urlParams.swarmURL + "" ).indexOf( "http" ) === 0;
302302
QUnit.basicTests = ( QUnit.urlParams.module + "" ) === "basic";
303303

304-
// Says whether jQuery positional selector extensions are supported.
305-
// A full selector engine is required to support them as they need to be evaluated
306-
// left-to-right. Remove that property when support for positional selectors is dropped.
307-
QUnit.jQuerySelectorsPos = true;
308-
309-
// Says whether jQuery selector extensions are supported. Change that to `false`
310-
// if your custom jQuery versions relies more on native qSA.
311-
// This doesn't include support for positional selectors (see above).
312-
// TODO do we want to keep this or just assume support for jQuery extensions?
313-
QUnit.jQuerySelectors = true;
314-
315304
// Support: IE 11+
316305
// A variable to make it easier to skip specific tests in IE, mostly
317306
// testing integrations with newer Web features not supported by it.
@@ -388,6 +377,18 @@ this.loadTests = function() {
388377

389378
// Get testSubproject from testrunner first
390379
require( [ parentUrl + "test/data/testrunner.js" ], function() {
380+
381+
// Says whether jQuery positional selector extensions are supported.
382+
// A full selector engine is required to support them as they need to
383+
// be evaluated left-to-right. Remove that property when support for
384+
// positional selectors is dropped.
385+
QUnit.jQuerySelectorsPos = includesModule( "selector" );
386+
387+
// Says whether jQuery selector extensions are supported. Change that
388+
// to `false` if your custom jQuery versions relies more on native qSA.
389+
// This doesn't include support for positional selectors (see above).
390+
QUnit.jQuerySelectors = includesModule( "selector" );
391+
391392
var i = 0,
392393
tests = [
393394
// A special module with basic tests, meant for not fully

test/unit/selector.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
QUnit.module( "selector", {
22
beforeEach: function() {
33
this.safari = /\bsafari\b/i.test( navigator.userAgent ) &&
4-
!/\bchrome\b/i.test( navigator.userAgent );
4+
!/\b(?:headless)?chrome\b/i.test( navigator.userAgent );
55
},
66
afterEach: moduleTeardown
77
} );
@@ -1908,6 +1908,21 @@ QUnit.testUnlessIE( "jQuery.contains within <template/> doesn't throw (gh-5147)"
19081908
assert.ok( true, "Didn't throw" );
19091909
} );
19101910

1911+
QUnit.test( "find in document fragments", function( assert ) {
1912+
assert.expect( 1 );
1913+
1914+
var elem,
1915+
nonnodes = jQuery( "#nonnodes" ).contents(),
1916+
fragment = document.createDocumentFragment();
1917+
1918+
nonnodes.each( function() {
1919+
fragment.appendChild( this );
1920+
} );
1921+
1922+
elem = jQuery( fragment ).find( "#nonnodesElement" );
1923+
assert.strictEqual( elem.length, 1, "Selection works" );
1924+
} );
1925+
19111926
QUnit.test( "jQuery.uniqueSort", function( assert ) {
19121927
assert.expect( 14 );
19131928

@@ -2156,7 +2171,7 @@ QUnit.test( "jQuery.escapeSelector", function( assert ) {
21562171
assert.equal( jQuery.escapeSelector( "\uD834" ), "\uD834", "Doesn't escape lone high surrogate" );
21572172
} );
21582173

2159-
QUnit.test( "custom pseudos", function( assert ) {
2174+
QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "custom pseudos", function( assert ) {
21602175
assert.expect( 6 );
21612176

21622177
try {

test/unit/support.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ testIframe(
5555
);
5656

5757
( function() {
58-
var expected,
58+
var expected, browserKey,
5959
userAgent = window.navigator.userAgent,
6060
expectedMap = {
6161
ie_11: {
@@ -80,6 +80,13 @@ testIframe(
8080
}
8181
};
8282

83+
// Make the selector-native build pass tests.
84+
for ( browserKey in expectedMap ) {
85+
if ( !includesModule( "selector" ) ) {
86+
delete expectedMap[ browserKey ].cssSupportsSelector;
87+
}
88+
}
89+
8390
if ( document.documentMode ) {
8491
expected = expectedMap.ie_11;
8592
} else if ( /chrome/i.test( userAgent ) ) {

0 commit comments

Comments
 (0)