Earlier, I answered this question, which was basically about removing a table row. This question came about as the result of the comments on that question. Given the following HTML:
<div><a href="#" class="removelink">remove</a></div>
<table>
<tr>
<td>Row 1</td>
</tr>
</table>
And the following jQuery:
$('.removelink').click(function(){
$(this).parent().siblings('table tr:last').remove();
});
I would expect nothing to happen, because the siblings method should select the siblings of the currently matched element, optionally filtered by a selector. From the jQuery docs:
The method optionally accepts a selector expression of the same type
that we can pass to the $() function. If the selector is supplied, the
elements will be filtered by testing whether they match it.
Based on that, I read the above code as “get the siblings of the current element (the div) which are the last tr within a table“. Obviously there are no elements that match that description – there is a tr within a table, but it’s not a sibling of the div. So, I wouldn’t expect any elements to be returned. However, it actually returns the entire table, as if it ignores the tr:last part of the selector entirely.
What confused me further was that if you remove the :last pseudo-selector, it works as expected (returning no elements).
Why is the entire table removed by the above code? Am I just being stupid and missing something obvious? You can see the above code in action here.
Edit – Here’s a simplified version. Given the following HTML:
<div id="d1"></div>
<div>
<span></span>
</div>
Why does the following jQuery return the second div:
$("#d1").siblings("div span:last");
I would expect it to return nothing, as there is not a span which is a sibling of #d1. Here’s a fiddle for this simplified example.
Update
Following the brilliant investigation from @muistooshort, I have created a jQuery bug ticket to track this issue.
Allow me to expand on my comment a little bit. All of this is based on your second simplified example and jQuery 1.6.4. This is a little long winded perhaps but we need to walk through the jQuery code to find out what it is doing.
We do have the jQuery source available so let us go a wandering
through it and see what wonders there are to behold therein.
The guts of
siblingslooks like this:wrapped up in this:
And then
jQuery.siblingis this:So we go up one step in the DOM, go to the parent’s first child,
and continue sideways to get all of the parent’s children (except
the node we started at!) as an array of DOM elements.
That leaves us with all of our sibling DOM elements in
retandnow to look at the filtering:
So what is
filterall about?filteris all about this:In your case,
elemswill have have exactly one element (as#d1has one sibling) so we’re off to
jQuery.find.matchesSelectorwhichis actually
Sizzle.matchesSelector:A bit of experimentation indicates that neither the Gecko nor WebKit
versions of
matchesSelectorcan handlediv span:firstso we endup in the final
Sizzle()call; note that both the Gecko and WebKitmatchesSelectorvariants can handlediv spanand yourjsfiddles work as expected in the
div spancase.What does
Sizzle(expr, null, null, [node])do? Why it returns an arraycontaining the
<span>inside your<div>of course. We’ll havethis in
expr:and this in
node:So the
<span id="s1">insidenodenicely matches the selectorin
exprand theSizzle()call returns an array containing the<span>and since that array has a non-zero length, thematchesSelectorcall returns true and everything falls apart in a pile of nonsense.
The problem is that jQuery isn’t interfacing with Sizzle properly in this case. Congratulations, you are the proud father of a bouncing baby bug.
Here’s a (massive) jsfiddle with an inlined version of jQuery with a couple
console.logcalls to support what I’m talking about above:A few things to note:
div spananddiv span:nth-child(1); both of these use the native Gecko and WebKit selector engine.div span:first,div span:last, and evendiv span:eq(0); all three of these go through Sizzle.Sizzle()call that is being used not documented (see Public API) so we don’t know if jQuery or Sizzle is at fault here.