Project collapsing blocks

In this project, we will write a very simple behavior that will be attached to blocks on a page. We will make blocks collapsible. Clicking on a block's title will cause the body of the block to slide up or slide down.

Here are the contents of a file called behaviors.js, which is part of the Frobnitz theme, included in using a scripts[] directive in frobnitz.info:

* Defines behaviors for Frobnitz theme.

* Toggle visibility of blocks (with slide effect). */

Drupal.behaviors.slideBlocks = function (context) { $('.block:not(.slideBlocks-processed)', context) .addClass('slideBlocks-processed') .each(function () {

$(this).children(".title").toggle( function () {

$(this).siblings(".content").slideUp("slow");

$(this).siblings(".content").slideDown("slow"); });

In the code, we define one behavior named Drupal.behaviors.slideBlocks(). When attached, this behavior will add a toggle to all blocks on the page. When a block's title is clicked, the block will slide up and disappear. Here's a screenshot of the sliding in progress:

mbutcher o My account n i. i i.

When the slide is complete, only the title —mbutcher—will be displayed.

When the title is clicked again, the contents will slide back down until they are fully visible.

Since our code is operating on blocks, it will be helpful to take a quick look at the HTML that Drupal generates for a block.

Here's the menu section as generated by the Frobnitz theme (which is inheriting this from the Bluemarine theme):

<div class="block block-user" id="block-user-1"> <h2 class="title">mbutcher</h2> <div class="content"> <ul class="menu">

<li class="leaf first">

<a href="/drupal/user/1">My account</a> </li>

<li class="collapsed">

<a href="/drupal/node/add">Create content</a> </li>

<li class="collapsed">

<a href="/drupal/admin">Administer</a> </li>

<li class="leaf last">

<a href="/drupal/logout">Log out</a>

This shows the complete contents of one specific block. We are more interested in the generic structure that all blocks share. To see this, we can simplify the previous code to something like this:

<div class="block" id="some_id"> <h2 class="title">Title</h2> <div class="content">Content</div> </div>

That's about all there is to a generic block. Every block has three structural pieces, and these are identified by class. There's a block that contains a title and some content.

Returning to our code, let's look at the behavior function:

Drupal.behaviors.slideBlocks = function (context) { $('.block:not(.slideBlocks-processed)', context) .addClass('slideBlocks-processed')

$(this).children(".title").toggle( function () {

$(this).siblings(".content").slideUp("slow");

$(this).siblings(".content").slideDown("slow");

Our behavior first tries to find all of the blocks that haven't already been processed by this behavior. The query to do this is: .block:not(.slideBlocks-processed). As we saw in the previous HTML, the class block indicates that a piece of HTML is a block.

Again, we want to prevent our behavior from being run twice on the same element. To do this, we write the behavior in such a way that it attaches its own class to an element once the element has been processed. In this case, the class is slideBlocks-processed, following one of the conventions used when defining behaviors.

Any block that gets processed by our slideBlocks behavior will be assigned the slideBlocks-processed class. So when we do the initial query, we can avoid blocks that have already processed by using .block:not(.slideBlocks-processed). The resulting jQuery object will only contain blocks that have not been processed.

The first thing we do with these matching blocks is append the slideBlocks-processed class to them. That way, later calls to Drupal. attachBehaviors() won't result in the behavior being attached again.

Let's continue in the jQuery chain:

$('.block:not(.slideBlocks-processed)', context) .addClass('slideBlocks-processed') .each(function () {

$(this).children(".title").toggle( function () {

$(this).siblings(".content").slideUp("slow");

$(this).siblings(".content").slideDown("slow"); });

The each() function, which we saw in Chapter 3, will iterate through each item in the jQuery object and call the anonymous function on each item.

Inside this anonymous function, the this keyword will point to the current item in the list. So if there are four blocks on the page, the anonymous function will be called four times, with this being set first to the first item in the list, then to the second item in the list, and so on.

Since our query is returning elements that are blocks, iterations through each() will set this to point to a block element.

What we want to do is get the title of each block and add an event handler to it, so that each time the title is clicked, the content slides up or slides down. In the code just shown, here's how this is done:

$(this).children(".title").toggle( function () {

$(this).siblings(".content").slideUp("slow");

$(this).siblings(".content").slideDown("slow"); });

Remember, this contains a block element (<div class='block'>...</div>). We wrap that in a jQuery object again, and then use the children() jQuery function to find all of the children of the current block that have the title class.

There will always be only one title per block.

To that title we want to attach an event handler that will fire when the title is clicked. But we want it to do one thing (slide up) the first time it is clicked, and another thing (slide down) the second time it is clicked.

The jQuery toggle() event handler is just what we need. It will fire the first function on odd clicks (1, 3, 5, and so on), and the second function on even clicks (2, 4, 6, and so on).

So on odd clicks it will execute this function:

$(this).siblings(".content").slideUp("slow");

The this variable is set to the element that was clicked, which is the block's title. We want to add the slide up effect to the content. Recall that the general structure of a block looks like this:

<div class="block" id="some_id"> <h2 class="title">Title</h2> <div class="content">Content</div> </div>

The title and block sections of a node are next to each other at the same level in the DOM tree. In other words, they are siblings. To get from our current title to the content sibling, we wrap the title element in a jQuery object and then use the jQuery siblings() function, passing it a CSS selector that will match the element with the content class.

Why don't we look for div.content?

Why do we search for .content instead of the more specific div. content? This is done mainly for the sake of portability. Possibly, a themer will want to change the HTML structure, perhaps using a <span> tag or wrapping the content inside a table. We wouldn't want such changes to break our JavaScript. So we do our best to decouple the CSS selector from the HTML tags.

Once we've got the content sibling, we simply add the slideUp() effect, setting the speed parameter to 'slow'.

The second toggle function performs an analogous task:

$(this).siblings(".content").slideDown("slow");

Here, instead of sliding up, this function causes the content to slide down. Otherwise, the functions are identical.

That's all there is to our behavior. When the page is loaded (and the ready event fires), all of the registered behaviors, including this one, will be attached. So from the moment the user can first interact with the page, she or he will be able to click on block titles and cause block contents to slide up until they disappear, and then (with another click) slide back down.

In the coming chapters, we will make use of the Drupal.attachBehaviors() function to make sure that new blocks that are added dynamically from JavaScript will also be given this behavior.

Was this article helpful?

+3 0

Responses

  • semira
    How to add toggle div in drupal?
    8 years ago

Post a comment