tobypitman.com

Multiple collapsable panels with cookies

There’s literally nothing about on the internet about using the jQuery toogle() function on multiple collapsable panels and saving their states with cookies from one page to another. This prompted me to try and write my own script to take care of the sidebar to the left of the window. All the separate panels are ‘widgets’ in Wordpress and are loaded into an ‘li’ tag with a class name of ‘widget’ making it simple to target the items. I’d also like to say this isn’t an Accordion as such and even the scripts for them don’t deal with unique cookies!

The collapsing effect

Collapsing the panels is the easy bit and just like every other ‘accordion’ and ‘collapsing panel’ plug-in can be achived with a simple toogle() function to slide the ‘ul’ inside each of the widgets when the ‘h2′ is clicked. The jQuery for that would look like this…

$("li.widget h2").toggle(
      function () {
            $(this).next("ul").slideUp(500);
      },
      function () {
            $(this).next("ul").slideDown(500);
      }
    );

Notice how we use .next(“ul”) to target the ‘ul’ as it immediately precedes the ‘h2′ which is the trigger for the effect. So far so good!

Next we need to add an ‘active’ class to the ‘h2′ on DOM ready so the user can see the state of the panels when the page loads, hopefully prompting them they can close it. When they click it we just remove the active ‘class’. This is really easy too by adding this….

$("li.widget h2").addClass("active"); // Add the class on page load.

$("li.widget h2").toggle(
      function () {
        $(this).next("ul").slideUp(500);
        $(this).removeClass('active');
                // Remove the class 'active' on slideUp
      },
      function () {
            $(this).next("ul").slideDown(500);
            $(this).addClass("active");
                // Add the class 'active' on slideDown
      }
    );

Here’s the CSS for the state change….

li.widget h2 {
background:transparent url(images/yourImage.jpg) no-repeat scroll right top;
/* various styling */
}

li.widget h2.active {
background-position:right bottom;
}

Pretty standard stuff! Now comes the interesting bits!

Baking the cookies!

First off I downloaded jQuery.cookies.js. This is a great jQuery plug-in for dealing with cookie operations. The basics of it are….

$.cookie('COOKIE_NAME','COOKIE_VALUE', { path: '/', expires: 10 });
//Make a cookie

$.cookie('COOKIE_NAME','null', { path: '/'});
 //Delete the cookie

$.cookie('COOKIE_NAME');
 //Get the cookie. Returns cookie value.

The problem is that all the tutorials for collapsing panels with cookies only dealt with one panel!..and one cookie! But I have multiple panels that need to have their states saved in multiple individual cookies! As Clay Davis from ‘The Wire’ would say,”SSSHHHIIIIIIIIIIIIIIIT!”.

Making unique cookies

So…first I needed to find the index of the widget I’d collapsed and store it’s state in an it’s own unique cookie. Finding the index or number of the widget can be done with this line…

$("li.widget h2").toggle(
      function () {
     
            var num = $("li.widget h2").index(this);
               // Find the index of the widget. Starts at 0.
     
            $(this).next("ul").slideUp(500);
            $(this).removeClass('active');
      },
      function () {
     
            var num = $("li.widget h2").index(this);
               // Find the index of the widget. Starts at 0.
     
            $(this).next("ul").slideDown(500);
            $(this).addClass("active");
      }
    );
   
    //Clicking the first widget 'h2' trigger will store 0 in the variable 'num'.

Now we can create some variables to store unique cookie names and values. Here’s how we do that….

var num = $("li.widget h2").index(this);
   
var cookieName = 'panel' + num;  
// Returns ----> panel0, panel1, panel2  etc....
var cookieValue = 'closed' + num;  
// Returns ----> closed0, closed1, closed2  etc....
</code>
</pre>

<h3>Create the cookies</h3>

Now for every widget clicked we can create a unique cookie with a unique value.
Let's see how we set up the cookie...
This creates a cookie for the first widget when it'
s clicked.

[cc lang="javascript"]
var num = $("li.widget h2").index(this); // Find index of 0.
   
$(this).next("ul").slideUp(500); // Slide up panel
$(this).removeClass('active'); // Remove 'active' class
   
