Revealing Details with jQuery
read 22 commentsA week or so ago, someone posted a comment on one of my previous articles, asking if I could help her split up the textual content of an element, showing the first part and replacing the second with a link that, when clicked, would reveal the text. This behavior would appear in an FAQ using a definition list (<dl>), with each question contained in a <dt> and each answer contained in a <dd>. I soon realized that the solution would be rather involved, so I decided to create a new entry out of it rather than simply answer her question in another comment.
Here is the simple definition list structure that I'll be using for the example:
- <dl class="expander">
- <dt><strong>Benedick's Question</strong>...</dt>
- <dd><strong>Beatrice's Answer</strong>...</dd>
- <dt><strong>Benedick's Question</strong>...</dt>
- <dd><strong>Beatrice's Answer</strong>...</dd>
- </dl>
First Steps
As usual, we start the script with a "document.ready" line so that it can be fired when the DOM has finished loading. Next comes our primary selector expression. For this example, I've given the targeted <dl> element a class of "expander" so that we don't inadvertently attach the behavior to other <dl>s. And since we're manipulating the contents of each <dd> independently of one another, we can make things happen inside an .each() method:
We ought to declare some variables now. The first two will go right after the document.ready line, while the others will appear inside the .each() method because they change with each <dd> element.
- slicePoint (integer): the number of characters at which the contents will be sliced into two parts. Note: any tag names in the HTML that appear inside the
<dd>before the slicePoint will be counted along with the text characters. - widow (integer): this is a threshold of sorts for whether we want to initially hide part of the
<dd>'s contents. If after slicing the contents in two there are fewer words in the second part than the value set by widow, we won't bother hiding anything.
- allText (string): the full contents of the
<dd> - startText (string): the first part of the
<dd>'s contents — the part that is immediately visible. First, all characters up to the slicePoint are included, and then any "word" characters at the end of the string are removed to avoid ending with a partial word. Keep in mind that in almost every case this approach will leave us with fewer characters than we indicated in the slicePoint variable - endText (string): the second part of the
<dd>'s contents. This part is initially hidden from view, replaced by a "read more..." link.
Here is what we have so far:
Although jQuery has its own .slice() method, which operates on a jQuery object, we're using JavaScript's native method, which can operate on both strings (as is the case here) and arrays. Our first use of .slice(), declaring the startText variable, has two arguments — one for the beginning position of the string and one for the ending. Our second has only one argument, for the starting position; the (allText) string's length is the implied ending position. Also, note that for startText, we're chaining .slice() and the .replace() regular expression method.
Manipulating the Contents
Now that we have our two blocks of content, we're going to add a "read more..." link after the startText and wrap the endText in a span (with a class of "details"), but only if there are more words than the value defined by the widow variable. (tangent: am I the only one who can't type "widow" right the first time, always typing "window" instead?). There are many ways to create new elements and insert them into the DOM with jQuery. For this example, we're going to use the .html() method, create an array of strings inside it, and join those strings.
- var slicePoint = 100;
- var widow = 4;
- // *** commence DOM manipulation ...
- if ( endText.replace(/\s+$/,'').split(' ').length> widow ) {
- startText,
- '<a href="#" class="read-more">read more...</a>',
- '<span class="details">',
- endText,
- '</span>'
- ].join('')
- );
- }
- });
- });
I picked up that technique of creating an array and joining its elements from the jQuery Google Group. It seems cleaner than string concatenation somehow, and rumor has it that it's a bit faster, too.
Finishing Touches
We have everything in place now, so all we need to do is hide the content inside our newly created <span class="details"> and attach a click handler to the "read more" link. Here is the completed code:
- var slicePoint = 100;
- var widow = 4;
- if ( endText.replace(/\s+$/,'').split(' ').length> widow ) {
- startText,
- '<a href="#" class="read-more">read more...</a>',
- '<span class="details">',
- endText,
- '</span>'
- ].join('')
- );
- }
- });
- // *** hide details until read-more link is clicked;
- // then hide link and show details.
- return false;
- });
- });
I used a .fadeIn() because I like the way it looks, but a simple .show() would do. The return false; line is important to prevent a jump to the top of the page on click.
the Demo
Here is a little demo:















