tags:

views:

85

answers:

2

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.

+6  A: 

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: http://stackoverflow.com/questions/3944878/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.

Moin Zaman
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.
patrick dw
The link you posted is specific to an issue where the selector starts with `>`.
patrick dw
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
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
@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.
patrick dw
@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)
patrick dw
@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
@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
A: 

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
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