8. Building a Module – Drupal 6 JavaScript and jQuery

Chapter 8. Building a Module

In the previous chapter, we used the AJAX features in jQuery and Drupal to add some new tools to our Frobnitz theme. But what if we wanted to add those tools to another theme, such as Bluemarine or Garland? Would we need to create new subthemes for those just to add a few JavaScript files?

That would certainly be one way of doing it. A better way would be to take our JavaScript tools and package them as standalone components that could be added independently. This is done through Drupal modules. In this chapter, we will learn how to create modules and use them for adding JavaScript features.

In this chapter, we will:

  • Understand how modules work

  • Create a bare-bones module

  • Add JavaScript to a module

  • Make our JavaScript available to other modules

In previous chapters, almost all of our code has been in JavaScript. In this chapter, we will use a little PHP. Don't worry, if you can write a PHP Template, you will be able to follow the simple code that we are going to write.

How modules work

In the previous six chapters, we have been creating JavaScript tools and adding them to our site using the info file for a theme. Themes are designed to take Drupal data and prepare it for presentation. Often, it serves us quite well to attach JavaScript to a theme.

But many of the tools we have written so far could just as easily be used with any theme. Why build our tool into a theme when it could be used more broadly? In this chapter, we are going to use modules as a way of packaging JavaScript into a component that can be used independently of a theme.

Modules provide a way for developers to add functional components to Drupal without having to modify the existing Drupal code.

For example, instead of opening a Drupal file and adding our own features, we can create a module to provide these features. This has several advantages:

  • Since we haven't changed Drupal itself, we can continue upgrading Drupal without having to carefully patch in our own changes.

  • We can enable and disable our module with a few mouse clicks. This additional functionality is easy to on and turn off.

  • We can share the module with others who might also make use of it.

Also, as we did in the previous chapter, we can get modules that others have developedsuch as Views and Views Datasourceand use them to build our own site.

In a nutshell, modules turn a good, general-purpose CMS, into a highly customizable and infinitely extendible web platform.

Modules are mostly written in PHP. In fact, one of the two required module files must always be written in PHP. But modules can provide JavaScript services as well. In fact, that is what we will be doing in this chapter. We will create modules that make our JavaScript tools available across Drupal, regardless of the theme.

The module structure

Drupal developers have gone to great lengths to make module writing easy. In fact, the process of creating a module is only four steps long:

  1. Create a directory for your module.

  2. Add a .info file to the module directory.

  3. Add a few configuration directives to the .info file.

  4. Add a .module file to your module directory.

After following these four steps, you will have a module that can be administered from within Drupal, though it won't do anything.

Let's briefly cover the pieces used in these steps: the directory, .info file, and .module file.

The directory

Each module must be placed in a directory. Most often, a module has its own directory. For example, if we want to create a module called my_module, it will be housed in the my_module/ directory.

On occasion, several similar modules will be grouped into a package. A package is simply a collection of related modules, all stored in the same directory. For example, we could create a my_tools module with modules named my_first_tool and my_other_tool. These would both be stored in the my_tools/ directory.

We will be creating simple modules, and each will be stored in its own directory.

The .info file

In Chapter 2, we created our first theme's .info file. That file is used by the Drupal theming system to glean information about the theme.

Modules also have a .info file. In fact, it looks very similar to a theme's .info file. In our case, it will be easier to create a module's .info file than it was to create a theme's .info file.

Just as a theme's .info file goes in the theme's directory, so a module's .info file goes in the module's directory.

The .module file

Drupal needs to know where the module's executable code is located. How does it do that? It looks in the module's directory for a file that ends with the .module extension. When it finds such a file, it loads it as a PHP script.

Writing PHP code for Drupal is just about as simple as creating the module file and writing some PHP functions.

Those are the three parts of a Drupal module.

Where do modules go?

Like themes, modules are stored in directories located in pre-defined locations beneath your site's Drupal directory.

Themes go in /themes (for built-in themes), /sites/all/themes, /sites/default/themes, and (if you are hosting multiple sites) /sites/SITENAME/themes.

Modules are similarly organized:

  • Modules included as part of the Drupal Core go in the /modules directory under the Drupal root

  • Modules used across the installation go in /sites/all/modules

  • Modules used only by the default site go in /sites/default/modules (usually you would only put modules here if you are hosting multiple sites)

  • Modules used only by other sites, on a multisite Drupal installation, go in /sites/SITENAME/modules, where SITENAME is replaced by the actual name of the site

Note

Drupal has the ability to run multiple sites with only one copy of the Drupal Core. For example, if you wanted to run my.example.com and your.example.com on the same website, you could configure Drupal to manage both domains (each with its own database) without having to run two Drupal instances. For more information, see http://drupal.org/getting-started/6/install/multi-site.

In this book, we will always put our modules in /sites/all/modules.

We have now seen the three parts of a module, and we know where to put the module files. In the spirit of our project-based method, it's time for us to dive right in and create a module.

How tough will this first module be? Here's a hint: the entire .module file is only 27-lines long, and 10 of those are comment lines. No rocket science here!

Project: creating a JavaScript loader module

When working with themes, we added JavaScript to our site by adding a line to the theme's .info file like this:

scripts[] = my_script.js

Like themes, modules also have .info files. The format of the module's .info file is the same, but the directives available in module .info files are different from those available in theme .info files.

The scripts[] directive is an example. It is unused by module .info files. If you put such an entry in your module's .info file, it will simply be ignored.

We are going to change this. In this project, we will build a JavaScript autoloader which reads all of the scripts from a module's .info file, and includes them in the HTML delivered back to the client.

Note

Why doesn't Drupal do this already?

The main reason why Drupal doesn't already do this is, in a word, performance. We are going to make it possible for one module to include its own script files. But what if we added scripts from all of the modules? Just checking all of the modules would add overhead to Drupal's page-rendering process. In the future, this could change if some clever developers figure an expedient way of doing this.

Our module is only going to read its own .info file. We could expand its scope to check all modules for scripts, but that would add significant complexity to the module and would have a significant performance impact.

We are going to build our module in the following order:

  1. Create the module directory.

  2. Add a sample JavaScript file for testing.

  3. Add the module's .info file.

  4. Write the short .module file.

There are two major things that I wish to convey as we walk through this project. First, this project should illustrate the process of creating a simple module. Second, we will see the primary tool for interacting with JavaScript from a module: the drupal_add_js() function.

Creating the module directory

The first step in developing a new module is creating a place for it to be stored. For basic modules, the directory should have the module's machine-readable name. Our module will be called jsloader, so we will create a directory with that name.

As mentioned in the previous section, custom Drupal modules go under the sites/ directory. Specifically, ours will go in sites/all/modules/jsloader/ as seen here:

In the previous screenshot, from the Finder in Mac OS X, the base Drupal directory is on the far left. Inside of sites/all/modules, we have created a new directory called jsloader. I have included an arrow to point to the exact location of the jsloader directory in this screenshot.

Note

You might notice the views and views_datasource modules in the same directory. We installed these two modules in the previous chapter. We used the potx module in Chapter 5, and js_theming was covered in Chapter 6.

Next, we will add a simple file to that directory.

A JavaScript sample

We want a simple script to test. This is certainly not the type of thing we would use on a public web site. But it will let us know whether or not our loader is working.

We will put this script in the jsloader/ directory we just created, and we'll name it jsloader.js. Here are the contents of that file:

// $Id$
/**
* Verify that the autoloader is working.
* @file
*/
jQuery(document).ready(function() {
alert("JS Loader is ready.");
});

This script is going to have the annoying characteristic of opening a new alert dialog box every time a page is loaded on our site. For our purposes, it does exactly what we want. It lets us know when the script is being loaded, and we don't even have to check the HTML source.

