20

The next, prev, nextAll and prevAll methods are very useful, but not if the elements you are trying to find are not in the same parent element. What I want to do is something like this:

<div>
    <span id="click">Hello</span>
</div>
<div>
    <p class="find">World></p>
</div>

When the span with the id click is pressed, I want to match the next element with the class find, which in this case is not a sibling of the clicked element so next() or nextAll() won't work.

Jonas
  • 121,568
  • 97
  • 310
  • 388
Gnuffo1
  • 3,478
  • 11
  • 39
  • 53
  • 1
    possible duplicate of [jQuery to find all previous elements that match an expression](http://stackoverflow.com/questions/322912/jquery-to-find-all-previous-elements-that-match-an-expression) – sachleen Jul 18 '12 at 19:29

5 Answers5

15

Try this. It will mark your Element, create a set of Elements matching your selector and collect all Elements from the set following your element.

$.fn.findNext = function ( selector ) {
    var set = $( [] ), found = false;
    $( this ).attr( "findNext" , "true" );
    $( selector ).each( function( i , element ) {
        element = $( element );
        if ( found == true ) set = set.add( element )
        if ( element.attr("findNext") == "true" ) found = true;
    })
    $( this ).removeAttr( "findNext" )
    return set
}

EDIT

much simpler solution using jquerys index method. the element you call the method from needs to be selectable by the same selector though

$.fn.findNext = function( selector ){
    var set = $( selector );
    return set.eq( set.index( this, ) + 1 )
}

to free the function from this handicap, we could youse the browsers own compareDocumentposition

$.fn.findNext = function ( selector ) {
  // if the stack is empty, return the first found element
  if ( this.length < 1 ) return $( selector ).first();
  var found,
      that = this.get(0);
  $( selector )
    .each( function () {
       var pos = that.compareDocumentPosition( this );
       if ( pos === 4 || pos === 12 || pos === 20 ){
       // pos === 2 || 10 || 18 for previous elements 
         found = this; 
         return false;
       }    
    })
  // using pushStack, one can now go back to the previous elements like this
  // $("#someid").findNext("div").remove().end().attr("id")
  // will now return "someid" 
  return this.pushStack( [ found ] );
},  

EDIT 2 this is far easier using jQuery's $.grep. here's the new code

   $.fn.findNextAll = function( selector ){
      var that = this[ 0 ],
          selection = $( selector ).get();
      return this.pushStack(
         // if there are no elements in the original selection return everything
         !that && selection ||
         $.grep( selection, function( n ){
            return [4,12,20].indexOf( that.compareDocumentPosition( n ) ) > -1
         // if you are looking for previous elements it should be [2,10,18]
         })
      );
   }
   $.fn.findNext = function( selector ){
      return this.pushStack( this.findNextAll( selector ).first() );
   }

when compressing variable names this becomes a mere two liner.

Edit 3 using bitwise operations, this function may be even faster?

$.fn.findNextAll = function( selector ){
  var that = this[ 0 ],
    selection = $( selector ).get();
  return this.pushStack(
    !that && selection || $.grep( selection, function(n){
       return that.compareDocumentPosition(n) & (1<<2);
       // if you are looking for previous elements it should be & (1<<1);
    })
  );
}
$.fn.findNext = function( selector ){
  return this.pushStack( this.findNextAll( selector ).first() );
}
Simon
  • 12,018
  • 4
  • 34
  • 39
lordvlad
  • 5,200
  • 1
  • 24
  • 44
  • one disadvantage i havent thought of is that the element you call findNext from, needs to be selectable by the same selector, else it will simply loop through everything without finding your marked element. – lordvlad Dec 02 '12 at 20:23
  • Thank you, I've looked all over and no one else has answered the non-sibling case as succinctly as you. – John Lehmann Jan 20 '13 at 03:35
  • Thank you for your work. With very large tabular documents, Edit 2 has sped up what I was trying to do by orders of magnitude! – David May 10 '13 at 20:37
  • also for finding the previous element, the function `findPrev` should look like `return this.pushStack( this.findNextAll( selector ).eq(-2) );`, because `.last()` will return the current element – dimaninc Jun 29 '13 at 21:53
  • Note that `.indexOf` will throw an error in <= IE8. I used [$.inArray](http://api.jquery.com/jQuery.inArray/) instead. – allicarn Dec 13 '13 at 14:42
  • 1
    @allicarn see my latest edit. bitwise operations are surely faster that the loops used in $.inArray – lordvlad Dec 13 '13 at 20:12
  • @dimanic, this won't work, because findNextAll finds all elements AFTER the current one. if i'm not immensely mistaken, there is no way you can find a previous item with that function. findNext should use a different findNextAll function, which is indicated in my comments – lordvlad Dec 13 '13 at 20:14
  • 1
    My apologies... it looks like there is also an issue with [compareDocumentPosition](http://www.w3schools.com/jsref/met_node_comparedocumentposition.asp): "Note: Internet Explorer 8 and earlier does not support this method." I guess I hadn't tested it properly after implementing $.inArray – allicarn Dec 16 '13 at 20:59
  • Hi! Just wanted to add if you're interested for a previous function, you need to add `& (1<<1)` and you need to reverse the array that `$.grep` gives you so that your first element is the first on the pushed stack :) [See here](http://stackoverflow.com/questions/31849564/filter-through-elements-with-same-class-but-not-in-the-same-wrapper) – dingo_d Aug 06 '15 at 09:15
8

I was working on this problem myself today, here's what I came up with:

/**
 * Find the next element matching a certain selector. Differs from next() in
 *  that it searches outside the current element's parent.
 *  
 * @param selector The selector to search for
 * @param steps (optional) The number of steps to search, the default is 1
 * @param scope (optional) The scope to search in, the default is document wide 
 */
$.fn.findNext = function(selector, steps, scope)
{
    // Steps given? Then parse to int 
    if (steps)
    {
        steps = Math.floor(steps);
    }
    else if (steps === 0)
    {
        // Stupid case :)
        return this;
    }
    else
    {
        // Else, try the easy way
        var next = this.next(selector);
        if (next.length)
            return next;
        // Easy way failed, try the hard way :)
        steps = 1;
    }

    // Set scope to document or user-defined
    scope = (scope) ? $(scope) : $(document);

    // Find kids that match selector: used as exclusion filter
    var kids = this.find(selector);

    // Find in parent(s)
    hay = $(this);
    while(hay[0] != scope[0])
    {
        // Move up one level
        hay = hay.parent();     
        // Select all kids of parent
        //  - excluding kids of current element (next != inside),
        //  - add current element (will be added in document order)
        var rs = hay.find(selector).not(kids).add($(this));
        // Move the desired number of steps
        var id = rs.index(this) + steps;
        // Result found? then return
        if (id > -1 && id < rs.length)
            return $(rs[id]);
    }
    // Return empty result
    return $([]);
}

So in your example

<div><span id="click">hello</span></div>
<div><p class="find">world></p></div>

you could now find and manipulate the 'p' element using

$('#click').findNext('.find').html('testing 123');

I doubt it will perform well on large structures, but here it is :)

Michael Clerx
  • 2,928
  • 2
  • 33
  • 47
4

My solution would involve adjusting your markup a bit to make the jQuery much easier. If this is not possible or not an appealing answer, please ignore!

I would wrap a 'parent' wrapper around what you want to do...

<div class="find-wrapper">
    <div><span id="click">hello</span></div>
    <div><p class="find">world></p></div>
</div>

Now, to find the find:

$(function() {
    $('#click').click(function() {
        var $target = $(this).closest('.find-wrapper').find('.find');
        // do something with $target...
    });
});

This gives you the flexibility to have whatever kind of markup and hierarchy you'd like inside the wrapper I suggested, and still reliably find your target.

Good luck!

Funka
  • 4,258
  • 2
  • 24
  • 27
0

I think the only way to solve this problem is to do a recursive search over the elements after the current element. There is no simple solution to this problem provided by jQuery. If you only want to find elements in the siblings of your parent element (as is the case in your example), it is not required to do a recursive search, but you have to do multiple searches.

I created an example (actually, it isn't recursive) which does what you want (I hope). It selects all elements after the current clicked element and makes them red:

<script type="text/javascript" charset="utf-8">
    $(function () {
        $('#click').click(function() {
            var parent = $(this);
            alert(parent);
            do {
                $(parent).find('.find').css('background-color','red'); 
                parent = $(parent).parent();
            } while(parent !== false);
        });
    });
</script>
Martin Sturm
  • 728
  • 1
  • 4
  • 14
0

The following expression should (barring syntax errors!) find all siblings of the parent that contain a p.find element and then find those p.find elements and change their colour to blue.

$(this).parent().nextAll(":has(p.find)").find(".find").css('background-color','blue');

Of course, if your page structure is such that p.find occurs in a totally different level of hierarchy (sibling of a grandparent for example), it won't work.

dnagirl
  • 20,196
  • 13
  • 80
  • 123