source: kleptones

jQuery UI – Accordion

I’ve started to add the Accordion widget to PJ jQuery UI Helper and thought I would share with the world my experience with it, so to begin we have…

What is an Accordion?

A physical accordion
A physical accordionsource: kleptones

While the image is a physical accordion, this is not what I am referring to. It is, however, a very good example of what a jQuery UI Accordion is. And the image is hilarious to me!

jQuery UI’s Accordion is a collection of ‘sections’ of information that can be shown when the section’s title is selected, ie:

Section Title 1

Section 1 Contents

Section Title 2

Section 2 Contents

How do you make an Accordion?

First you have the basics (Include jQuery and jQueryUI scripts and stylesheets). Which I have glossed over because it’s a separate instructional on it’s own, perhaps I’ll create a post on it and link to that later… Keep you folks guessing.

Now for the basic Accordion you will need a div tag that has an ID, inside of that div we have an h3 tag to set the title of a section. And lastly an extra div tag that contains the section’s content. Here is the HTML code for the example above:
<div id="accordion">
 <h3>Section Title 1</h3>
 <div>Section 1 Contents</div>
 <h3>Section Title 2</h3>
 <div>Section 2 Contents</div>
</div>

After you’ve added the HTML to your page, you need to get it looking spiffy and there’s only one way to do this… MAGIC! Of course by ‘MAGIC’ I mean Marginally Accurate…. who am I kidding? I can’t make an acronym out of that, but before you fully understand it it does seem a lot like magic.

What I really mean is you need to tell jQueryUI that your div tag with the ID ‘accordion’ is in fact an Accordion, this can be done with JavaScript using jQuery’s great selectors and jQueryUI’s accordion function. Like this:
<script>
jQuery( document ).ready(function($) { $( '#accordion' ).accordion()});
</script>
Magic! :)

Kind of threw you in the deep end on that one, but let’s break it down into manageable chunks.

First you have the JavaScript inside of script tag to tell the browser that you’re using JavaScript.

Now for the actual JavaScript used let’s work it from the inside out:

  • $( '#accordion' ).accordion()
    This is technically all you need if you’re using this in a regular HTML file, we’ll get into that a bit further on.What this code is doing is selecting the div tag with the ID (#) of accordion and applying the accordion() function to it. This is a very simple use of the accordion function, if you’d like to see more of the advanced uses you can check out it’s full reference.

  • jQuery( document ).ready(function($) { });
    This beautiful piece of code does quite a few things, most importantly it prevents the code inside the curly brackets ({ }) from being executed until the whole web page is ready to be displayed. This is useful for a lot of things, although in the instance of using this Accordion inside of a normal HTML page it is not necessarily needed (Especially if you add the script tag directly below your HTML or in the footer, but if your script tag is in the Header of your HTML, you’ll want this code to wait until the actual HTML has been created before executing).

    Will get into the why’s of all of this further in this post, I promise.

    I’ve realized that the rest of the explanation for this code snippet will also be explained further in the post… So I’ll leave it at this for now.

And that is how you can make your very own jQueryUI Accordion! It’s basic, but it works ;) So if you came here to find out how to make your own Accordion, job done… I hope. And now we’ll get into the meat of this post, and the major reason why I decided to make it, with…

Pitfalls I found while adding Accordion to my Plugin

Source
Source: jitterbit

jQuery in WordPress is loaded in compatibility mode

Technically this was not a pitfall for this part of the plugin, it was a pitfall when I started the plugin overall. But I promised further explanation on the whys of the complication in the JavaScript above and I will now deliver.

Usually in jQuery, when you want to do anything jQuery-ish you just need to use the $ symbol followed by whatever you fancy jQuery-wise. And what compatibility mode does is make jQuery more compatible with other JavaScript libraries that might use the $ shortcut, so $ is replaced with jQuery.Which isn’t that much of a problem, especially when you’re just doing the one line of code. But when your code starts to become more than that, it’s a helluva lot easier to simply type $ than it is to type out jQuery all of the time (Remember that it’s case-sensitive as well, meaning the Q has to be capitalised EVERY time ;) ).

