5

I have a gnarly navigation structure that can be generalized as:

<ul id="navigation">
    <li>
        A
        <ul>
            <li>
                B
                <ul>
                    <li>C</li>
                </ul>
            </li>
            <li>
                D
                <ul>
                    <li>
                        E
                        <ul>
                            <li>F</li>
                        </ul>
                    </li>
                </ul>
            </li>
        </ul>
    </li>
</ul>

Sub-items are hidden until hover. I want to indicate that B, D, and E have sub-items by styling them so I used the selector:

$('#navigation > li li:has(ul)')

Which only returned B and D. Changing it to:

$('#navigation > li li').has('ul')

returned all of the correct items but I'm confused as to why.

EDIT

:has() doesn't appear to be affected (entirely) by nesting as

$('#navigation ul > li:has(ul)')

returns the same results as .has() above.

Matthew Jacobs
  • 4,344
  • 1
  • 20
  • 20
  • It seems to me that the results should be the same. I don't know why they're different. Perhaps Sizzle gives up looking in nested elements when a match is found. If so, this would seem to ignore the possibility of identical nested elements (as you have in your example). – user113716 Oct 19 '10 at 17:37

2 Answers2

7

From the jQuery API documentation:

:has selects elements which contain at least one element that matches the specified selector.

.has() reduces the set of matched elements to those that have a descendant that matches the selector or DOM element.

There is a related question here: jQuery: subtle difference between .has() and :has() , that seems to point to what the difference in the two could be.

However, It appears that :has doesn't look within a match in a nested fashion while .has() does because it returns a subset of matches as a jQuery object is able to match all descendants, even the nested one, like a regular jQuery selector.

Community
  • 1
  • 1
Moin Zaman
  • 25,281
  • 6
  • 70
  • 74
  • 3
    Those are the descriptions copied from the docs [here](http://api.jquery.com/has-selector/) and [here](http://api.jquery.com/has/). How does it make them different? It would seem to me that they should return the same results. – user113716 Oct 19 '10 at 17:31
  • 1
    The link you posted is specific to an issue where the selector starts with `>`. – user113716 Oct 19 '10 at 17:35
  • 1
    I have to agree with @patrick - this doesn't at all answer the question, the `:has()` description means it *should* find that third element, `E`. Also, you should like to where these descriptions come from. – Nick Craver Oct 19 '10 at 17:40
  • I think the link gives us a clue as to what might be going on regarless of the specific use of the child selector – Moin Zaman Oct 19 '10 at 17:49
  • 1
    @Moin - Not really. The opposite is true in that question. There, the `:has()` selector version works, and the `.has()` method doesn't. In fact the method returns `0` results. Different issue. – user113716 Oct 19 '10 at 17:53
  • @Nick - I've got a new strategy getting for rep points. Plagiarize docs, post unrelated links, and re-word other people's comments. I'll catch up to you before you know it. ;o) – user113716 Oct 19 '10 at 18:02
  • @patrick - heh true in this case...the question does have me figuring out where the issue is in Sizzle though, it seems to be a post processing step that goes astray, will answer when I can explain it better. – Nick Craver Oct 19 '10 at 18:06
  • @patrick: I didn't even read your comment under the Q until after I posted, if the strategy as you've stated works in a collaboratively edited site, with folks like you around, then there's something else wrong... @Nick: Might be useful to post your findings in the ticket the poster from the other Q created: http://bugs.jquery.com/ticket/7205 – Moin Zaman Oct 19 '10 at 18:20
0

The :has() selector selects only those elements that have descending elements that are matched by the given selector. Internally, :has is defined as (Sizzle is jQuery default selector library):

function(elem, i, match){
    return !!Sizzle( match[3], elem ).length;
}

This is nearly equivalent to testing jQuery("selector", elem).length or jQuery(elem).find("selector").length for each selected element to filter those that don’t have such descendants.

In contrast to that, the has method can take either a selector or a DOM element and returns only those elements that do not contain any of the given elements. Because internally, the has method is defined as:

function( target ) {
    var targets = jQuery( target );
    return this.filter(function() {
        for ( var i = 0, l = targets.length; i < l; i++ ) {
            if ( jQuery.contains( this, targets[i] ) ) {
                return true;
            }
        }
    });
}

So it uses the contains method to check if the given elements are contained to filter the selected elements. Note that it suffices that only one of the given elements are contained.

Gumbo
  • 643,351
  • 109
  • 780
  • 844
  • 1
    This doesn't explain the behavior though, this is a bug in Sizzle as it *should* return these elements, the problem is the array of selector parts being popped incorrectly in this scenario. For example: `$('#navigation li li:has(ul)')` will give 3 elements. – Nick Craver Oct 19 '10 at 21:05