3. jQuery: Do More with Drupal – Drupal 6 JavaScript and jQuery

Chapter 3. jQuery: Do More with Drupal

In the last chapter, we built our first Drupal JavaScript. There, we used only standard JavaScript functions and tools to build our printing library. One of the benefits of working with Drupal is having access to libraries bundled with the core Drupal platform.

In this chapter, we will look at one such library: jQuery. Specifically, we will look at the following:

  • An overview of the jQuery library

  • The basics of using jQuery

  • Using jQuery within Drupal

  • Building a sticky node rotation tool with jQuery

  • jQuery effects, DOM manipulation, and events

As of Drupal 6, jQuery plays a major role in Drupal-centered JavaScript. For this reason, we will make a heavy use of jQuery in the remainder of this book. By the end of this chapter, you will be able to understand jQuery code. This will be a tremendous help not only in the rest of this book, but in your future Drupal JavaScript development.

jQuery: the write less, do more library

I wrote my first JavaScript code in the year 1995. It ran in Netscape 2.0, and looked something like this:

alert("Welcome to our site!");

In the next few years, I wrote lots of calculators, image rollovers, and scrolling status bar messages. My overall impression of JavaScript, which I suspect was the attitude shared by most web developers at the time, was that JavaScript was a low-powered tool for adding cutesy effects to web pages.

I've never been happier to eat my words.

With the birth of dynamically refreshing page renderers and the XMLHttpRequest (XHR) object (or control), JavaScript suddenly became a much more powerful tool for manipulating the contents of a page without requiring a full round trip to a remote web server.

Note

XMLHttpRequest and AJAX (Asynchronous JavaScript And XML) will be the subject of Chapter 5.

But with all of this new power came a fair amount of complexity. The core JavaScript librariesthe tools bundled in the major browsershave remained relatively small. And, as newer versions of browser have been released, writing working code often requires an awful lot of boilerplate "cross-browser compatibility" code.

Where changes have been made, they are often complex. The event model in Microsoft's IE and the one in Mozilla-based browsers diverged quite a while ago. The DOM API, implemented on all major browsers, is anything but simple. Anyone who has written an AJAX library from scratch will be quick to voice an opinion on the trials and tribulations of getting that library to work on all major browsers. Even manipulating a stylesheet from JavaScript has come with its share of subtle implementation differences.

In short, even while the capabilities of JavaScript had reached new heights, the JavaScript developer was often forced to use complex APIs and work around lots of compatibility issues while building a program.

As JavaScript's capabilities and usefulness grew, another change was occurring. This was a culture change among the JavaScript developers.

For a long time, the online JavaScript developers released useful snippets of code. These were not libraries in the proper sense of the word, but short snippets of code that could be copied and pasted into a project, tweaked, and then reused.

As JavaScript matured, so did the community around it. The focus shifted (to some degree at least) from producing "useful doodads" to creating polished libraries designed for reuse in a wide variety of settings.

Today's popular libraries, such as Prototype, YUI, Dojo, and the like, have all been built for general use.

One of these libraries, jQuery, has enjoyed a meteoric rise to fame, and deservedly so. Why has it been so successful? It's because the focus of the library has been on taking the really difficult (but important) JavaScript tools and making them easy to use. Thus, in the words of its creators, jQuery is the "Write Less, Do More JavaScript Library."

Note

The jQuery site is a great source for API documentation, tutorials, plug-ins, and additional tools: http://jquery.com.

With a library like this, less time must be spent on writing boilerplate code or trying to address cross-browser compatibility issues. Instead, more time can be spent on creating a powerful and robust JavaScript code.

Note

jQuery is an open source software, dual-licensed under either the GPL (the GNU General Public License) or the MIT license. Just as the Drupal developers included jQuery in Drupal 6, so can you use jQuery on your own sites and web applications.

jQuery provides a single, compact library focused on simplifying the following tasks:

  • Finding things in a document

  • Manipulating a document through the DOM

  • Interacting with a document's CSS

  • Working with events

  • Providing flexible AJAX tools

Moreover, the library works on all major browsersInternet Explorer, Firefox, Opera, and Safari.

As if this weren't enough, there's one more feature of jQuery that makes it a compelling tool for developers: It uses a tremendously compact syntax that makes it easy to accomplish surprisingly complex tasks in just a line or two of code. With a simple plug-in architecture (with hundreds of plug-ins already available), it can be extended to provide additional functionality.

Note

One popular addition to jQuery is jQuery UI. This library provides complex widgets such as calendars, accordions, and tabs. It also provides support for drag-and-drop, sorting, and other similar tasks. You can learn more about it at http://ui.jquery.com.

Okay, so what's the catch?

There is one aspect of jQuery that gets criticized on occasion. jQuery code looks different from other JavaScript, and this can make that initial jump into jQuery seem daunting. But once you've made that leap, it will change the way you write JavaScript.

Hopefully, this chapter will help make that first step an easy one. Let's take that first step now.

A first jQuery script

When you read the first few pages of this chapter, you must have noticed that very little was said about Drupal. jQuery is a standalone library with no dependency on Drupal, which means it can be used on its own.

In fact, that's the way we are going to dip our toes into the topic. Don't worry though. We will be coming back to Drupal just a little later in the chapter.

We are going to start out with a static HTML document and take a look jQuery in this simplified context. In the next section, we will apply this knowledge in the Drupal environment.

Getting jQuery

For our examples here, you will need the jQuery library. At this point, you have two options. You can either download a copy from http://jQuery.com or you can make a copy of the jQuery library that comes with Drupal. It's located in misc/jquery.js.

