How to generate markup for drop-down menus
Drop-down menus are an accepted and intuitive way of presenting links for large numbers of nested pages on a web page, but Drupal currently lacks a built-in mechanism for easily implementing them. For drop-down menus to work, our generated HTML should include all the items of a given menu, not just those at the top level and in the active trail, plus a flag indicating the current page, if any. While modules like Nice Menus already exist for providing nested menus in a block, this method allows for drop-downs anywhere that primary or secondary links currently appear in your theme.
The main challenge with getting this to work is generating the underlying data structure representing the menu hierarchy. While Drupal doesn’t (yet) include a function that does what we need, it does have two functions whose output can be combined. They are:
- menu_tree_page_data(), which when given the name of a menu, returns a tree (a nested array of menu items and metadata) leading to the current page. You can see this function at work generating the main navigation menu. Its output depends on the page that called it.
- menu_tree_all_data(), which returns a tree containing every item in a given menu.
What we want is a tree which contains every menu item, plus data indicating which item in the menu is the current page. To do that, we’re going to modify the built-in menu_navigation_links() function. In its default form, it generates a 1-dimensional array representing the top-level links of a given menu. We’re going to split it into two functions and have them recursively generate a multidimensional array of all the links in a menu. All the following code would be located inside your theme’s folder in the template.php file.
<?php
/**
* Return a multidimensional array of links for a navigation menu.
*/
function themename_navigation_links($menu_name, $level = 0) {
// Don't even bother querying the menu table if no menu is specified.
if (empty($menu_name)) {
return array();
}
// Get the menu hierarchy for the current page.
$tree_page = menu_tree_page_data($menu_name);
// Also get the full menu hierarchy.
$tree_all = menu_tree_all_data($menu_name);
// Go down the active trail until the right level is reached.
while ($level-- > 0 && $tree_page) {
// Loop through the current level's items until we find one that is in trail.
while ($item = array_shift($tree_page)) {
if ($item['link']['in_active_trail']) {
// If the item is in the active trail, we continue in the subtree.
$tree_page = empty($item['below']) ? array() : $item['below'];
break;
}
}
}
return themename_navigation_links_level($tree_page, $tree_all);
}
/**
* Helper function for themename_navigation_links to recursively create an array of links.
* (Both trees are required in order to include every menu item and active trail info.)
*/
function themename_navigation_links_level($tree_page, $tree_all) {
$links = array();
foreach ($tree_all as $key => $item) {
$item_page = $tree_page[$key];
$item_all = $tree_all[$key];
if (!$item_all['link']['hidden']) {
$l = $item_all['link']['localized_options'];
$l['href'] = $item_all['link']['href'];
$l['title'] = $item_all['link']['title'];
if ($item_page['link']['in_active_trail']) {
if (empty($l['attributes']['class'])) {
$l['attributes']['class'] = 'active-trail';
}
else {
$l['attributes']['class'] .= ' active-trail';
}
}
if ($item_all['below']) {
$l['children'] = themename_navigation_links_level($item_page['below'], $item_all['below']);
}
// Keyed with unique menu id to generate classes from theme_links().
$links['menu-'. $item_all['link']['mlid']] = $l;
}
}
return $links;
}
?>As you can see, the second function incorporates active trail information into the final array by simultaneously checking items in $tree_page and $tree_all. Since both trees represent the same data, the keys from one tree can be used to look up information in the other. And if a menu item contains nested items, as indicated in the tree with a key labelled “below”, themename_navigation_links_level() calls itself, passing in the nested items of each tree and returning an array of the nested items which are given the key “children” in the final array.
So we’re halfway there. We have an array (in the same format used by menu_primary_links() and menu_secondary_links()) which we can use for generating the menu’s HTML, but the built-in theme_links() function is not prepared to handle multidimensional arrays of links. Fortunately, since it’s a themeable function, we can override it by including it in template.php and naming it themename_links() without any additional changes to our theme. (Of course, you should always replace instances of “themename” with your own theme's machine name.)
<?php
/**
* Return a themed set of links. (Extended to support multidimensional arrays of links.)
*/
function themename_links($links, $attributes = array('class' => 'links')) {
$output = '';
if (count($links) > 0) {
$output = '<ul'. drupal_attributes($attributes) .'>';
$num_links = count($links);
$i = 1;
foreach ($links as $key => $link) {
$class = $key;
// Add first, last and active classes to the list of links to help out themers.
if ($i == 1) {
$class .= ' first';
}
if ($i == $num_links) {
$class .= ' last';
}
if (isset($link['href']) && ($link['href'] == $_GET['q'] || ($link['href'] == '<front>' && drupal_is_front_page()))) {
$class .= ' active';
}
// Added: if the link has child items, add a haschildren class
if (isset($link['children'])) {
$class .= ' haschildren';
}
$output .= '<li'. drupal_attributes(array('class' => $class)) .'>';
if (isset($link['href'])) {
// Pass in $link as $options, they share the same keys.
$output .= l($link['title'], $link['href'], $link);
}
else if (!empty($link['title'])) {
// Some links are actually not links, but we wrap these in <span> for adding title and class attributes
if (empty($link['html'])) {
$link['title'] = check_plain($link['title']);
}
$span_attributes = '';
if (isset($link['attributes'])) {
$span_attributes = drupal_attributes($link['attributes']);
}
$output .= '<span'. $span_attributes .'>'. $link['title'] .'</span>';
}
// Added: if the link has child items, print them out recursively
if (isset($link['children'])) {
$output .= "\n" . theme('links', $link['children'], array('class' =>'sublinks'));
}
$i++;
$output .= "</li>\n";
}
$output .= '</ul>';
}
return $output;
}
?>The only change we’ve made is adding a statement just before the $i++; causing the function recursively call itself to generate the HTML for any nested items it detects.
We’ll add one last function to make calling these functions a little more concise. The code below, when called, generates markup for the primary links. This is completely optional, though.
<?php
function themename_primary_links() {
return themename_navigation_links(variable_get('menu_primary_links_source', 'primary-links'));
}
?>With the back end finished, changes to our theme’s template is relatively simple. The following code from page.tpl.php, or something similar to it,
<?php
print theme('links', $primary_links, array('class' =>'links', 'id' => 'primary-links'))
?>can be replaced with
<?php
print theme('links', themename_primary_links(), array('class' =>'links', 'id' => 'primary-links'))
?>in order to generate nested menus for the primary links. Here's some sample output of a primary links menu with a single parent link when viewed from the “North America” page:
<div id="menu">
<ul class="links" id="primary-links">
<li class="menu-115 first haschildren"><a href="/drupal6-5/" title="description text" class="active-trail">Continents</a>
<ul class="sublinks">
<li class="menu-238 first"><a href="/drupal6-5/" title="">Australia</a></li>
<li class="menu-235"><a href="/drupal6-5/" title="">Europe</a></li>
<li class="menu-233"><a href="/drupal6-5/" title="">Asia</a></li>
<li class="menu-231 last active haschildren"><a href="/drupal6-5/?q=node/4" title="" class="active-trail active">North America</a>
<ul class="sublinks">
<li class="menu-239 first"><a href="/drupal6-5/" title="">Canada</a></li>
<li class="menu-232 last"><a href="/drupal6-5/" title="">USA</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>Of course, all this is useless without proper CSS styling. As this topic has been covered before, I'd suggest you look at how I did it in my theme, NoProb. The main idea is that all ul.sublinks elements have "display: none" applied to them by default, and are shown only when their parent li.haschildren element is hovered over. Since IE6 doesn't apply the :hover pseudo-class to anything other than links, some JavaScript is required, which I originally got from http://www.alistapart.com/articles/horizdropdowns and modified to support more than one level of nesting.
Questions, comments, improvements, and pointing out of glaring oversights are welcome.

Have you thought of posting
Have you thought of posting this into the handbook http://drupal.org/handbooks? Will be easier for people to find it there...
Also are all the links for Australia, Europe, Asia etc. intended to point to the home page ("/drupal6-5/")?
gpk
----
www.alexoria.co.uk
This is more of a hack to
This is more of a hack to fill a void in what the official code provides, so I doubt it's handbook material. It doesn't matter where the links point to; I should have made them all say href="#" to make that clearer.
---
Eric3
OK, I guess it's all
OK, I guess it's all generated by the code anyway.
I think Nicemenus can also be got to play nicely on primary links... http://drupal.org/node/240831 (not that you probably want to know now ..!)
gpk
----
www.alexoria.co.uk
bookmark, no time to read
bookmark, no time to read through now
Reneging on my previous
Reneging on my previous comment, I've posted the above article in the handbook at http://drupal.org/node/327252 as a Theme Snippet. Unlike this post, it's editable, and thus contains a number of updates and fixes not present here, including a patch from the Drupal 6.6 update.
---
Eric3
:-)
You seem to be saying that a bit of JS is needed (for IE6) - but I couldn't find that?
gpk
----
www.alexoria.co.uk
It's available in recent
It's available in recent development snapshots; a release incorporating it hasn't been created yet, but will be soon.
---
Eric3