Have you ever seen wikipedia’s mobile version? Or ever heard about <details> HTML element? Well, basically, these examples are good implementations of “collapsible panels” that allows users to see just what they need to see. And they are specially good for mobile devices with those small screens.
The downside of them is that they need additional markup to work. You know, it works fine when you are the only one who updates content, but won’t work at all when you are dealing with CMS or user (client) generated content.
Here we’ll see the whole process of planning, coding, testing and correcting a Rocking jQuery Plugin for Collapsible Panels. Hope you enjoy it!
Demo & Download!
What our Plugin will do is to search for every hN element (where N is the number of top level heading defined by our plugin), wrap it and all its content around a <div> tag and show a pretty show / hide button.
We’ve used bootstrap as our start point so we have a good looking demo with almost no work. Let’s take a look at our demo and download our sample files so you can edit it as you want.

HTML
Let’s get started with our HTML, we created a simple div with dummy content (with all sort of tags, h2, h3, blockquote, p…) and called jQuery (1.7.1) and our Plugin file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>Rocking Collapsible Panel Plugin</title>
<meta name="description" content="jQuery plugin to dinamycally create colapsible panels" />
<meta name="author" content="Rochester Oliveira - RockingCode.com" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<base target="_blank" />
<!-- CSS: implied media="all" -->
<link rel="stylesheet" href="css/style.css?v=2" />
<link rel="stylesheet" href="css/bootstrap.min.css" />
<link type="text/plain" rel="author" href="humans.txt" />
</head>
<body>
<div id="container" class="content">
[content]
</div>
<!-- JavaScript at the bottom for fast page loading -->
<!-- Grab Google CDN's jQuery, with a protocol relative URL; fall back to local if necessary -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js"></script>
<script>window.jQuery || document.write("<script src='js/libs/jquery-1.7.1.min.js'>\x3C/script>")</script>
<!-- scripts-->
<script src="js/rcp.js"></script>
<script type="text/javascript">
$(function(){
$("#container").rcp();
});
</script>
<!-- end scripts-->
</body>
</html>
JS
Cool, now we have to do our Plugin file itself. Here we’ll use jQuery boilerplate as a simple starter to our Plugin.
Our basic logic for will be:
FOREACH children in our target element IF this element's tag is h2 (or any top level tag set up in our Plugin) CREATE a new div (panel) element MOVE this element to this new div ELSE MOVE this element to last previously created div ENDIF END FOREACH
And our basic logic for event binding will be:
CLICK() IF panel is not hidden ANIMATE height to 0 ELSE ANIMATE height to original value ENDIF END CLICK()
CSS
We’ll be using before pseudo element, and will just copy bootstrap’s rule for buttons, so we don’t need additional elements to get that button working, like this:
body {
background: url("../img/bg.png") repeat;
}
#container {
position: relative;
width: 500px;
margin: 15px auto 0;
background: url("../img/bg_logo.jpg") no-repeat center top;
}
.rcpPanel {
overflow: hidden;
padding-top:30px;
margin-bottom: 10px;
}
.rcpPanel:before {
content: "- " attr(title);
margin-top: -30px;
display:block;
/**** bootstrap button styles *****/
cursor:pointer;
padding:5px 14px 6px 18px;
font-size:13px;
line-height:normal;
border:1px solid #ccc;
border-bottom-color:#bbb;
-webkit-border-radius:4px;
-moz-border-radius:4px;
border-radius:4px;
-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);
-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);
text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);
color:#333;
background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);
}
.rcpPanelHidden:before {
content: "+ " attr(title);
padding: 5px 14px 6px;
/***** button styles *****/
color:#ffffff;
background-color:#0064cd;
background-repeat:repeat-x;
background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
background-image:-moz-linear-gradient(top, #049cdb, #0064cd);
background-image:-ms-linear-gradient(top, #049cdb, #0064cd);
background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);
background-image:-o-linear-gradient(top, #049cdb, #0064cd);
background-image:linear-gradient(top, #049cdb, #0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);
text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);
border-color:#0064cd #0064cd #003f81;
border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
}
Issues
As a wise man said once “Anything that can go wrong will go wrong” (yeah, you guess it). Since from pseudo code to real code crazy things may happens, they will actually happen. So let’s see how can we solve a couple of common crazy issues.
Animate height : “toggle” = display none (!!!!!!)
This one is kinda crazy… Since I use :before pseudo element (and I didn’t want to create a element just for that button) if we do “display:none” in our .rcpPanel our button will be hidden also and users will never see its content, because boxes would never expand (no button to expand, no box expanding, right?).
But if you just use toggle to animate height, the final result will be a undesired display:none rule:
$elem.animate({"height": "toggle"}); // will animate but in the end adds "display:none"
The solution? I introduce you, dear Padawan, the jQuery data. It allows you to store data associated with a specific element, so you don’t need to create crazy-non-standard-attributes (like <a original-height=”150px”>), you use jQuery to store these values. And we need to hide all panels after wrapping our elements inside those divs, so we’ll do these 2 actions together. Our logic will be:
FOREACH .rcpPanel jQuery.data( $(this), 'height', $(this).height() ); // so we'll have jquery data height for each element with its original height $(this).CLICK(); // run click function for this rcpPanel ENDFOREACH
Links collapsing panels
When we bind clicks inside an element, all it’s children will be binded too. It’s good, because if you click in a “p” element, it’s supposed to trigger the “collapse” function. That’s right, but when we have links (a.k.a. <a>), well, it’s not good. Think about it, what if we have a list of links and the user want to open them all in different tabs. Every click will hide panel so she’ll have to click once again to reopen it.
Then what we need to do is a little adjustment in our click function, it’ll detect if target element is “<a>” and then if it isn’t we can hide our panel.
CLICK() IF panel is not hidden IF $(this) is NOT "<a>" ANIMATE height to 0 ENDIF ELSE ANIMATE height to original value ENDIF END CLICK()
Final result
Wrapping up all our pseudo code and translating it to JavaSrcript our final Plugin will be:
/*
* Project: Rocking Collapsible Panel (a.k.a. rcp)
* Description: Adds "show / hide" options without any additional markup (uses HTML h1-6 tags as reference)
* Author: Rochester Oliveira
* License: GNU General Public License ( http://en.wikipedia.org/wiki/GNU_General_Public_License )
*/
// awesome structure from http://jqueryboilerplate.com/
;(function ( $, window, document, undefined ) {
// Create the defaults once
var pluginName = 'rcp',
defaults = {
topLevel: 2,
panel: "<div class='rcpPanel'></div>"
};
// The actual plugin constructor
function Plugin( element, options ) {
this.element = element;
this.options = $.extend( {}, defaults, options) ;
this._defaults = defaults;
this._name = pluginName;
this.obj = $(this.element);
this.init();
}
Plugin.prototype.init = function () {
var obj = this.obj,
$this = "",
container = "",
level = this.options.topLevel,
panel = this.options.panel;
//let's loop every children of our target element
obj.children().each(
function(){
$this = $(this);
if ( $this[0].tagName.toLowerCase() == ("h"+level) ) {
container = $(panel).appendTo(obj);
container.attr( "title", $this.text() );
}
$this.appendTo(container);
}
);
//now we'll bind that "show / hide" function
var panels = $(".rcpPanel");
panels.click(
function(event){
$this = $(event.target);
if ( ! ($this[0].tagName.toLowerCase() == "a") ) {
showHide($(this));
}
}
)
.each(
function() {
$this = $(this);
jQuery.data($this[0], 'height', $this.height() + "px" );
showHide($this);
}
);
};
function showHide(elem) {
if (elem.hasClass("rcpPanelHidden") != 1) {
elem.animate({ "height": "toggle"}, function() { elem.css( { "display": "block", "height": "0px"} ); } ).addClass("rcpPanelHidden");
} else {
elem.animate({ "height": jQuery.data(elem[0], "height") }).removeClass("rcpPanelHidden");
}
};
// A really lightweight plugin wrapper around the constructor, preventing against multiple instantiations
$.fn[pluginName] = function ( options ) {
return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName, new Plugin( this, options ));
}
});
}
})(jQuery, window, document);

