9. Integrating and Extending – Drupal 6 JavaScript and jQuery

Chapter 9. Integrating and Extending

In this final chapter, we will look at a few advanced topics. We will look at integrating existing Drupal JavaScript tools with our own site design, and then we will see how to extend the JavaScript libraries with the jQuery UI library. Finally, we will take one more step and extend jQuery's library with our own functions, building a jQuery plug-in in the process.

We will cover the following:

  • Using the autocomplete.js Drupal library

  • Installing and using the jQuery UI library

  • Building a custom jQuery plug-in

  • Using a custom jQuery plug-in both inside and outside of Drupal

As with the previous chapter, the focus of this chapter will be on completing projects.

Project: autocompletion and search

One of the more successful Web 2.0 uses of JavaScript has been autocompletion. Start typing a search term in a text entry field and the browser displays a list of suggested terms. Type a few more characters and the list changes as you type, refining and narrowing the terms that might be completions of what you are typing. This is known as autocompletion.

How does autocompletion work? Here's what happens behind the scenes. As you type text into the field, a piece of JavaScript is monitoring the field, sending AJAX requests to the server to tell it what text you have entered so far. The server then does some preliminary searching or matching routine and returns data to the JavaScript. The script then displays the choices that match. Usually, the script displays the options in a way that makes them look like part of a combo box (a text entry field with a selectable list).

There are a lot of nuances to autocompletion scripts. How often should the script perform its AJAX query? If queries happen too often, autocompletion becomes a performance burden. If they happen too rarely, the autocomplete functionality will be useless. Should the script send short strings (such as 'is') to the server? How many results should it show? These and other such questions make implementing autocompletion a sizable task.

The Drupal Core makes use of autocompletion in several places. For that reason, Drupal developers created a special-purpose JavaScript library that provides the client-side facilities. This library can then be used to add autocompletion to other text fields on your site.

One area where autocomplete is not enabled in Drupal is on the search pages. As always, there are a variety of reasons for this, the most important being performance. Having lots of autocompletion scripts, on various clients, all running numerous searches would significantly increase the load on the search engine.

In our first project, we are going to devise a simple autocompletion tool for searches.

The theory

At one time I was on a team charged with analyzing a large-scale search implementation for a web site that handled hundreds of thousands of hits a day. Sifting through the search data, we were surprised to find that while there were many thousands of unique searches; most of the searches were for one of the eighty different search strings. We could reduce the load on our server by almost 75 percent simply by caching the results for those eighty search strings.

We can extrapolate from this example. Perhaps autocompletion would be useful if the server only returned a subset of all of the possible search terms. That subset would have to reflect the popular information on the site in order to be useful. Implementing things this way could cost less performance-wise, while still being useful to the user.

Our plan

For our implementation, we will add autocompletion to the search field in the search module's block. But rather than running a search for each autocompletion request, we will build a scaled-down list of terms that we will define. Only that list of terms will be used for autocompletion.

If this were a book on PHP development, I would suggest writing some server-side code to collect and analyze search engine data, and then use that information to pre-populate an autocompletion field. But we can't readily get that information. So we will try another approach.We will use a Drupal taxonomy as the source for our suggestions.

Taking this approach, we will allow content creators to add keywords to articles. Those keywords will then be used for autocompletion.

First step: creating the taxonomy

We are going to use a taxonomy to seed our search terms. Drupal taxonomies are collections of terms that are sometimes structured (such as a hierarchy of categories) and sometimes unstructured (such as user-entered tags). Using taxonomies you can add keywords, categories, and tags to the content on your site. But there are many other uses for taxonomies. Ours will be used for providing our search autocompletion with a list of suggestions.

Note

On occasion, Drupal uses the term vocabulary instead of taxonomy. Sometimes, a tenuous distinction is made (a taxonomy is made up of vocabularies, which are in turn made up of terms). Practically speaking, the term 'vocabulary' is a synonym for taxonomy.

The taxonomy will be created through Drupal's administrative interface. Our new taxonomy will be called Tags. Here is the relevant portion of the taxonomy creation page:

The taxonomy defined in the screenshot will be applied to all of our content types and will use a tagging structure. When a user creates new content, she or he can add tags, which in turn will be used by our autocompletion tool.

Since it has been applied to all content types, any time we go to the Create content page and create a new node, we should have the option of tagging that content. Here's a new piece of content:

Here we added just over a dozen tags to the story. When we finish with this project, those tags should be displayed as autocompletion recommendations when a user types the appropriate text into a search box.

