4. Drupal Behaviors – Drupal 6 JavaScript and jQuery

Chapter 4. Drupal Behaviors

In the previous chapter, we spent some time getting to know jQuery. We will now look at another library that comes bundled with Drupal. In fact, the library we will see here is Drupal-specific. The drupal.js library is composed of tools commonly used in Drupal-centred JavaScript.

There are four major sets of tools in drupal.js: translation functions, theming system, some utility functions, and the core code for Drupal Behaviors. In this chapter, we are going to focus on Drupal Behaviors and then examine some of the utility functions. Translation and theming will each get their own chapter.

In this chapter we will cover the following:

  • The basics of the drupal.js library

  • Understanding and using Drupal Behaviors

  • Avoiding pitfalls in Drupal Behaviors

  • Using utility functions

We will also do two projects in this chapter. The first project, a short one, will focus on behaviors. The second project will be grander in scope. We will combine our jQuery tools with the features we discover in drupal.js in order to create a simple text editor.

The drupal.js library

In the Using jQuery in Drupal section of the previous chapter, we saw how Drupal will automatically include the jQuery library when it detects that the page request uses JavaScript. When a theme or module, for example, includes a JavaScript file with a scripts[] entry in the .info file, Drupal will automatically include the standard JavaScript libraries.

Let's take another look at the scripts that get included:

<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>

Drupal generated this code from our printer tool script developed in Chapter 2. There are three JavaScript files included here. The first one is jquery.js, which was the focus of the previous chapter. The last one is printer_tool.js, the script we created in Chapter 2.

Sandwiched between them is drupal.js. Like jquery.js, drupal.js plays a prominent role in Drupal's client-side scripting efforts, which is included by default on any page that includes JavaScript.

But what exactly does it do?

Essentially, it is composed of four classes of tools:

  1. Theming functions

  2. Translation functions

  3. Utility functions

  4. Support for Drupal Behaviors

Theming functions provide a partial JavaScript implementation of the Drupal theming system. Using the theming system, we can theme JavaScript objects just like the PHP developers do on the server side.

Translation functions, which we will explore in the next chapter, provide language translation facilities to JavaScript. Just as is the case with the theming system, the translation system is designed to provide an API similar to the server-side PHP translation system.

Some of the tools provided by drupal.js cater to commonly needed services. I am referring to these as Utility functions.

Finally, Drupal offers a framework for adding scripted behaviors to a page. When the page is loaded, these behaviors are automatically "attached" to the page, and immediately take effect. This framework is called Drupal Behaviors (often shortened to just behaviors). If this all sounds unclear, don't worry. We will look at this topic in more detail later in this chapter.

Now we are ready to move on to a discussion of behaviors.

Drupal JavaScript behaviors

As I mentioned at the beginning of the chapter, the drupal.js library is Drupal-specific. The main advantage of using a tightly-coupled library is that the tools provided are aware of the Drupal structure and do things the Drupal way.

Behaviors are a good example.

The Drupal Behaviors feature provides a standard method for attaching some particular information (called a behavior) to zero or more elements on a page. To understand this admittedly vague description, let's start with an example and build a better explanation.

In the previous chapter we looked at registering a handler for the jQuery ready eventjquery('document').ready(). We can use that function to run some JavaScript as soon as the DOM is ready for manipulation.

For example, we might write a JavaScript snippet that dynamically adds a new class attribute to all paragraph tags:

$(document).ready(function () {
$('p').addClass('fancy');
});

This script finds all paragraph elements and adds the fancy class. Since it is executed during the ready event, it will happen as soon as the DOM is ready for manipulation.

But what if we have a script that executes a little later and adds a new paragraph?

$('body').append('<p>Another paragraph</p>');

Now we have a paragraph that doesn't have the fancy class because it was added to the DOM after the ready event was handled.

Defining a behavior to handle repeatable events

The ready event that jQuery defines (and that we saw at the end of the previous chapter) is fired as soon as the DOM is ready for manipulation. But that's the only time it fires.

The DOM can undergo many changes between the time the page loads and when the user leaves the page. Some of those DOM changes may have an impact on how the other JavaScript works. In cases like this, we want to be able to re-run our query to add the fancy class whenever a new paragraph is inserted into the DOM.

This is the sort of case that Drupal Behaviors were designed to address. Instead of having our paragraph query run (once) when the ready event fires, we can define this as a behavior. In essence, we would be creating a behavior that says, "<p></p> elements should behave this way."

When we define a behavior, we describe what elements should be modified and how they should be modified. We then let Drupal take care of the attaching.

Note

I have heard some Drupal JavaScript developers state that behaviors are a replacement for jQuery.ready(). But this is not an accurate description. Behaviors should be used when you are attaching some behavior to elements in the DOMand only when you want those behaviors to be attached to new (matching) content when it is inserted into the DOM. Treating behaviors as a better ready event can cause unexpected errors and less efficient code.

When you define a new behavior, Drupal takes over the management of that behavior. When a page is loaded, the behavior is executed. When other important DOM-altering events occur, Drupal re-runs the behaviors.

However, when writing JavaScript in Drupal, there are a couple of things you need to do to keep behaviorsboth yours and others'working correctly:

  • When writing behaviors, to make sure you don't accidentally attach the same behavior to the same thing multiple times. We'll see how to do this later.

  • When writing code that modifies the DOM, remember to notify the behaviors system that behaviors may need reattaching. This is done using the Drupal.attachBehaviors() function, and we will see examples of this later.

Let's take a look at an example that re-implements our early codethis time as a behavior:

Drupal.behaviors.addFancy = function (context) {
$('p:not(.fancy)', context).addClass('fancy');
};

