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 makes it possible for drop-downs to appear anywhere lists of menu 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']) {
       
$class = '';
     
$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']) {
         
$class = ' active-trail';
      }
      if (
$item_all['below']) {
       
$l['children'] = themename_navigation_links_level($item_page['below'], $item_all['below']);
      }
     
// Keyed with the unique mlid to generate classes in theme_links().
     
$links['menu-'. $item_all['link']['mlid'] . $class] = $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 changes we’ve made are adding a statement to tack on a "haschildren" class to parent menu items, and another statement just before the $i++; causing the function to 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="#" title="" class="active-trail">Continents</a>
      <ul class="sublinks">
        <li class="menu-238 first"><a href="#" title="">Australia</a></li>
        <li class="menu-235"><a href="#" title="">Europe</a></li>
        <li class="menu-233"><a href="#" title="">Asia</a></li>
        <li class="menu-231 last active haschildren"><a href="#" title="" class="active-trail active">North America</a>
        <ul class="sublinks">
          <li class="menu-239 first"><a href="#" title="">Canada</a></li>
          <li class="menu-232 last"><a href="#" 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.

A template.php file containing the above code is attached to this page. Questions, comments, improvements, and pointing out of glaring oversights are welcome.

AttachmentSize
template.php_.txt4.41 KB

Excellent

Sam P86 - April 26, 2009 - 14:52

The only tutorial I found that actually works! Basically I added the template.php to my theme after renaming all it's functions to my own theme's name, then I put in this line:

print theme('links', MyThemesName_primary_links(), array('class' =>'nav', 'id' => 'primary-links'))

*nav is the class where in your css you have defined your navigation style

Works like a charm

spacecowboyian - May 28, 2009 - 21:07

If you need nested menu markup for suckerfish, superfish or any kind of JS fish. This is the way to go. I searched for 3 hours before I found the right solution and it took 5 mins to implement.

THANKS!

Got a donate link?

Looks like I don't

Eric3 - June 29, 2009 - 01:51

Nah... donate to the Drupal project instead.

---
Eric3

Integrating your snippets in fusiontheme

Roulion - October 22, 2009 - 19:19

I try to integrate your snippet to fusiontheme but i can't get all the tree menu. I only have one level.
Here is the part of my template.php file

<?php
// $Id: template.php,v 1.1 2009/09/20 17:19:55 blagoj Exp $

/**
* Return a multidimensional array of links for a navigation menu.
*
* @param $menu_name
*   The name of the menu.
* @param $level
*   Optional, the depth of the menu to be returned.
* @return
*   An array of links of the specified menu and level.
*/
function afterfoot_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
afterfoot_navigation_links_level($tree_page, $tree_all);
}


/**
* Helper function for afterfoot_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 afterfoot_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']) {
       
$class = '';
     
$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']) {
         
$class = ' active-trail';
      }
      if (
$item_all['below']) {
       
$l['children'] = afterfoot_navigation_links_level($item_page['below'], $item_all['below']);
      }
     
// Keyed with the unique mlid to generate classes in theme_links().
     
$links['menu-'. $item_all['link']['mlid'] . $class] = $l;
    }
  }
  return
$links;
}


/**
* Helper function to retrieve the primary links using afterfoot_navigation_links().
*/
function afterfoot_primary_links() {
  return
afterfoot_navigation_links(variable_get('menu_primary_links_source', 'primary-links'));
}


/**
* Return a themed set of links. (Extended to support multidimensional arrays of links.)
*
* @param $links
*   A keyed array of links to be themed.
* @param $attributes
*   A keyed array of attributes
* @return
*   A string containing an unordered list of links.
*/
function afterfoot_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;
}






/**
* Override or insert PHPTemplate variables into the page templates.
*
* @param $vars
*   A sequential array of variables to pass to the theme template.
* @param $hook
*   The name of the theme function being called ("page" in this case.)
*/

function afterfoot_preprocess_page(&$vars, $hook) {
 
$vars['footer_msg'] = ' &copy; ' . $vars['site_name'] . ' ' . date('Y');
 
$vars['search_box'] = str_replace(t('Search this site: '), '', $vars['search_box']);
 
$vars['site_logo'] = '<a href="'. $vars['front_page'] .'" title="'. t('Home page') .'" rel="home"><img src="'. $vars['logo'] .'" alt="'. $vars['site_name'] .' '. t('logo') .'" /></a>';
}
?>

here is my page.tpl.php call

<div id="menu">
<?php if (isset($primary_links)) { ?>
<?php print theme('links', afterfoot_primary_links(), array('class' => 'links', 'id' => 'primary-links')) ?>
<?php } ?>
                </div>

I anyone could help, please

 
 

Drupal is a registered trademark of Dries Buytaert.