Accordion FAQ with Searching and Highlighting
Wednesday, March 2, 2011 1:02:11 AM
Recently I was working on a website redesign and one of the items that was specified in the
requirements was a Frequently Asked Questions (FAQ). These are pretty run of the mill these
days. In the design they showed a search box for the FAQ in addition to the full site-wide
search. They also wanted collapsible (accordion) sections with collapsible questions within
each section. Aside from the search, not a biggie.
Now, normally I would go back to the customer and seek some clarification, but this time I
decided I would try something, just to see if I could do it. I didn't want to implement a
server-side solution to the searching, since everything was on this page and already loaded.
I figured there would be a way to accomplish all of my goals with a little bit of JavaScript
and the handy jQuery library. As it turns out it was not
as easy as I thought it would be, yet the amount of code that it required was still incredibly
minimal.
The FAQ structure
The structure of the FAQ that I was provided was pretty basic, nothing fancy or out of the
ordinary to see here.
<div>
<label for="FAQSearch">Frequently Asked Questions Search</label>
<input type="text" name="FAQSearch" id="FAQSearch" />
<input type="submit" id="SearchFAQ" value="search" />
</div>
<div>
<a id="ExpandAll" href="javascript:void(0);">Expand All</a> /
<a id="CollapseAll" href="javascript:void(0);">Collapse All</a>
</div>
<div id="FAQ">
<h3 class="Topic">Topic 1</h3>
<div class="TopicContents" style="display: none;">
<h4 class="Question">Question #1-1</h4>
<div class="Answer" style="display: none;">Answer #1-1</div>
<h4 class="Question">Question #1-2</h4>
<div class="Answer" style="display: none;">Answer #1-2</div>
<h4 class="Question">Question #1-3</h4>
<div class="Answer" style="display: none;">Answer #1-3</div>
<h4 class="Question">Question #1-4</h4>
<div class="Answer" style="display: none;">Answer #1-4</div>
<h4 class="Question">Question #1-5</h4>
<div class="Answer" style="display: none;">Answer #1-5</div>
<h4 class="Question">Question #1-6</h4>
<div class="Answer" style="display: none;">Answer #1-6</div>
</div>
<h3 class="Topic">Topic 2</h3>
<div class="TopicContents" style="display: none;">
<h4 class="Question">Question #2-1</h4>
<div class="Answer" style="display: none;">Answer #2-1</div>
<h4 class="Question">Question #2-2</h4>
<div class="Answer" style="display: none;">Answer #2-2</div>
<h4 class="Question">Question #2-3</h4>
<div class="Answer" style="display: none;">Answer #2-3</div>
<h4 class="Question">Question #2-4</h4>
<div class="Answer" style="display: none;">Answer #2-4</div>
<h4 class="Question">Question #2-5</h4>
<div class="Answer" style="display: none;">Answer #2-5</div>
</div>
</div>
Like I said, pretty run of the mill stuff. But what this lacks is functionality to
actually perform the searching and expanding. Lets take a look at the JavaScript
to see what I did.
The script that makes it all happen
<script>
$('h3.Topic').click(function () {
$(this).next().toggle(300);
});
$('h4.Question').click(function () {
$(this).next().toggle(300);
});
$('#ExpandAll').click(function () {
$('#FAQ').children('div.TopicContents').show(300).children('div.Answer').show(300);
});
$('#CollapseAll').click(function () {
$('#FAQ').children('div.TopicContents').hide(300).children('div.Answer').hide();
});
jQuery.expr[':'].Contains = function (a, i, m) {
return jQuery(a).text().toUpperCase().indexOf(m[3].toUpperCase()) >= 0;
};
$('#SearchFAQ').click(function () {
$('#FAQ').children('div.TopicContents').hide().children('div.Answer').hide();
if ($('#FAQSearch').val() != '') {
$('div.Answer:Contains(' + $('#FAQSearch').val().toUpperCase() + ')').show().parent().show(300);
try {
$('.highlight').removeClass("highlight");
$('div.Answer:Contains(' + $('#FAQSearch').val().toUpperCase() + ')').each(function () {
$(this).html(
$(this).html().replace(
new RegExp($('#FAQSearch').val(), "ig"),
function(match) {
return '<span class="highlight">' + match + '</span>';
}
)
)
});
}
catch (err) {
}
}
return false;
});
</script>
Lets break this up into individual functions.
The Accordion Effect
$('h3.Topic').click(function () {
$(this).next().toggle(300);
});
This should be pretty straight forward. When the user clicks on a Topic it toggles the
visibility of the next tag, which should always be a div with class TopicContents.
$('h4.Question').click(function () {
$(this).next().toggle(300);
});
This is almost identical to the code before, except this toggles the element following
an h4 with class Question. This should be a div with class Answer.
$('#ExpandAll').click(function () {
$('#FAQ').children('div.TopicContents').show(300).children('div.Answer').show(300);
});
When the user clicks the Expand All link at the top this will set all divs with
the class TopicContents to visible and then set all child divs with class Answer
to visible.
$('#CollapseAll').click(function () {
$('#FAQ').children('div.TopicContents').hide(300).children('div.Answer').hide();
});
Much like the previous item but in reverse. This will set the visibility of all divs with
class of TopicContents or Answer to hidden.
Searching
jQuery.expr[':'].Contains = function (a, i, m) {
return jQuery(a).text().toUpperCase().indexOf(m[3].toUpperCase()) >= 0;
};
This was a major piece of the magic that made the searching work. This adds a special
selector, :Contains, that will match with case insensitivity. It does this by converting
the text to search to upper case and the text you are searching for to upper case and then
performs the actual comparison. I did not write this but do not recall which site I had
pulled this off of. I believe it was on the jQuery forums or Stack Overflow.
$('#SearchFAQ').click(function () {
$('#FAQ').children('div.TopicContents').hide().children('div.Answer').hide();
if ($('#FAQSearch').val() != '') {
try {
$('.highlight').removeClass("highlight");
$('div.Answer:Contains(' + $('#FAQSearch').val().toUpperCase() + ')').each(function () {
$(this).show().parent().show(300);
$(this).html(
$(this).html().replace(
new RegExp($('#FAQSearch').val(), "ig"),
function(match) {
return '' + match + '';
}
)
)
});
}
catch (err) {
alert(err);
}
}
return false;
});
This is the entire searching function. Lets go through it line by line:
- When the user clicks the search button
- Collapse all Topics and Questions
- If the user actually typed something into the search box
- Try the next block of code (so we can handle any errors gracefully)
- Remove all highlighted terms from previous searches
- Using the previously mentioned selector, look for the term typed in the search box in all divs with class Answer and execute the following code
- Set the answer and parent topic to visible
- change the Answer's HTML contents to:
- Perform a search and replace on the Answer's HTML
- Create a new case insenstive regular expression with the search term
- Perform the following when it finds the search term
- Wrap the search term in a span with class highlight
- Catch any errors
- Show an alert with the error - for debugging purposes
- return false so that it does not cause a page post-back/form submission.
In my css file I just set .highlight to 'background-color: yellow;' as a quick demo.
Hope you enjoyed this and learned something from it. It was quite an adventure getting
all of the pieces to line up properly. The hardest parts to get working, mostly due to
my lack of experience with JavaScript/jQuery, was the Regular Expression setup and the
proper order to perform the HTML substitution. Even when it ran without errors it would
not change any of the text. May my battle be your benefit.
View the demo