This three line behavior essentially does the same thing as our previous jQuery snippet, with only minor exceptions. Let's look at the differences.

First of all, a behavior is a function that is attached to the Drupal.behaviors object. In our case, the function will be named Drupal.behaviors.addFancy().

When Drupal executes a behavior, it passes one parameter, context.

The context object contains information about the part of the document that is being evaluated. This should always be a location within the DOM. When behaviors are first attached (in a call to $(document).ready()), the document object is passed in as the context. In other cases, only a smaller subset of DOM may be passed in.

Generally, it is advised that a behavior restricts its changes to the context (though that is not always a good idea, and we will discuss the point later in the chapter). So we will add the fancy class only to elements in our given context:

$('p:not(.fancy)', context).addClass('fancy');

Notice that the second argument to $() is the context object. That optional argument provides the object that jQuery should query.

There's another item of note here: The query has been modified. Originally, we began with $('p'). Now we have $('p:not(.fancy)', context). What is the :not(.fancy) part for? The :not() is a pseudo class in CSS. It tells jQuery to not match anything inside of that query. So<p></p> will match and therefore be included in the returned results, but<p class='fancy'></p> will not.

Note

The :not() functions with an implicit AND. Any matching query must match the initial query and not the query within :not().

So the jQuery line, if translated into plain English, would read "Find all paragraphs that don't have the class fancy and give them that class."

Technically in this particular situation, it is not necessary to add the :not(.fancy) part. There's no way to add two of the same classes to an element, and jQuery will gracefully handle the situation. But this illustrates a point: in many cases, you want to ensure that your behavior does not do the same thing twice.

When we work on our project later in the chapter, we will see another way of using CSS classes to make sure that a behavior isn't attached twice when it should only be attached once.

Telling Drupal to attach behaviors

After modifying a portion of the document, it might be desirable to notify Drupal so that behaviors can be re-attached. This is done with yet another function from drupal.js: Drupal.attachBehaviors().

Note

When should attachBehaviors() be called?

There are some DOM modifications which probably don't warrant calling Drupal.attachBehaviors(). For example, just changing the text of a node (without altering any elements) can usually be done safely. Also, there are other elements, such as<br/>, that rarely have behaviors attached. In these cases, you should not run Drupal.attachBehaviors(). On the converse, any time you load HTML from an AJAX/AHAH call you should run Drupal.attachBehaviors().

Let's look at an example of Drupal.attachBehaviors(). At the beginning of the part entitled Drupal JavaScript behaviors, we inserted a paragraph using jQuery. We did this with a quick jQuery one-liner:

$('body').append('<p>Another paragraph</p>');

We will now build on that example:

var context = $('<p>Another paragraph</p>')
.appendTo('body').get(0);
Drupal.attachBehaviors(context);

Whoa! This new snippet of code looks a lot different. But surprisingly, it functions similarly. Let's take a closer look at the jQuery call:

$('<p>Another paragraph</p>').appendTo('body').get(0);

Just as with our initial example, this jQuery chain adds a new paragraph to the end of the body element. However, it does so in reverse order. Instead of finding the body and adding an element, this code creates the new element and then appends it to the body.

Along with the other things the $() function can deal with, it can also recognize HTML embedded in strings. It converts this information to a DOM fragment. By default, this value is held in its own DOM. It is not immediately put into the document that is being displayed.

So in this example, the $() call returns a jQuery, wrapping a paragraph element that is currently not a part of the main document. The next thing to do is to append this element to the document object at the end of the body. That is done with the appendTo() method, which takes a jQuery query string (a CSS selector).

The call to appendTo() returns a jQuery object that is still wrapping the<p></p> element. But now, that element is part of the main document. The appendTo() call took care of that.

Note

append() and appendTo(): don't overlook the differences!

An easily overlooked difference between append() and appendTo() is in the return value; both return jQuery objects. But where append() returns a jQuery object wrapping the thing(s) that have been appended to, the appendTo() call returns the elements that were appended.

The final function in that chain is get(). This too is a jQuery function. It extracts the object(s) wrapped by jQuery and return(s) those objects. It takes an optional index value. Since jQuery stores its elements in an indexed list, we can get the wrapped objects by position. In our case, we want the first paragraph (there should be only one), so we use get(0).

The return value of get(0) is stored in the context variable. So context is now pointing to the<p></p> element that we just created.

The last line in our script looks like this:

Drupal.attachBehaviors(context);

This simply tells Drupal to try to attach behaviors.

If we had called Drupal.attachBehaviors() with no arguments, then a default context would have been built for us. That context would have been the document object. Since we know that our script only inserted this one paragraph tag, we can reasonably restrict the context to just new<p></p> elements.

In general, it's better to set the context appropriately. The more specific the context, the faster the code runs. So in our case, we have created a very limited context.

But beware! This might not always be the best choice, for the impact of inserting one elementeven at the end of a documentmight extend well beyond the intended target.

Context and behaviors: bug potential

In some cases, working with the context and behaviors can get tricky, leading to bugs that may be difficult to debug.

For example, let's look at another behavior that we assume would work with the one we just created:

Drupal.behaviors.countParagraphs = function (context) {
if ($('#lots', context).size() > 0) {
return;
}
else if ($('p', context).size() > 5) {
$('body').append('<p id="lots">Lots of Text!</p>');
}
};

The previous code snippet does the following:

  • It checks to see if an element with the ID lots exists. If it does, that means this behavior has already been properly processed. So it returns early.

  • If the lots ID does not exist, it checks to see if there are more than five paragraphs.

  • If there are more than five paragraphs, a short piece of text Lots of Text!is appended to the end of the document. The ID of the paragraph that wraps this text is lots. So we know this has already been processed (and that there are more than five paragraphs) by the existence of the lots ID.