So to allow for $ in the JavaScript, we need to first start off with jQuery( document ) to select our document as a jQuery object. Now we can call jQuery functions on that object using . followed by the function name, in this case ready() and inside of the parenthesis of the ready function you can insert a function to be executed. The function could be a function you’ve already declared or, as is in this case, simply declare it in the ready function.

The ready function also allows for the function you create to receive a variable, which you can name however you like and this variable is the global jQuery object itself. Which is why we had function($) { } inside of the ready function above and from then used $ as jQuery.

You could also do something like this:
jQuery( document ).ready(function(Hulk) {
  Hulk("head").empty()
});
Granted this does nothing useful… except showcase that you can now use anything as your global jQuery object and make me giggle :) .

To clarify, this will theoretically strip out the tags inside of your HTML’s head. I had originally put in the ‘theoretically’ because I wasn’t prepared to test this snippet of code… but curiosity got the best of me.

Further reading:
Using ‘$’ instead of ‘jQuery’ in WordPress
jQuery.noConflict()

StyleSheets can be a royal pain in the ass

After completing the additions required to allow for Accordion in the plugin, I played around with it. Which ended up with me having an Accordion inside of a Tab… because nesting objects inside of other objects is my version of hallucinogenic drugs :) . This little test showed me a fatal flaw in my theory of simply using jQueryUI’s themes to style the plugin.

So far the theory has worked out rather smashingly. Apart from a few minor tweaks to each CSS (Cascading StyleSheet), the widgets look good to me. But the walls crumbled, even before the nesting test, to reveal that I may need to put a little more effort into the styling… And so I devised workarounds!

Here’s a list of the flaws found and their current workarounds:

  1. Gap between Section title and Section
    This flaw was find in the first Accordion my plugin created, my theme’s h3 tag specifies a margin-bottom of 20 pixels (20px). So what this means is that the Section title has a margin (Could also call it a boundary) below it of 20 pixels.

    The workaround I devised for this little issue was to add an inline style to each Section title of margin-bottom:0, nullifying the h3 tag’s margin-bottom because inline styles take preference. Inline styles are generally frowned upon, but I’m going with this one for now in the spirit of completing the task at hand and then polishing out the kinks.

  2. Section title font becomes smaller when nested inside of another object
    This is a tricky beast, that I still haven’t fully wrapped my head around. The major problem is that the Section title’s font-size attribute is not set specifically to a size and instead is set to 100% which then sets it to 100% of it’s parent tag’s font-size which causes an issue due to my meddling with jQueryUIs font-size to make it a bit more appealing to my eye. At least that’s my current theory.

    The workaround… once again inline style :) but for this one I set it to be dependent on the user wanting it. So the section shortcode allows for an attribute of title_font_size which the user can define if he/she believes the font-size should be adjusted. In the spirit of completeness, I also added allowance for a section_font_size attribute to allow the user to set font-size of the Section contents.

    The downside to this sort of workaround is that it means the user will have to play with it to find the right size for his/her site, and upon writing this I realize that doing it in the section shortcode means it will need to be repeated for each section of the Accordion… Think I’ll allow the attribute in the Accordion shortcode to be used for all sections in the released version :) .

Further reading:
CSS margin-bottom Property
CSS font-size Property

A basic Accordion’s height can vary

This flaw became apparent when I filled 1 out of 2 sections with Lorem Ipsum and the other with one line of content. I noticed that the height of the Accordion was then based on the content from the first section, and when clicking on the second section’s title it would float up to the first section title leaving the browser where it was.

