Reputation: 165961
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.
Upvotes: 4
Views: 1819
Reputation: 434635
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 siblings
looks like this:
siblings: function( elem ) {
return jQuery.sibling( elem.parentNode.firstChild, elem );
}
wrapped up in this:
// `name` is "siblings", `fn` is the function above.
jQuery.fn[ name ] = function( until, selector ) {
var ret = jQuery.map( this, fn, until )
//...
if ( selector && typeof selector === "string" ) {
ret = jQuery.filter( selector, ret );
}
//...
};
And then jQuery.sibling
is this:
sibling: function( n, elem ) {
var r = [];
for ( ; n; n = n.nextSibling ) {
if ( n.nodeType === 1 && n !== elem ) {
r.push( n );
}
}
return r;
}
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 ret
and
now to look at the filtering:
ret = jQuery.filter( selector, ret );
So what is filter
all about? filter
is all about this:
filter: function( expr, elems, not ) {
//...
return elems.length === 1 ?
jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
jQuery.find.matches(expr, elems);
}
In your case, elems
will have have exactly one element (as #d1
has one sibling) so we're off to jQuery.find.matchesSelector
which
is actually Sizzle.matchesSelector
:
var html = document.documentElement,
matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
//...
Sizzle.matchesSelector = function( node, expr ) {
// Make sure that attribute selectors are quoted
expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");
if ( !Sizzle.isXML( node ) ) {
try {
if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
var ret = matches.call( node, expr );
// IE 9's matchesSelector returns false on disconnected nodes
if ( ret || !disconnectedMatch ||
// As well, disconnected nodes are said to be in a document
// fragment in IE 9, so check for that
node.document && node.document.nodeType !== 11 ) {
return ret;
}
}
} catch(e) {}
}
return Sizzle(expr, null, null, [node]).length > 0;
};
A bit of experimentation indicates that neither the Gecko nor WebKit
versions of matchesSelector
can handle div span:first
so we end
up in the final Sizzle()
call; note that both the Gecko and WebKit
matchesSelector
variants can handle div span
and your
jsfiddles work as expected in the div span
case.
What does Sizzle(expr, null, null, [node])
do? Why it returns an array
containing the <span>
inside your <div>
of course. We'll have
this in expr
:
'div span:last'
and this in node
:
<div id="d2">
<span id="s1"></span>
</div>
So the <span id="s1">
inside node
nicely matches the selector
in expr
and the Sizzle()
call returns an array containing the
<span>
and since that array has a non-zero length, the matchesSelector
call 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.log
calls to support what I'm talking about above:
A few things to note:
div span
and div span:nth-child(1)
; both of these use the native Gecko and WebKit selector engine.div span:first
, div span:last
, and even div 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.Upvotes: 5
Reputation: 2637
Update with this
$('.removelink').click(function(){
$(this).parent().siblings('table').find('tr:last').remove();
});
Check Fiddle Example
Upvotes: 0