Another important thing to notice is that both queries (the checking queries) use the context. This is the recommended procedure, but it can have unanticipated results, as we shall see.

We have two behaviors, both making use of the context in the recommended way. In our code that attaches the new paragraph we call attachBehaviors(), passing it the most narrow context we can (the paragraph that was inserted).

So what happens when the number of paragraphs in the document exceeds five? Nothing.

Here's an example from Firebug's console:

>>> $('p').size();
5
>>> var cxt = $('<p>Sixth paragraph</p>')
.appendTo('body').get(0);
>>> Drupal.attachBehaviors(cxt);
>>> $('p').size();
6
>>> $('#lots').size();
0

At the beginning of this script, our document had five paragraphs. On the next two lines, we added a sixth and re-attached behaviors.

But the<p id="lots">Lots of Text!</p> was not added to the document. We can tell this in two waysFirst, by the fact that it would have added a seventh paragraph. Second, $('#lots').size() would have returned 1. That means our Drupal.behaviors.countParagraphs() behavior did not run.

This happened because our behaviors are configured to use the context. Our attachBehaviors() call keeps the context limited to just the changed elements. This is correct, isn't it? But in this case, our zeal to optimize actually caused the failure.

Let's look at why it fails.

Here's how the context is set up:

var cxt = $('<p>Sixth paragraph</p>').appendTo('body').get(0);
Drupal.attachBehaviors(cxt);

The context object, cxt, points only to the new paragraph.

So Drupal.attachBehaviors(cxt) only passes that one paragraph to all of the registered behaviors.

Now, let's look at the behavior that counts paragraphs:

Drupal.behaviors.countParagraphs = function (context) {
if ($('#lots', context).size() > 0) {
return;
}
else if ($('p', context).size() > 5) {

$('body').append('<p id="lots">Lots of Text!</p>');
}
};

The highlighted line is the problematic one. Since the context is limited to just one paragraph, the jQuery chain $('p', context).size() will always return 1 when our Drupal.attachBehavior(cxt) function is executed. So regardless of how many paragraphs the actual document contains, the content's else if statement won't get executed.

This is a simple case, but we could imagine fairly elaborate ones that come from similar problems. In order to be sure that a call to Drupal.attachBehaviors() will not result in a bug like the one we just saw, you must be familiar with all of the behaviors that might run in your Drupal instance. (If you are creating portable code, you should have some way to ensure that no other code could possibly have problems like what was just described.)

What's the solution?

There are two possible solutions:

  1. In your behaviors, ignore context.

  2. When you call Drupal.attachBehaviors(), don't specify context.

The first solution has two problems. First, it goes against the design of behaviors. Second, it would require that all the developers implement it before we could be sure that it was working. Since behaviors are supposed to be context-aware, it would be hard to achieve this.

I'm inclined to suggest that the second solution is the best. The context should not be narrowed. That is, behaviors should use the document object as the context. In practice, what this means for you is that you should call Drupal.attachBehaviors() with no arguments. While it may result in a (small) performance hit, it will prevent bugs like the one seen previously.

Project: collapsing blocks

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

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

// $Id$
/**
* Defines behaviors for Frobnitz theme.
* @file
*/
/**
* Toggle visibility of blocks (with slide effect).
*/
Drupal.behaviors.slideBlocks = function (context) {
$('.block:not(.slideBlocks-processed)', context)
.addClass('slideBlocks-processed')
.each(function () {
$(this).children(".title").toggle(
function () {
$(this).siblings(".content").slideUp("slow");
},
function () {
$(this).siblings(".content").slideDown("slow");
});
});
};

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

When the slide is complete, only the titlembutcherwill be displayed.

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

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

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

<div class="block block-user" id="block-user-1">
<h2 class="title">mbutcher</h2>
<div class="content">
<ul class="menu">
<li class="leaf first">
<a href="/drupal/user/1">My account</a>
</li>
<li class="collapsed">
<a href="/drupal/node/add">Create content</a>
</li>
<li class="collapsed">
<a href="/drupal/admin">Administer</a>
</li>
<li class="leaf last">
<a href="/drupal/logout">Log out</a>
</li>
</ul>
</div>
</div>

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

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

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

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

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

.each(function () {
$(this).children(".title").toggle(
function () {
$(this).siblings(".content").slideUp("slow");
},
function () {
$(this).siblings(".content").slideDown("slow");
});
});
};

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

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

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

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

Let's continue in the jQuery chain:

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

$(this).children(".title").toggle(
function () {
$(this).siblings(".content").slideUp("slow");
},
function () {
$(this).siblings(".content").slideDown("slow");
});
});

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

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

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

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

$(this).children(".title").toggle(
function () {
$(this).siblings(".content").slideUp("slow");
},
function () {
$(this).siblings(".content").slideDown("slow");
});

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

There will always be only one title per block.

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

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

So on odd clicks it will execute this function:

function () {
$(this).siblings(".content").slideUp("slow");
};

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

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

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

Note

Why don't we look for div.content?

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

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

The second toggle function performs an analogous task:

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

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

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

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

Utilities

At the beginning of the chapter, I listed the four toolsets that can be found in drupal.js: behaviors, translations, theming, and utilities. We've already looked at behaviors. Translations and theming will the subjects of the next two chapters. But before we continue, let's look at some of the more important utilities.

All of the utilities covered in this section are part of the drupal.js library and will be available any time you include JavaScript files in your theme or module.

Checking capabilities with Drupal.jsEnabled