Imagine this scenario, you have 2 sections ‘Chapter 1′ and ‘Chapter 2′ respectively. In each of those sections you have content that fills 2 screens. Your user happily starts reading the first section, upon reaching the bottom is intrigued to find out what happens in the second section and eagerly clicks on ‘Chapter 2′. ‘Chapter 2′ floats to the heavens and disappears out of the browser window and your user is now reading the conclusion of the second section…

If I was that user, I would either indignantly scroll up to the start and be fairly irritated at the inconvenience (I’m a rather lazy user) or I would just discard the second section and carry on down the webpage. So being the developer of this Accordion I don’t want to irritate the user in me. I decided to adjust the JavaScript for my accordion, which came to this little beauty:
$("#accordion").accordion({
 beforeActivate: function( event, ui ) {
  newHeadOffset = ui.newHeader[0].offsetTop;
  oldHeadOffset = ui.oldHeader[0].offsetTop;
  if (newHeadOffset > oldHeadOffset && oldHeadOffset < $(document).scrollTop()) {
   $("html,body").animate({ scrollTop: oldHeadOffset });
  }
 }
});
Glorious! No?

As you can see we have $("#accordion").accordion() as you’ve seen before, just with a whole lot more inside of the accordion function’s parenthesis. This is one of the great things about jQueryUI’s widgets, you can simply call the function () if you like or you can pass it a key:value array (Technically speaking this is an object in JavaScript, but I like to refer to them as arrays). In that array you can set options, override methods or even add functions to events (using the specific key of course).

In the JavaScript above we are defining our array inside of the function… I think I can explain this a bit better with psuedocode:
array = {key:value}; – Defining an array. And in our code above, we have this:
$("#accordion").accordion( {key:value} ) – Very simple version ;)

So… now let’s change out that simple version for what we have:
key is beforeActivate which is an event triggered before a section is activated (It’s kind of in the name).
value is a function that will be run when the event happens, and when this event occurs jQuery calls the function passing two variables to it (event, ui), the event variable is an Event which we don’t use for this function. And the ui variable is an Object which we use to get the height offset (How many pixel’s the top of an object is from it’s parent object) of our oldHeader (The header that used to be activated) and our newHeader (The header that was clicked on).

After getting our height offsets for old and new headers we can have some fun with them:
if (newHeadOffset > oldHeadOffset && oldHeadOffset < $(document).scrollTop())
In this if statement we’re checking firstly whether newHeadOffset is greater than oldHeadOffset, reason for that is if the new header’s height offset is less than the old header we don’t need to do anything (The newly activated header is higher on the browser than the old header, which means we don’t need to do any scrolling). And then… (hehe, just made me think of Dude Where’s My Car) we check if the old header is still in the browser by checking that it’s offset is less than the current scroll position ($(document).scrollTop()) which would also mean no scroll necessary if our old header offset is less than the scrollbar’s current position.

Now if all of those conditions are met (New header is below the old header and the old header is out of view in the browser) we run the following code:
$("html,body").animate({ scrollTop: oldHeadOffset });
Here we are using a jQuery function animate() to animate the selected objects ($("html,body") – selects either the html or the body of the web page) to change the property (scrollTop), to the value (oldHeadOffset), set inside of the function ({ scrollTop: oldHeadOffset }). This will make the page scroll nice and fluidly to the top of the old header, saving the user from having to do it.

This is the simplest and easiest way of doing this, possibly further down the line I would like it to scroll to just the top of the new header (With the given solution, if you have 4 sections and with the first section active you select the 3rd section, the page will scroll to the top of the first section. But if you select the 3rd section from the 2nd section, it will scroll to the top of the 2nd section), but this requires a bit more complexity in the JavaScript and I don’t see it as absolutely necessary for now.

Further reading:
Accordion API (List of options/methods/events)
Reason for using both html and body selectors

My height solution has it’s own flaw

The above solution is awesome, but after a few tests I found a bit of a bug. I found that when I clicked on a nested Accordion’s lower header with the upper header still in view, the page scrolled up… which shouldn’t happen.

