Custom Submenus for Drupal

Drupal's menu behavior is probably one of the most confusing aspects of building a site in Drupal 6.  There are plenty of "fixit" modules out there (most notably Nicemenus), but when you want to get fancy, you have to go custom and write your own module.  While I'm not going to cover the basics of module building, I will discuss some menu-specific stuff that can give your site a usability boost without requiring a lot of configuration .

Things this module achieves :

  • A fully themeable drop down menu. 
  • A Submenu block to display the current menu in a sidebar or elsewhere on the page. 
  • A next/previous block to quickly flip through the menu links. 
  • A sitemap block suitable for embedding in a page.  On the project I made this module for, it was the fallback page for empty search results, but you can put it wherever you like.

First things first, we will use hook_block to add these special blocks to the list of blocks you can use at admin/build/block. This is nothing remarkable, but you can see it in the attached custom_menus.module.  Next, we'll take care of the dropdown menu:

function custom_menus_primary_links_content(&$title) {
  $primary = menu_tree_page_data('primary-links');
  foreach($primary as &$item) {
    $item['link']['localized_options']['attributes']['class'] = 'top-level';
    foreach($item['below'] as &$subitem) {
        $subitem['below'] = FALSE;
    }
  }
  return menu_tree_output($primary);
}

In this function, we load up our primary links with menu_tree_page_data, and start iterating through.  I give the first level of links a class of top-level to help me theme them later.  Under each top-level link sits a group of second-level links.  Because I don't want to show any third level links in this case, I manually prune off everything below the second level.  That's about it.  It doesn't look like much, but when you compare it to Drupal's wacky menu rendering, it is simple and clean. 

Next, I'm going to do the submenu block.  This function is a little meatier, but it will make navigation much easier. 

function custom_menus_block_content(&$title) {
  foreach(menu_tree_page_data('primary-links') as $top_level) {
    if($top_level['link']['in_active_trail']) {
      $links = $top_level['below'];
      $title = $top_level['link']['title'];
      $top_level['link']['title'] = 'Overview';
      $top_level['below'] = FALSE;
      
      if($links) {
          foreach($links as &$link) {
              if($link['link']['in_active_trail']) {
                  $top_level['link']['in_active_trail'] = FALSE;    
              }
              $link['below'] = FALSE;
          }
        array_unshift($links, $top_level);
        
        return menu_tree_output($links);
      }
      return menu_tree_output($top_level);      
    }
  }
}

Again, we start with the top-level primary links and work our way down.  This time, we only want to grab the active menu item.  So we will end up with a single top level link and everything below it.  By finding the active trail we are able to do that.  For this particular project, the client wanted the top-level link to be called "Overview" in this block, so I change the name to overview.  We don't want any errors trying to loop through the submenu items if they don't exist, so we check to make sure they do.  Next, we check to see if the we are actually on the top-level page, or a page below it.  If we aren't actually on the "Overview" page, we don't want it to be active, so we remove that active-trail class.  Then, we'll pop the top-level link onto the array with the sub-navigation and render it. 

Probably the most complicated function is the one to generate next/previous links for the menu items.  Drupal really wasn't designed to give this functionality, so we have to do some work. 

function custom_menus_next_prev_content(&$title) {
  foreach(menu_tree_page_data('primary-links') as $item) {
    if($item['link']['in_active_trail']) {
      if(is_array($item['below'])) {
        $item['link']['link_title'] = 'Overview';
        $siblings = $item['below'];
        $item['below'] = FALSE;
        array_unshift($siblings, $item);
        
        while($i = next($siblings)) {
          if($i['link']['in_active_trail']) {
            $prev = prev($siblings);
            next($siblings);
            $next = next($siblings);
            break;
          }
        }
        
        if(!$next && !$prev) { //we skipped the top level item earlier, return to it.
          reset($siblings);
          $next = next($siblings);
        }
        if($prev) {
          $output = '<span class="nav_aid">Previous:&nbsp;</span>';
          $output.= l($prev['link']['link_title'], $prev['link']['link_path'], $prev['link']['options']);
          $output.= '<span class="raquo">&nbsp;&raquo;</span>';
        }
        if($next) {
          if($prev) {
            $output.= '<br />';
          }
          $output.= '<span class="nav_aid">Next:&nbsp;</span>';
          $output.= l($next['link']['link_title'], $next['link']['link_path'], $next['link']['options']);
          $output.= '<span class="raquo">&nbsp;&raquo;</span>';
        }
        return $output;
      }
    }
  }
}

We start the same way we did on the subnavigation links, changing the top-level name and adding the top-level link to the sub-navigation array.  Then, we loop through the array looking for the active link.  Again, the top-level item should always be in the active-trail, so we don't necessarily pay attention to it until we've checked all the other links.  When we find it, we use prev() and next() to grab the items in front and behind.  Finally, we render those links and return the output. 

Last and probably least, we need to get our sitemap.  Again, Drupal makes this difficult because by default it only renders submenu items when they are in the active trail.  So we need our own function to override this behavior. 

function custom_menus_sitemap_content($title) {
    $output .= "<div id=\"sitemap\">";
    $output .= custom_menus_sitemap_render_menu(menu_tree_all_data("primary-links"));
    $output .= "</div>";
    return $output;
}

function custom_menus_sitemap_render_menu($menu, $level = 0) {
    $output = "<ul>";
    foreach ($menu as $item) {
        $link = $item["link"];
        if ($link["hidden"]) {
            continue;
        }

        $output .= "<li class='level-$level'><a href=\"" . check_url(url($link["href"], $link["options"])) . "\" class='level-$level'>" . $link["title"] . "</a>";

        if ($item["below"]) {
            $output .= custom_menus_sitemap_render_menu($item["below"], $level+1);
        }
        $output.= '</li>';
    }
    
    $output .= "</ul>";
    return $output;
}
 

All we do here is use a recursive function to build each level of the menu, skipping any links that are marked as hidden.  Not really complex, but impossible to achieve without a special function.  In my opinion, this ought to be the default behavior for a menu. 

Well, that's about it.  I'm not going to include the CSS for this stuff because it is very specific to the client's project, but if you want to use the dropdown functionality, I'd reccomend Suckerfish Dropdowns as an excellent, CSS based option. 

AttachmentSize
custom_menus.zip2.29 KB