Theming a menu

Menus are a familiar fixture in Drupal. In fact, once Drupal is installed, one of the first things we do is use the menu. It looks like this:

m butcher o My account

► Create content

► Administer o Log out

If you quickly scan through the templates included in Bluemarine (or Garland, or any of the default themes), you won't find the template that generates this menu. Why not? That's because it comes from a set of theming functions buried deep in Drupal's includes/menu.inc file.

The main menu function is menu_tree_output(), which is not actually a theming function, technically speaking. While we're not going to dwell on it, here's how that function looks:

function menu_tree_output($tree) { $output = ''; $items = array(); foreach ($tree as $data) {

$num_items = count($items); foreach ($items as $i => $data) { $extra_class = NULL; if ($i == 0) {

$output .= theme('menu_item', $link, $data['link,][,has_ children'], menu_tree_output($data['below']), $data['link,][,in_

active_trail'], $extra_class);

$output .= theme('menu_item', $link, $data['link,][,has_

children'], ■', $data[,link,][,in_active_traill], $extra_class); }

return $output ? theme('menu_treel, $output) : '■;

The basic idea of the previous function is to traverse a menu tree and theme it so that it is ready for display. Each call to the theme() function (the PHP equivalent of the Drupal.theme() JavaScript function) is highlighted. There are four theme() calls to three different theming functions: menu_item_link(), menu_item(), and menu tree().

I _The three theming functions are much less daunting and also bear some I

I resemblance to the block function we just created. Feel free to look at I

I them. They are all in /includes/menu.inc. I

If we were to reproduce this in JavaScript, we would need to write at least four functions (assuming none of the themes would call something else).

Now we should ask ourselves some questions: What are we trying to implement in our JavaScript theme function? Do we need the complex logic present above? We could create a JavaScript equivalent, but do we need to?

All we really want to do, in our example at hand, is create a simple menu containing links. We don't need to support an elaborate tree structure at all. We don't really need to break out the theming of menu items and menu item links, or even of the menu as a whole and each of the menu items.

The most important thing is getting our code to look like the menu generated from menu_tree_output(). The fastest way of finding out how to generate our theme will not be analyzing the code. It's going to be analyzing the generated HTML.

In fact, let's take a look at the HTML source for the menu we saw in a screenshot a few pages back. Here's what that looks like:

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

Ah, that's much better. We can generalize a little more to see the structure of a menu in its simplest form like this:

<a href='link'>name</a> </li> </ul>

Also, from the example before, we can see that the <li></li> for the first node in a menu has the additional class first. Similarly, the last one has the last class.

We're now ready to code this up in JavaScript. This time, we will use the jQuery-based method for building:

* Build a single (non-colapsed) menu list.

* Mimics the complex menu logic in menus.inc.

* @param items

* An array of objects that have a name and a link property.

* @returns

* String representation of a link list. */

Drupal.theme.prototype.shallow_menu = function (items) { var list = $('<ul class="menu"></ul>');

for (var i = 0; i < items.length; ++i) { var item = items[i];

// Get text for menu item var menuText = null; if (item. link) {

menuText = item.name.link(item.link);

menuText = item.name;

// Create item var li = $('<li class="leaf"></li>');

// figure out if this is first or last if (i == 0) {

li.addClass('first');

else if (i == items.length - 1) { li.addClass('last');

// Add item to list li.html(menuText).appendTo(list);

return list.parent().html

Don't let the size of this function distract you. It's actually not very complex.

This function takes a list of objects that represent links. We saw this list in the jQuery ready handler we created earlier:

{name: 'Drupal.org', link:'http://drupal.org'}, {name: 'jQuery', link: 'http://jquery.org'}, {name: 'No Link'}

This is the object that is passed into our Drupal.theme.prototype.shallow _menu() function.

[ _ * Our theme is named shallow_menu() because it does not take the deep I

\ tree-structured menu data that its PHP counterpart did. Instead, this I

function only creates shallow menus. I

We start out by creating a list object, which is our list element wrapped inside of a jQuery object. Once again, we are taking advantage of jQuery's flexible constructor to pass it an HTML fragment instead of a CSS selector.

Once our list container is ready, all we need to do is loop through each object in the list of links, formatting and adding it to the list as we go.

The loop starts out like this:

for (var i = 0; i < items.length; ++i) { var item = items[i];

// Get text for menu item var menuText = null; if (item. link) {

menuText = item.name.link(item.link);

menuText = item.name;

The first thing we do in the for loop is store the current item in the item variable. We know that each item will have a name, but we don't know if it will have a link property.

If it has a link, we want to create a piece of HTML that looks like this: <a href='link'>name</a>. But if it doesn't, we want HTML that looks like this: name.

This is all done within that first if/else conditional. Easy linking

A useful JavaScript function that is used surprisingly infrequently is the string method link() . Any string can be turned into a link by calling this method and passing in a URL like this: 'a string'. link('http://example.com') . This little snippet of code will generate an HTML looking like this: <a href="http://example. com">a string</a>.

The next part of the for loop looks like this:

for (i = 0; i < items.length; ++i) { var item = items[i];

// Get text for menu item var menuText = null; if (item. link) {

menuText = item.name.link(item.link);

menuText = item.name;

// Create item

var li = $('<li class="leaf"></li>');

// figure out if this is first or last if (i == 0) {

li.addClass('first');

else if (i == items.length - 1) { li.addClass('last');

// Add item to list li.html(menuText).appendTo(list);

The highlighted portion is the new code. First, we create a jQuery-wrapped list element:

var li = $('<li class="leaf"></li>');

Once we have the li object we check it to see if it is either the first or last menu item. If it's the first, we add the first class. And if it is last, we add the last class. All others will have only the leaf class.

Now that we have our list item element (li) ready, we add content and then append the entire thing to the list object:

li.html(menuText).appendTo(list);

By the time the for loop is done, a new li object will have been added to the list for each item that was in the original array of links.

Finally, after the for loop we have one last line. We bring things together with a last jQuery chain:

return list.parent().html();

A theme function needs to return a string, so we grab a string representation of the main <ul></ul> element (together with its contents) using parent().html().

When all of this is put together, we should get a menu embedded in a block. The entire thing should look something like this:

JavaScript Menu o Drupal.org

We've finished our project. We created two themes — one based on a PHP Template file and one based on a very complex function. But when we put everything together, our JavaScript themes look just like their PHP-generated counterparts.

Was this article helpful?

0 0

Post a comment