30

I'm setting up a very straightforward FAQ page with jQuery. Like so:

<h2>What happens when you click on this question?</h2>
<p>This answer will appear!</p>

This is all inside a very specific div, so I'll be selecting the header with $('#faq h2'). Simple, right? Click on the H2, and use this.next() to make the next paragraph show up.

(The caveat with this page is that a non-programmer will be maintaining it, which is why I'm not using classes: there's no guarantee that any new entries would have the right classes in them.)

So! The problem:

<h2>What happens when you click on the next question?</h2>
<p>That is an interesting conundrum.</p>
<p>Because the maintainer is kind of long-winded</p>
<p>and many answers will span a few paragraphs.</p>

So how, without adding in divs and classes and whatnot, can I have my this.next() routine select everything between the question-that-was-clicked-on and the next question (H2 header)?

Shog9
  • 156,901
  • 35
  • 231
  • 235
Eileen
  • 6,630
  • 6
  • 28
  • 29

9 Answers9

27

I realise that this is an old question, but jQuery 1.4 now has nextUntil. So something like this should now work:

$('h2').click(function(){
    $(this).nextUntil('h2').show();
})
John McCollum
  • 5,162
  • 4
  • 34
  • 50
21

Interesting problem. First let me say that I think the best strategy is to enclose the whole answer in a div and then the problem becomes trivial to solve:

<h2>Question here</h2>
<div>
<p>Answer here</p>
</div>
</h2>Next Question</h2>
...

with:

$(function() {
  $("h2").click(function() {
    $(this).next().toggleClass("highlighted");
  });
});

But that being said, it is solvable without that.

$(function() {
  $("h2").click(function() {
    $(this).nextAll().each(function() {
      if (this.tagName == 'H2') {
        return false; // stop execution
      }
      $(this).toggleClass("highlighted");
    });
  });
});

Not supremely elegant but it'll work.

Note: This assumes the questions are siblings. If they are not it gets much more complicated.

cletus
  • 616,129
  • 168
  • 910
  • 942
  • I disagree with you on the DIV part. Isn't it more elegant to wrap every question/answer pair inside a DIV, rather than only the answers? That being said, this is the true answer to Eileen's question! – Mathias Bynens May 14 '09 at 13:54
  • Wrapping the whole question/answer in a div is a reasonable variation but (imho) when you're showing/hiding or highlighting the *answer*, that's what you should be wrapping. Toggling a div is more efficient than toggling all elements in it bar the question. – cletus May 14 '09 at 14:01
  • In my humble opinion, you shouldn't consider behavior when writing your HTML. That's the whole point of unobtrusive JavaScript: just write the HTML you want, then write the JavaScript that does with it what you want. That's why I think it's semantically more desirable to wrap every question/answer pair. – Mathias Bynens May 14 '09 at 14:09
  • It's a fair point but also arbitrary and subjective. To me, an answer is a semantically significant concept in its own right rather than having the concepts of "Question/Answer", "Question" and "Question/Answer minus Question". – cletus May 14 '09 at 14:19
  • +1 This is where I was headed, but as usual, a little nicer and a lot faster. – cgp May 14 '09 at 14:21
  • PERFECTO! This works exactly as I had hoped; thanks so much! – Eileen May 14 '09 at 14:22
  • My answer now includes a jQuery function which gets "next" elements until finding a match, and then can be used to chain. Not sure if the implementation of the jQuery function is as clean as it could be.... – cgp May 14 '09 at 16:27
  • You may wish to update your answer as jQuery now supports this directly with nextUntil as of 1.4. http://stackoverflow.com/questions/863356/jquery-how-to-select-from-here-until-the-next-h2/2215046#2215046 – gradbot May 28 '10 at 03:47
  • Couldn't you use `$(this).nextUntil("h2")` rather than the `.each()` loop? – nnnnnn Oct 27 '13 at 05:04
7

wouldn't it make more sense to use a css styled DL list?

<dl class="faq">
    <dt>Question?</dt>
    <dd>
         <p>Answer</p>
         <p>Answer</p>
         <p>Answer</p>
    </dd>
</dl>

And then easy selection using:

$('+ dd', this); 

this being the current dt selection.

Or just wrap each answer in a div, since it makes sense semantically too. However I think a DL list makes a lot more sense semantically.