var cookieName = 'panel' + num;  // Returns ----> panel0
var cookieValue = 'closed' + num;   // Returns ----> closed0
   
$.cookie(cookieName, cookieValue, { path: '/', expires: 10 });
   
// Cookie is stored as panel0 with a value of closed0.

Deleting the cookies

To delete the cookie when widget one is clicked or ‘toogled’ we do this…..

var num = $("li.widget h2").index(this); // Find index of 0.
           
$(this).next("ul").slideDown(500); // Slide down panel
$(this).addClass("active");   // Add 'active' class    
   
var cookieName = 'panel' + num; // Returns ----> panel0
   
$.cookie(cookieName, null, { path: '/', expires: 10 });
   
// Cookie with the name panel0 is deleted.

The whole code for this looks like this…..

$("li.widget h2.active").toggle(
      function () {
            var num = $("li.widget h2").index(this);
        $(this).next("ul").slideUp(500);
        $(this).removeClass('active');
        var cookieName = 'panel' + num;
            var cookieValue = 'closed' + num;
            $.cookie(cookieName, cookieValue, { path: '/', expires: 10 }); 
           
      },
      function () {
            var num = $("li.widget h2").index(this);
            $(this).next("ul").slideDown(500);
            $(this).addClass("active");
            var cookieName = 'panel' + num;        
        $.cookie(cookieName, null, { path: '/', expires: 10 });
      }
    );

Getting the cookie values

Now we have to retrieve the cookies and their values to set the state of the widgets when the page is reloaded. This is easily achived using a for loop with an if statement inside it.

First were going to add a line to see how many widgets we have so we know how long the loop should be. I’ve also created a variable to define the ‘ul’ as ‘panel’. If you have nested lists you may want to add :first to the selector.

var l = $('li.widget h2').length;
var panel = $("li.widget ul");

I’ve done this for any widget that has a title or ‘h2′ tag. Some widgets like ‘text’ or ‘RSS’ widgets you may want to leave out. This is optional….

Heres the loop with some comments. c=0 is important it’s the loop index.

for (c=0;c<=l;c++){                    
// Loop up to the number of widgets. l is the length.

var cvalue = $.cookie('panel' + c);  
// Store the value of found cookie in cvalue while looping.
 
if ( cvalue == 'closed' + c ) {      
// If values match do this...
   
    $(panel).eq(c).css({display:"none"});            
       // Hide the ul if cookie is found for element.
       
    $(panel).eq(c).prev().removeClass('active').addClass('inactive');
       // Change state of effect trigger.
   };
};

If the loop finds a cookie with the name ‘panel0′ it will return a value of ‘closed0′ which tells the if statement to hide the widget ‘ul’ with an index of 0 and remove the ‘active’ class and add a class of ‘inactive’ on page load.

When the page loads any widget that has an ‘h2′ with the class ‘inactive’ will be hidden as it doesn’t have the class ‘active’…….Great!

Major Problem!!!

…..or not! Just like Microsoft there is one massive flaw in this code!!! I’d like to add that up to here took about an hour, what follows took a whole day!

The toogle() function works on the premise that the first part of the code (the first ‘click’ function) will slide the element UP and the second part (the second ‘click’ on the element) will slide the element DOWN. Well what if the element is already UP on page load. Clicking the trigger returns nothing as the element is already UP…?!? You need to click it again to get it to slide down which from a UI point of veiw it pretty…well, crap!

I tried so many different things from ‘if else’ statements to ‘click’ functions, the list goes on. It seemed if one thing worked something else wouldn’t. It was like a dream that you’re chasing something but can never catch up with it. At one point I thought I was losing my mind!

The answer turned out to be really simple in the end (Doh!) an lay in something I’d added to the code but never thought of using… the ‘inactive’ class. I discovered that if I duplicated the toggle() function and reversed the functions so the first click performed a DOWN slide and assigned that to $(‘li.widget h2.inactive’) it produced the desired result. Not very elegant but then neither am I!

The whole thing looks like this….

