featured

Rocking Collapsible Panels jQuery Plugin

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);

Trackbacks for this post

  1. How To Create An Effective FAQ For Your Website
  2. Progressive Enhancement Implementation Tips for JavaScript and jQuery | RockingCode

Leave a Comment