Theming a Multi-level Responsive Menu in Drupal 7


In my last post, I discussed my new photography site I'm building and theming with Drupal. The site is responsive and I've been searching for an ideal responsive touch friendly multi-level menu. I looked at and tried several jQuery plugins, CSS3 styles and a few Drupal Modules but nothing was quite right.

The Issue

Part of the issue is that I need multi-level menus that are touch friendly, but many of the menus I looked at had severe UX drawbacks. It's confusing to click (touch) on a menu item, have it expand and then make it go to a link all at the same time. FlexNav, a jQuery menu plugin written by Jason Weaver solves this where it separates a linked menu item from navigation through the DOM itself.

Implementation

So FlexNav checks all the boxes on my touch friendly multi-level menu wish list. This is most of the task but the tricky part is dovetailing this into my Drupal theme. Part of the difficulty when digging into the Drupal API is fully understanding what's going on with the menu and how to alter it, of course without hacking core.

Digging into Drupal's API

I checked out all the documentation of menus within the Drupal API and looked at the code in the core file menu.inc but came up empty with adding an ID, class and attributes to the top level <ul> menu item but not any child <ul> tags below it. I figured I could do this with jQuery and it's pretty trivial actually but by doing so the menu was just plain buggy. So that left me with the tried and true preprocess function, the bane of any Themer's existence.

A Solution

In my research, I found lots of people asking how to do this very same task in forum posts on drupal.org and Drupal Answers on Stack Exchange but as is often the case, there were varied and confusing suggestions. Finally I was looking at how the Drupal Bootstrap Theme implements its main menu and bingo, I found a solution that I could trivially implement into my own custom Unsemantic Framework based Drupal Theme. Note that I didn't use Bootstrap's actual menu as I don't like the way it prevents clicking on a top level menu item if it has sub-menu items.

Prototype - desktop menu on the left, mobile on the right

Prototype - desktop menu on the left, mobile on the right

API Functions

We'll be leveraging a few bits from the Drupal API here:

  • function menu_tree - To render the full menu tree
  • function drupal_render - To attach classes, attributes only to the main <ul> wrapper and not to its children.
  • function menu_configure - To build the links and specify the menu we want to theme for FlexNav. (In our case, the Main Menu.)
  • function hook_css_alter - This is just for good measure to unset the nasty system.menu.css file that plagues all themers.

The Code

The first bit of code goes into a preprocess_page function in your theme's template.php file.

function MYTHEME_preprocess_page(&$vars, $hook) {
  // Primary nav.
  $vars['primary_nav'] = FALSE;
  if ($vars['main_menu']) {
  // Build links.
  $vars['primary_nav'] = menu_tree(variable_get('menu_main_links_source', 'main-menu'));
  // Provide default theme wrapper function.
  $vars['primary_nav']['#theme_wrappers'] = array('menu_tree__primary');
 }
}

Essentially the above grabs the links tree from the main menu and then creates a wrapper that we can then leverage with function menu_tree as such:

/**
* Theme wrapper function for the primary menu links
*/
function MYTHEME_menu_tree__primary(&$vars) {
  return '<ul class="flexnav" data-breakpoint="769">' . $vars['tree'] . '</ul>';
}

Note the custom class and data attribute here, this is the key to the entire exercise. The beauty in this is, it only gets added to the top level <ul> tag in our menu because we specified #theme_wrappers above. That's crucial for getting this all to work. For good measure, I unset system.menu.css using a hook_css_alter function but that's optional, you just might need to do a few CSS overrides if you don't do this.

Finally in the theme's page.tpl.php file, we render the menu:

 <div class="menu-button">Menu</div>
  <nav class="menu-navigation">
    <?php if (!empty($primary_nav)): ?>
    <?php print render($primary_nav); ?>
  <?php endif; ?>
<p></nav></p>

Finally, be sure to add Flexnav's CSS and JS files - drupal_add_js and drupal_add_css are good methods here - and then call FlexNav:

$(".flexnav").flexNav();

...typically from your theme's custom JS scripts file.

As always, I learned an incredible amount of knowledge on this after working on this specific problem over the course of a few weeks and I'll be contributing back a lot of this code for some feature requests I have for my theme, Bamboo.

Resources

Tags