Overriding menu item themes

In an earlier article, I tried to argue, somewhat inconvincingly, that theming menu items is less than ideal. I think my example was not well chosen. Fortunately, real life furnished me with a perfect example, during the course of my work.

A client required to associate specific icons with menu items, by specifying a custom class for specific menu items. A legitimate request that has been around for years on desktop menu systems. How to implement this on Drupal? Not so simple using the current menu theming setup. Here's the problem:

  • The non-theme function menu_tree_output is responsible for displaying a menu tree. For each menu item, this function:
  • Finally, this function calls theme('menu_tree') that places the whole rendered structure in an HTML unordered list.

The crucial failure of this function is to call theme('menu_item') with the rendered link instead of the original menu item object. This effectively prevents anyone from simply overriding this theme function to add classes or attributes based on the item's metadata. IMO, menu_tree_output needs to be rewritten.

But I had to deliver that functionality, so here's how I did it (without PECL runkit this time):

  • Enable user input of the extra classes.
<?php
// @file mymodule.module

/**
 * Implementation of hook_form_FORMID_alter() for menu_edit_item.
 */
function mymodule_form_menu_edit_item_alter(&$form, &$form_state) {
 
$form['menu']['classes'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Extra classes'),
   
'#description' => t('Add extra classes for this menu item that are relevant for your theme.'),
   
'#default_value' => variable_get('menu_item_extra_classes_' . $form['menu']['mlid']['#value'], ''),
  );
 
$form['#submit'][] = 'mymodule_menu_edit_item_submit';
}

function
mymodule_menu_edit_item_submit($form, &$form_state) {
 
variable_set('menu_item_extra_classes_' . $form_state['values']['menu']['mlid'], $form_state['values']['menu']['classes']);
}
?>
  • Override both theme('menu_item') and theme('menu_item_link') to enable the first to receive the menu item object from the second. This is done using a global variable.
<?php
// @file mymodule.module

/**
 * Implementation of hook_theme_registry_alter().
 */
function mymodule_theme_registry_alter(&$theme_registry) {
 
$theme_registry['menu_item_link']['function'] = 'mymodule_menu_item_link';
 
$theme_registry['menu_item']['function'] = 'mymodule_menu_item';
}

function
mymodule_menu_item_link($link) {
 
$GLOBALS['mymodule']['link'][] = $link; // push the link object
 
return theme_menu_item_link($link);
}

function
mymodule_menu_item($link, $has_children, $menu  = '', $in_active_trail = FALSE, $extra_class = NULL) {
 
$link_object = array_pop($GLOBALS['mymodule']['link']); // pop the topmost link object
 
$class[] = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
  if (!empty(
$extra_class)) {
   
$class[] = $extra_class;
  }
  if (
$in_active_trail) {
   
$class[] = 'active-trail';
  }
 
$class[] = variable_get('menu_item_extra_classes_' . $link_object['mlid'], 0);
 
$attributes['class'] = implode(' ', array_filter($class));
 
$attributes['id'] = 'menu-item-' . $link_object['mlid'];
  return
'<li'. drupal_attributes($attributes) .' ">'. $link . $menu ."</li>\n";
}
?>

Now the site builder/themer is free to specify any number of classes for any menu item, and to design their CSS accordingly!

Comments

There's still time to get bug

There's still time to get bug fixes into D7 ;)

Menu Attributes module

The Menu Attributes module works great for this sort of thing as well.

http://drupal.org/project/menu_attributes

-mike

Thanks for the link. I have

Thanks for the link. I have to check out hook_menu_link_alter which is the core of your module. It might save me from doing the $GLOBALS acrobatics :-)

Oh, menu attributes module is

Oh, menu attributes module is just great, thanks.

Thanks

The menu attributes function is something I've been trying to get my head round for days and now I find this!! Wonderful.

I know!

Been working with these functions all day, trying to get multi-column dropdown menus. It would have been easy if I could override menu_tree_output (in the theme) but this is impossible. I had to count the number of list items in a

<

ul> but by the time it gets to theme_menu_tree submenus are included in the variable, and the number of columns has to be determined by counting direct childs only. Now I have to resort to jQuery to style the menus.

could be helpful?

I have a small module that does something similar: http://drupal.org/project/menu_css_names

In my case, I was wanting to do highlight particular menu items, but also do css sprtes if needed (although there are modules that handle that more completely). Just thought I'd mention it!