Next, we need to go into Administer | Site building | Modules and turn on the Search module (if it's not already on). Once you have enabled that, you should also go to the Administer | Site building | Blocks page and put the Search block into one of the regions. Our script will turn that Search block's text field into an autocompletion field.

We are now finished with our preparations. We can move on to the code.

Note

It is wise to manually run Cron before continuing. The new content we just created will not be available to your search engine until the re-indexing operation has been run. Cron will run that operation.

The new module

We are going to implement our new package as a module. Creating JavaScript-centered modules was covered in the previous chapter. Nothing new will be introduced here.

To begin with, we will create our new module in the appropriate directory. Under the Drupal root, we create the directory sites/all/modules/search_autocomplete/. In that directory, we will create the .info, .module, and .js files.

Here are the contents of search_autocomplete.info:

; $Id$
name = Search Autocomplete
description = Provide autocompletion for search fields
dependencies[] = search
core = 6.x

We only need the bare minimum for our module. We added one new directive dependencies[], which indicates that this module depends upon another module. In this case, it is the search module that comes with Drupal.

Note

The square brackets ([]) at the end of the dependencies directive indicate that the directive can have multiple lines. If we need to declare another dependency, we should do so by adding another line like this: dependency[] = another_module.

Next, we need some boilerplate PHP code to add the appropriate JavaScript files and set a few variables that will be sent to the JavaScript. This code is similar to the modules we created in the previous chapter. Here is the search_autocomplete.module file in its entirety:

<?php
/**
* Provide autocomplete functionality to Drupal search.
* @file
*/
/**
* Implementation of hook_help().
*/
function search_autocomplete_help($path) {
if ($path == 'admin/help#search_autocomplete') {
return t('Provide autocompletion to Drupal search.');
}
}
/**
* Implementation of hook_init().
*/
function search_autocomplete_init() {
$path = drupal_get_path('module', 'search_autocomplete');
// The Taxonomy ID.
$tid = 2;
$autocomplete_uri = url('taxonomy/autocomplete/' . $tid);
$inline = 'SearchAutocomplete.url = "' .
$autocomplete_uri . '";';
drupal_add_js($inline, 'inline');
drupal_add_js('misc/autocomplete.js');
drupal_add_js($path . '/search_autocomplete.js');
}

The search_autocomplete_init() function is very important. It implements the Drupal hook_init() hook, which means this function will be run when Drupal initializes on each request.

For our module, the duty of this function is to send the correct JavaScript to the client.

The first line simply gets the URL path to the current module. $path can then be used to construct URLs.

Next, we do a little bit of hard coding. We set $tid (the Drupal shorthand term for Taxonomy ID) to 2. This is the TID of the taxonomy we just created.

Note

Finding the ID of a taxonomy

One easy way of finding an ID for your taxonomy is to go to Administer | Content management | Taxonomy and then click list terms for your taxonomy. Your browser will display a URL like this: http://example.com/admin/content/taxonomy/2. The number at the end (2) is your TID.

For the sake of simplicity, this has been hard coded. If we were to spend some more time coding with PHP, we might create an administrative interface for choosing the taxonomy we want to use. We could then augment the previous function , to retrieve the appropriate taxonomy ID from the database. But since our focus is on JavaScript, and not PHP, we will forgo the better solution in favor of the most expedient.

Our AJAX form, when we create it, will need to callback to the server. The server will need to provide the correct taxonomy terms to the client. How are we going to get that list?

Fortunately, Drupal again has a tool that we can use. There is a built-in taxonomy autocompletion script that comes standard with the taxonomy module. We can reuse that feature. All we need to do is pass the correct URL down to the client-side JavaScript, which we will do in three simple steps:

$autocomplete_uri = url('taxonomy/autocomplete/' . $tid);
$inline = 'SearchAutocomplete.url = "' .
$autocomplete_uri . '";';
drupal_add_js($inline, 'inline');

First, we use the url() function, a Drupal built-in, to create a URL pointing to the relative path taxonomy/autocomplete/2. (2 is the ID of our taxonomy and is stored in $tid.)

Next, we build that into a JavaScript fragment, which will look something like this:

SearchAutocomplete.url =
'http://example.com/taxonomy/autocomplete/2';

That data is stored in $inline.

On the third line, we send the previous code to the client in the form of an inline script. This is similar to the way we passed settings to the Better Editor in the previous chapter.

We have now finished the brunt of the configuration for our autocompletion script. The last thing to do is make sure that our two necessary JavaScript libraries are loaded by the client. That is done with the following two lines:

drupal_add_js('misc/autocomplete.js');
drupal_add_js($path . '/search_autocomplete.js');

The first script added is the Drupal autocomplete.js library, which is stored in the misc/ directory.

The second script added is our search_autocomplete.jsa file that we will look at next.

That is all that there is to our server-side component.

The search autocomplete JavaScript

The last thing we need to do is create a JavaScript tool that will turn our plain old search box into an autocomplete-capable search box.

We will put this code in search_autocomplete.js, the last JavaScript file that we added in our hook_init() implementation.

The main task of this code will be to modify the search block in order to turn it into an autocomplete function, and then let the Drupal autocomplete behavior do the requisite processing on that field.

Here is the code in its entirety:

// $Id$
/**
* Turn the search block into an autocomplete form.
* @file
*/
var SearchAutocomplete = SearchAutocomplete || {};
$(document).ready(function () {
$('input[name="search_block_form"]:not(.form-autocomplete)')
.each(function () {
var newId = $(this).attr('id') + '-autocomplete';
var newElement = $('<input type="hidden"/>')
.addClass('autocomplete')
.attr('id', newId).attr('disabled','disabled')
.attr('value', SearchAutocomplete.url);
$(this).after(newElement);
}).addClass('form-autocomplete');
Drupal.attachBehaviors();
});

The first thing done here is to create an empty SearchAutocomplete object. This will be our namespace. In our PHP code, we added an inline script that adds the SearchAutocomplete.url object, so we need to make sure that that namespace exists here in order for that script to successfully run.

Next, we handle the remainder of our code using the jQuery ready event.

But wait! Why aren't we doing this in a behavior? After all, this is the situation that behaviors are good for, right?

Here is the reason for our choice: We need to make sure the behaviors are run again after we finish processing. But if we put code to run Drupal.attachBehaviors() inside of a behavior, we then have to write some extra code to prevent infinite recursion. In our case, the simplest solution to this problem is to run our code in the ready event handler instead of trying to write a recursion detection device.

Let's take a look at the beginning of this function:

$('input[name="search_block_form"]:not(.form-autocomplete)')
.each(function () {
// More code here...
}).addClass('form-autocomplete');

This first jQuery object grabs elements that match the query input[name=" search_block_form"]:not(.form-autocomplete). This is a little longer than our usual query, but it is not difficult to understand.

The query is composed of three parts: an element query (input), an attribute query ([name="search_block_form"]), and a negation pseudo-class (:not(.form-autocomplete)). In short, the query looks for all<input/> elements with an attribute name that has the value search_block_form. But due to the negation pseudo-class, only elements that don't have the class form-autocomplete match.

Note

The search_block_form name is used by the search module when it creates a block containing a search text field. This part of the script could be extended to find other text fields and turn them into autocompletion fields. For example, to enable autocompletion in your theme's search box, you would use the name search_theme_form.

In short, it matches all search text boxes that don't already have an autocomplete feature. This query should match any Drupal search block, which typically looks something like this:

<input type="text" maxlength="128" name="search_block_form"
id="edit-search-block-form-1" size="15" value=""
title="Enter the terms you wish to search for."
class="form-text" />

As we can see from the previous code, it iterates through each of the matching elements. In a moment, we will look at what happens to each element as it is iterated over. Then, once that is done, the class form-autocomplete is added to each of the matching elements.

Adding this class serves two purposes. First, it makes sure that the same code cannot be run on it again (not a likely event, given that we run the code in the ready handler). Second, it identifies that element as an autocomplete form. The Drupal CSS then styles the element accordingly, adding the standard autocomplete throbber inside the text box:

Note the grey circle near the end of the search box. That is the location of the throbber icon. When the autocompletion AJAX script is running, the throbber icon will be displayed as a spinning circle.

Let's now turn to the anonymous function that is run inside the each() function:

$('input[name="search_block_form"]:not(.form-autocomplete)')
.each(function () {
var newId = $(this).attr('id') + '-autocomplete';
var newElement = $('<input type="hidden"/>')
.addClass('autocomplete')
.attr('id', newId).attr('disabled','disabled')
.attr('value', SearchAutocomplete.url);
$(this).after(newElement);

}).addClass('form-autocomplete');

In a nutshell, the highlighted code creates a new hidden element that contains instructions for the autocomplete handler. This hidden element is then added after the search box input element.

The anonymous function called by each does the following. First, it creates a new ID based on the id attribute of the current input element. This ID will be assigned to the new hidden element. The autocomplete.js library will use the ID to correlate the new hidden element with the field that it describes. So the naming convention used (original ID plus -autocomplete) is important.

Next, the script creates the new element:

var newElement = $('<input type="hidden"/>')
.addClass('autocomplete')
.attr('id', newId).attr('disabled','disabled')
.attr('value', SearchAutocomplete.url);

Once created, the new hidden element is assigned a new class (autocomplete), an ID, and a value.

The class is used by Drupal's autocomplete behaviors to identify autocompletion fields. The value attribute is expected to contain a URL that will be used for AJAX operations.

In the last line of our anonymous function, the new element is added after the search box input element:

$(this).after(newElement);

Once the main jQuery is done, only one thing remains in the function:

$(document).ready(function () {
$('input[name="search_block_form"]:not(.form-autocomplete)')
.each(function () {
var newId = $(this).attr('id') + '-autocomplete';
var newElement = $('<input type="hidden"/>')
.addClass('autocomplete')
.attr('id', newId).attr('disabled','disabled')
.attr('value', SearchAutocomplete.url);
$(this).after(newElement);
}).addClass('form-autocomplete');
Drupal.attachBehaviors();
});

Lastly, Drupal.attachBehaviors() is run. This will load and run all of the behaviors.

This line raises an interesting point. We know that all behaviors are run during the jQuery ready event. This function is also run during the jQuery ready event. But it is run after Drupal's behaviors and modifies the DOM. We need to give the behaviors another chance to process the new material we just added. So we have to run Drupal.attachBehaviors() again to be sure that the autocomplete behavior correctly attaches.

This shouldn't cause any problems. After all, behaviors are intended to be run multiple times.

That's all there is to our autocomplete module. To enable this module, simply go to the administration menu Administer | Site building | Modules, and enable Search Autocomplete.

Once the module is enabled, search dialogs should respond automatically. Here's a screenshot showing the autocompletion in action:

Typing in the first three letters, we get a list of four matching words from our tag list.

Now we are done with our first project. In the next one, we will use the jQuery UI plug-in to add richer user interface elements to our site.

Project: jQuery UI

Although we are going to write a small amount of code in this project, we are going to get some big results with it. We will focus on integrating the jQuery UI with Drupal. We already have a sufficient background to make this task a breeze.

What is jQuery UI?

The jQuery library has received considerable attention in this book, and deservedly so. After all, not only does jQuery come packaged with Drupal, but much of Drupal's own JavaScript code uses jQuery.

The small jQuery library doesn't provide widgets or other components popular in JavaScript libraries. Instead, it is focuses on providing a rich toolset for working with the DOM, CSS, events, and AJAX.

Interested in calendar widgets, or tabs, or sliders, or drag-and-drop support? You won't find any of those in jQuery. But you will find them in an official jQuery add-on package called the jQuery UI. The jQuery UI library provides a huge repository of tools for building rich user interfaces. It builds on the solid foundation of jQuery, but it adds much more.

Note

The official jQuery UI web site is co-located with the jQuery home page. Visit http://ui.jquery.com to learn more about the project and to view some of the demos.

Here's a partial list of the things you will find in jQuery UI:

  • Drag-and-drop support: Declare elements as draggable or configure them as dropable containers.

  • Over a dozen additional effects (in addition to slide and fade): Some, like explode, are tantalizingly elaborate.

  • Make elements or containers resizable: you can even add drag bars to images, allowing your users to click and drag on an image border to make the image larger.

  • Sortable lists: Turn a list or group of items into a sortable list with drag-and-drop support.

There are even more features, but these should whet your appetite In this project, we will use the accordion tool to turn our menus into an elaborate expanding and collapsing accordion widget.

But the best part about this library is that, like jQuery, the tools are compact. Much can be done with only a few lines of code.

Getting jQuery UI

The jQuery UI library does not come standard with Drupal nor is it to be included in the near future. So to use it with Drupal, you will need to download the library from http://ui.jquery.com.

Note

There is a jQuery UI Drupal module which adds some convenience functions for PHP developers working with jQuery UI. While we won't use it here (our PHP code isn't complex enough), it is a good option for PHP module developers looking to integrate with jQuery UI. For more information, see http://drupal.org/project/jquery_ui.

