views:

11370

answers:

3

Using jQuery, how do you match elements that are prior to the current element in the DOM tree? Using prevAll() only matches previous siblings.

eg:

<table>
    <tr>
        <td class="findme">find this one</td>
    </tr>
    <tr>
        <td><a href="#" class="myLinks">find the previous .findme</a></td>
    </tr>
    <tr>
        <td class="findme">don't find this one</td>
    </tr>
</table>

In my specific case, I'll be searching for the first .findme element prior to the link clicked.

+1  A: 

Presumably you are doing this inside an onclick handler so you have access to the element that was clicked. What I would do is do a prevAll to see if it is at the same level. If not, then I would do a parent().prevAll() to get the previous siblings of the parent element, then iterate through those backwards, checking their contents for the desired element. Continue going up the DOM tree until you find what you want or hit the root of the DOM. This a general algorithm.

If you know that it is inside a table, then you can simply get the row containing the element clicked and iterate backwards through the rows of the table from that row until you find one that contains the element desired.

I don't think there is a way to do it in one (chained) statement.

tvanfosson
great suggestion with the algorithm. I actually implemented it (see the first edit on my answer), but it ran a little slow. What I ended up doing was selecting everything and then slicing that array on either side of the current object, which seems to be faster.
nickf
+1  A: 

edit: this solution works for both your original problem, the problem you mention in your first comment, and the problem you detail in the comment after that.

$('.myLinks').click(function() {
    var findMe = '';

    $(this).parents().each(function() {
        var a = $(this).find('.findme').is('.findme');
        var b = $(this).find('.myLinks').is('.myLinks');
        if (a && b) {                             // look for first parent that
                                                  // contains .findme and .myLinks
            $(this).find('*').each(function() {
                var name = $(this).attr('class');
                if ( name == 'findme') {
                    findMe = $(this);             // set element to last matching
                                                  // .findme
                }
                if ( name == 'myLinks')    {
                    return false;                 // exit from the mess once we find
                                                  // .myLinks
                }
            });
            return false;
        }   
    });
    alert(findMe.text() );               // alerts "find this one"
});

this works for your example in the OP as well as a modified example as explained in the comments:

<table>
  <tr>
    <td class="findme">don't find this one</td>
  </tr>
  <tr>
    <td class="findme">find this one</td>
  </tr>
  <tr>
    <td><a href="#" class="myLinks">find the previous .findme</a></td>
  </tr>
  <tr>
    <td class="findme">don't find this one</td>
  </tr>
</table>

as well as this test case which you added:

<table>
  <tr>
    <td class="findme">don't find this one</td>
  </tr>
  <tr>
    <td class="findme">don't find this one</td>
  </tr>
  <tr>
    <td class="findme">find this one</td>
  </tr>
</table>

<a href="#" class="myLinks">find the previous .findme</a>
Owen
hey that's not bad! some points though: it doesn't search the siblings of the current node, and since you're searching forwards instead of backwards (like parents()), i guess you'd be wanting to use :last instead of :first. i was probably a bit ambiguous about that in the question
nickf
as long as the .findme is before the actual click, it should work fine in any markup. this specific solution works for your test code. i may be misunderstanding you, but it searches back first (looks through parents) then forward to find .findme
Owen
or do you mean in cases where there are two .findme's before the link, it should match the last one (before the link)?
Owen
yeah :\ sorry about the ambiguity
nickf
points for effort, but it doesn't work sorry. If you put a link after the table, it won't find anything. I've knuckled down and written another solution which I'm just about to write up now...
nickf
did you actually try it? it works on that test case too
Owen
yeah: check it out: http://jsbin.com/onoyu
nickf
+4  A: 

Ok, here's what I've come up with - hopefully it'll be useful in many different situations. It's 2 extensions to jQuery that I call prevALL and nextALL. While the standard prevAll() matches previous siblings, prevALL() matches ALL previous elements all the way up the DOM tree, similarly for nextAll() and nextALL().

I'll try to explain it in the comments below:

// this is a small helper extension i stole from
// http://www.texotela.co.uk/code/jquery/reverse/
// it merely reverses the order of a jQuery set.
$.fn.reverse = function() {
    return this.pushStack(this.get().reverse(), arguments);
};

// create two new functions: prevALL and nextALL. they're very similar, hence this style.
$.each( ['prev', 'next'], function(unusedIndex, name) {
    $.fn[ name + 'ALL' ] = function(matchExpr) {
        // get all the elements in the body, including the body.
        var $all = $('body').find('*').andSelf();

        // slice the $all object according to which way we're looking
        $all = (name == 'prev')
             ? $all.slice(0, $all.index(this)).reverse()
             : $all.slice($all.index(this) + 1)
        ;
        // filter the matches if specified
        if (matchExpr) $all = $all.filter(matchExpr);
        return $all;
    };
});

usage:

$('.myLinks').click(function() {
    $(this)
        .prevALL('.findme:first')
        .html("You found me!")
    ;

    // set previous nodes to blue
    $(this).prevALL().css('backgroundColor', 'blue');

    // set following nodes to red
    $(this).nextALL().css('backgroundColor', 'red');
});


edit - function rewritten from scratch. I just thought of a much quicker and simpler way to do it. Take a look at the edit history to see my first iteration.

edit again - found an easier way to do it!

nickf
Shouldn't this be "nextALL()" in your "set following nodes to red" call?
Tomalak
oh right, yeah :)
nickf
Cool solution. I like it.
Tomalak
Perfect! Worked without a hitch exactly the way I wanted it to! Nice.
briandus
Thanks for this code, solved a problem we had been struggling with for a while! :D
DavidGouge