The copy that comes with Drupal is packed. Extraneous whitespace (such as newlines) has been removed, and long variable names have been replaced with short computer-generated names. That makes it difficult to read the jQuery library code, should you so desire.

For that reason, I suggest going to jQuery.com and downloading the Uncompressed version (it's right there on the front page).

Note

For more about packing code with Packer, see http://dean.edwards.name/packer/

Once you have the jquery.js file (or jquery.1.2.X.js), just make sure it is in the same directory as the HTML we will create in the next part.

Again, since Drupal already includes jQuery, there is no need to worry about putting this file where Drupal can see it. In fact, you should not put this in a Drupal directory.

Now, let's work up some simple HTML for a few examples.

Starting with a basic HTML document

Here is a very basic HTML document that will serve as a starting point. Notice that we are including the jQuery library in the<script></script> tag in the document's head.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xml:lang="en" lang="en">
<head>
<meta http-equiv=>>Content-Type>>
content=>>text/html; charset=utf-8>>/>
<title>sample</title>
<script src="jquery.js" type="text/javascript"></script>
</head>
<body>
<h1 id="title">Title</h1>

<p class="odd">Paragraph 1</p>
<p class="even">Paragraph 2</p>
<p class="odd">Paragraph 3</p>
<p class="even">Paragraph 4</p>

</body>
</html>

This document is just a standard XHTML document. We are concerned mainly with the highlighted sections. The first highlighted section loads jQuery with the file jquery.js. If you grab a copy from jQuery.com, it may have a name such as jquery.1.2.X.js. You can either rename that or change the src attribute to point to your version.

The second highlighted section exhibits the elements that we will be using in this section. Nothing should look unfamiliar here. It's just a header and four paragraphs.

Notice that the<h1></h1> has an id, id='title'. Also, the paragraph elements have class attributes. The even paragraphs have the even class, and the odd paragraphs have the odd class.

We are going to make use of these id and class attributes.

Now we're ready for some jQuery.

Querying with jQuery (and the Firebug console)

We could demonstrate jQuery by adding a script to the previous HTML code. But that would require us to write a complete script first. For a gentler method of introducing jQuery, we will start out with the Firebug console introduced in Chapter 1, instead.

Firebug is a Firefox add-on that provides a debugging and inspection environment for web developmentparticularly for HTML, CSS, and JavaScript. One of the tools it provides is a JavaScript console, which can be used to interactively run JavaScript. That's exactly what we are going to do.

Note

While we will use Firebug here, the Safari browser also includes similar developer tools that can be enabled by opening Safari's preferences window and then checking the Show Develop menu in menu bar check box. The examples that follow can also be duplicated on Safari's Web Inspector console.

To start off, let's use jQuery to find the<h1></h1> element in our document. Locating parts of an HTML document was, after all, the original purpose of using jQuery.

In the previous screenshot, we are entering the command jQuery('h1'). When we run this command, it should search the document and return a jQuery object that wraps a list of all<h1></h1> elements found in the document.

So if we hit ENTER to run the command, we will get something that looks like this:

In this case, when we executed jQuery('h1'), it returned an object with a length property set to 1. We know that there is only one<h1></h1> element in our document, so this length is what we would expect. In fact, if we re-ran the command as jQuery('p'), we would get the output: Object length=4. The length is four because there are four<p></p> tags in our document.

Note

What is the output on the console?

When you run a command on the console, Firebug always prints a representation of the returned value to the screen. In this way, you can get an idea of what is coming back from any executed function. If you come from a Ruby or Python background, this behavior is similar to the interactive shells of these languages.

But what is the object? It's a jQuery object. To show this, we could enter the following command into the console:

jQuery('h1') instanceof jQuery

The instanceof operator will compare the object type on the left hand with the type on the right hand. It will return true if both objects are of the same type. More specifically, if the object on the left side has the type of the object on the right side, the result will be true. Since prototypes are chainable, they may not have exactly all the same prototypes).

It should come as no surprise that the returned value is true. In fact, even if we were to search for an element that does not exist in our document, jQuery() will still return a jQuery object (though the length of this object will be 0).

How do you run a function called jQuery() without the new keyword, and get a jQuery object? The short answer is that jQuery() acts like a factory method. This function simply creates a new jQuery object and then returns that object. For this reason, there is no need to use new jQuery(). Instead, just jQuery() will suffice.

Note

This method of creating a new jQuery object resembles the Factory pattern. The Factory is a common design pattern in object-oriented programming, where one object (the factory) takes responsibility for creating new instances of another object. Of course in jQuery's case, the jQuery object is responsible for creating new jQuery objects. The flexibility of JavaScript's prototyping system allows this sort of thing to be done.

What else can we query with jQuery?

We can get elements by their id attribute:

jQuery('#title');

To find an element by ID, we prefix the ID with a "#" (pound sign).

We can also get elements by class:

jQuery('.even');

Just as an ID is prefixed with #, a class is prefixed with a "." (dot).

Now we've seen four jQuery query strings: h1, p, #title, and .even. Do these look familiar? If you have worked with CSS stylesheets before, they should. jQuery's query language is none other than the CSS selector language.

To style all the even paragraphs in CSS, you might write a CSS statement like this:

p.even {
/* style info */
}

That same selector, p.even, can be used in jQuery to find all even paragraphs like this: jQuery('p.even'). Since jQuery supports all CSS selectors through CSS version 3, you can even build more complex queries. For example, we could grab only the first even paragraph with this:

jQuery('p.even:first');