Interesting, seems to be the foundation of a good plugin. Select your dl and use the plugin. Of course the slicePoint, widow, and link text would need to allow for user defined override. Also the ability to re-collapse would be handy, both from a manual click and an optional auto collapse when another answer is expanded.
With that you would have instant FAQs!
This demo was exactly what I was looking for, a way to create a question and answer content section for a website. I had one issue in IE6, that I'm not sure how to resolve.
In IE6, the content is starting after a line break when the user clicks on the "read more" link. I modified the slicePoint variable, reducing it to 90, and eventually was able to get the content to display on the next line, without the line break. Kind of odd, any ideas on what is happening and what I can do to resolve the line break issue?
@Wade: yeah, those are all great suggestions. I wanted to keep this entry short and sweet, but conversion to a plugin and addition of the features you mention wouldn't be hard to add.
@Claire: It looks like jQuery's
.fadeIn()(as well as other animations) is adding azoom:1declaration to the<span class="details">element after it has finished fading in. This has the effect of applying MS's proprietaryhasLayoutattribute to the element, which pushes it to its own line. There are two things you can do to fix this: (1) use a simple.show()instead of the.fadeIn(), or (2) remove the zoom value in the.fadeIn()method's callback, like so:With the second technique, you might see the text appear on the new line first before it quickly jumps back to its rightful place. This is admittedly not ideal, but if you really need the animation, it's the best I can come up with at the moment.
Hi again Claire,
I just looked at the jQuery source code, and apparently the
zoom:1setting is only applied to animations that modify opacity (such as.fadeIn()). Here are the relevant comments from the source:Thanks! I would not have known how to troubleshoot the cause - how did you do it? I appreciate the different solutions; I chose to implement the .show option.
Hi Claire,
After revealing the details, I looked at the DOM for that span element using the IE Developer toolbar. One of the inline style properties that was applied to it was "zoom:1". Knowing that setting the zoom property is a common technique for forcing IE's
hasLayout, I tried removing it to see what would happen. Voila!Great post! its just what i was looking for! I'm just gettings my feet wet with Jquery and I'm wondering how you would add an "Hide" function to the answer?
Hi Erick,
I'm glad you like the entry. After writing it, and seeing Wade's comment above, I decided to turn it into a plugin. The plugin has a "hide" function as well, along with a number of other options. You can see it here.
I recently made a similar (non-jQuery) script that toggles an items details.
See this example and click on the "we'll beat any..." graphic on the sidebar
Some of my newer projects make use of jQuery so I'll most likely be using the plugin on those sites to take advantage of the fancy fades and stuff.
Good stuff, although it might be rather better to not use a definition list for something that...isn't a definition list.
Would you lose anything by using divs and spans? Then it should degrade gracefully.
Hi Rob,
I see your point about the definition list. Interestingly, though, the W3C HTML 4.01 spec seems to think it's okay to use a
<DL>for dialog(ue): "Another application of DL, for example, is for marking up dialogues, with each DT naming a speaker, and each DD containing his or her words."But to answer your question — no, you wouldn't lose anything by using divs. The Expander Plugin that I put together (still in "beta") will operate on any selector you give it.
But I don't think it works fine with Chinese Characters. I've done some test....
As a webdeveloper in China, to deal with Chinese characters is really involved. You need to do many actions as decoding, replacing, scanning.... :)
anyway, great post. Very detailed! Thanks from China !!
Your code box doesn't work in plain-text mode on Safari (shrinks)!
Hi Ricardo,
Thanks a lot for letting me know about that. It was a problem with the syntax-highlighter plugin I've been using. I fixed the bug, so there should be no more shrinkage.
Thanks a lot for the excellent tutorial!! I have one minor suggestion: It will be even more reader friendly if the code can append a … mark at the slice point to give the readers a visual hint that there are more text to come.
With current implementation if the slice point is right after a period then readers might mistakenly think that's the end of the paragraph and skip the link.
Hi Bryan,
Thanks for the comments. Take a look at my Expander Plugin, as it gives options for that sort of thing.
I can't test right now, but I guess animate doesn't add the "zoom: 1". So, we can try this another way:
$('span.details').css({ display: none; opacity: 0 });$('a.read-more').click(function() {
$(this).hide();
$(this).next('span.details')
.css({ display: "inline" })
.animate({ opacity: 1 });
});
The display: inline, should deal with the line-break, and the animation with opacity gives the fade sensation. We could also add speed to our animation, or do any other effects with it.
This is all guessing, since I can't test this right now, but it shoudl work.
Just correcting an error, it should be:
$('span.details').css({ display: "none", opacity: 0 });In the first line. ;D
Just curious, how would one create a link to hide the string back to the 100 characters after revealing it?
Hi Neil,
Take a look at my expander plugin. It expands and collapses the details.
Hello, I can't for the life of me get the expander plugin you wrote to work. The expanding code on this post works fine, but when I try and implement the plugin (following the provided link) it does nothing. I seem to have tried it from every angle.
Does the demo at the provided link work for you? Do you have a link for the page where you're attempting this so I can see what you're doing?