Sticking to the official jQuery API, is there a more concise, but not less efficient, way of finding the next sibling of an element that matches a given selector other than using nextAll
with the :first
pseudo-class?
When I say official API, I mean not hacking internals, going straight to Sizzle, adding a plug-in into the mix, etc. (If I end up having to do that, so be it, but that's not what this question is.)
E.g, given this structure:
<div>One</div> <div class='foo'>Two</div> <div>Three</div> <div class='foo'>Four</div> <div>Five</div> <div>Six</div> <div>Seven</div> <div class='foo'>Eight</div>
If I have a div
in this
(perhaps in a click
handler, whatever) and want to find the next sibling div that matches the selector "div.foo", I can do this:
var nextFoo = $(this).nextAll("div.foo:first");
...and it works (if I start with "Five", for instance, it skips "Six" and "Seven" and finds "Eight" for me), but it's clunky and if I want to match the first of any of several selectors, it gets a lot clunkier. (Granted, it's a lot more concise than the raw DOM loop would be...)
I basically want:
var nextFoo = $(this).nextMatching("div.foo");
...where nextMatching
can accept the full range of selectors. I'm always surprised that next(selector)
doesn't do this, but it doesn't, and the docs are clear about what it does, so...
I can always write it and add it, although if I do that and stick to the published API, things get pretty inefficient. For instance, a naïve next
loop:
jQuery.fn.nextMatching = function(selector) { var match; match = this.next(); while (match.length > 0 && !match.is(selector)) { match = match.next(); } return match; };
...is markedly slower than nextAll("selector:first")
. And that's not surprising, nextAll
can hand the whole thing off to Sizzle, and Sizzle has been thoroughly optimized. The naïve loop above creates and throws away all sorts of temporary objects and has to re-parse the selector every time, no great surprise it's slow.
And of course, I can't just throw a :first
on the end:
jQuery.fn.nextMatching = function(selector) { return this.nextAll(selector + ":first"); // <== WRONG };
...because while that will work with simple selectors like "div.foo", it will fail with the "any of several" option I talked about, like say "div.foo, div.bar".
Edit: Sorry, should have said: Finally, I could just use .nextAll()
and then use .first()
on the result, but then jQuery will have to visit all of the siblings just to find the first one. I'd like it to stop when it gets a match rather than going through the full list just so it can throw away all results but the first. (Although it seems to happen really fast; see the last test case in the speed comparison linked earlier.)
Thanks in advance.
You can pass a multiple selector to .nextAll()
and use .first()
on the result, like this:
var nextFoo = $(this).nextAll("div.foo, div.something, div.else").first();
Edit: Just for comparison, here it is added to the test suite: http://jsperf.com/jquery-next-loop-vs-nextall-first/2 This approach is so much faster because it's a simple combination of handing the .nextAll()
selector off to native code when possible (every current browser) and just taking the first of the result set....way faster than any looping you can do purely in JavaScript.
How about using the first
method:
jQuery.fn.nextMatching = function(selector) { return this.nextAll(selector).first(); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With