Dmitri Farkov
  • 9,133
  • 1
  • 29
  • 45
  • I totally agree. This is, in my opinion, one of the best uses of a definition list. The question is the term, the answer is the definition. +1! (I'd give +2 if I could :P) – Erik van Brakel May 14 '09 at 14:37
  • Brilliant use of the tag, but I doubt the editor his content people are using will do this. +1 regardless, even though it doesn't really help the OP – Samantha Branham May 14 '09 at 14:41
  • Ooh, that is a good idea and if I was doing it from scratch (instead of trying to work around legacy code) that is a great solution. FWIW the FCKEditor is pretty easy to customize and adding in buttons with custom code (like DLs, or even custom-classed spans/divs/etc) is quite easy. – Eileen May 14 '09 at 15:02
5

Sure! Just make a while loop.

$('h2').click(function() {
    var next = $(this).next();
    while (next.length != 0 && next[0].nodeName == 'P')
    {
        next.toggle();
        next = next.next();
    }
});

This assumes that you only have p tags after your h2. You can add more exceptions to the while loop if you want to add something like img.

Or if you don't care what's between the H2 tags you can check for not equal to H2.

$('h2').click(function() {
    var next = $(this).next();
    while (next.length != 0 && next[0].nodeName != 'H2')
    {
        next.toggle();
        next = next.next();
    }
});

This will hide everything after the H2 that is clicked on until either the next H2 is found or you go up a level in the dom.

gradbot
  • 13,732
  • 5
  • 36
  • 69
1

Just in case there are still people who need an answer for this... This also includes the h2 line in the group.

Here if the sample html for which my solution works:

<h2> text1 </h2>
<p>asd</p>
<p>asd</p>
<p>asd</p>
<p>asd</p>
<h3>kjdf</h3>
<h2>asdk</h2>
<h3>kjdf</h3>
<p>asd</p>
<p>asd</p>
<p>asd</p>
<p>asd</p>
<h2>aqdsa</h2>

solution:

jQuery( function ( $ ) {
//defining groups for wrapping them in custom divs

    $('h2').each( function () {
        var grpForDiv= [];          
        var next = $(this).next();  
        grpForDiv.push(this);
        while ( next.length!==0 && next!== undefined && next[0].nodeName !== 'H2')
            {
            grpForDiv.push(next[0]);
            next = next.next();
            }       
        jQuery(grpForDiv).wrapAll('<div class="Group1" />');

    } );
} );
Arshdeep
  • 11
  • 1
1

Maybe this isn't really answering your question, but you could wrap every FAQ item (i.e. every question/answer pair) in a DIV element. This would make sense semantically, and the non-programmer maintaining the page would simply have to copy a full DIV (no need for classes).

HTML:

<div id="faq">
 <!-- start FAQ item -->
 <div>
  <h2>Question goes here</h2>
  <p>Answer goes here.</p>
  <p>And here.</p>
  <ul>
   <li>Really, use any HTML element you want here.</li>
   <li>It will work.</li>
  </ul>
 </div>
 <!-- end FAQ item -->
 <!-- start FAQ item -->
 <div>
  <h2>Second question goes here</h2>
  <p>Answer to question two.</p>
 </div>
 <!-- end FAQ item -->
</div>

JavaScript (jQuery):

$('#faq div h2').click(function() {
 $(this).parent().find(':not(h2)').show();
});
Mathias Bynens
  • 144,855
  • 52
  • 216
  • 248
  • The problem is that the maintainer will be using a WYSIWYG editor (FCKEditor), so I can't depend on him to do ANYTHING with the code except to make the questions H2s, and the editor will make the answers into p's. – Eileen May 14 '09 at 13:46
  • 1
    FCKEditor is an online WYSIWYG editor, so I'm assuming you're building a content management system of some sorts. Why not just hardcode the DIV and H2 elements? The maintainer would then only have to enter a title (which will be wrapped in an H2) and an answer (which will be wrapped in paragraphs, or whatever.) I can't really imagine a situation where you'd want the entire HTML page to be editable through FCKEditor... – Mathias Bynens May 14 '09 at 13:50
  • Well, this is one of those situations -- a long story, but basically there is an existing verrrrrry long page that no one wants to re-input as individual entries. – Eileen May 14 '09 at 14:08
  • @mathias: just FYI, I recently wrote something that bears some similarity to this. In my case I used TinyMCE but it wasn't a CMS I was doing. – cletus May 14 '09 at 14:31
0

Note that the answer selected is a good one, but I'm looking into doing something similar so I worked overtime on it :)

I've put this into an example.

Given HTML:

<h2>Question 1</h2> 
<p>Answer 1</p> 
<p>Answer 1</p> 
<p>Answer 1</p> 
<h2>Question 2</h2> 
<p>Answer 2</p> 
<p>Answer 2</p> 
<p>Answer 2</p> 
<h2>Question 3</h2> 
<p>Answer 3</p> 
<p>Answer 3</p> 
<p>Answer 3</p> 

Do the following and you'll get your answer. I think it's now the most elegant of all the solutions given, but I would value others input on this quite a bit.

jQuery.fn.nextUntilMatch = function(selector) { 
  var result = [];   
  for(var n=this.next();(n.length && (n.filter(selector).length == 0));n=n.next())  
      result.push(n[0]);   
  return $(result); 
} 

$(function() { 
  $('h2~p').hide(); 
  $('h2').click(function() { 
    $(this).nextUntilMatch(':not(p)').toggle(); 
  }) 
});
cgp
  • 41,026
  • 12
  • 101
  • 131
  • Your method is approximately 32.1% slower than Cletus' method. Given the HTML you provided, I ran both Cletus' and your solutions and Cletus' solution took about 2.28ms per run over 10 attempts while yours took 3.36ms per run over 10 attempts. Granted... we are talking about 1ms difference here... but, if in some odd case there were millions of elements that needed to parsed through, it seems that Cletus' solution would finish quicker (then again, yours might be more of a long-distance runner where his is more of a sprinter). Valiant effort though. – KyleFarris May 14 '09 at 17:16
  • Interesting. I probably need to use the internal jQuery function to filter instead of coming around with the n.filter.... – cgp May 15 '09 at 10:19
0

You can also do this without editing you HTML. This is a plugin I just wrote for this particular purpose:

(function($){
    $.fn.nextUntill = function(expr){
        var helpFunction = function($obj, expr){
            var $siblings = $obj.nextAll();
            var end = $siblings.index( $siblings.filter(expr) );
            if(end===-1) return $([]);
            return $siblings.slice(0, end);
        }
        var retObject = new Array();
        this.each(function(){
            $.merge(retObject, helpFunction($(this), expr));
        });
        return $(retObject);
    }
})(jQuery);

You can use it like this:

 $('h2').click(function(){
    $(this).nextUntill('h2').show(); //this will show all P tags between the clicked h2 and the next h2 assuiming the p and h2 tags are siblings
 });
Pim Jager
  • 31,965
  • 17
  • 72
  • 98
-3

I dont think your strategy for solving the problem is correct you've had 6 answers that partly solve your problem.....

But your biggest problem is that you say you cannot trust the maintainers to use divs/spans/classes etc.

What I think you should do is simplify the process for them, and train them to use your method or it WON'T work.

add some simple mark up of your own and interpret for them.

[Q] What happens when you click this link?[/Q]
[A] This marvellous answer appears [/A]

(jQuery will not fix the 'user' bug)

Mesh
  • 6,262
  • 5
  • 34
  • 53
  • If you read another comment, his content people are using a WYSIWYG. – Samantha Branham May 14 '09 at 14:39
  • 2
    I did simplify the process for them! Now they just use H2s and Ps like on every other page in the whole site. And VOILA! You're right that we can't ever solve the 'user' bug, but I don't think the right solution is to teach them some made up code that only works on this one site. – Eileen May 14 '09 at 14:59
  • But YOUR behaviour will ONLY work one one site. Where you have implented the js for the onclick. In fact one specific div on one specific website. You are not teaching them HTML.... So from you comment the correct solution should be to have divs with classes. But you excluded those answers in your question....and then accepted an answer that included adding a div! – Mesh May 14 '09 at 15:54
  • I dont understand why using a WYSIWYG , FCKeditor in this case - ensures your users will only use H2 and P tags....... – Mesh May 14 '09 at 16:00