The first utility we will check out is not a function. It's a property of the Drupal object: Drupal.jsEnabled. This variable indicates whether or not the browser will support drupal.js and jquery.js.

The name of this property is slightly misleading. It doesn't actually indicate whether JavaScript is enabled. (That wouldn't be all that useful, would it? If JavaScript was truly disabled, a JavaScript variable would be worthless.)

Instead, this flag is set to true if the requisite level of JavaScript is supported. Some browser, such as antiquated desktop browsers and bare-bones embedded browsers, have a limited level of JavaScript support. But they don't provide a full implementation like what you would find in modern editions of IE, Firefox, Safari, or Opera.

When drupal.js loads, it will evaluate whether the JavaScript implementation supports DOM manipulation of the sort that jQuery and drupal.js rely upon. If the correct functions exist then this flag will be set to true. Otherwise, the flag will be set to false.

Internally, Drupal uses this flag to determine if behaviors are supported. If Drupal.jsEnabled is false, then Drupal.attachBehaviors() doesn't attempt to attach any behaviors. It just silently returns.

Also, Drupal uses this flag to set a cookie. If Drupal.jsEnabled is true, a cookie is set which indicates that JavaScript support is sufficient. This way, server-side code can send back appropriate responses based on a browser's JavaScript capabilities. (This cookie is named has_js, and is available in PHP using $_COOKIE['has_js'].)

Feel free to use Drupal.jsEnabled whenever you are concerned that a browser might not support the necessary JavaScript for DOM manipulations. But don't get overly concerned about checking with the use of this flag. Most jQuery functions will silently fail if JavaScript support isn't good enough, and the main Drupal features (like behaviors) will be skipped as well.

The Drupal.checkPlain() function (and the jQuery alternative)

The first function we will look at is the Drupal.checkPlain() function. If you have already written some Drupal PHP code, you will probably recognize the name.

The Drupal.checkPlain() function takes a string and prepares it for display, escaping symbols that have a special meaning in HTML. While the use of the term check implies that this will return a Boolean value (true if the text is plain, false if otherwise), it actually performs the escaping, and returns the escaped string.

So what does it escape? Let's look at an example. While viewing a node on our Drupal system, we can use the Firebug console to manipulate the document:

>>> myString = "A string with <em>HTML</em> embedded.";
"A string with <em>HTML</em> embedded."

>>> $('.node .content').html(myString);
Object length=1

The first line creates a new string named myString. Note that the contents of this string contain embedded HTML: A string with <em>HTML</em> embedded

In the second line we use jQuery to find the main content section for the node displayed on the current page, and replace the existing contents of that section with the value of myString. Since this involves a little bit of new jQuery, let's look at it closely.

The query we use here is .node .content. The most important character in this query is the space between .node and .content. It indicates a descendent relationship between .node and .content. We might rephrase this query as "find all elements with class node, and then find any elements that are descendants of this node and that have the class content."

Note

Descendants and children

A descendant relationship is not limited to just children of the selected node. Child nodes are directly under the selected node. A descendant may be more than one level beneath a node. If you are interested in only children, you can use the> operator instead of the empty space: $('.node > .content'). Note that in this case the whitespace around the> is treated as insignificant.

So this query will select the main content for the main node or nodes displayed on the page. Why do we use this more complex form of the query? Why not just use $('.content')? That's because even blocks use the .content class, and we don't want to select the content of our blocks.

The second part of our jQuery chain is the html() method. This replaces the entire HTML under the current node with the string passed in. The string is interpreted and inserted into the DOM, so HTML tags in the string are actually recognized as HTML markup.

Let's look at the results of running this command to see how the string is interpreted:

Notice that<em>HTML</em> is displayed above as HTML (in italics).

What if we wanted to display the HTML tags and not have them interpreted? That's where the Drupal.checkPlain() function comes in. Let's take a look at a similar snippet of code:

>>> myString = "Add a line break using the <br/> tag.";
"Add a line break using the <br/> tag."

>>> escapedString = Drupal.checkPlain(myString);
"Add a line break using the &lt;br/&gt; tag."

>>> $('.node div.content').html(escapedString);
Object length=1

If we were to look at this in the browser, it would look something like this:

In the screenshot, the<br/> tag is displayed as is, and doesn't appear to be treated as HTML. What's going on behind the scenes?

Take a look at the second command entered on the console: escapedString = Drupal.checkPlain(myString);. Here, we use the Drupal.checkPlain() function to escape the contents of myString. The output displayed on the console shows what happens when we do so:

"Add a line break using the &lt;br/&gt; tag."

The tag<br/> that we originally entered in myString has now been converted to&lt;br/&gt;. The< and> characters were encoded into their HTML entity equivalents. The Drupal.checkPlain() function encodes four characters:

  • < becomes&lt;.

  • > becomes&gt;

  • " (double quote) becomes&quot;

  • & becomes&amp;

Why escape these four? They all represent HTML elements. By escaping them, we can ensure that we are not inserting HTML elements into the string. So there is no danger that the HTML is interpreted by the browser.

There is good reason for doing this. Not only does it allow us to display HTML tags, but it adds a layer of security when we are dealing with user-entered data. We wouldn't want the string<script>doSomethingBad();</script> to get rendered!

So when should you use Drupal.checkPlain() in your scripts? The usual answer is anytime you are displaying unknown or untrusted information. Actually, the situation is compounded by the fact that jQuery handles most cases where you'd use Drupal.checkPlain(), and it does so gracefully.

Earlier, when we inserted our new content, we did this:

>>> myString = "Add a line break using the <br/> tag.";
"Add a line break using the <br/> tag."

>>> escapedString = Drupal.checkPlain(myString);
"Add a line break using the &lt;br/&gt; tag."