So far, we have only one file in our jsloader/ directory, the test script. As things stand, Drupal won't even recognize this as a module yet. First, we need to create a .info file.

The module's .info file

The purpose of the .info file is to provide Drupal with some basic information about our module. An info file is always named with the module's name, so ours will be jsloader.info. Also, it must be stored inside of our module's directory.

Here's a very basic .info file for our new module:

; $Id$
name = JS Loader
description = "Load JavaScript files"
core = 6.x

This file only has four lines.

The first line is a comment (note the leading semicolon (;)) with the $Id$ tag we discussed in Chapter 2. It serves the same purpose here as it does in a theme's .info file. It is a placeholder for CVS versioning information that will be inserted when code is checked into the CVS repository at Drupal.org.

Next is the name directive, whose value is a human-readable name for this module. This field is used to display the module's name in the administration interface.

The third line is the description directive, which contains a short description about what the module does. The one above is enclosed in quotation marks. The quotation marks are not required if the description fits on one line. Only when values require more than one line of text do you need quotation marks.

The final line, core, indicates the minimum version of Drupal required by this module. 6.x indicates that this module should run on any Drupal 6 system.

There are a handful of other directives that can be added to a .info file. Some of these are discussed in my book "Learning Drupal 6 Module Development", Packt Publishing, 978-1847194442, and all are documented in the release notes for Drupal 6.

A custom addition

We will add one more line to our basic .info file. This line won't be used by Drupal, but will be used by our new module:

scripts[] = jsloader.js

This fifth line indicates that our module should include the jsloader.js file as a script sent to the client. In a few moments, we'll see how this is used. For now, it is only important to note that this is not a standard directive for a .info file. Drupal will simply ignore it.

We now have the test script and the .info file. The last file to create is the .module file.

The .module file

In the same directory where we put our other two files, we will now create the .module file. As with the .info file, the .module file must be named after the module. For the jsloader module, the file name must be jsloader.module.

Note

This file will contain PHP code. However, it does not have the .php extension. If you are using an IDE to edit your code, you may need to manually set it to use PHP syntax on the .module files.

While the file is only 27-lines long, we will dwell on it for a while, explaining various aspects of Drupal's behavior as we go.

Here is the .module file in its entirety:

<?php
// $Id$
/**
* A module to load JS files.
*/
/**
* Implementation of hook_help().
*/
function jsloader_help($path, $args) {
if ($path == 'admin/help#jsloader') {
return t('JS Loader loads JavaScript files based on the
contents of the .info file.');
}
}
/**
* Implementation of hook_init().
*/
function jsloader_init() {
$path = drupal_get_path('module', 'jsloader');
$info = drupal_parse_info_file($path . '/jsloader.info');
foreach ($info['scripts'] as $script) {
drupal_add_js($path . '/' . $script);
}
}

Taking a quick look at this file. We can see the following:

  • It begins with a<?php tag to indicate that this is a PHP file.

  • Next comes a comment (//) to hold the $Id$ CVS keyword.

  • After that is the main documentation block for the file.

  • There are two functions in this file: jsloader_help() and jsloader_init(), each has its own documentation block.

Note

Although the file begins with a<?php, it does not end with the customary ?> tag. Why? For library files such as this, the closing tag is optional and can actually lead to application-breaking mistakes. Drupal conventions recommend omitting the closing tag for this reason.

In Chapter 2, we discussed coding standards, which included spacing, commenting, and the general structure of code. We will not repeat that discussion, but there are two things to note about the way PHP code is generally structured in Drupal.

First, most Drupal PHP code is procedural, not object-oriented. Functions are defined in the global namespace and are not usually assigned to objects. This will change in Drupal 7, which will be object-oriented to a greater extent.

Second, function names and variable names are always lowercase, with words separated by underscores. Camel case (myVariable) is never used in Drupal's procedural code.

With these few notes addressed, we are ready to look at the first function, jsloader_help().

The jsloader_help() function

The purpose of this first function, jsloader_help(), is to provide some content for the Drupal built-in help system. Always provide a help function for your module, even if it does no more than provide basic information about what the module does. A little help is better than no help.

Admonitions aside, let's explore the code, starting with the comment. Believe it or not, this one line comment opens the door for a discussion of Drupal's most powerful feature:

/**
* Implementation of hook_help().
*/
function jsloader_help($path, $args) {
if ($path == 'admin/help#jsloader') {
return t('JS Loader loads JavaScript files based on the
contents of the .info file.');
}
}

This comment says Implementation of hook_help(). What does it mean?

Drupal uses a very powerful system, called the hook system, to give custom modules the ability to strategically interact with the Drupal Core system. Here's how it works. When a client requests a page, Drupal begins processing the request by walking through a long series of steps. It begins by loading the necessary code, and ends with the sending of HTML (and other files) to the client.

At specific points during this process, Drupal checks to see if any modules need to "hook into" Drupal and do some processing of their own. If Drupal finds any modules that respond to the current state, it will call them and temporarily hand over control to them. These modules do what they need to do (even modify Drupal's data) and then return control to Drupal.

This whole procedure is so fundamental to Drupal, that even foundational pieces of Drupal, such as the node, user, and comment systems, are implemented using modules and hooks.

That's a high-level explanation. Here's how it works in practice. Drupal defines certain callback points. These are points where it will turn over control to modules. When one of these points is reached, a hook is called. A hook is simply a pre-defined pattern for a function name. Usually, these are represented with a function call such as hook_help(). In actuality, Drupal checks each module for a function that follows a pattern. It looks for a function of the form<modulename>_help(), where<modulename> is replaced with the name of the module. If it finds such a function, it executes it.

For example, when the help page is loaded, Drupal goes through each module looking for a hook_help() implementation. In the node module, it looks for node_help(). In comment, it looks for comment_help(), and in jsloader it will look for jsloader_help().

There are dozens of hooks defined in the Drupal 6 Core. Module developers can create their own hooks, effectively passing control off to different modules! As you may guess, the module system provides potentially infinite extensibility.

Note

Hooks for all of the core modules are documented at http://api.drupal.org/api/group/hooks/6.

With the hook system in mind, we can take another look at how modules function. Essentially, a module implements hook functionsit defines functions that follow hook patterns, and so will be called by Drupal. Therefore, a module can be very specific, containing only the code necessary to add the desired features at the desired places.

That is why we can create our module in only a few dozen lines of code. We are implementing two hooks, and each will be called only at the desired time in the life cycle of a Drupal request.

Hooks are the very heart of Drupal PHP programming. They make it easy to write simple, yet functional code.

Note

Why aren't hooks used in Drupal JavaScript?

If hooks are so powerful, one might ask why they aren't used in the JavaScript libraries. In fact, the same strategy is used in JavaScript (though we often use anonymous functions instead of writing hook functions). The JavaScript event model, and even the Drupal theme and behavior systems, use callback-driven logic. However, because JavaScript is object-oriented, much of the pattern-based method invocation can be omitted. We can simply pass in objects that implement certain patterns (or as we do with behaviors and themes, attach functions to already existing objects). Drupal will then call these at the appropriate time.

Like user interface programming, Drupal hooks work on an event-like model. Certain conditions arise, and hooks are called. For example, when a user loads a help page, the hook_help() implementations are called. Each module is then expected to determine (based on context) whether or not it should return help information:

function jsloader_help($path, $args) {
if ($path == 'admin/help#jsloader') {
return t('JS Loader loads JavaScript files based on the
contents of the .info file.');
}
}

A hook_help() implementation will be passed two arguments: $path and $args. The first contains the path that was requested by the user, and the second contains an array corresponding to the components in the path. Imagine it as the results of splitting the $path at each slash (/) character.

For our help function, we only want to handle the case where a general overview of the module is requested. The path for general help will always be of the form admin/help#<modulename>.

This function will only return help text when the path is admin/help#jsloader. While our help text can be as long as we need, our demonstration module will just get a simple string description:

return t('JS Loader loads JavaScript files based on the
contents of the .info file.');

Normally, the code above would all be on one line, without the backslash.

Notice that our help text is passed through the t() function. This is the PHP equivalent of the Drupal.t() translation function we saw a few chapters ago. It will translate the string when necessary.

That's all there is to the function. If help text is requested for our module, this simple description will be returned. If we enable the module in Administer | Site building | Modules, then go to Administer | Help, and then click on JS Loader, we should see something like this:

We have now successfully implemented our first hook. Our module only needs one more before we finish.

The jsloader_init() function

The first hook we implemented printed the help text. This second one will do the rest of the work for our module. It will read the module's .info file and load any JavaScript files specified in the scripts[] directive. For example, when we created our jsloader.info file, we added a line to the bottom that looked like this:

scripts[] = jsloader.js

Drupal will simply ignore this directive when it parses the .info file. However, we can make use of it.

To do so, we are going to implement another hook: hook_init(). This hook is executed near the beginning of a page request, after Drupal has loaded all of the core libraries, and then loaded the modules. However, it is called before any HTML is sent back to the client.

We want to hook into the Drupal request cycle early. This allows us to add the JavaScript files to the HTML's head section (inside the<head></head> tags).

The hook_init() hook is easy to implement. It takes no arguments and nothing needs to be returned. To implement the hook_init() function, we simply create jsloader_init(), a function that follows the hook naming convention:

/**
* Implementation of hook_init().
*/
function jsloader_init() {
$path = drupal_get_path('module', 'jsloader');
$info = drupal_parse_info_file($path . '/jsloader.info');
foreach ($info['scripts'] as $script) {
drupal_add_js($path . '/' . $script);
}
}

This function reads the .info file, and then load all of the scripts that the info file points to. Both of these functions will require us to know the path to the module. Fortunately, Drupal can compute this for us.

On the first line, we retrieve the path to our module and then assign this information to the variable $path. The drupal_get_path() function takes two arguments. The first is the type of path we want to get. The string module indicates that we are interested in a module directory. In contrast, to get a theme path, we would use the string theme.

The second parameter is the module whose path we want. We are interested in our own module's path, so we pass jsloader to the function.

Now that we have the base path to our module, we can load the .info file. Again, this is a simple task because Drupal provides a function for reading and parsing this file.

The drupal_parse_info_file() takes the full path to the .info file as its parameter. We build this by concatenating the $path and'/jsloader.info'. Drupal will then read the file, parse it, and return an array.

Note

PHP arrays

In JavaScript, we made frequent use of arrays and object literals. PHP's array-type functions are a combination of these two. That is, it may contain a list of values with numeric indexes or key/value pairs.

The array returned is an associative array that, if done in JavaScript, would look something like this:

{
'name' : 'JS Loader',
'description': 'Load JavaScript files',
'core': '6.x',
'scripts': ['jsloader.js']
}

Items in the .info file, that have square brackets in the name, are converted to arrays. Thus, scripts[] is transformed into a key, scripts, and it has an array for a value. Each time the scripts[] directive is parsed, its contents are appended to the array. If we had two scripts[] directives, the scripts entry would look like this:

'scripts': ['jsloader.js', 'other.js']

The results of the parsed .info file are stored in the $info variable. We know that the scripts[] entries are now stored in the $info array as the name scripts and an array of script file names. What we want to do next is loop through $info['scripts'] and tell Drupal to add each script to the output.

In PHP, this can be done conveniently with a foreach loop, which will iterate through each item in an array, temporarily assigning each item to a variable.

A foreach loop follows the pattern foreach ($array as $array_item) {}. In each iteration, the $array_item will contain the value of the current item in the array:

foreach ($info['scripts'] as $script) {
drupal_add_js($path . '/' . $script);
}

For each script[] item added in the .info file, this will use the drupal_add_js() function to add that JavaScript file.

The drupal_add_js() function is multifunctional. It can be used three ways:

  1. To add a JavaScript file to the head of the generated HTML.

  2. To add inline JavaScript to the output, inside of<script></script> tags. This will also go in the HTML head.

  3. To add an individual setting to the Drupal.settings array. We have seen the Drupal.settings.basePath variable in previous chapters. This extends that same object.

Unfortunately, the arguments are different in each case and therefore mean different things. Here, we have used the simplest case as we are simply adding a file. The syntax for doing this from within a module is drupal_add_js($filename). As we can see, the drupal_add_js() function needs to know the path to the filename.

In the above code snippet, this loads each script file.

In the next project, we will see how drupal_add_js() can be used to add settings.

Note

The three uses of this function are documented in the Drupal API at http://api.drupal.org/api/function/drupal_add_js/6.

That is all there is to our loader. Now, every script that we add to the .info file using the scripts[] directive will automatically be loaded on every page request. At the beginning of this project, we created a module that fired an alert() every time a page loaded. Now that our module is complete, we should be able to test the loader by loading a page on our site. Here's a screenshot of what that should look like:

If the alert box pops up, we know that our script is being loaded.

The module we just created provides a way for integrating JavaScript files into Drupal, without relying on a theme, and without requiring any additional code. With this single module, we could add all of the scripts created since Chapter 3. The one in Chapter 2 relied a little too heavily on a template file to be used outside of a theme. No additional PHP would be necessary.

On the other hand, this module doesn't exploit the possibility of using PHP code to inform JavaScript. In the next project, we will see how we can use PHP code in modules to pass information on to a JavaScript tool.

Project: the editor revisited

In the previous project, we created a simple module to automatically load any JavaScript files indicated in the .info file. In this project, we are going to create a module that provides features for a specific JavaScript tool.

This project will improve the Simple Editor project we did in Chapter 4. Here, we will package an editor as a module, making it easy to use in many themes. We'll also make a little more use of PHP as a way of passing configuration options from the server to the client.

The goal of this project is to illustrate server-side PHP code can be used to generate JavaScript for the client. Here's a theme you may notice as we go: There is more than one way to write code like this, and in some cases, it's hard to tell which is preferable. We will run into two specific cases during our coding where we will need to make decisions about how something ought to be done.

We are going to make some improvements on the Simple Editor, add a few features, and rewrite some parts in light of what we now know. We will call our new editor Better Editor.

When we wrote our Simple Editor back in Chapter 4, we had not yet learned about themes. Instead of using theme functions to edit our code, we simply built strings where needed. Here, we are going to progress to theme functions.

Also, Simple Editor had only a few buttons, and adding more buttons required laborious re-writing of the code. Several things needed to be changed for each new button.

In Better Editor, we are going to change the way buttons are created. We will make it possible for the server to dictate what buttons get created. This means we will be writing a little extra PHP code for this section. Once again, it is simple code. Even if you are new to PHP, this code should be easy to follow.

Now, let's create our module.

First step: creating the module

Just as with the previous module, we will begin by creating a module directory, bettereditor, in sites/all/modules/. We will create two files: the .info file and the .module file.

Here's what bettereditor.info looks like:

; $Id$
name = Better Editor
description = Provide a simple editor for HTML.
core = 6.x
php = 5.2

One again, we set a name and description. We have also indicate the version of Drupal Core that this module is designed to work with. We also add one new directive, php = 5.2, at the end. This directive tells Drupal that the module will only work on servers running PHP 5.2 or later.

While Drupal 6 still runs on PHP 4, it is not advised to run anything on PHP 4. It is now unsupported and is no longer maintained. Instead, PHP 5.2 or later should be used on any server. In fact, Drupal 7 will require at least PHP 5.2.

Our module will make use of a function introduced in PHP 5.2. Therefore, we added this last directive to let Drupal know that an earlier version of PHP will not work for this module.

Note

On rare occasions, I have heard programmers argue that it is "bad practice" to not support PHP 4, since the Drupal Core supports it. This is not the case. PHP 4 has long been deprecated and unsupported. There is no good reason to write applications for it. By noting in the configuration file that our module requires PHP 5.2, we are abiding by good coding practices.

That is all we need for our .info file. At this point, if we were to look in Administer | Site building | Modules, we would see our module listed there.

The next thing to do is create bettereditor.module. This also, goes in sites/all/modules/bettereditor/. As you may recall, this is the file that will store our PHP code.

But before we open that file for editing, we will create two more files. These files aren't required by all modules, but our particular module will need them.

The first is a CSS file, and the second is our JavaScript library. These will be called bettereditor.css and bettereditor.js, respectively. They will go in the bettereditor/ directory along with our .info and .module files.

We now have four files in our module. The simplest one, bettereditor.info, is already done. Let's take a quick look at the CSS file.

The CSS file

The bettereditor.css file provides a few definitions that will be used to style the editor. To be more precise, it contains four class definitions:

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

The first two define how the buttons on our editor should look. The third class definition defines how our button bar should look. The last one, .strikethrough, is intended to be applied to text-containing elements. It will display the text as having a single line through it. We will use this last class to demonstrate an additional feature of our new editor.

That's all there is to our CSS file. Next up, we will turn to the .module file.

The bettereditor.module file

Our new .module file will implement the same pair of hooks that the previous module used, which are hook_help() and hook_init():

<?php
// $Id$
/**
* A better version of the simple editor.
* @file
*/
/**
* Implementation of hook_help().
*/
function bettereditor_help($path, $args) {
if($path == 'admin/help#bettereditor') {
return t('This module provides a JavaScript based text \
editor.');
}
}
/**
* Implementation of hook_init().
*/
function bettereditor_init() {
$buttons = array();
$buttons[] = array(
'name' => 'B',
'tag' => 'strong',
'style' => 'font-weight: bold',
);
$buttons[] = array(
'name' => 'I',
'tag' => 'em',
'style' => 'font-style: italic',
);
$buttons[] = array(
'name' => 'S',
'tag' => 'del',
'cssClass' => 'strikethrough',
'style' => 'text-decoration: line-through',
);
$buttons[] = array(
'name' => 'ul',
'tag' => array('ul', 'li'),
);
$buttons[] = array(
'name' => 'ol',
'tag' => array('ol', 'li'),
);
$buttons[] = array(
'name' => 'li',
'tag' => 'li',
);
$buttons[] = array(
'name' => 'table',
'tag' => array('table', 'tbody', 'tr', 'td'),
);
$buttonJS = json_encode($buttons);
$script = 'BetterEditor.buttons = ' . $buttonJS;
$path = drupal_get_path('module', 'bettereditor');
drupal_add_css($path . '/bettereditor.css');
drupal_add_js($path . '/bettereditor.js');
drupal_add_js($script, 'inline');
}

The help function, bettereditor_help(), does the same thing here as it did in the previous project. It provides very basic help that will be shown on the help screen for this module. Again, when building a production-quality module, it is a good idea to write longer, more descriptive help text.

The more important part of our module is the bettereditor_init() function. As in jsloader_init() in the previous project, this function implements Drupal's hook_init() hook. This means it will be called toward the beginning of the page-rendering cycle, right after Drupal has finished its own initialization.

Here, we will insert all of the necessary code to load our JavaScript. In our new editor, this will be a little more complex than our previous example.

We will do two things in this function. First, we will define all of the buttons that our editor should have. We will then add the appropriate JavaScript library (bettereditor.js) and export our button properties.

Once Drupal has executed our hook, which is called at the beginning of every page load, it will continue with the rest of the processing and finally deliver the finished HTML document to the user. Our goal in using hook_init(), is to make this script available to all pages, since we don't know which pages will have text areas and which won't.

Let's look at the first part of the bettereditor_init() function:

function bettereditor_init() {
$buttons = array();
$buttons[] = array(
'name' => 'B',
'tag' => 'strong',
'style' => 'font-weight: bold',
);
$buttons[] = array(
'name' => 'I',
'tag' => 'em',
'style' => 'font-style: italic',
);
$buttons[] = array(
'name' => 'S',
'tag' => 'del',
'cssClass' => 'strikethrough',
'style' => 'text-decoration: line-through',
);
$buttons[] = array(
'name' => 'ul',
'tag' => array('ul', 'li'),
);
$buttons[] = array(
'name' => 'ol',
'tag' => array('ol', 'li'),
);
$buttons[] = array(
'name' => 'li',
'tag' => 'li',
);
$buttons[] = array(
'name' => 'table',
'tag' => array('table', 'tbody', 'tr', 'td'),
);

This is a big block of very repetitive code. What's going on here?

We are building a nested series of arrays. The $buttons variable contains an array and each position in that array, contains another array. In other words, this is a two-dimensional array (in fact, it is deeper in some places).

In the previous project, I mentioned that PHP arrays can be either numerically indexed arrays ($array[0], $array[1], $array[2]...) or associative arrays with keys and values ($array['key1'], $array['key2']...). In this example, we have both.

The $buttons array is numerically indexed. Here, we do array assignments like this:

$buttons[] = 'something';

This has the practical effect of pushing a new value to the end of the array, similar to the JavaScript practice of appending values like this: myArray.push('new value').

Inside each array item in this array, we build a new associative array. The first entry looks like this:

$buttons[] = array(
'name' => 'B',
'tag' => 'strong',
'style' => 'font-weight: bold',
);

The array() function in PHP works as an array constructor of sorts. It builds a new array.

Note

Unlike their JavaScript counterparts, PHP arrays are not objects. Arrays are a special type in PHP.

The array() function can be initialized with a set of data. Here, we are initializing an associative array with a list of keys and values. The syntax is'key' => 'value', and each pair is separated by a comma.

Associative arrays in PHP can be used for most of the purposes that JavaScript object literals are used. In that respect, you might think of the "first entry" we just saw as a PHP equivalent of a JavaScript structure such as this:

var buttons = [
{
'name': 'B',
'tag': 'strong',
'style': 'font-weight: bold'
}
]

While these two constructs are certainly not identical (JavaScript is much more object-oriented than PHP), the basic analogy certainly works as a heuristic when mentally translating between PHP and JavaScript.

Note

When defining arrays in PHP, it is OK to leave a trailing comma after the last pair or item. In fact, in Drupal, this is a recommended practice because it reduces errors when new array items are coded in at the end of a list.

In this snippet, we add a new array to the first index of the $buttons array. This new associative array contains three elements:

'name' => 'B',
'tag' => 'strong',
'style' => 'font-weight: bold',

Basically, we have invented a data structure that describes our buttons. The data structure is designed to tell us how the button should look and what tag the button should represent.

So in this case, the name shown on the button will be B. The tag that will wrap selected text will be strong. This will be translated to<strong></strong> by our JavaScript. Finally, we might want to style the button a little, so we add the style property to make the B on the button show as bold text.

Some of the other elements in this array use different properties:

$buttons[] = array(
'name' => 'S',
'tag' => 'del',
'cssClass' => 'strikethrough',
'style' => 'text-decoration: line-through',
);

This button also has a class set to strikethrough. We're going to use this to add a class, not to the button, but to the tag before it is inserted. The above definition should create an S button that will, when clicked, insert tags such as this:<del. class='strikethrough'></del>.

In our bettereditor.css file, we defined a .strikethrough class. This is the item for which that class is used.

We will now add a new feature to our editor. So far, we've created buttons that can insert a single tag. What about cases where we want to add multiple tags with the click of a button?

Let's extend our tag attribute, allowing it to have an array of values. Here are a few examples:

$buttons[] = array(
'name' => 'ul',
'tag' => array('ul', 'li'),

);
$buttons[] = array(
'name' => 'ol',
''tag' => array('ol', 'li'),

);
$buttons[] = array(
'name' => 'li',
'tag' => 'li',
);
$buttons[] = array(
'name' => 'table',
''tag' => array('table', 'tbody', 'tr', 'td'),

);

The highlighted rows show cases where more than one tag is provided. We want our editor to automatically nest the tags, with the left-most tag being the outermost tag. Then, the first unordered list definition should create tags like this:

<ul>
<li></li>
</ul>

Likewise, the tag for table should render like the following. It has table, tbody, tr, and td:

<table>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>

This data structure describes how we would like our buttons to look and function. It will be the responsibility of the JavaScript code to take these definitions and interpret them. But before we can turn to the JavaScript, we have a few more items to work on in PHP.

Namely, we need to take the $buttons array and translate it into JavaScript. We then need to have that data, and the bettereditor.js and bettereditor.css files, sent back to the client. Let's take a look at the last part of the bettereditor_init() function to see how this is done:

$buttonJS = json_encode($buttons);
$script = 'BetterEditor.buttons = ' . $buttonJS;
$path = drupal_get_path('module', 'bettereditor');
drupal_add_css($path . '/bettereditor.css');
//drupal_add_js(array('buttons' => $buttons), 'setting');
drupal_add_js($path . '/bettereditor.js');
drupal_add_js($script, 'inline');

At this point, we have two interesting choices to make.

Earlier, I mentioned that there were three ways of calling drupal_add_js(). Already, we have seen how it can be called to include an entire script file. It can also be used to inject scripts, either in whole or in part, or to add settings to the Drupal.settings object.

Right now, we have the buttons defined in $buttons. Should these be added to the Drupal.settings object or sent as a script fragment? Technically speaking, either one can be done.

Adding things to the Drupal.settings array has the advantage of being easier to code (by a few characters). Modules, such as JavaScript Theming, make liberal use of Drupal.settings. But this is not actually a good idea most of the time.

Earlier in the book, we talked about namespaces. The chief rationale for namespacing applies even here. If we just add our settings to Drupal.settings, we may eventually encounter problems where other JavaScript files which use settings with the same name. This will lead to conflicts as two different libraries will try to store their data in the same place. The Drupal API docs (http://api.drupal.org/api/function/drupal_add_js/6) have this to say on the matter:

You might want to wrap your actual configuration settings in another variable to prevent the pollution of the Drupal.settings namespace.

Is the suggestion that we ought to wrap settings in another variable and then use Drupal.settings (creating something like Drupal.settings.BetterEditor.buttons), or that we should use an altogether different variable?

The first option is not good. Frankly, repartitioning a namespace like this is silly. We've now created another namespace inside of a namespace, and for what purpose?

In addition to this, calling the function is even more difficult. That's because we have to add another layer of testing to our array. Our array would look more like this:

$buttons = array(
'BetterEditor' => array(
'buttons' => array(
array(
'name' => 'B',
'tag' => 'strong',
// ...
),
array(
'name' => 'I',
// ...
),
// and more buttons....
),
);

Gaining another layer of complexity on the server isn't the only place. When this is encoded as JavaScript, we now have to reference the settings with unwieldy calls like this:

alert(Drupal.settings.BetterEditor.buttons[0].name);

This does nothing to improve the readability of the code and, given the additional typing we must do to call the variables, increases the probability of typos.

It doesn't really make sense to add all of this complexity just to work with the Drupal.settings object.

Instead, let's look at the third form of drupal_add_js(). Let's add a script inline. The idea here is to pass a valid snippet of JavaScript code into drupal_add_js(). It takes on the responsibility of wrapping this inside of the<script></script> tags, and placing it in the document (after all of the other scripts).

This does put a little more of the scripting burden back on you, the coder. But if used correctly, it has the advantage of reducing code complexity on both the client and server side.

This is the approach taken in our code:

$buttonJS = json_encode($buttons);
$script = 'BetterEditor.buttons = ' . $buttonJS;
$path = drupal_get_path('module', 'bettereditor');
drupal_add_css($path . '/bettereditor.css');
drupal_add_js($path . '/bettereditor.js');
drupal_add_js($script, 'inline');

Here, we need to accomplish the following:

  • Turn our big $buttons array into a valid JavaScript fragment

  • Include the CSS and the JavaScript file

  • Include our script

The order of these last two is not really important. Drupal will sort things out in a particular order. It will always order things with CSS first, then JavaScript libraries, and then custom scripts.

First, we should ask ourself this question. How do we translate $buttons into JavaScript?

It turns out that we have two options again. First, there is a Drupal built-in function, drupal_to_js(), that takes an array and converts it to JavaScript. Second, since we are using PHP 5.2, there is a JSON encoding function, json_encode(), which can convert PHP data structures to JSON data.

The JSON notation is identical to the JavaScript literal notation. In other words, json_encode() will create valid JavaScript.

How do we choose?

There are three key differences between the two:

  1. drupal_to_js() is available in all platforms that Drupal 6 supports . json_encode() requires PHP 5.

  2. json_encode() is implemented in C code, which means it is very fast. drupal_to_js() is written in PHP and is not terribly efficient.

  3. drupal_to_js() does an extra encoding routine. It encodes HTML-reserved characters (<.>, and&) .json_encode() adheres strictly to the standard and does not do this encoding.

Note

In Drupal 7, drupal_to_js() has been re-implemented using json_encode(). When Drupal 7 is released, the last point will be the only difference.

Choosing the function depends on your needs. We don't need the extra encoding (that would just make one more decoding run on the client), and the speed improvement is nice to have. So I have chosen to use json_encode(). But again, this argument can go either way.

With two lines, we can generate a script suitable for sending to the client:

$buttonJS = json_encode($buttons);
$script = 'BetterEditor.buttons = ' . $buttonJS;

On the second line, we simply create JavaScript that will assign BetterEditor.buttons the value of our JSON-encoded $buttons array. The variable $script will hold the JavaScript data.

We can now add the CSS and JavaScript in three calls:

drupal_add_css($path . '/bettereditor.css');
drupal_add_js($path . '/bettereditor.js');
drupal_add_js($script, 'inline');

The drupal_add_css()function does the same thing for CSS files, that drupal_add_js() does for JavaScript.

The first drupal_add_js() call adds the bettereditor.js script, which we have not yet created.

The last line adds the BetterEditor.buttons data as an inline script. Note that the string inline, which is passed in as the second parameter, tells Drupal to include this as an inline script, instead of interpreting $script as a file name.

If we were to enable the module (you can do this now) and then load a page, we would see something like this in the source code:

<script type="text/javascript">
BetterEditor.buttons = [
{"name":"B","tag":"strong","style":"font-weight: bold"},
{"name":"I","tag":"em","style":"font-style: italic"},
{"name":"S","tag":"del","cssClass":"strikethrough",
"style":"text-decoration: line-through"},
{"name":"ul","tag":["ul","li"]},
{"name":"ol","tag":["ol","li"]},{"name":"li","tag":"li"},
{"name":"table","tag":["table","tbody","tr","td"]}
]
</script>

This array of object literals is the result of running json_encode() over the PHP array. When our script loads and is executed, the previous data will all be available to it.

That's all there is to our module file. The last thing to do is work on the bettereditor.js script.

The bettereditor.js script

The last file of the project is the bettereditor.js script. This file will contain a modified version of the simpleeditor.js script we created in Chapter 4. Since we won't go over the repeated pieces, you may find it helpful to skim the relevant section of that chapter again.

The script has undergone a few changes. First, the namespace object is now BetterEditor instead of SimpleEditor. Second, there are three new theming functions. Third, the internals of a few functions have changed to accommodate the new server-provided BetterEditor.buttons data.

Let's start with a quick glance at the code:

// $Id$
/**
* A better version of the simple editor.
* @file
*/
var BetterEditor = BetterEditor || {};
BetterEditor.selection = null;
/**
* Record changes to a select box.
*/
BetterEditor.watchSelection = function () {
BetterEditor.selection = Drupal.getSelection(this);
BetterEditor.selection.id = $(this).attr('id');
};
/**
* Attaches the editor toolbar.
*/
Drupal.behaviors.editor = function () {
$('textarea:not(.editor-processed)')
.addClass('editor-processed')
.mouseup(BetterEditor.watchSelection)
.keyup(BetterEditor.watchSelection)
.each(function (item) {
var txtarea = $(this);
var txtareaID = txtarea.attr('id');
var buttons = [];
for (var i = 0; i < BetterEditor.buttons.length; ++i) {
button = BetterEditor.buttons[i];
buttons.push(Drupal.theme('button', button));
}
var id = 'buttons-' + txtareaID;
var bar = $(Drupal.theme('buttonBar', buttons, id));
$(bar).insertBefore('#' + txtareaID)
.children('.editor-button')
.click(function () {
var txtareaEle = $('#' + txtareaID).get(0);
var sel = BetterEditor.selection;
if (sel.id == txtareaID && sel.start != sel.end) {
var buttonName = $(this).html();
var targetButton = null;
for (i = 0; i < BetterEditor.buttons.length; ++i) {
if (BetterEditor.buttons[i].name == buttonName) {
targetButton = BetterEditor.buttons[i];
break;
}
}
if (targetButton) {
txtareaEle.value = BetterEditor.insertTag(
sel.start,
sel.end,
targetButton,
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.
*/
BetterEditor.insertTag = function (start, end, tag, value) {
var front = value.substring(0, start);
var middle = value.substring(start, end);
var back = value.substring(end);
var formatted = Drupal.theme('addTag', tag, middle);
return front + formatted + back;
};
/**
* Theme a button bar.
*
* @param buttons
* Array of buttons that should be added.
* @param id
* ID for the button bar. This is used to distinguish button
* bars on screens where there are multiple editors.
*
* @return
* Themed button bar as a string of HTML.
*/
Drupal.theme.prototype.buttonBar = function (buttons, id) {
var buttonBar = $('<div class="button-bar"></div>')
.attr('id', id);
jQuery.each(buttons, function (i, item) {
buttonBar.append(item);
});
return buttonBar.parent().html();
};
/**
* Theme an individual button.
*
* @param button
* Individual button object.
*
* @return
* Themed button HTML as a string.
*/
Drupal.theme.prototype.button = function (button) {
var tag = $('<div class="editor-button"></div>');
if (button.style) {
tag.attr('style', button.style);
}
return tag.html(button.name).parent().html();
};
/**
* Theme a tag before inserting it into the text area.
* This wraps text inside of the appropriate tags. If the
* button object contains an array of tags, then the tags
* will be nested, with the text in the innermost tag.
*
* @param button
* Button object that describes what the button does.
* @param text
* Text that the tag will be wrapped around.
*/
Drupal.theme.prototype.addTag = function (button, text) {
var tag = null;
if (button.tag instanceof Array && button.tag.length > 0) {
var placeholder = $('body')
.append('<div class='placeholder'></div>')
.children('.placeholder').hide();
var current = placeholder; // Copy for working with
jQuery.each(button.tag, function (i, data) {
var newTag = '<' + data + '>\n</' + data + '>\n';
current.append(newTag);
if (button.cssClass) {
current.addClass(button.cssClass);
}
current = current.children();
if (i == button.tag.length -1) {
current.html(text);
}
});
var html = placeholder.html();
placeholder.remove();
return html;
}
else {
tag = $('<' + button.tag + '></' + button.tag + '>');
if (button.cssClass) {
tag.addClass(button.cssClass);
}
return tag.html(text).parent().html();
}
};

This is the largest chunk of code we have seen in any single project. Fortunately, much of it is repeated from our earlier project.

Other than the transition from SimpleEditor to BetterEditor, the first part of the code has not changed. For that reason, we will skip the opening definitions and the BetterEditor.watchSelection() function, which simply track what part of the text area is selected.

The editor() behavior

We will begin with the behavior Drupal.behaviors.editor(). This function is called when the document is ready, as well as any time a major DOM change results in behaviors being reattached:

Drupal.behaviors.editor = function () {
$('textarea:not(.editor-processed)')
.addClass('editor-processed')
.mouseup(BetterEditor.watchSelection)
.keyup(BetterEditor.watchSelection)
.each(function (item) {
var txtarea = $(this);
var txtareaID = txtarea.attr('id');
var buttons = [];
for (i = 0; i < BetterEditor.buttons.length; ++i) {
button = BetterEditor.buttons[i];
buttons.push(Drupal.theme('button', button));
}
var id = 'buttons-' + txtareaID;
var bar = $(Drupal.theme('buttonBar', buttons, id));
$(bar).insertBefore('#' + txtareaID)
.children('.editor-button')
.click(function () {
var txtareaEle = $('#' + txtareaID).get(0);
var sel = BetterEditor.selection;
if(sel.id == txtareaID && sel.start != sel.end) {
var buttonName = $(this).html();
var targetButton = null;
for (i = 0; i < BetterEditor.buttons.length; ++i){
if (BetterEditor.buttons[i].name == buttonName){
targetButton = BetterEditor.buttons[i];
break;
}
}
if (targetButton) {
txtareaEle.value = BetterEditor.insertTag(
sel.start,
sel.end,
targetButton,
txtareaEle.value
);
}
sel.start = sel.end = -1;
}
});
});
};

The main jQuery chain has not changed:

$('textarea:not(.editor-processed)')
.addClass('editor-processed')
.mouseup(BetterEditor.watchSelection)
.keyup(BetterEditor.watchSelection)
.each( /* anonymous function here */);

This finds all unprocessed text areas and adds a few things. First, it adds the class to indicate that the textarea has been processed. Next, it adds two event handlers. When a mouse button is released, or when a key on the keyboard is released, the BetterEditor.watchSelection() function will be executed.

Finally, it uses the $().each() function to loop through every textarea and adds the editor.

Let's turn to the function called inside of $().each():

function (item) {
var txtarea = $(this);
var txtareaID = txtarea.attr('id');
var buttons = [];
for (i = 0; i < BetterEditor.buttons.length; ++i) {
button = BetterEditor.buttons[i];
buttons.push(Drupal.theme('button', button));
}

var id = 'buttons-' + txtareaID;
var bar = $(Drupal.theme('buttonBar', buttons, id));

$(bar).insertBefore('#' + txtareaID)
.children('.editor-button')
.click(function () {
var txtareaEle = $('#' + txtareaID).get(0);
var sel = BetterEditor.selection;
if(sel.id == txtareaID && sel.start != sel.end) {
var buttonName = $(this).html();
var targetButton = null;
for (i = 0; i < BetterEditor.buttons.length; ++i) {
if (BetterEditor.buttons[i].name == buttonName) {
targetButton = BetterEditor.buttons[i];
break;
}
}
if (targetButton) {
txtareaEle.value = BetterEditor.insertTag(
sel.start,
sel.end,
targetButton,
txtareaEle.value
);
}

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

The basic job of this function is to attach the editor to the passed-in textarea. The highlighted sections represent the areas where changes have been made.

The function starts by setting up a few variables with information about the target textarea element. After this is a for loop:

var buttons = [];
for (i = 0; i < BetterEditor.buttons.length; ++i) {
button = BetterEditor.buttons[i];
buttons.push(Drupal.theme('button', button));
}

The buttons array, which was declared on the first line, will hold all of the themed buttons. How do we get those? We loop through the values in BetterEditor.buttons (this is the array we created on the server) and pass that data into a theming function. Later, we will see how the Drupal.theme.prototype.button() function themes buttons.

We now have information about the target text areas and a themed list of buttons. The next step is to turn our buttons into a button bar:

var bar = $(Drupal.theme('buttonBar', buttons, id));

This uses another new theme, Drupal.theme.prototype.buttonBar(). This takes a list of buttons and returns a single chunk of HTML which will function as the button bar.

The next thing to do is insert our new button bar into the document immediately before the textarea. This is also done with a jQuery chain:

$(bar).insertBefore('#' + txtareaID)
.children('.editor-button')
.click( /* anonymous function here */ );

This inserts the new button bar, and then adds a click handler to every child of the button bar. What are the children of the button bar? Those are the nodes one-level down (the individual buttons we created before. Therefore, each one of these buttons will be assigned a click event handler.

The click handler has changed since the SimpleEditor version. Let's take a look at this new version:

function () {
var txtareaEle = $('#' + txtareaID).get(0);
var sel = BetterEditor.selection;
if(sel.id == txtareaID && sel.start != sel.end) {
var buttonName = $(this).html();
var targetButton = null;
for (i = 0; i < BetterEditor.buttons.length; ++i) {
if (BetterEditor.buttons[i].name == buttonName) {
targetButton = BetterEditor.buttons[i];
break;
}

}
if (targetButton) {

txtareaEle.value = BetterEditor.insertTag(
sel.start,
sel.end,
targetButton,

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

The code above has been highlighted even more carefully to show the particular lines that were changed.

This function handles the response to a button click. For example, when a user clicks on, the B button, this event handler needs to figure out what button was clicked. Then it takes the appropriate action, which means it finds the selected text and surrounds it with the<strong></strong> tags.

The first if statement in this function determines whether or not a portion of the document has been selected. As you may recall, our initial design for the editor only wrapped HTML around the existing selected text (it did not insert empty tags).

Once inside of the if statement, we need to find out what button was called. To do this, we will match the button's name with the text inside of the button element. Here's why this works.

We know that the bold button will be named B. That is, we know that the text displayed to the user will be B. We also know that this value comes directly from the button object's name property.

So in the click handler function, we can get the HTML value of the button using $(this).html(). This gets stored in buttonName. We can then loop through all of the buttons in BetterEditor.buttons looking for an object with a name equal to the value of buttonName. Using our previous example, if a user clicks on a button with the name B, buttonName would have the value B. As this code loops through BetterEditor.buttons, it will hit the first object:

{"name":"B","tag":"strong","style":"font-weight: bold"},

It has the name B. So that object will be stored in targetButton and the loop will be terminated by the break statement. After all, there's no point continuing to loop through the list of buttons if we have already found a match.

Now that we have identified the correct button object, we can find what tag(s) we should use when inserting that button.

The following part of the code simply checks to make sure a button has been found. There is the possibility that something might go wrong and a bogus button may be displayed on the button bar:

if (targetButton) {
txtareaEle.value = BetterEditor.insertTag(
sel.start,
sel.end,
targetButton,

txtareaEle.value
);
}

If a button object has been found to answer the click event, the BetterEditor.insertTag() function is executed. In the old version, some HTML formatting was passed in the third argument of the function. Now, the target button object is passed.

Let's turn to that function and see what it does.

The insertTag() function

This function is responsible for finding the correct piece of text and inserting the HTML. It is largely unchanged from the Simple Editor version in Chapter 4. However, we will cover some differences that exist here:

BetterEditor.insertTag = function (start, end, tag, value) {
var front = value.substring(0, start);
var middle = value.substring(start, end);
var back = value.substring(end);
var formatted = Drupal.theme('addTag', tag, middle);
return front + formatted + back;

};

As you may recall, this function proceeds by finding the selected text (captured in the middle variable). It then surrounds the selected text with tags, rebuilds the content, and then sends it back to the calling function.

This function has been changed slightly. Instead of building the HTML markup, it passes the button data (stored in the tag variable) to a theming function. The theming function, Drupal.theme.prototype.addTag(), does the actual formatting and returns a formatted string.

BetterEditor.insertTag() simply rebuilds the contents of the textarea and sends the data back to the calling function.

Not much is new in this function. However, the functionality of the addTag theme is surprisingly complex. We have three functions left to look at. Let's begin with Drupal.theme.prototype.addTag().

The addTag() theme

The three theming functions, which we are about to look at, are all new in BetterEditor. We will begin with the most complex.

Drupal.theme.prototype.addTag() is responsible for taking some text and wrapping it in the appropriate HTML. It is passed two arguments. First, the button object for the button that was just clicked. This contains the data passed from the server. The second argument is the text that should be surrounded:

Drupal.theme.prototype.addTag = function (button, text) {
var tag = null;
if (button.tag instanceof Array && button.tag.length > 0) {
var placeholder = $('body')
.append('<div class='placeholder'></div>')
.children('.placeholder').hide();
var current = placeholder; // Copy for working with
jQuery.each(button.tag, function (i, data) {
var newTag = '<' + data + '>\n</' + data + '>\n';
current.append(newTag);
if (button.cssClass) {
current.addClass(button.cssClass);
}
current = current.children();
if (i == button.tag.length -1) {
current.html(text);
}
});
var html = placeholder.html();
placeholder.remove();
return html;
}
else {
tag = $('<' + button.tag + '></' + button.tag + '>');
if (button.cssClass) {
tag.addClass(button.cssClass);
}
return tag.html(text).parent().html();
}
};

To get our bearings before we look at this code, let's look back at the button.tag property that we defined.

Here's the JSON data for the B button:

{
"name":"B",
"tag":"strong",
"style":"font-weight: bold"
},

Now compare that to our table button:

{
"name":"table",
"tag":[
"table",
"tbody",
"tr",
"td"
]
}

The table button has an array of tags, while the B button only has a single tag value. So when a user clicks on the B button, the highlighted text will only be surrounded by one pair of tags:<strong></strong>.

But if the user were to click on the table button, the text should be nested in the middle of this tag set:<table><tbody><tr><td> </td></tr></tbody></table>. The Drupal.theme.prototype.addTag() function that we are now examining is responsible for handling these two cases.

The function begins by making a quick choice based on the type of data inside of the button.tag object:

if (button.tag instanceof Array && button.tag.length > 0) {
// Complex case: Nested tags
}
else {
// Simple case: One tag.
}

If the button.tag object is an array, we should assume that we will be working with nested tags. Otherwise, we will assume that there is only one tag.

Note

If we were building a production quality version of this, we would spend more time evaluating the button.tag object. We might try to streamline the case where the button.tag array has only one item. We might also try to account for cases where no tag data was supplied on the server (an issue that doesn't concern our closed-system example here). But for the sake of brevity, we will cover a simplified case.

Let's quickly dispense with the second case, where button.tag is not an array. In this case, we simply take the value of the tag and create start and end tags out of it. We then wrap that in a jQuery object named tag:

if (button.tag instanceof Array && button.tag.length > 0) {
/* Handle array case */
else {
tag = $('<' + button.tag + '></' + button.tag + '>');
if (button.cssClass) {
tag.addClass(button.cssClass);
}
return tag.html(text).parent().html();

}

If the button.class object is set (recall that the S button had a class property), we add that too. Finally, wrap the tag around the passed-in text, and return a text representation of the HTML.

Let's now turn back to the more complicated case of nested tags:

if (button.tag instanceof Array && button.tag.length > 0) {
var placeholder = $('body')
.append('<div class='placeholder'></div>')
.children('.placeholder').hide();
var current = placeholder; // Copy for working with
jQuery.each(button.tag, function (i, data) {
var newTag = '<' + data + '>\n</' + data + '>\n';
current.append(newTag);
if (button.cssClass) {
current.addClass(button.cssClass);
}
current = current.children();
if (i == button.tag.length -1) {
current.html(text);
}
});
var html = placeholder.html();
placeholder.remove();
return html;

}
else {
tag = $('<' + button.tag + '></' + button.tag + '>');
if (button.class) {
tag.addClass(button.class);
}
return tag.html(text).parent().html();
}

There are a couple of ways to do this sort of tag building. I chose a method that made use of jQuery, and that seemed simple to me.

Let's start with the first few lines:

var placeholder = $('body')
.append('<div class='placeholder'></div>')
.children('.placeholder').hide();

var current = placeholder; // Copy for working with

First, we need a root jQuery object to work with. But we don't have any tags to work with at the beginning. So we just create a pair of tags that we don't really need. However, IE requires that these tags be a part of the document. So we append them to the end of the body, and then hide the div so that it won't disrupt the user experience. Once we have the placeholder, the first thing we do is make a copy of it in current. Why is this done? We are going to use current to traverse down the DOM as we create it, but placeholder will always point to the top-level placeholder node.

Next, we will loop through each of the items in button.tag, create an element, and then set that as the root element. In essence, we are creating the DOM and descending it as we go.

Here's how that happens. The anonymous function is called once for each tag. It begins by constructing a string representing the new tag:

jQuery.each(button.tag, function (i, data) {
var newTag = '<' + data + '>\n</' + data + '>\n';
current.append(newTag);
if (button.class) {
current.addClass(button.class);
}
current = current.children();
if (i == button.tag.length -1) {
current.html(text);
}
});

The newTag variable holds the new tag data, and current points to the current node (starting with the placeholder that we created).

Note

Note that the newTag string has \n line endings encoded into it. This causes the output to be displayed with line breaks and makes it easier to read in an HTML editing environment like the one we are creating.

The newTag is appended to as the last child of current.

Next, if there is a class property on this button, we add the class to the current element.

Note

According to this setup, the same class is added to all of the tags in button.tag. In practice, that is not usually desirable. Alternatives to this may be to ignore the button.class properties for nested tags, or to simply add the class only to the first tag in a nested series.

After this, we reset current to current.children():

current = current.children();

Here, we are changing the jQuery object to point at the new element that we just created. For example, let's take the case of an unordered list. We will begin with a DOM representable like this:

<div class='placeholder'></div>

The current variable points to this element. The first time through the jQuery.each() iterator, the DOM is changed to this:

<div class='placeholder'>
<ul></ul>
</div>

First the tag is added. Next, when current = current.children() is executed, the current node is moved from<div></div> down to the child element<ul></ul>.

The next time through the loop, the new tag<li></li> will be added. It is appended to the end of current, and current points to the<ul></ul> element. The result is a DOM fragment looking like this:

<div class='placeholder'>
<ul>
<li></li>
</ul>
</div>

All we need to do now is add the text. This is easy because we know from the outset how deep the DOM fragment will be, based on the number of things in the button.tag array:

if (i == button.tag.length -1) {
current.html(text);
}

Once our counter (maintained by jQuery.each()) is only one less than the length of the array, we know we are in the innermost tag and can add the text here. This results in a DOM fragment looking something like this:

<div class='placeholder'>
<ul>
<li>Text Here!</li>
</ul>
</div>

We are done creating the DOM fragment. Since we still have our placeholder variable pointing to the main div, we can get our newly marked up text with a call to placeholder.html(). Remember, since $().html() returns only the children of the present node, the current node is not returned. In other words, given the HTML we have in the previous DOM fragment, the following will be returned by placeholder.html():

<ul>
<li>Text Here!</li>
</ul>

The placeholder is not present in the results as only the children of the placeholder were returned. Once we are done with our special-purpose placeholder, we remove it from the DOM using placeholder.remove().

At the end of the Drupal.theme.prototype.addTag() function, a string containing the HTML markup is returned. As this gets passed back to the BetterEditor.insertTag() function, the contents for the textarea are recreated. The new text is returned back to the click event handler, which inserts the text into the textarea. And, voila! The user sees the new HTML wrapped around the selected text as seen here:

We have made it through the most complicated parts. Just two theme functions are left.

The button() theme function

While looking at Drupal.behaviors.editor(), we saw the Drupal.theme.prototype.button() theme get called. That theme is responsible for taking a button object and turning it into an HTML button. In Simple Editor, this was handled by some very generic string concatenation and a little bit of jQuery. We haven't gained much sophistication in moving to themes. The code is simple:

Drupal.theme.prototype.button = function (button) {
var tag = $('<div class="editor-button"></div>');
if (button.style) {
tag.attr('style', button.style);
}
return tag.html(button.name).parent().html();
}

The button object is passed in. We create a jQuery object with the base<div></div> tag. If the button object has a style property, we add a style attribute to the div tag. For example, with the B button, we add style='text-weight: bold' to the div tag.

Finally, we run one last jQuery chain, adding the button name as the text content of the div tag. We then returning the whole thing as a string containing HTML.

Every button is passed through this theme function. Together, all of the buttons are handed over to the Drupal.theme.prototype.buttonBar() function for additional theming.

The buttonBar() theme function

The Drupal.theme.prototype.buttonBar() theme is also very straightforward. It's job is to take an array of buttons and combine them into a single button bar. Since more than one button bar may exist on a page, each button bar needs to have its own ID.

Here's the theme:

Drupal.theme.prototype.buttonBar = function (buttons, id) {
var buttonBar = $('<div class="button-bar"></div>')
.attr('id', id);
jQuery.each(buttons, function (i, item) {
buttonBar.append(item);
});
return buttonBar.parent().html();
}

First, the new button bar jQuery object is created and the ID is appended.

Next, we loop through each of the items in the buttons array and append each one to the button bar.

Finally, the button bar is converted to an HTML string and returned. It is then inserted by our main behavior, Drupal.behaviors.editor(), into the appropriate place in the DOM.

We have made it through our largest project. Although we began with an existing project, there has been a substantial amount of information to cover. However, we have created a second module. A module tied closely to server-generated content.

A last question

There is a question that might arise in regard to this last project. Why involve the server in the process the way that we did? Why not simply create the buttons in JavaScript to begin with?

The point of this project was to exhibit how data could be passed from the server. However, if we wanted to delve into a little more PHP, we could derive even more from server integration. We could, for example, use the Forms API (FAPI) to create an administration form, which would allow system administrators to select the buttons that should appear on a form, or even define their own buttons.

We could also use the input filter logic (which determines what tags are allowed in a given input box) to determine which buttons are displayed to the user.

In short, there are many directions we could go with our server-side development. But we have hit the boundaries defined for this book. Delving into serious PHP programming is beyond our scope. Of course, if you are interested in Drupal 6 PHP development, you might like my book Learning Drupal 6 Module Development, Packt Publishing, 978-1847194442.

Summary

The focus of this chapter was on module development. We set out to use Drupal modules as a way to encapsulate JavaScript functionality. We did this in a way that would be portable across themes and even across Drupal installations. During this chapter, we created two projects. The first was a script autoloader that provided module-side .info file processing for scripts. This essentially replicated the behavior of themes and their .info files.

Our second project was more ambitious. Starting with the code from the Simple Editor that we created in Chapter 4, we created a Better Editor. This editor obtained configuration information from the server, allowing the JavaScript editor to be customized from PHP.

In the next and final chapter, we will focus again on Drupal JavaScript libraries and add-ons. Now that we know how to create modules, we have a more robust toolset for future projects.