Pseudo-selectors with the siblings method

I previously answered this question , which was mainly about deleting a table row. This question arose as a result of comments on this issue. 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 not expect that nothing would happen, because the siblings method should select the siblings of the current matching element, it is not necessary to filter the selector. From jQuery docs:

The method does not necessarily accept a select expression of the same type that we can go to the $ () function. If the selector is on, the elements will be filtered out to check if they match.

Based on this, I read the above code as "getting siblings of the current element ( div ) that are the last tr within the table ". Obviously, there are no elements matching this description - table has tr , but it is not the sibling of a div . Therefore, I did not expect any items to be returned. However, it actually returns the entire table, as if it completely ignored the tr:last selector.

What confused me was that if you remove the :last pseudo :last , it works as expected (without returning any elements).

Why is the whole table deleted by the above code? Am I just stupid and skipping something obvious? You can see the above code in action here .

Change Here is a simplified version. Given the following HTML:

 <div id="d1"></div> <div> <span></span> </div> 

Why the following jQuery returns the second div :

 $("#d1").siblings("div span:last"); 

I would expect it to return nothing, since there is no span which is brother #d1 . Here is the script for this simplified example.

Update

Following brilliant research by @muistooshort, I created a jQuery error to track this issue.

+4
source share
2 answers

Let me expand my comment a bit. All of this is based on your second simplified example and jQuery 1.6.4. It may be a little longer, but we need to go through jQuery code to find out what it does.


We have a jQuery source, so let's take a walk through it and see what miracles are there.

The feelings of siblings are as follows:

 siblings: function( elem ) { return jQuery.sibling( elem.parentNode.firstChild, elem ); } 

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

 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 first child, and continue sideways to get all the parent children (except node we started with!) As an array of DOM elements.

This leaves us with all of our Sibling DOM elements in ret and now let's look at the filtering:

 ret = jQuery.filter( selector, ret ); 

So what is a filter ? filter is everything:

 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 exactly one element (as #d1 has one brother), so we will go 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 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 little experimentation shows that neither Gecko nor the WebKit version of matchesSelector can handle div span:first , so we end up in the final Sizzle() call; note that both Gecko and WebKit matchesSelector options can handle div span and your jsfiddles work as expected in the case of div span .

What does Sizzle(expr, null, null, [node]) do Sizzle(expr, null, null, [node]) ? Why does it return an array containing a <span> inside your <div> , of course. We will have this in expr :

 'div span:last' 

and this is in node :

 <div id="d2"> <span id="s1"></span> </div> 

So, <span id="s1"> inside node matches the selector in expr nicely, and calling Sizzle() returns an array containing <span> , and since this array is non-zero length, matchesSelector call returns true, and everything falls apart in a bunch of rubbish .

The problem is that jQuery does not interact with Sizzle in this case. Congratulations, you are the proud father of a bouncing baby.

Here's a (massive) jsfiddle with a built-in version of jQuery with the password console.log to support what I'm talking about above:

http://jsfiddle.net/ambiguous/TxGXv/

A few notes:

  • You will get reasonable results with div span and div span:nth-child(1) ; both of them use their own Gecko and WebKit selection engine.
  • You will get the same broken results with div span:first , div span:last and even div span:eq(0) ; all three of them go through Sizzle.
  • The four argument version of the call to Sizzle() , which is not documented (see the Open API ), so we don’t know if jQuery or Sizzle is to blame.
+5
source

Update with

 $('.removelink').click(function(){ $(this).parent().siblings('table').find('tr:last').remove(); 

});

Check Example Script

0
source

All Articles