>>> $('.node div.content').html(escapedString);
Object length=1

We could have done this with jQueryand in a more succinct way:

>>> myString = "Add a line break using the <br/> tag.";
"Add a line break using the <br/> tag."

>>> $('.node div.content').text(myString);
Object length=1

In this case, instead of using the Drupal.checkPlain() function to do the encoding and then using the jQuery html() function to insert the content, we just use the jQuery text() function. It implicitly handles the encoding and the insertion.

The guideline for using the text() function is simple: Any time you are inserting text that should not contain HTML (including, for example, user-entered text), you should insert it with text() instead of html().

So when should Drupal.checkPlain() be used? It should be used in cases where you need to encode a string, but not insert it into a document.

The Drupal.parseJson() function

Some of the functions in drupal.js were created before comparable functions existed in jQuery. One such function is Drupal.parseJson().

This function was intended to be used for parsing AJAX data that was returned from the server in the JSON (JavaScript Object Notation) format. JSON data looks like JavaScript. Here's an example describing a person's name:

jsonString = "{'first': 'Matt', 'last': 'Butcher'}";

If we were to remove the double quotes, we would have a literal JavaScript object declaration. That's the idea behind JSON. As a part of an AJAX exchange, a server can send the client JSON data, which the client can then parse into JavaScript objects.

We will see JSON in action in Chapter 7. Until then, here's a short example of how the Drupal.parseJson() function works:

>>> jsonString = "{'first': 'Matt', 'last':'Butcher'}";
"{'first': 'Matt', 'last':'Butcher'}"
>>> name = Drupal.parseJson(jsonString);
Object first=Matt last=Butcher

>>> name.first
"Matt"

The line Object first=Matt last=Butcher in the code shows the main feature of Drupal.parseJson(). The string is parsed and converted to an object. We can then use that object directly in JavaScript.

This function may be of limited use as the version of jQuery that ships with Drupal 6 already contains functions for dealing with JSON content during AJAX calls. When we look at AJAX later in the book, we will see how jQuery functions can be used to handle JSON data.

The Drupal.encodeURIComponent() function

Earlier, we looked at encoding HTML with Drupal.checkPlain(). Here, we will look at another encoding functionone designed for encoding pieces of a URL or URI.

Drupal PHP programmers may also recognize this function. It essentially performs the same task as the drupal_urlencode() PHP function. Those familiar with JavaScript will recognize this as having the same name as a built-in JavaScript function.

The built-in JavaScript function, encodeURIComponent(), is used to encode values that will be used when constructing a query string or a URI. Certain values, such as a slash (/), have special meaning in URLs. A slash indicates a directory.

How do we write a request for a document on the server named pros/cons.html, where the slash is part of the file name and not an indicator that pros/ is a directory? It should be escaped with "/" replaced by a hexadecimal representation of the character. We can see how this is done with the built-in browser function encodeURIComponent():

>>> encodeURIComponent('pros/cons.html');
"pros%2Fcons.html"

Looking at the Firebug output, we can see that the slash was replaced by %2F, where 2F is the ASCII hexadecimal representation of the / character.

But Drupal presents something of a special case. It uses paths as query parameters. So we would expect strings, such as node/1/edit with the slashes left as is. At the same time, we would want other special characters, such as %, to be escaped correctly.

That's where Drupal.encodeURIComponent() comes in. It correctly converts other characters while preserving the slashes. To see this in action, let's compare how the two different functions convert the fiction link node/1/calc%:

>>> myString = 'node/1/calc%';
"node/1/calc%"

>>> encodeURIComponent(myString);
"node%2F1%2Fcalc%25"

>>> Drupal.encodeURIComponent(myString);
"node/1/calc%25"

In this example we can see the difference: The Drupal.encodeURIComponent() call preserved the slashes in the path.

In general, Drupal.encodeURIComponent() should be used any time you are constructing links back to Drupal. However, when making calls to other non-Drupal services, you should continue to use the browser-defined encodeURIComponent() function.

The Drupal.getSelection() function

The last utility function that we will look at is the Drupal.getSelection() function. This tool is used to find out what portion of a text area (<textarea></textarea>) is selected.

For example, when you select a section of text with your mouse, this function can be used to find out the starting and ending locations of the selection.

The Drupal.getSelection() function returns an object with two attributes: start and end. These two properties are integers which represent the beginning and ending of the selection. In our next project, we will see this function in action.

There are a few other functions in the drupal.js file that may be used in rare circumstances, but the ones we have seen here are the ones you are most likely to use in your own scripts. In the last part of this chapter, we will do another project. We will create a simple editor with jQuery and some of the Drupal capabilities we have seen in this chapter.

Project: a simple text editor

In this project we are going to create a simple text editor. We are going to begin with text areas and add a couple of buttons that insert HTML tags for us. In doing this project, we will make use of the jQuery techniques we learned in the previous chapter, as well as behaviors and some of the utility functions we saw earlier in this chapter.

Note

There are already several rich text editors available for Drupal, all of which are more advanced than the simple tool we will create here. The WYSIWYG API module is poised to become the de facto text editor going forward. It can be found at http://drupal.org/project/wysiwyg.

Our editor will have two buttonsa B button to make some text bold, and an I button to add italics. The editor will insert markup into the text so that the tags are visible to the user. For example, if the user types in the string This is bold, highlights the word "bold", and presses the B button, she or he will see the text This is <strong>bold</strong>.

Before we look at the code, let's take a quick look at what it should produce:

The new simple editor attaches to existing<textarea></textarea> elements and adds the two-button toolbar.

We want to write our code so multiple text areas on the same screen can all have editors. We also want the editor to load as soon as the page is loaded and attach to all text areas.