There are several ways of getting jQuery UI, including building a custom package. (See http://ui.jquery.com/download for all of the options.)When learning jQuery UI, the best bet is the Development bundle distribution. This includes the entire library along with examples and unit tests.

The library is constructed in a modular fashion. Individual features are stored in separate files. For the most part, it is easy to pick and choose which elements you want to install on your server.

When you are ready, download a version of the jQuery UI. We will be using the ui.core.js and ui.accordion.js files, so make sure you at least have these two. If you get the Development bundle, you will have everything. Later on, we will copy the necessary files into our module.

The next step in our project will be to create a new module.

The accordion module

We are going to write a small module, using the jQuery accordion library, to turn our left-side navigation blocks into an accordion widget. This will keep our site navigation compact and easy to use.

Note

Earlier in the book, we added a Drupal behavior to our blocks to make them collapsible. Before proceeding, that behavior should be turned off because it will conflict with our accordion widget. To do this, comment out scripts[] = behaviors.js in the frobnitz.info file, or simply switch to another theme.

As usual, the first thing we need to do is create a new module directory in the appropriate location under the Drupal root directory. Our module will be named accordion, so we will be creating sites/all/modules/accordion.

Inside of that folder, we will be creating four files:

  1. accordion.info.

  2. accordion.module.

  3. accordion.css.

  4. accordion.js.

These four files should all be familiar by now. The .info file will describe the module. The .module file will hold some spartan PHP code. The .js file will hold our JavaScript, and the .css file will hold our CSS.

Along with these four files, we will create a directory named ui/. This is where our jQuery UI files will go. How you downloaded jQuery UI will determine how you need to copy the appropriate files:

  • If you downloaded the Development bundle, you can simply copy the bundle's ui/ folder into sites/all/modules/accordion/

  • If you built a custom package, copy ui.core.js and ui.accordion.js into sites/all/modules/accordion/ui/

Next, we will create the accordion.info and accordion.module files.

The .info and .module files

The accordion.info file will follow the same structure as our previous .info files:

; $Id$
name = Accordion
description = Display the left-hand blocks as an accordion.
core = 6.x
php = 5.2

There should be nothing surprising here.

Next, we will create a very simple module file. The PHP code for our module will simply add the requisite JavaScript libraries. Like our previous module, it will implement only the hook_help() and hook_init() hooks:

<?php
// $Id$
/**
* Attach an accordion effect to menus.
* @file
*/
/**
* Implementation of hook_help().
*/
function accordion_help($path, $args) {
if ($path == 'admin/help#accordion') {
return t('This module adds accordion effects to menus.');
}
}
/**
* Implementation of hook_init().
*/
function accordion_init() {
$path = drupal_get_path('module', 'accordion');
drupal_add_css($path . '/accordion.css');
drupal_add_js($path . '/accordion.js');
drupal_add_js($path . '/ui/ui.core.js');
drupal_add_js($path . '/ui/ui.accordion.js');
}

The accordion_init() function adds four files.

First, the accordion.css file (which we will create in a moment) contains styling information. It is a CSS file, not a JavaScript file. Therefore, we add it with the drupal_add_css() function.

Next, we have three JavaScript files that need to be added:

  1. accordion.js: This holds our custom JavaScript code. We will take a close look at the contents of this file shortly.

  2. ui/ui.core.js: This is the base library for jQuery UI. It contains functions used by the rest of the jQuery UI components. Any time you use jQuery UI you will need to include this library.

  3. ui/ui.accordion.js: This contains the jQuery UI accordion widget code.

That is all there is to our module. Next, we will look at the JavaScript.

The accordion JavaScript

Our module code was short, but our JavaScript code is going to be even shorter. In accordion.js, we need to write the necessary glue code to find the right part of our document and turn it into an accordion. With jQuery at our disposal, and jQuery UI tightly integrated, this process is as easy as writing a simple jQuery chain.

To make things even simpler, we will wrap this in a Drupal behavior and allow Drupal to control the initialization of our widget:

// $Id$
/**
* JavaScript for initializing and adding accordion effect.
* @file
*/
Drupal.behaviors.accordion = function () {
$('#sidebar-left:not(.ui-accordion)').accordion({
header: 'h2'
});
};

The body of our newly defined behavior has a single jQuery chain that consists of two parts. First, the jQuery call executes this query:

#sidebar-left:not(.ui-accordion)

This will look for an element with the ID sidebar-left that does not have the class ui-accordion.

The sidebar-left ID is a standard ID for Drupal. It identifies the lefthand region where blocks are typically located. Here's what my sidebar-left column looks like before running the previous code:

This is the area that the ID identifies. But there is the additional :not(.ui-accordion). The ui-accordion class is added by an accordion widget. As with other behaviors, we add this extra check to ensure if the behavior is run multiple times, we won't try to repeatedly turn the left column into an accordion widget.

Let's now take a look at the second part of the query:

$('#sidebar-left:not(.ui-accordion)').accordion({
header: 'h2'
});

The jQuery UI functions are added onto the main jQuery object (we will see how to do this in our final project). So adding the accordion is as simple as calling the accordion() method.

That method takes an object literal containing settings. There are over half a dozen possible settings for the accordion, all documented at http://docs.jquery.com/UI/Accordion/accordion#options, but we will only use one.

We need to tell the accordion effect what element to use as header information. To explain this, let's take a quick look at the structure of the HTML in the left sidebar:

<td id="sidebar-left">
<div class="block block-user" id="block-user-1">
<h2 class="title">
mbutcher
</h2>
<div class="content">
<!-- Menu content -->
</div>
</div>
<div class="block block-menu" id="block-menu-menu-custom-content-management">
<h2 class="title">
Content Management
</h2>
<div class="content">
<!-- Menu content -->
</div>
</div>
<div class="block block-menu" id="block-menu-devel">
<h2 class="title">
Development
</h2>
<div class="content">
<!-- Menu content -->
</div>
</div>
<div class="block block-search" id="block-search-0">
<h2 class="title">
Search
</h2>
<div class="content">
<!-- Search form -->
</div>
</div>
</td>

In the previous code, I have removed the content of every block to simplify the HTML. It's the general structure that we are interested in.

First, the<td></td> element has the ID sidebar-left. That is going to be the main container for our new accordion widget.

The blocks are inside of the<td></td> element. The blocks have the following structure:

<div class="block other-class" id="block-ID">
<h2 class="title">
<!-- TITLE -->
</h2>
<div class="content">
<!-- CONTENT -->
</div>
</div>

Each of these blocks will be a collapsible region in our accordion. A collapsible region looks like this:

Looks familiar? This is a block. The title of the block becomes the header of the collapsible region, and the block's content becomes the body of the collapsible region.

The jQuery UI accordion widget assumes that all direct children of the container element (the<td></td>, in our case) are collapsible regions. However, it needs information about what element holds the title of the collapsible region. So when we call $().accordion(), we pass it the needed information:

$('#sidebar-left:not(.ui-accordion)').accordion({
header: 'h2'
});

We tell it that the header of the region should be composed from the<h2></h2> elements inside of each block.

Note

This will not work well for blocks that do not have headers. Regions in an accordion are expanded by clicking on the header. If no header exists, there is no way to expand the region. On possible remedy would be to use jQuery to dynamically add titles to all blocks before adding an accordion. Of course, the more pragmatic solution is to avoid using blocks with no titles in an accordion.

That is all there is to our code. The accordion widget is added to the<td></td> element with the ID sidebar-left, and we get something that looks like this (or will look like this once we add a little CSS):

The previous screenshots capture the accordion in three different states.

The leftmost screenshot shows the accordion as it looks when it initially loads. The main Drupal menu block is expanded and the remaining blocks are collapsed.

If we click on the title of the second block, Content Management, the second block slides upward and hides the first. The middle screenshot shows the block in this state.

Now if we click on the last item in the accordionSearchit too slides up, pushing the Development section upwards. The rightmost screenshot shows the end result. The Search block is displayed in its entirety, and the other blocks are collapsed above it.

If we click on Content Management again, it would expand downward until its contents are displayed. At that point, it would again look like the screenshot in the middle.

By using the jQuery UI library, we added this sophisticated component in just five lines of JavaScript. However, to get it to look as it does in these screenshots, a little CSS is needed. We won't go through the styles in detail, but in the interest of completeness, here is the accordion.css:

/*
* Accordion module CSS.
*/
.ui-accordion div.block {
background-color: #efefef;
margin-bottom: 0px;
padding-bottom: 0px;
}
h2.ui-accordion-header {
border: 1px solid black;
background-color: #6699CC;
color: white;
margin-bottom: 0px !IMPORTANT;
}

There are two selectors in this code. The first selects all of the blocks in the UI accordion and sets the background color to light-grey. It also fixes the bottom padding and margin to prevent gaps between titles in our accordion.

The second item styles the accordion header. It makes it look more like a clickable area by surrounding it with a black border, setting the background color to blue, and setting the text color to white. Here too, we need to adjust the bottom margin. This adjustment is marked as !IMPORTANT to prevent the default style sheet from overriding it.

That wraps up our accordion widget project. Even though this has been an easygoing project, I hope it has done three things. First, I hope it has illustrated the ease with which jQuery UI effects and widgets can be integrated into Drupal. Second, I hope it inspires further experimentation with the library. Other widgets, such as tabs, spinners, dialogs, grids, and so on, are just as easy to work with. These widgets can really make a site stand out.

My third objective was a little more subtle. By looking at $().accordion(), we have seen how a jQuery extension can integrate cleanly with jQuery. For our final project, we will go back to the main jQuery library and write a simple plug-in.

Project: writing a jQuery plug-in

Earlier in the book, we talked about jQuery plug-ins. Plug-ins are to jQuery what modules are to Drupala tool for extending functionality. In this project, we will write a small jQuery plug-in.

Throughout the book, we have looked at numerous ways to work with JavaScript in a Drupal site. We are going to look at one final way. A jQuery plug-in extends the capabilities of jQuery. This is not a Drupal-specific feature, but a feature of the jQuery library.

Why would we learn this technique if we can already write JavaScript in a Drupal site?

There are a few reasons for this:

  • With close integration into jQuery, we can build compact code using jQuery's fluent interface pattern.

  • When it comes to manipulating DOM and CSS, managing events, or working with effects, you can often make the coding task easier by writing it as a jQuery plug-in.

  • A plug-in is more portable than a Drupal module and you can use it in non-Drupal (or even non-PHP) web applications. Thus, reusability is good.

  • The jQuery architecture makes plug-in writing easier, simpler in some respects than adding a Drupal behavior.

Our example here will be very basic. Robust and complex jQuery plug-ins can certainly be written (many such plug-ins are available for download at http://jquery.com). Our project should give you the tools needed to write more complex plug-ins.

The plug-in code

The basic principle of writing a jQuery plug-in is very simple: You write a function and attach that function to the jQuery object. We are going to add a new method to the jQuery object.

The most basic pattern for writing such a plug-in is similar to this:

jQuery.fn.myNewPlugin = function () {
// Do something.
}

The new plug-in could then be called like this:

$('p').myNewPlugin();

What are we doing here? The jQuery.fn object is where jQuery functions are attached to the jQuery prototype object. In other words, all of the functions that are attached to jQuery.fn will be available to jQuery objects, and can be called (as in the previous example) by $().someFunction().

Note

As a de facto rule, a plug-in should use only one namespace inside of the jQuery.fn namespace. When adding multiple functions to jQuery, these functions should be grouped into another namespace. Instead of jQuery.fn.myPluginFirst() and jQuery.fn. myPluginSecond(), it should be jQuery.fn.myPlugin.first() and jQuery.fn.myPlugin.second().

We now have a basic idea about how to write a plug-in. Following this prescribed pattern for plug-in development, we are going to create our own simple plug-in.

Plug-ins for jQuery should be stored in files named jquery.<plug-in>.js, where<plug-in> is replaced with the name of the plug-in (all in lowercase). Our plug-in is going to wrap matched elements in a<div></div> tag. For example, we might create a jQuery object that finds all anchors:

$('a');

Our little tool could then be applied in order to enclose each of those anchor elements inside of a div:

$('a').divWrap();

This would transform something like<a href="http://example.com"> into<div><a href="http://example.com"></div>. Though our plug-in may never win any awards for innovativeness, it is a good starting point for investigating jQuery plug-ins.

We are going to call this plug-in divWrap, so it should be stored in a file called jquery.divwrap.js.

Note

This library will work just like any JavaScript library in Drupal. You can add it to a theme using the theme's .info file, or you can include it in a module with the drupal_add_js() function. While developing, you might find it handy to simply start with a static HTML file and include only jQuery and your plug-in.

Here's the code that we will put in jquery.divwrap.js:

(function ($) {
/**
* Wrap the selected element or elements in <div></div>
* tags.
*
* @param attributes
* An object containing name/value pairs for attributes.
*/
jQuery.fn.divWrap = function () {
var attrs = (arguments.length > 0) ? arguments[0] : {};
this.each(function (index, item) {
var div = $(item).wrap('<div></div>').parent();
if (attrs) {
div.attr(attrs);
}
});
return this;
};
})(jQuery);

The first thing to note about this plug-in is the outermost wrapper, which looks like this:

(function ($) {
// Code here
})(jQuery);

What is this? In JavaScript, you can both define and call a function in one step, and this is how it is done. The previous code performs a task similar to what this code does:

myFunc = function ($) {
// Code here
}
myFunc(jQuery);

There are two important reasons why we begin a jQuery plug-in with code such as this:

  1. We can conveniently work with jQuery using the $() function alias.

  2. We also create a closure that protects our plug-in's context.

Neither of these may be obvious, so let me explain.

Although we have used the $() function many times in this book. However, the name $ is sometimes overridden, or turned off, by other JavaScript libraries. In short, there is no guarantee that $ refers to jQuery. The convention that we saw overcomes this uncertainty by wrapping the entire plug-in in a function that takes an argument named $. The function is then called with the globally scoped jQuery object. In effect, it re-maps $ to jQuery so that we can confidently use the $() function in our code.

Second, this convention creates a closure around our plug-in. Closures are often treated as an advanced aspect of JavaScript development. In fact, they are used frequently in a variety of contexts. (We have used them a few times already, but without the fancy name and in a subtler fashion.) We will take a brief look at this topic, and you will find that they are not as mysterious as they may seem at first.

A brief introduction to closures

A closure provides a convenient way of providing a context in which we can store local variables and functions that we don't want other outside scripts to have access to. A closure basically seals-off a context for us.

Inside of a closure, we can create variables and functions that would not be accessible to the outside world. However, code inside the closure can access these functions and variables. With a closure, we can hide data from outsiders, while making it available to the code inside.

Note

If you have done object-oriented programming in Java, PHP, or other languages, you can compare the closure method of protecting access to the private keyword used in class variable declarations. While they are technically quite different, they are functionally similar.

All of this may sound abstract, and perhaps even a little lofty and impractical. However, a quick look at some simple code should make things clear.

Let's start with a simple test:

var text = 'This is a test';
console.log(text);

If we were to run this in Firefox with Firebug turned on, we would see the contents of the text variable printed to the Firebug console.

But what if we wrapped the text variable inside of a closure as shown:

(function () {
var text = 'This is a test';
})();
console.log(text);

In this case, Firebug would show an error saying something like ReferenceError: text is not defined.

This happens because text in the previous code is scoped only to the anonymous function. In other words, the text variable is not available outside that anonymous function.

This comes in handy when we want to have private variables or functions (variables or functions that are available only inside of our plug-in and not to code outside). To see how this works, let's make a few additions to the previous code:

var MyObject = {};
(function () {
var text = 'This is a test';
MyObject.getText = function () {
return text;
};
})();
console.log(MyObject.getText());
console.log(text);

The highlighted lines were added.

The first console.log() call will print This is a test to the console. However, the second one will give the same ReferenceError that we saw earlier. Let's see why.

In this new addition, we have done the following:

  • At the top, we create an object, MyObject, which is globally scoped (since it is outside of the function).

  • Inside of the closure we add a new function, MyObject.getText(), that returned the value of the private text variable. Note that this new function is attached to the globally scoped MyObject object.

  • Outside of the closure, we run MyObject.getText() and print the returned value to the console.

In our simpler example, we saw that console.log(text) fails. What will happen when console.log(MyObject.getText()) is run? It will print This is a test to the console.

The reason why this happens is simple: When we create MyObject.getText(), we create it in a context that has access to the text variable. We might say that MyObject.getText() can see text. Also, it can see text because text is in its context. Both are inside of the closure.

Even when we call the MyObject.getText() method outside of the closure, as we do in the console.log() call, that function can still see the text variable, and so it can return it. Since text is available only to code inside of the closure, we can say that text is a private variable.

The getText() function is attached to MyObject, which makes it available in any context where MyObject is available. Since MyObject is a globally scoped object, MyObject.getText() is available just about anywhere.

Note

Should we need, we could also create functions inside of the closure. By not attaching them to an object outside of the closure, we can create private functions.

Returning to our closure, the (function ($) {})(jQuery) construct is not fundamentally different from the code we just wrote. The jQuery object is globally scoped, and anything we explicitly add to jQuery.fn will be available outside of our closure. However, anything else we define in our closure will be accessible only to other code in the same scope.

That's the general strategy we are using with our plug-in. We define the entire plug-in inside of a closure so that we can carefully control access to the code we write.

Glancing back at the code, you may notice that we don't really make use of this feature. There are no private variables or functions. But writing it this way will make it easier to extend later, if we so choose, and conforms to suggested practices for jQuery plug-in development. And we are still getting the benefit of the aliasing of $ to jQuery.

Now that we understand the basics of closures, let's get back to our plug-in.

The divWrap() function

Inside of our closure we define one new function:

jQuery.fn.divWrap = function () {
var attrs = (arguments.length > 0) ? arguments[0] : {};
this.each(function (index, item) {
var div = $(item).wrap('<div></div>').parent();
if (attrs) {
div.attr(attrs);
}
});
return this;
};

This function takes all of the items wrapped in the current jQuery object and wraps each in <div></div> tags. It takes an optional parameteran object. The attributes of that object will be used as attributes for the new div element.

The first thing our divWrap function does is check to see if any arguments were passed in. Recall that JavaScript's built-in arguments variable is an array-like object that lists all of the parameters passed into the function.

The first line checks to see if arguments.length is greater than 0. If it is, then the attrs variable will be assigned the first argument. Otherwise, attrs will be assigned an empty object ({}).

Next, we use this.each() to loop through all of the objects currently wrapped by jQuery. This works because a plug-in added to jQuery.fn is part of jQuery. It's this variablea variable automatically created by JavaScriptthat points to the object of which this function is a part. So in this case, it points to a jQuery object.

To understand this, let's look at how we would call our new plug-in:

$('a').divWrap();

In this line of code, we use $() to search for all<a></a> elements. So when divWrap() is called, the jQuery object should contain a list of all<a></a> elements. When divWrap() is executed, this will point to the current jQuery objectthe very object that contains the list of<a></a> elements.

Now returning to our code, we want to loop through each element in the current jQuery object and wrap it in a div tag. We do that with the anonymous function inside of this.each():

function (index, item) {
var div = $(item).wrap('<div></div>').parent();
if (attrs) {
div.attr(attrs);
}
}

Recall that the $().each() function receives two arguments: index, which is the numeric index of the current object within jQuery's list of objects, and item, which is the current object.

Note

This anonymous function acts as a closure. Inside of the context of the $().each() method, this anonymous function has access to the attrs variable. But for the rest of jQuery that variable is out of scope. We've been creating closures all along!

The first thing we do in this function is run a jQuery chain to wrap the current item inside of a<div></div> tags. At the end of this chain, we call parent(), which will select the div element we just wrapped with. This is done for the sake of the second step.

In the divWrap() function, we assigned attrs the value of arguments[0] or {}. Here, we add the contents of attrs as attributes to the div. A quick look at an example will clarify this.

We could call our plug-in like this:

attrs = { style: 'background-color: #F0F' };
$('a').divWrap(attrs);

In this case, the attrs object would be used to add attributes to the div that wraps any found<a></a> elements. The result of this would look something like this:

<div style='background-color: #F0F'>
<a href='http://example.com'>Some link</a>
</div>

Notice how attrs was turned into attributes for the<div></div> tag.

That's all there is to this anonymous function executed by the $().each() function. There's only one more thing our plug-in function does:

(function ($) {
jQuery.fn.divWrap = function () {
var attrs = (arguments.length > 0) ? arguments[0] : {};
this.each(function (index, item) {
var div = $(item).wrap('<div></div>').parent();
if (attrs) {
div.attr(attrs);
}
});
return this;
}
})(jQuery);

Notice the highlighted line in the code? Why do we return this? We are returning the jQuery object so that other functions can be chained off to this one. We could, for example, do something like this:

$('a').divWrap().text();

This would return the text of each<a></a> element after wrapping all of the<a></a> elements inside of the div tags.

So that's the basic method for creating a jQuery plug-in. How do you make use of this tool inside of Drupal? This is done the same way you would use any JavaScript library. In a theme, you might add it to the .info file using a scripts[] directive. Or in PHP (such as a module), you can add it with the drupal_add_js() function.

Note

Other references for writing jQuery plug-ins

There are two very helpful jQuery basic plug-in writing tutorials that may be helpful. The official plug-in writing guide is at http://docs.jquery.com/Plug-ins/Authoring. The standard plug-in writing pattern is explained here: http://www.learningjquery.com/2007/10/a-plug-in-development-pattern.

When should you write a jQuery plug-in instead of JavaScript in Drupal? To a large extent, the answer will depend on your own needs. However, here are some guidelines:

  • If you might need to use the code outside of Drupal, a plug-in is easier to port. In fact, you may want to release a generic plug-in to the jQuery community at http://jquery.com.

  • If your code would work well as part of a jQuery chain, you might consider adding it as a jQuery plug-in, even if it does depend on Drupal.

  • If you find a jQuery plug-in that already does much of what you want, you might consider writing another plug-in that extends the base jQuery plug-in. Even in this case, it is easier to work from within jQuery than from Drupal's JavaScript library.

As we have seen throughout this book, the JavaScript integration in Drupal is very robust. Whether it's a jQuery plug-in or a Drupal-centered library, or even an unrelated JavaScript library, Drupal makes it easy to integrate.

Summary

In this chapter we covered three projects. In the first project, we saw how we could integrate another existing Drupal JavaScript libraryautocomplete.jsinto our site. We used a taxonomy to add suggestions to our search box.

After that, we integrated one of the jQuery UI toolsthe accordion widgetinto our site. Using this tool, we turned our lefthand navigation into a compact, but elegant accordion menu.

Finally, we learned how to extend jQuery by writing a jQuery plug-in. In a couple dozen lines of code, we wrote a complete plug-in that takes advantage of jQuery's DOM tools and looping structures. During this section, we talked about closures and discovered that we'd been writing them all along.

This is the final chapter of the book. In our earlier chapters, we started out with some fairly clumsy JavaScript, just barely integrated into a Drupal theme. Now, seven chapters later, we know how to write Drupal-centred JavaScript with the use of jQuery, Drupal's own JavaScript libraries, and even external libraries such as jQuery UI. We've worked with both themes and modules. We've even written a little bit of PHP code.

The purpose of this book has been to give you access to the JavaScript tools which are often used for Drupal development, and to show you how to use them in this context. We have only scratched the surface of what can be done with these tools. This book, if I have been successful, gives you a foundation. From here, you can begin building the next generation of JavaScript-enabled Drupal web applications.