🌐 AI搜索 & 代理 主页
Skip to content

Commit 3798e3d

Browse files
authored
Selector: Use Sizzle :has if CSS.supports(selector(...)) non-compliant
Sizzle has followed the following logic for selector handling for ages: 1. Modify the selector to adhere to scoping rules jQuery mandates. 2. Try `qSA` on the modified selector. If it succeeds, use the results. 3. If `qSA` threw an error, run the Sizzle custom traversal instead. It worked fine so far but now CSS has a concept of forgiving selector lists that some selectors like `:is()` & `:has()` use. That means providing unrecognized selectors as parameters to `:is()` & `:has()` no longer throws an error, it will just return no results. That made browsers with native `:has()` support break selectors using jQuery extensions inside, e.g. `:has(:contains("Item"))`. Detecting support for selectors can also be done via: ```js CSS.supports( "selector(SELECTOR_TO_BE_TESTED)" ) ``` which returns a boolean. There was a recent spec change requiring this API to always use non-forgiving parsing: w3c/csswg-drafts#7280 (comment) However, no browsers have implemented this change so far. To solve this, two changes are being made: 1. In browsers supports the new spec change to `CSS.supports( "selector()" )`, use it before trying `qSA`. 2. Otherwise, add `:has` to the buggy selectors list. Fixes jquery/jquery#5098 Closes gh-486 Ref jquery/jquery#5107 Ref w3c/csswg-drafts#7676
1 parent d3af661 commit 3798e3d

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-2
lines changed

src/sizzle.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,27 @@ function Sizzle( selector, context, results, seed ) {
356356
}
357357

358358
try {
359+
360+
// `qSA` may not throw for unrecognized parts using forgiving parsing:
361+
// https://drafts.csswg.org/selectors/#forgiving-selector
362+
// like the `:has()` pseudo-class:
363+
// https://drafts.csswg.org/selectors/#relational
364+
// `CSS.supports` is still expected to return `false` then:
365+
// https://drafts.csswg.org/css-conditional-4/#typedef-supports-selector-fn
366+
// https://drafts.csswg.org/css-conditional-4/#dfn-support-selector
367+
if ( support.cssSupportsSelector &&
368+
369+
// eslint-disable-next-line no-undef
370+
!CSS.supports( "selector(" + newSelector + ")" ) ) {
371+
372+
// Support: IE 11+
373+
// Throw to get to the same code path as an error directly in qSA.
374+
// Note: once we only support browser supporting
375+
// `CSS.supports('selector(...)')`, we can most likely drop
376+
// the `try-catch`. IE doesn't implement the API.
377+
throw new Error();
378+
}
379+
359380
push.apply( results,
360381
newContext.querySelectorAll( newSelector )
361382
);
@@ -651,6 +672,31 @@ setDocument = Sizzle.setDocument = function( node ) {
651672
!el.querySelectorAll( ":scope fieldset div" ).length;
652673
} );
653674

675+
// Support: Chrome 105+, Firefox 104+, Safari 15.4+
676+
// Make sure forgiving mode is not used in `CSS.supports( "selector(...)" )`.
677+
//
678+
// `:is()` uses a forgiving selector list as an argument and is widely
679+
// implemented, so it's a good one to test against.
680+
support.cssSupportsSelector = assert( function() {
681+
/* eslint-disable no-undef */
682+
683+
return CSS.supports( "selector(*)" ) &&
684+
685+
// Support: Firefox 78-81 only
686+
// In old Firefox, `:is()` didn't use forgiving parsing. In that case,
687+
// fail this test as there's no selector to test against that.
688+
// `CSS.supports` uses unforgiving parsing
689+
document.querySelectorAll( ":is(:jqfake)" ) &&
690+
691+
// `*` is needed as Safari & newer Chrome implemented something in between
692+
// for `:has()` - it throws in `qSA` if it only contains an unsupported
693+
// argument but multiple ones, one of which is supported, are fine.
694+
// We want to play safe in case `:is()` gets the same treatment.
695+
!CSS.supports( "selector(:is(*,:jqfake))" );
696+
697+
/* eslint-enable */
698+
} );
699+
654700
/* Attributes
655701
---------------------------------------------------------------------- */
656702

@@ -917,6 +963,18 @@ setDocument = Sizzle.setDocument = function( node ) {
917963
} );
918964
}
919965

966+
if ( !support.cssSupportsSelector ) {
967+
968+
// Support: Chrome 105+, Safari 15.4+
969+
// `:has()` uses a forgiving selector list as an argument so our regular
970+
// `try-catch` mechanism fails to catch `:has()` with arguments not supported
971+
// natively like `:has(:contains("Foo"))`. Where supported & spec-compliant,
972+
// we now use `CSS.supports("selector(SELECTOR_TO_BE_TESTED)")` but outside
973+
// that, let's mark `:has` as buggy to always use jQuery traversal for
974+
// `:has()`.
975+
rbuggyQSA.push( ":has" );
976+
}
977+
920978
rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) );
921979
rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) );
922980

@@ -1719,7 +1777,7 @@ Expr = Sizzle.selectors = {
17191777
return elem.nodeName.toLowerCase() === "input" &&
17201778
elem.type === "text" &&
17211779

1722-
// Support: IE<8
1780+
// Support: IE <10 only
17231781
// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
17241782
( ( attr = elem.getAttribute( "type" ) ) == null ||
17251783
attr.toLowerCase() === "text" );

test/unit/selector.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -842,11 +842,19 @@ QUnit.test( "pseudo - nth-last-of-type", function( assert ) {
842842
} );
843843

844844
QUnit.test( "pseudo - has", function( assert ) {
845-
assert.expect( 3 );
845+
assert.expect( 4 );
846846

847847
t( "Basic test", "p:has(a)", [ "firstp", "ap", "en", "sap" ] );
848848
t( "Basic test (irrelevant whitespace)", "p:has( a )", [ "firstp", "ap", "en", "sap" ] );
849849
t( "Nested with overlapping candidates", "#qunit-fixture div:has(div:has(div:not([id])))", [ "moretests", "t2037" ] );
850+
851+
// Support: Safari 15.4+, Chrome 107+?
852+
// `qSA` in Safari throws for `:has()` with only unsupported arguments
853+
// but if you add a supported arg to the list, it will run and just potentially
854+
// return no results. Make sure this is accounted for.
855+
t( "Nested with list arguments",
856+
"#qunit-fixture div:has(faketag, div:has(faketag, div:not([id])))",
857+
[ "moretests", "t2037" ] );
850858
} );
851859

852860
QUnit.test( "pseudo - contains", function( assert ) {

0 commit comments

Comments
 (0)