Note

If we were implementing a complete editor solution, we would probably not want the editor to attach to all text areas, since not all areas accept HTML input. But for our project, we will simplify the process by attaching the editor to all text areas.

Let's start out by looking at our frobnitz.info file, which will point to a new CSS file and a new JavaScript file:

; $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
stylesheets[all][] = simpleeditor.css

scripts[] = printer_tool.js
scripts[] = sticky_rotate.js
scripts[] = behaviors.js
scripts[] = simpleeditor.js

The two new files, highlighted in the previous code, will provide a stylesheet and a JavaScript library for our tool.

The stylesheet is very simple. Here it is:

.editor-button {
border: 1px solid gray;
padding: 3px;
width: 1em;
text-align: center;
background-color: #eee;
float: left;
}
.editor-button:hover {
background-color: #ccc;
}
.button-bar {
clear: both;
}

It simply takes the elements that make up the buttons and button bar, and makes them look and behave a little more like form buttons. Now, let's move on to the JavaScript.

Here is the simpleeditor.js script in its entirety. After taking a quick look at the entire file we will go through it more carefully:

// $Id$
/**
* Simple text editing controls for a textarea.
*/
var SimpleEditor = SimpleEditor || {};
SimpleEditor.buttonBar =
'<div class="button-bar">' +
'<div class="editor-button bold"><strong>B</strong></div>' +
'<div class="editor-button italics"><em>I</em></div>' +
'</div>';
SimpleEditor.selection = null;
/**
* Record changes to a select box.
*/
SimpleEditor.watchSelection = function () {
SimpleEditor.selection = Drupal.getSelection(this);
SimpleEditor.selection.id = $(this).attr('id');
};
/**
* Attaches the editor toolbar.
*/
Drupal.behaviors.editor = function () {
$('textarea:not(.editor-processed)')
.addClass('editor-processed')
.mouseup(SimpleEditor.watchSelection)
.keyup(SimpleEditor.watchSelection)
.each(function (item) {
var txtarea = $(this);
var txtareaID = txtarea.attr('id');
var bar = SimpleEditor.buttonBar;
$(bar).attr('id', 'buttons-' + txtareaID)
.insertBefore('#' + txtareaID)
.children('.editor-button')
.click(function () {
var txtareaEle = $('#' + txtareaID).get(0);
var sel = SimpleEditor.selection;
if (sel.id == txtareaID && sel.start != sel.end) {
txtareaEle.value = SimpleEditor.insertTag(
sel.start,
sel.end,
$(this).hasClass('bold') ? 'strong' : 'em',
txtareaEle.value
);
sel.start = sel.end = -1;
}
});
});
};
/**
* Insert a tag.
* @param start
* Location to insert start tag.
* @param end
* Location to insert end tag.
* @param tag
* Tag to insert.
* @param value
* String to insert tag into.
*/
SimpleEditor.insertTag = function (start, end, tag, value) {
var front = value.substring(0, start);
var middle = value.substring(start, end);
var back = value.substring(end);
return front + '<' + tag + '>' + middle +
'</' + tag + '>' + back;
};

The previous code has been organized in the order in which we will look at it below. This makes the overarching structure a little less evident. Here's a high-level description of what's going on.

The SimpleEditor namespace will hold most of the information pertinent to our editor. We will use a Drupal behavior for attaching and handling various events. Most of the major logic will be inside of our behavior.

Other than that, we will add a few helper function that will be part of our SimpleEditor namespace.

Let's look at the first several lines of code in the simpleeditor.js file:

var SimpleEditor = SimpleEditor || {};
SimpleEditor.buttonBar =
'<div class="button-bar">' +
'<div class="editor-button bold"><strong>B</strong></div>' +
'<div class="editor-button italics"><em>I</em></div>' +
'</div>';

The SimpleEditor.buttonBar variable holds some generic HTML that creates a basic button bar. We will use this as a template to add buttons to text areas.

Next, we will look at the new behavior. This is the most complex piece of code in our project.

The main behavior

There is one main behavior registered for our simple editor. It learns about all of the text areas in the document and then adds editor support to those areas.

This behavior illustrates the compactness that is achievable with jQuery and drupal.js. It also makes use of constructs that you are likely to see in Drupal code, such as nested anonymous functions.

Since it is complex, we will walk through it carefully.

Here's the behavior code:

Drupal.behaviors.editor = function () {
$('textarea:not(.editor-processed)')
.addClass('editor-processed')
.mouseup(SimpleEditor.watchSelection)
.keyup(SimpleEditor.watchSelection)
.each(function (item) {
var txtarea = $(this);
var txtareaID = txtarea.attr('id');
var bar = SimpleEditor.buttonBar;
$(bar).attr('id', 'buttons-' + txtareaID)
.insertBefore('#' + txtareaID)
.children('.editor-button')
.click(function () {
var txtareaEle = $('#' + txtareaID).get(0);
var sel = SimpleEditor.selection;
console.log(sel.id + ' ' + txtareaID);
if(sel.id == txtareaID && sel.start != sel.end) {
txtareaEle.value = SimpleEditor.insertTag(
sel.start,
sel.end,
$(this).hasClass('bold') ? 'strong' : 'em',
txtareaEle.value
);
sel.start = sel.end = -1;
}
});
});
};

The main part of this function is controlled by a large jQuery chain. This chain serves three purposes:

  1. It finds all of the text areas that need processing.

  2. It adds event handlers to those text areas.

  3. It loops through each text area and attaches a button bar to the text area.

Step 1: find text areas that need processing

Here's the chain:

$('textarea:not(.editor-processed)')
.addClass('editor-processed')
.mouseup(SimpleEditor.watchSelection)
.keyup(SimpleEditor.watchSelection)
.each(/* more code here */);

Take a look at the first pair of lines.

We saw this pattern earlier in the chapter. The main query looks for text areas that have not already been processed by the behavior (textarea:not (.editor-processed)). To the returned list, it first adds the class that indicates that the behavior has been processed (addClass('editor-processed')).

Step 2: add event handlers

Once we've found the appropriate text areas and marked them as processed, we can move on to lines three and four. Here, we need to attach two event handlers:

  1. mouseup: This event will be triggered when a mouse button is released.

  2. keyup: This event will be triggered when a key is released.

These are the two events which might indicate that some text within the text area has just been selected.

How? Consider the case where a user is selecting text with a mouse. The user presses the mouse button down, drags the mouse to select the text, and then releases the mouse button. We want to check button releases to see if new text was selected.

The keyup event works the same way. The user may hold down the shift key while selecting text with the arrow key. It's the key release that we want to use as a clue. We should check it to see if the user selected any text.

In both cases, the same function is assigned. The SimpleEditor.watchSelection() function checks a text area to find out what has been selected. Let's take a look at that function before we continue examining the third part of the jQuery chain:

SimpleEditor.selection = null;
SimpleEditor.watchSelection = function () {
SimpleEditor.selection = Drupal.getSelection(this);
SimpleEditor.selection.id = $(this).attr('id');
};

The SimpleEditor.watchSelection() function does two things. First, it calls the Drupal.getSelection() function to find out what text (if any) is selected in the given text area. The value of this in the event handler function will be the element from which the event was fired. In other words, this will be the text area element that most recently changed.

The returned object will be stored in SimpleEditor.selection, where other parts of our editor can access it.

The second thing this function does is get the ID of the current text area element. Since we can have multiple text areas on the same page, we need to track which one was modified. That's what SimpleEditor.selection.id is used for.

Note

The id property is not a part of the object returned from Drupal.getSelection(). Instead, we add it on an ad hoc basis.

At this point we have handled one of the major tasks that our editor must do. We have created code that will track what is happening in the text area. Next, we will move onto the third step in the jQuery chain.

Step 3: attach the button bar

First, the jQuery chain found the text areas to process. Next, the event handlers were added. In the third step, the jQuery chain loops through each of the matching text areas with a call to the each() function.

Inside of the each() function, we define an anonymous function that will operate on each text area element. Here's that function:

function (item) {
var txtarea = $(this);
var txtareaID = txtarea.attr('id');
var bar = SimpleEditor.buttonBar;
$(bar).attr('id', 'buttons-' + txtareaID)
.insertBefore('#' + txtareaID)
.children('.editor-button')
.click(/* Event handler function */);
}

This function will be called once for each text area that is found on the page.

This function defines three variables. The txtarea variable points to a jQuery object wrapping the current text area. In order to identify the text area, we will need to get the ID of the current area. The ID gets stored in txtareaID. We also need a local copy of the button bar string that we created at the beginning of the script. This gets stored in the bar variable.

As we've seen before, jQuery can distinguish between a string that contains HTML and the one that contains a CSS selector. So when we call $(bar), jQuery parses bar into an HTML DOM fragment. From there, we can manipulate the bar DOM just as we do with the main document.

The first thing we do is generate an ID for our button baran ID based on the ID of the text area that this button bar will be attached to.

We can use this ID later to distinguish one button bar from another. Again, if we have several text areas on a page, we will have several button bars too. We need to make sure that clicking on a button for one text area doesn't change text in another text area.

Now we have a copy of the button bar and we've given it a unique ID. The next thing to do is insert it into the document's DOM immediately above the text area that it will be attached to. What we want to accomplish is to attach the current jQuery DOM to some point in the document's DOM. We do that with the insertBefore() jQuery function, which will insert the button bar above the desired text area.

Our button bar is structured like this:

<div class="button-bar" id="button-someID">
<div class="editor-button bold"><strong>B</strong></div>
<div class="editor-button italics"><em>I</em></div>
</div>

Our current jQuery object is pointing to the outer<div></div> element. But now we want to work on the two inner divs, each describing a button. To go from the outer element to the inner elements, we use $().childrend('.editor-button').

Now the current jQuery object will wrap boththe B button and the I button.

Currently, these aren't really buttons at all. Nothing will happen when you click them. We need to add an event handler to these two so that they respond when clicked. We use jQuery's click() function to add an event handler for the click event.

The click() function takes a function as an argument. Once again, we define an anonymous function to handle this.

Here's the event handler function that is attached to the click event:

function () {
var txtareaEle = $('#' + txtareaID).get(0);
var sel = SimpleEditor.selection;
if(sel.id == txtareaID && sel.start != sel.end) {
txtareaEle.value = SimpleEditor.insertTag(
sel.start,
sel.end,
$(this).hasClass('bold') ? 'strong' : 'em',
txtareaEle.value
);
sel.start = sel.end = -1;
}
}

Each time one of our buttons is clicked, a version of the above function will be executed.

What do I mean by a version?

This function, as an anonymous function, is created once per button. Furthermore, the function definition is nested within another function definition (the definition for the anonymous function inside of each()). Each time the function is defined in the each() loop, it takes with it the environment defined by its parent function. In short, it's not just a function, but also a snapshot of the environment in which the function was created.

While each version of this function does the same thing, each has access to a different environment.

For example, txtareaID, defined in the parent function (as we just saw), is still available inside of this function. Furthermore, the txtareaID for the function assigned to the first text area is different than the txtareaID assigned to the second text area. In spite of the fact that the variable name is the same, the scope of the variable is such that one click handler retains a copy, while another click handler retains a different copy with a different value.