This looks for all paragraphs with the even class, and then returns only the first of those. Incidentally, since CSS 3 defines a pseudo-class for selecting the even and odd children, we could drop the odd and even classes altogether and use queries such as jQuery('p:nth-child(even):first') or even jQuery('p:even:first') (We've just used the ":" (colon) symbol to indicate that we are using a built-in CSS pseudo-class instead of the even class we defined ourselves.)

Throughout this book we will be using these CSS selectors to create queries. But if you are eager to gain a detailed insight into selectors, you might want to take a look at the W3C's current CSS 3 draft standard at http://www.w3.org/TR/ css3-selectors/.

Bye bye, jQuery(); hello $()

We've seen our last jQuery() function call.

From now on, we will be using an alias for that function. Instead of calling jQuery('h1'), we will be making calls such as $('h1').

What is this "$" thing?

Surprising as it may seem, the $ (dollar) sign is a legal character for function or variable names in JavaScript. That means iNeed$, $tar, and my$amount are all legal tokens in JavaScript. And as it turns out, so is $.

Making use of this, the jQuery developers aliased the jQuery() function to $(). That saves you five whole characters of typing. And it looks cool.

Generally speaking, you (as the software developer) can use either or both of these names to call jQuery. But the Drupal JavaScript convention suggests using the $() version. And so we shall.

Doing more with jQuery

We now have a glimpse of how we can find parts of a document with jQuery. But a querying engine alone doesn't get us too far. We need the ability to manipulate the information we've retrieved. Of course, jQuery provides such tools.

To start, let's get the text content of our<h1></h1> element. This is done with the text() function:

var title = $('h1');
title.text();

The first line retrieves a jQuery object containing our<h1></h1> element and stores this in the title variable. The second line calls the jQuery object's text() function. This will return all of the text inside of that element. Any HTML tags will be removed. (If you want the HTML content, use html() instead of text().) Running this pair of statements would cause Firebug to output"Title" to the console.

Likewise, if we wanted to change the title of our document, we could call:

var title = $('h1');
title.text('New Title');

Immediately, the bolded text in the document would be changed as if we had written<h1>New Title</h1> instead of<h1>Title</h1>. What would Firebug output to the console when we set text? A string representing the jQuery object (for example Object length=1). This means that even a setter function returns an object. As we will see in a moment, this is an important aspect of the jQuery library.

Our two-line text-getting scriptlet is functional, but we can shorten it down to one line by making use of a convention called function chaining or method chaining. When we call a function that returns an object, we often store that object in a variableas we did with the title variable. That way, we can use the object later by referring to title.

Sometimes we don't need to store the object, we just need to call one of its functions. Rather than storing the object in a variable, we can just use the dot operator (.) to chain the function to the function that returned the object.

Sounds confusing? An example will clear it up. We can rewrite our previous scriptlet like this:

$('h1').text('A New Title');

The text() function operates on whatever jQuery object the $() function returns.

Note

This is one of the important reasons why $() returns a jQuery object even if it doesn't find any matches to the query. Otherwise, scripts would be plagued with errors when a chain returned, say, a null instead of an object.

Just as with the earlier code, the previous line will find the<h1></h1> element and change its content.

jQuery makes greater use of the chaining concept by using a design pattern called the Fluent Interface. In a fluent interface, any member function that would normally return void (that is, nothing) returns its parent object instead.

Note

A member function is a function that belongs to an object. In most object-oriented languages, all functions are members of an object.

The jQuery object provides this type of fluent interface. So when we call $('h1').text('A New Title'), the title is changed; but we also get the jQuery object returned again.

How is this useful? Well, we can combine several tasks into the same line. Let's say we not only want to change the text of the title, but also want to underline the title. This additional step would involve changing the<h1></h1> element's CSS. We can chain another function to our code to accomplish this:

$('h1').text('New Title').css('text-decoration', 'underline');

The result should look something like this:

There are two things to notice here.

First, the title and text decoration have indeed changed.

Second, as you can see in the console output, the css() function still returned our jQuery object. That means we could continue our chain if we so desired.

Not every function returns a jQuery object

Before we get too addicted to the fluent interface, we should take another look at an earlier example as an illustration (or perhaps a reminder ) that not all jQuery functions return jQuery objects.

In our first example of the text() function, we saw that running $('h1').text() returned the string Title.

In this case, we have a string object, not a jQuery object. Sure, we could continue chaining, but we could only call functions of the string object. For instance, the following would work just fine:

$('h1').text().substring(0,1);

The text() function returns a string, and substring() is a member function of a string object. However, we could not do this:

$('h1').text().css('color','red');

This would generate an error looking something like this: TypeError: $("h1").text().css is not a function. That happens because a string object does not have a css() function.

At this point, the important things to understand about jQuery are:

  • How queries can be composed from CSS selectors

  • How $() and jQuery() return jQuery objects (that often wrap lists of elements from the document)

  • That jQuery function calls are often chained together to produce longer lines that do more

We will pick up more about jQuery as we proceed.

Feeling comfortable with the basic concepts of jQuery? Now it's time to get back to Drupal. We will be making use of jQuery to build Drupal-centric JavaScript.

Using jQuery in Drupal

So far, we have seen just a few lines of jQuery code. But, our examples have been independent of Drupal. How can we use jQuery inside of Drupal?

The answer is simple. If we write our JavaScript "the Drupal way", then Drupal will make it very easy to use jQuery. Here's what I mean.

How do we include JavaScript files in Drupal themes? By using the scripts[] directive in the theme's .info file. And if you're a PHP programmer, you can use the drupal_add_js() function in either a theme or a module.

If you use either of these means, then Drupal will automatically include the basic JavaScript libraries, including drupal.js and jquery.js. That's right. Drupal not only includes jQuery in its core distribution, but automatically includes it for you.

To see an example of this, we need not look further than the code we wrote in the previous chapter. Looking in the<head></head> section of the HTML for our theme, we can see the following<script></script> tags:

<script type="text/javascript"
src="/drupal/misc/jquery.js"></script>
<script type="text/javascript"
src="/drupal/misc/drupal.js"></script>
<script type="text/javascript"
src="/drupal/sites/all/themes/frobnitz/printer_tool.js">
</script>
<script type="text/javascript">
jQuery.extend(Drupal.settings, { "basePath": "/drupal/" });
</script>

All we added to our .info file was scripts[] = printer_tool.js. Drupal took care of adding the other two script files. But what is that last section? Well it is:

jQuery.extend(Drupal.settings, { "basePath": "/drupal/" });

This is another use of jQuery. It provides a few functions for extending an object, which assigns new attributes to an existing object on the fly.

In this case, the extend() function is taking the Drupal.settings object and adding a new property, basePath, with the value /drupal/. Here, /drupal/ is the absolute web path to Drupal. That is, my Drupal installation resides at http://localhost/drupal/. The basePath property can then be used for constructing URLs.

Using the Firebug console, we can see the result of this extension. If we enter Drupal.settings.basePath on the console, Firebug will print /drupal/.

Note

Using the jQuery object without constructing a new instance

In the Drupal-generated code that we just saw, you might notice that jQuery was referenced as an object, not as a function (note the missing parentheses after jQuery). Some jQuery functions do not need a list of elements or other contents before they can operate. Effectively, they can be used as static functions. The extend() function is such an example. In such cases, we don't need to build a new jQuery object. We can just use the main jQuery object.

The point to be made here is simply this: When we allow Drupal to manage our JavaScript, it handles the basic including necessary to make use of jQuery, and even initializes some things for us.

Of course, there's a converse side to this.

Don't do it yourself!

There may be a temptation to do things "the old fashioned way." One may be inclined to simply edit the page.tpl.php template and add<script></script> tags manually. After all, isn't this how we do things when we write HTML by hand?

Don't do it.

When you start adding scripts by hand, you are circumventing Drupal's JavaScript system. Drupal doesn't inspect the HTML to see what files are included in the template. So you may not get the default script files included automatically (unless some other part of the theme or some module triggers the automatic inclusion).

In addition to perhaps not getting the autoloading that we saw, adding scripts by hand can also lead to conflicts. For example, a library can get loaded manually, and then get loaded by a module. If the library versions are different (or if two libraries share functions with the same name), the results can be surprising.

There's one more thing in addition to these two: If you are planning on submitting your code as a theme or module, hardcoding scripts this way will no doubt prompt experienced Drupal developers to file bug reports against your code.

The moral of this story is simply to let Drupal handle the JavaScript library loading.

That's all there is to using jQuery within Drupal. We are now ready to move on to a project.

Project: rotating sticky node teasers

Now we're ready to start our first jQuery project. We're going to write some code that rotates sticky node teasers. With a description like that, it's got to be good!

Seriously, the phrase "rotating sticky node teasers" is pretty jargon-laden. Here's what I mean.

On various Drupal pages, the main content area of the page is composed of node teasers. The following front page is one such example:

In the content shown in the screenshot, there are three different nodes displayed. But only the teaser version is shown. (One would have to click on Read more to see the full story.)

By default, items are arranged from the newest to the oldest. The newest node is listed first on the page.

But what if we wanted to have a node that was always displayed on the top?

This can be accomplished by marking a node as sticky. A sticky node floats to the top of the list of nodes on a page.

Nodes are marked as sticky using the content editor. Near the bottom of the editor page, there is a collapsed section called Publishing options. In that section, there are three checkboxes: Published, Promoted to front page, and Sticky at top of lists. This third box influences an item's stickiness. In the examples to follow, we will check all three to display sticky items on the front page. You can also mark a node as sticky by going to Administer | Content management | Content, checking the desired node, and selecting Make sticky from the drop-down list of Update options.

If we were to configure three items to be sticky at the top, the result would look something like this:

In the screenshot, the three nodes with light gray backgrounds are marked as sticky. Even when we add new nodes, these three will remain at the top of the page.

The screenshot also provides the basis for understanding the project we are about to do.

Having three sticky nodes at the top is in some ways counterproductive, for only one of those is really at the top. The others are de-emphasized in virtue of being displayed in a lower position on the page.

In addition, the regular content (like the important This is a node story we created in Chapter 2) is in danger of disappearing beneath the browser's fold. A viewer has to scroll down on the page to see what's new. This isn't an attractive feature.

So, we're going to change the display of sticky nodes. We're going to build a tool, enlisting the help of jQuery, which will rotate through the sticky nodes. It will display them one at a time, in sequence.

After the first node has been displayed for a period of time, it will "fade out" into the background, and then the second sticky node will "fade in". It, too, will be displayed before fading out again. This process will continue as long as the page is displayed. This is a process often referred to as rotating a list.

This is what the phrase "rotating sticky node teasers" means.

Using this technique, all of our sticky nodes will be displayed in the page's top slot. In addition, the standard content display will begin in the second spot (rather than the fourth spot in the case of our earlier screenshot). Since sticky items no longer take up a lot of space, we can make four, five, or even six nodes sticky without making the display cumbersome to the visitor.

And we hope this will be an attractive, eye-catching feature for our visitors as well.

That's our goal. Let's move on to the code.

The StickyRotate functions

We are going to continue building on the frobnitz theme we created in the previous chapter. To start off, we are going to create a new JavaScript file called sticky_rotate.js. As usual, this will go in the sites/all/themes/frobnitz directory. And once again we will have to edit the frobnitz.info file in that directory.

; $Id$
name = Frobnitz
description = Table-based multi-column theme with JavaScript enhancements.
version = 1.0
core = 6.x
base theme = bluemarine
stylesheets[all][] = frobnitz.css
scripts[] = printer_tool.js
scripts[] = sticky_rotate.js

Only that last highlighted line is new. It adds the sticky_rotate.js script to the list of scripts automatically loaded by the theme. Remember that you may need to clear the theme cache before Drupal adds this new script to the rendered output.

From here, we will begin editing the sticky_rotate.js file. The code we create will come in at around sixty lines, some of which is just comments.

Again, let's take a quick glance at the lines at the top of the file:

// $Id$
/**
* Rotate through all sticky nodes, using
* jQuery effects to display them.
*/
// Our namespace:
var StickyRotate = StickyRotate || {};

We start the file with the $Id$ keyword as explained in the last chapter. Again, this is used by the version control system. (CVS is the official version control system for Drupal.)

Next, we have a documentation block describing the contents of the file. This is standard practice in Drupal. It is not always treated as required for JavaScript (though it is required for PHP). However, it's a good idea to include it.

Finally, once again we create a new object that we will use as a namespace for our library. In this case, StickyRotate will be our namespace, and all of our functions and public variables will be attached to that namespace.

Note

Using the "or" operator for default assignment

In the previous code, the StickyRotate function is assigned StickyRotate || {}. The "||" (or) operator works in the following manner: If the StickyRotate variable already exists, it will be assigned to the StickyRotate variable on the lefthand side, and the Boolean logical operator will short circuit. But if StickyRotate doesn't already exist, then the right half of the || will be evaluated, and StickyRotate will be assigned an empty object ({}). In short, this construct allows us to assign StickyRotate a default value, only if StickyRotate is not already defined. This idiom is used frequently in JavaScript, and you will notice it in many Drupal files.

First, we will start out by writing a function that will take care of initializing the process.

The init() function

To accomplish our node rotation effect, we will need to do some setup when the page first loads. Our first function will take care of initializing sticky rotation:

/**
* Initialize the sticky rotation.
*/
StickyRotate.init = function() {
var stickies = $(".sticky");
// If we don't have enough, stop immediately.
if (stickies.size() <= 1 || $('#node-form').size() > 0) {
return;
}
var highest = 100;
stickies.each(function () {
var stickyHeight = $(this).height();
if(stickyHeight > highest) {
highest = stickyHeight;
}
});
stickies.hide().css('height', highest + 'px');
StickyRotate.counter = 0;
StickyRotate.stickies = stickies;
stickies.eq(0).fadeIn('slow');
setInterval(StickyRotate.periodicRefresh, 7000);
};

Taking a quick glance over the function we just saw, we will notice more than a few invocations of jQuery's $() function. In fact, we will be using jQuery to provide DOM and CSS manipulation, and also to provide some nice visual effects.

Let's take a closer look at the first few lines of the function:

StickyRotate.init = function() {
var stickies = $('.sticky');
// If we don't have enough, stop immediately.
if (stickies.size() <= 1 || $('#node-form').size() > 0) {
return;
}
/* The rest of the code... */
};

The first line is our function assignment. Just as we did in the previous chapter, we make use of JavaScript's ability to assign anonymous function declarations to variables. So this function can be called as StickyRotate.init().

On the first line of this function, we create a local variable: stickies. It is assigned a jQuery object containing the results of the $('.sticky') query. As we saw earlier in the chapter, when jQuery is given the query string .sticky, it will search the document for elements assigned the CSS class named sticky.

Note

Effectively, this is the same as saying that it will search for all elements with the attribute declaration class="sticky". However, one should keep in mind that the class may be added by JavaScript. Thus, it may not be visible when viewing the raw HTML source.

So what is this sticky class and where does it come from? Drupal's theming system assigns the sticky class to all sticky nodes. To see this for yourself, view the node.tpl.php in Bluemarine or Garland. (The class can also be set in PHP code. In Chameleon, which is a PHP-driven theme, this happens in a theming function chameleon_node().)

In other words, we can rely upon Drupal to flag those sticky nodes for us. Our jQuery needs only to look for the right class.

So stickies should contain a jQuery-wrapped list of all of the sticky nodes on the page.

We only want to apply our effect on pages where there is more than one sticky node preview. After all, it wouldn't be all that useful or attractive to rotate through a list of one.

To address this situation, we add the next conditional:

// If we don't have enough, stop immediately.
if (stickies.size() <= 1 || $('#node-form').size() > 0) {
return;
}

jQuery's size() function retrieves the number of items that the jQuery object is currently wrapping. Since our stickies variable refers to a jQuery object, we can call the size() function to determine whether our list is long enough to warrant rotating through it.

Note

A jQuery object also has a public attribute named length, which contains the same information. We saw this attribute in the output of the Firebug console earlier in the chapter. Determining which one to use is a matter of preference, as both provide identical information. Some might argue that length is faster, since it is a property. However, size() just returns that property, so there is no real performance impact.

But that's not all our conditional does. It also runs another query to make sure that the current document does not contain an element with the ID node-form.

Why? This takes care of one possible place where our JavaScript might cause confusion. When a user is previewing a node, the node may show up twice (as a preview of the teaser version and of the full version). In such cases, it might appear that there are two sticky nodes, when really it's just the same node displayed twice. For example, take a look at the following screenshot:

Unfortunately, there are no obvious cues in the node display that might make it easy for us to avoid the situation. But fortunately, preview screens also include the node-editing form. And the node-editing form always has the node-form ID.

So in that one corner case, we can avoid adding our effect when $('#node-form').size() is greater than 0.

In the case where there is only one sticky node on a page, or when the sticky node is displayed as part of a content editing preview, the function simply returns without doing anything.

But if neither of those conditions is met, initialization continues. Let's take a look at what's next:

StickyRotate.init = function() {
var stickies = $(".sticky");
// If we don't have enough, stop immediately.
if (stickies.size() <= 1 || $('#node-form').size() > 0) {
return;
}
var highest = 100;
stickies.each(function () {
var stickyHeight = $(this).height();
if(stickyHeight > highest) {
highest = stickyHeight;
}
});
/* The rest of the code... */
};

We are interested in the highlighted portion of the code. The focus of this snippet of code is the height of the area where our sticky nodes will be displayed.

Looking back at our three sticky nodes, we have two fairly large teasers (the two Latin Descartes' quotes), but we also have a very short one. The node entitled A Sticky Node has a one-line teaser: This node should always be displayed near the top of the home page.

What we don't want is for a short node like this to cause the page layout to shift significantly. We don't want the content to slide up for the short node, and then slide back down to make room for longer nodes.

What we do want then, is to control the height of the sticky nodes. We want them all to take up the same amount of screen space.

The code we just saw is designed to find the largest sticky node, get that node's height, and then use that as the height for all of the sticky nodes.

The highest variable will store the size of the highest node. By default, we set it to 100 pixels.

Next, we make use of another jQuery feature. Along with the rest of the jQuery goodies, there are a handful of basic list-traversing functions. One of these is the each() function, which iterates through each item in the list of nodes wrapped by a jQuery object. On each object, it executes the function passed into the each () function.

Note

There are actually two jQuery each() functions: One that iterates through the contents of a jQuery object, and one that iterates through any list-like object. The list-iterating version is called on the main jQuery object like this: jQuery.each(myList, function () {}).

So this version of each() takes one argumenta function object.

In our case, we are going to supply the each() function with an anonymous function that will do the height comparison:

stickies.each(function () {
var stickyHeight = $(this).height();
if(stickyHeight > highest) {
highest = stickyHeight;
}
});

When that anonymous function is executed, the this variable is set to the current list item. In our case, this will contain the current DOM element in the list of stickies.

Note

This method of formatting inline anonymous functions is conventional, and you will see the same formatting throughout jQuery and Drupal.

It is important to note that this is a DOM element object, not a jQuery object. We want to find the height of that object, but the easiest way to get the height of an element is to use the jQuery library. So in the second line, we pass this into $() to get a new jQuery object, Then we use jQuery's height() function to find out the element's height. This is all packed into the line that reads: var stickyHeight = $(this).height().

While the first two lines of this code sample are complex, the next part is straightforward:

if(stickyHeight > highest) {
highest = stickyHeight;
}

All we do here is check to see if the height of the current element is higher than the current highest (again, 100px by default). If it is higher, then the value of stickyHeight is assigned to highest. All we're doing, then, is finding the highest element and setting highest to that element's height.

There is one point of interest here. The highest variable is local to the init() function, and is declared outside of the anonymous function declaration. Doesn't it seem like this variable ought to be out of scope for the function inside our each() loop?

Yet, as the previous example shows, it is still accessible inside of the function's body.

This works because in JavaScript, functions inside other functions retain access to the variables of their parent functions. In other words, if function a() (the outer function) encloses the definition of function b() (the inner function), then function b() will have access to all of function a()'s local variables.

By the time the each() function returns, highest should be set to the pixel height of the highest sticky node that we will display.

We're ready to move onto the next part of the init() function:

StickyRotate.init = function() {
var stickies = $(".sticky");
// If we don't have enough, stop immediately.
if (stickies.size() <= 1 || $('#node-form').size() > 0) {
return;
}
var highest = 100;
stickies.each(function () {
var stickyHeight = $(this).height();
if(stickyHeight > highest) {
highest = stickyHeight;
}
});
stickies.hide().css('height', highest + 'px');
StickyRotate.counter = 0;
StickyRotate.stickies = stickies;
stickies.eq(0).fadeIn('slow');
setInterval(StickyRotate.periodicRefresh, 7000);

};

These last several lines do all of the visible work.

To start off, we know that we currently have three sticky nodes. By default, they all will be displayed, each at its own height.

But we want them to be hidden initially, displaying them only one at a time. We also want to give them all the same heightthe height of the tallest sticky node.

Making use of jQuery's chaining features, we can do this all in one line of code:

stickies.hide().css('height', highest + 'px');

What this will do is take all of the items wrapped in the stickies jQuery object and hide them (hide()), and then assign each the CSS property height set to the height of the highest element.

Note

PHP programmers should note that the "+" (plus) operator in JavaScript is used for string concatenationa task done by the. (dot) operator in PHP.

Just as we saw earlier, in many cases, when a jQuery function is called on a jQuery object, every item that that jQuery object wraps is affected. Both the hide() and css() functions are examples of this.

Now that the list is hidden, we can get to work displaying the items one at a time.

First, we initialize a few public variables, StickyRotate.counter and StickyRotate.stickies. This first will help the later functions determine the index position of the currently displayed item. The second will provide our other functions with access to our jQuery stickies object.

Note

These variables are public because their scope is such that they can be accessed from anywhere in the script being currently executed.

Preparation is done. The next thing to do is display the first sticky node. And we want to do that with some panache. Specifically, we want to make it fade in.

stickies.eq(0).fadeIn('slow');

On this line, we take the stickies list and reduce it to just the first item. The eq() jQuery function is used to reduce a list to just one item, and take as a parameter the index of the item. We pass it 0 because we want the first item in the list.

Next, we use another jQuery function, fadeIn(), to provide us with an effect.

jQuery comes with several built-in effects, including fadeIn(), fadeOut(), slideDown(), and slideUp(). We use the fadeIn() effect here to gradually alter the opacity from transparent to completely opaque. For example, the following screenshot illustrates what a node item looks like when it's about a third of the way through the fading-in process:

The fadeIn() function takes up to two parametersthe speed of the effect and a callback function. We will look at the callback later in this chapter. The first is the speed of the effect. It can be specified either as an integer representing the number of milliseconds that the effect should take, or one of three keywords: slow, def (or normal), and fast.

We used slow in our example.

The full effect of this, then, is to gradually fade-in only the first sticky node. The others remain hidden.

Finally, we are up to the last line in our init() function. This line sets up a time-delayed loop that will handle the fading-out and fading-in of the subsequent node displays.

setInterval(StickyRotate.periodicRefresh, 7000);

The setInterval() function is a built-in JavaScript function that takes as arguments a callback function and an interval in milliseconds. In this case, every 7000 milliseconds (seven seconds), the StickyRotate.periodicRefresh() function will be executed.

It is important to note that when we pass the function name to setInterval(), we do not add parentheses at the end of the function name. That would result in sending the results of that function to the setInterval() function.

Instead, we want to pass the function object so that setInterval() can call that function at the appropriate interval.

Note

If we wanted to get really fancy, we could rewrite this function to pass a closure to the setInterval() function. In such a case, we wouldn't need to set any public variables. For the sake of simplicity (and performance), we will stick to our current implementation.

Of course, the StickyRotate.periodicRefresh() function has not been written yet. Let's turn to that now.

The periodicRefresh() function

This is the second (and the last non-anonymous) function in our project. As we saw earlier, it is called by the setInterval() function. In fact, barring some interruption, it will be called every seven seconds.

Fortunately, this new function makes use of the same tools just introduced, so our review of the function should go quickly.

/**
* Callback function to change show a new sticky.
*/
StickyRotate.periodicRefresh = function () {
var stickies = StickyRotate.stickies;
var count = StickyRotate.counter;
var lastSticky = stickies.size() - 1;
var newcount;
if (count == lastSticky) {
newcount = StickyRotate.counter = 0;
}
else {
newcount = StickyRotate.counter = count + 1;
}
stickies.eq(count).fadeOut('slow', function () {
stickies.eq(newcount).fadeIn('slow');
});
};

Let's break this function into two chunks. Here is the first:

StickyRotate.periodicRefresh = function () {
var stickies = StickyRotate.stickies;
var count = StickyRotate.counter;
var lastSticky = stickies.size() - 1;
var newcount;
if (count == lastSticky) {
newcount = StickyRotate.counter = 0;
}
else {
newcount = StickyRotate.counter = count + 1;
}
/* More code... */
};

To make things easier on ourselves, we use stickies and count to store local copies of the StickyRotate.stickies and StickyRotate.counter variables. This makes for a cleaner code and reduces the chances for mistakes to be made when typing variable names.

The lastSticky stores the index number of the last sticky in the stickies list. (Actually, stickies is a jQuery object wrapping the list of stickies.)

At this point, we have the list of stickies, the index (count) of the current sticky, and the index (lastSticky) of the last sticky. Based on this, we can compute what item should be displayed next.

That is done with the simple conditional:

var newcount;
if (count == lastSticky) {
newcount = StickyRotate.counter = 0;
}
else {
newcount = StickyRotate.counter = count + 1;
}

Note

On a stylistic note, we could omit the curly braces for the if and else blocks. However, Drupal's coding standards strongly encourage developers to leave these braces in, for the sake of readability. For that same reason, it is also often suggested that the if or else statement be used rather than the ternary (? : ) operator. However, in simple cases where the ternary is easier to read, it is acceptable.

Our conditional is basically checking to see if we have reached the last sticky node. If we have, then we want to start over again with the first node. Otherwise, we want to keep going through the list.

One interesting aspect of the previous code is that we want to set two variables at once. We want a local variable, newcount, pointing to the index of next sticky node. We also want to set the StickyRotate.counter variable to this new value so that future invocations of our preiodicRefresh() function will access the correct item in the list of sticky nodes.

To accomplish this, we use the double assignment:

newcount = StickyRotate.counter = 0;

Both variables will be assigned the value 0 if the current count is equal to lastSticky. Otherwise, newcount and StickyRotate.counter will be set to count + 1.

Now there are only a couple of additional lines to investigate:

StickyRotate.periodicRefresh = function () {
var stickies = StickyRotate.stickies;
var count = StickyRotate.counter;
var lastSticky = stickies.size() - 1;
var newcount;
if (count == lastSticky) {
newcount = StickyRotate.counter = 0;
}
else {
newcount = StickyRotate.counter = count + 1;
}
stickies.eq(count).fadeOut('slow', function () {
stickies.eq(newcount).fadeIn('slow');
});
};

The highlighted lines take care of fading-out the old sticky node, and fading-in the next sticky node in the list.

This is accomplished by (again) getting a single item from our list of stickies using the eq() function, and then calling the fadeOut() function.

This time, we call the fadeOut() function with both of its arguments. We set the speed to slow, and then give it an anonymous callback function.

The callback function uses the newcount variable to select the next item from the list of stickies and fades it in.

At first, a glance at these three lines of code might make it look unnecessarily complex. After all, can't we write something like the following instead?

stickies.eq(count).fadeOut('slow');
stickies.eq(newcount).fadeIn('slow');

This won't work as expected. Instead of getting a full fade-out followed by a fade-in, both will happen at the same time. In this case, two sticky node teasers will be displayed at the same time. This, in turn, means that the viewer will temporarily see two stacked sticky nodes. Then the top one will go away and the bottom one will slide up.

It's ugly.

The reason behind this is that the fading process takes time. For all practical purposes, though, fading is handled in a separate "thread". As soon as the fade is started, the script interpreter continues to execute the next line of the script. So the fading-in process will begin before the fading-out process has completed.

To work around this, we use the anonymous callback function that jQuery will fire when it has finished the fading-out. While this might seemingly lead to more complexity, it's only gained us a line of code. And as we will see in later chapters, being able to launch multiple effects at the same time can have its benefits.

Now we have two functions that, when used together, will provide our sticky node a rotation feature.

There's only one thing left to do. We need to make sure that our little StickyRotate tool gets started when a page loads.

Adding an event handler with jQuery

When a page loads, we want to start our StickyRotate tool. Practically speaking, we want to have the browser execute the StickyRotate.init() function when the page is done loading and is ready to add effects.

Note

We want it to be loaded because we want first, all of our sticky nodes to be loaded, and second, all of the CSS styles to be applied so that they don't override the styles we set dynamically. But we don't really want to wait for the images to load first.

Unfortunately, the two popular ways of accomplishing this have significant drawbacks.

The first method of calling the init() method is to embed a snippet of JavaScript somewhere in the page. For example, a section of code like this might be inserted somewhere in the document's body:

<script>
StickyRotate.init()
</script>

What's wrong with this? For starters, it's messy. In order to make sure that it is executed correctly, we have to put the script somewhere in the HTML after the last sticky node. Otherwise, we run the risk of having the script execute before the HTML is completely loaded. This means that we have to hack the page.tpl.php file to insert the code in the appropriate place. This, in turn, can end up being a pain to maintain (and it is not very portable).

Instead, we might try to fire the event by adding onload="StickyRotate.init()" to the<body></body> element. This has two drawbacks. First, it means we have to hack page.tpl.php, which is undesirable. Second, onload is not implemented consistently across browsers, and it may take longer to fire than we would like (or it may fire before the document is really ready).

Fortunately for us, jQuery provides a third alternative. We can write a few lines of JavaScript that will attach an event handler to a special jQuery ready event. According to the jQuery documentation, the ready event is fired the instant the DOM is ready to be read and manipulated. (See http://docs.jquery.com/Events/ready for details.)

Here and throughout the book, we will use this method to execute code as soon as we can reliably do so.

Here's how we will do it for our present code:

$(document).ready(StickyRotate.init);

This code doesn't go in the HTML body, and does not need to be launched from onload or another HTML attribute. Instead, it is included in our sticky_rotate.js file.

Here's how this line works.

$(document) creates a jQuery object that wraps the document object. The ready() function assigns an event handler to the ready event of that object. This ready event is a special jQuery event that fires when the DOM is ready for manipulation. Roughly, though, it functions like the (on)load event, and should almost always be used instead of an onload handler.

The ready() function takes a function as a parameter. Often, developers will write an inline anonymous function to do the initialization. But we've already created an init() function that is suitable for our needs. So all we have to do is pass in that function object, and jQuery takes care of the rest.

But what if we have multiple scripts that need to do something when the document is ready? Do we need to write a master function to handle all of the initialization?

No we do not. One of the nice things about jQuery's event model design is that we can register multiple handlers for the ready event (or any other event)and we can even do this in different script files. So, we can use $(document).ready() multiple times.

Now we have a tool that will rotate through sticky nodes, displaying them in order. Using the fade effect provided by jQuery, it will smoothly transition from one sticky node to the next. It will accomplish our goals of keeping the important information right at the top, while not occupying too much space (and subsequently pushing other information further down the page).

More importantly, we've gotten to see jQuery in action, providing querying tools, DOM manipulation features, effects, and event handlers. This will lay groundwork for later chapters.

A brief look backward

Now that we've seen the possibilities that come with jQuery, let's briefly reconsider how we built the printer tool in Chapter 2.

To build that tool, we had to make changes within the HTML of our theme, and we had to work with various DOM methods such as getElementById().

With jQuery, we could have skipped the template part altogether, using the ready() function (and other jQuery event handlers) to create the printing link as the page was created. In fact, we will do something like this in the coming chapter.

Likewise, we could probably have trimmed down the size of our script by making use of jQuery's chaining feature.

But in the interest of moving forward, we will not spend time revising that code. Instead, the next chapter will cover some of the additional JavaScript functionality provided by Drupal.

Summary

This chapter has focused on jQuery. Initially, we looked independently at jQuery. We manipulated a static HTML document using jQuery. From there, we took a closer look at how jQuery is integrated with Drupal. Our main project in this chapter explored how jQuery can interact with HTML created by Drupal. We created a tool to take Drupal's default display of sticky nodes. We transform it into a rotating display, fading one item into view, and then fading it out again, only to fade-in the next item.

As we move on from here, we will explore more Drupal libraries. In the next chapter, we will look at Drupal behaviorsanother powerful component in Drupal JavaScript development. But this isn't the last you will see of jQuery. On the contrary, it figures prominently in Drupal 6 JavaScript development.