$(document).ready(function(){

$("li.widget h2").addClass("active");

var l = $('li.widget h2').length;

var panel = $("li.widget ul");

for (c=0;c<=l;c++){

   var cvalue = $.cookie('panel' + c);
 
   if ( cvalue == 'closed' + c ) {
   
        $(panel).eq(c).css({display:"none"});
        $(panel).eq(c).prev().removeClass('active').addClass('inactive');
   };
};


$("li.widget h2.active").toggle(
      function () {
            var num = $("li.widget h2").index(this);
            var cookieName = 'panel' + num;
            var cookieValue = 'closed' + num;
        $(this).next("ul").slideUp(500);
        $(this).removeClass('active');
            $.cookie(cookieName, cookieValue, { path: '/', expires: 10 }); 
           
      },
      function () {
            var num = $("li.widget h2").index(this);
            var cookieName = 'panel' + num;
            $(this).next("ul").slideDown(500);
            $(this).addClass("active");        
        $.cookie(cookieName, null, { path: '/', expires: 10 });
      }
    );


$("li.widget h2.inactive").toggle(
      function () {
            var num = $("li.widget h2").index(this);
            var cookieName = 'panel' + num;
            $(this).next("ul").slideDown(500);
            $(this).addClass("active");
            $(this).removeClass('inactive');       
        $.cookie(cookieName, null, { path: '/', expires: 10 });
           
      },
      function () {
            var num = $("li.widget h2").index(this);
            var cookieName = 'panel' + num;
            var cookieValue = 'closed' + num;
        $(this).next("ul").slideUp(500);
        $(this).removeClass('active');
            $.cookie(cookieName, cookieValue, { path: '/', expires: 10 }); 
      }
    );

           
});

Conclusion

Multiple collapsable panels with cookie states for every unique panel. Awesome!!!

I hope somebody finds this useful and is spared the brain ache I went through. To coolest thing is as the demo relies on Wordpress’ markup you can drop it straight into a theme your developing. I’m going to turn it into a plugin soon too (once I’ve figured out how to do that!). I’d love to hear if any knows of a better and more elegant way of doing this, so leave a comment if you do!

For a demo, look no further than the sidebar menu at the top of this page. Close some widgets and reload the page!

Posted on Thursday, December 18th, 2008 at 2:30 pm.

Filed under jQuery.

12 Responses to “Multiple collapsable panels with cookies”

  1. Rob Barrett says:

    Nice work, Toby! And thanks for sharing — definitely appreciated, given how long it took to make (plus however long it took to write this tutorial!).

    This could be just in time for me — I’m working on a client site, and I’ve got some nice collapsable containers working well, but I wanted to have their states saved between page jumps. This could be perfect! I’ll have a play around later…

  2. taotsu-pro says:

    Woa! This is really cool! I have been too looking for some tutorial to store those states, but couldn’t find anything! This will help me a lot for upcoming projects since my js background is not too strong yet. Cheers!
    P

  3. cawlin says:

    yeah! Man I was trying to do this for an internal debugger UI we built for our framework. I became so frustrated trying to save the multiple states I never implemented it. Can’t wait to give it a shot tomorrow.

  4. Matt says:

    Maybe you can compare it with this one:

    http://www.i-marco.nl/weblog/jquery-accordion-menu-redux2/

    The sad thing about that one is that commercial usage is not allowed by default.

    I think people that share the same thought in webdevelopment should share their thought on small codeprojects and just be happy with the result :)

    Thanks so far !

  5. Matt says:

    I really like the code you developed, easy to understand and easy to change if you want !!

    Good job :)

    /me forgot to tell in my previous post ;)

  6. David says:

    I got this working for my page http://xnation.getmarci.com/np.php but I missed how to make all panels closed by default. Can you lend a hand? Thanks!

  7. Andy says:

    Awesome, been looking for something like this all morning. There is hardly anything on the web that shows how to do this. Thanks.

  8. panosms says:

    What is the HTML markup used/needed for this. It will make it easier if you just included the needed/applicable HTML markup rather than to have to guess by looking at the source code.

  9. Tom says:

    One other question. Using your code above, what can I change so that the panel contents are already closed on page load. I’d like the panels to be closed on the initial visit and then let the user click the h2 tags to open them. Thanks again. Great job!

  10. You’re the man, this was just what i was looking for, many thanks ;)