I looked into this and found that (as I described above) the height offset I’m getting in the JavaScript is an offset based on the placement of the Accordion. Here’s a little example for you:

Height Offset Bug Example

As you can see in the image above, we have an Accordion inside of a Tab. Now what happens when you activate the second section of the Accordion, the JavaScript laid out in the last segment will check whether the new header is lower than the old header which is true. After that it checks whether the offset of the old header is less than the offset of the scrollbar from the top of the page… which will be returned as true in the above example. The reason for this, is that the old header offset is not the offset between it and the top of the page but instead the offset between old header and the top of it’s parent (The top of the Tab). Which is far less than the offset of the scrollbar and will scroll the page even higher than it already is.

So… how do we get the old header’s total offset?
From a little debugging I found that the oldHeader object also contains a value of offsetParent which is the parent of the Accordion AND has it’s own offsetTop :)

Happy days, we’ve found a solution! And in the given situation we could simply alter the JavaScript to look like this:
$("#accordion").accordion({
 beforeActivate: function( event, ui ) {
  newHeadOffset = ui.newHeader[0].offsetTop;
  oldHeadOffset = ui.oldHeader[0].offsetTop;
  totalOffset = oldHeadOffset + ui.oldHeader[0].offsetParent.offsetTop;
  if (newHeadOffset > oldHeadOffset && totalOffset < $(document).scrollTop()) {
   $("html,body").animate({ scrollTop: totalOffset });
  }
 }
});
Fortunately this code will work even if the Accordion doesn’t have a parent, because in a case like that the parent would be the body of the HTML which has an offset of 0 (Saving us from errors of trying to use an undefined variable and not affecting the calculation of our totalOffset).

I could leave it there, but this brings a question to mind “What if the Accordion is nested 3 levels down instead of just 2?”. So I figured it would be best to solve that before it became an issue, let’s start with the code first:
function getParentOffset(offsetParent) {
 returnInt = offsetParent.offsetTop;
 if (offsetParent.offsetParent) {
  if (offsetParent.offsetParent.offsetTop > 0) {
   returnInt = returnInt + getParentOffset(offsetParent.offsetParent);
  }
 }
 return returnInt;
}
$("#accordion").accordion({
 beforeActivate: function( event, ui ) {
  newHeadOffset = ui.newHeader[0].offsetTop;
  oldHeadOffset = ui.oldHeader[0].offsetTop;
  if (newHeadOffset > oldHeadOffset) {
   totalOffset = oldHeadOffset + getParentOffset(ui.oldHeader[0].offsetParent);
   if (totalOffset < $(document).scrollTop()) {
    $("html,body").animate({ scrollTop: totalOffset });
   }
  }
}
});
Thanks to recursion with this snippet of code we are able to place an Accordion n levels deep :)

For this explanation I’ll just go through the new function getParentOffset that has been added. This function receives an offsetParent as a variable and stores it’s offsetTop. Once we have that value we check whether the current offsetParent has it’s own offsetParent (We don’t want to try using the offsetParent if it’s not defined). After that we check whether the defined offsetParent has an offsetTop greater than 0, if it is we need to get that value. We could just add offsetTop to our current offsetTop, but that would only allow us to account for 3 levels. So instead, we call getParentOffset again to increment our current offset (Which will loop until either the next parent is null or it’s offsetTop is 0) and lastly return the total for use in our Accordion function.

You’ll also notice that I no longer use && in the if statement for the beforeActivate function, I did this mainly because I don’t like to call functions if I don’t have to ;) so we will only use the getParentOffset function if the new header is below our old header.

Further reading:
Recursion

If you’re reading this, you’re a weird one. But if this has been of any use to you or you think I’ve gotten something wrong, please let me know in the comments.

3 thoughts on “jQuery UI – Accordion”

Leave a Reply to Peter Jokumsen Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>