Note

For all practical purposes, we have created what is called a closure. We have defined a function that carries with it some values it got from its original context, but which are now closed off to any outside context. The function may still have access to the txtareaID that was set when the function was defined, but that txtareaID is closed off from any other parts of the program. It no longer lives in any scope outside of the anonymous function's scope. In Chapter 9, we will take another look at closures.

By using anonymous methods in this way, we have made it possible to register event handlers so that the context is carried with them. For example, there's no need to add onclick attributes in our HTML that provides the ID of the text area or some other bit of information. All of that is stored in the anonymous function as the behavior is attached.

So what does this function do? The first thing it does is define a few variables. The txtareaEle variable contains the actual text area element that this button relates to. Notice that this function uses the txtareaID variable defined by the parent to get this information.

The second variable, sel, is a copy of the SimpleEditor.selection variable. It contains information about what text area was last active, and what part of that area is selected. This SimpleEditor.selection object is maintained by the code we looked at earlier, notably the SimpleEditor.watchSelection() function.

We now have the ID of the text area that this handler is attached to, and we have information about the last text area that was active.

The next thing this click handler does is to find out whether it needs to make any changes to the text area that it is responsible for. There are two criteria for this:

  • First, is the currently active text area (sel.id), the text area that this click handler is responsible for (txtareaID)? Remember, there will be one click handler for every button attached to every text area. Each click function needs to find out if the current text area target is the one that it is responsible for.

  • Second, is there any text selected? Our primitive editor will only surround selected text with tags. So it will only insert tags if there is selected text to surround.

If either of these two criteria fails, the click handler will quietly return.

But if the target text area is the one this handler handles, and if there is also some selected text, then this function will go about wrapping the selected text in the appropriate tags:

if(sel.id == txtareaID && sel.start != sel.end) {
txtareaEle.value = SimpleEditor.insertTag(
sel.start,
sel.end,
$(this).hasClass('bold') ? 'strong' : 'em',
txtareaEle.value

);
sel.start = sel.end = -1;
}

The highlighted code is responsible for inserting the tag. It resets the value of its text area (txtareaEle.value) to whatever gets returned from SimpleEditor.insertTag().

In just a moment we will look at SimpleEditor.insertTag(). But first, let's look at the parameters that are passed in:

  • The first parameter, sel.start, indicates where the selection starts. It's the index of the first character in the text area that is selected.

  • The second parameter, sel.end, is the last selected character in the text area.

  • The third parameter determines what tag is going to be inserted. If the button that was clicked has the class bold, then clicking this button should make the text bold (strong). Otherwise, we assume that the text should be put in italics (em).

  • Finally, we pass in the string that contains all of the text in the text area. In this case, we simply get the value property of the text area element.

So this last function call in the click event handler will take the text in the target text area, find the selected text in that area, and surround the selected text with the appropriate HTML tag.

After the call to SimpleEditor.insertTag() returns, we clear out the old selection by setting sel.start and sel.end to -1. We clear it because the selection disappears in the browser window when a button is clicked, and also because we want to make sure that another button click doesn't insert another tag.

It's time to turn to the SimpleEditor.insertTag function to see how that works:

SimpleEditor.insertTag = function (start, end, tag, value) {
var front = value.substring(0, start);
var middle = value.substring(start, end);
var back = value.substring(end);
return front + '<' + tag + '>' + middle + '</' + tag + '>'
+ back;
};

We saw the four parameters that are passed to SimpleEditor.insertTag(): the position of the first selected character(start), the position of the last selected character (end), the name of the tag to surround the selection with (tag), and the string that contains all of the text (value).

There are a few ways we could do this, some probably more economical than this. But this function follows a very simple path. It breaks the string into three parts: The part before the start tag should be inserted, the part between the start and end tags, and the part after the end tag.

For example, let's imagine the value of value to be this:

The cat sat on the mat.

Now let's imagine that the word cat has been selected. That would make the value of start equal to 4, and the value of end equal to 7. This would result in the following three parts:

var front = 'The ';
var middle = 'cat';
var back = ' sat on the mat.';

And if the I button was clicked then the value of tag is em.

Now, all the function does is glue these strings back together:

front + '<' + tag + '>' + middle + '</' + tag + '>' + back;

This would produce the string The <em>cat</em> sat on the mat.

When SimpleEditor.insertTag() returns this value, it would replace the old text as the value of the text area. The result, then, is that the selected text has been wrapped with the appropriate HTML tags.

We've now created an editor capable of adding bold and italic tags to text in a text area. We've done this with a combination of jQuery and drupal.js functions, including a behavior and the Drupal.getSelection() utility function.

While this editor is certainly primitive, it's also less than 100 lines of code (including comments). This should give you some idea of how powerful tools can be efficiently built using the libraries included with Drupal 6.

Summary

In this chapter, we looked at Drupal Behaviors and the major utility functions provided by drupal.js.

We began with an overview of the drupal.js file, which provides functions for behaviors, translation, theming, as well as other utility functions. Then looked at what Drupal Behaviors are and how they work. We even saw how seemingly correct uses of behaviors can result in bugs that are difficult to diagnose.

In our first project, we used behaviors to add a sliding effect to all blocks on a page.

Then we looked at several utility functions included in drupal.js, learning when and how to use them. This led us to our second project, where we created a simple editor using jQuery, behaviors, and the Drupal.getSelection() function.

In the next chapter, we will continue our exploration of drupal.js by looking at JavaScript translation features. In the chapter after that, we will look at the JavaScript theming engine where we will again encounter behaviors.