Primary links menu active trail - yet another method

Last modified: August 26, 2009 - 22:26

Note: The menu trails module also provides this functionality.

There are quite a few posts about setting the active trail properly with primary & secondary links. In Drupal 5.x, its actually handled pretty well if you use the following code to print your primary & secondary links in page.tpl.php

<?php
print theme('links', $primary_links);
?>

<?php
print theme('links', $secondary_links);
?>

This is all good and well, but for us themers, there's a little snag. You cannot use a generic style like a.active to hit the active menu items. By default they've got a bunch of numbers tagged onto the word "active" e.g. menu-1-3-2-active. So, basically, you have to know that menu id number to style it. Throw in 20-30 menu items, and your stylesheet is going to get seriously bloated.

The function that creates the output for the primary and secondary links is in menu.inc and is not a theme function. Hence, no simple theme override in template.php. After trolling the forums and checking out the menutrails module, I hit upon the following technique that seems to work nicely.

Step 1 - override the primary & secondary links function
Yes, you're right, you cannot do a direct theme override, because its not a theme function. But you can redefine the primary and secondary links variables by using _phptemplate_variables, before they get to page.tpl.php.

In template.php add the following code:

<?php
function _phptemplate_variables($hook, $vars = array()) {
  switch (
$hook) {
    case
'page':
     
// reset primary & secondary to use our own function
     
$vars['primary_links'] = new_primary_links();
     
$vars['secondary_links'] = new_secondary_links();
      break;
  }
  return
$vars;
}
?>

If you already have that function in your template.php file (quite likely), then you most likely have the case 'page' bit too. In this case just add inside that section:

      $vars['primary_links'] = new_primary_links();
      $vars['secondary_links'] = new_secondary_links();

Step 2 - rewrite the menu function
Now we can write our own function to create the menu links. For me, the core function was good enough, except for that "active" class. So all I did was copy and paste the functions for primary and secondary links, and add in a separate "active" class. Here's the code (somewhere in template.php):

<?php
/**
* Returns an array containing the primary links - modified to add in a separate "active" class.
*/
function new_primary_links($start_level = 1, $pid = 0) {
  if (!
module_exists('menu')) {
    return
NULL;
  }
  if (!
$pid) {
   
$pid = variable_get('menu_primary_menu', 0);
  }
  if (!
$pid) {
    return
NULL;
  }

  if (
$start_level < 1) {
   
$start_level = 1;
  }

  if (
$start_level > 1) {
   
$trail = _menu_get_active_trail_in_submenu($pid);
    if (!
$trail) {
      return
NULL;
    }
    else {
     
$pid = $trail[$start_level - 1];
    }
  }

 
$menu = menu_get_menu();
 
$links = array();
  if (
$pid && is_array($menu['visible'][$pid]) && isset($menu['visible'][$pid]['children'])) {
   
$count = 1;
    foreach (
$menu['visible'][$pid]['children'] as $cid) {
     
$index = "menu-$start_level-$count-$pid";
      if (
menu_in_active_trail_in_submenu($cid, $pid)) {
       
$index .= "-active active"; // ************ HERE'S THE CHANGE *************
     
}
     
$links[$index] = menu_item_link($cid, FALSE);
     
$count++;
    }
  }

 
// Special case - provide link to admin/build/menu if primary links is empty.
 
if (empty($links) && $start_level == 1 && $pid == variable_get('menu_primary_menu', 0) && user_access('administer menu')) {
   
$links['1-1'] = array(
     
'title' => t('Edit primary links'),
     
'href' => 'admin/build/menu'
   
);
  }

  return
$links;
}

/**
* Returns an array containing the secondary links - calls the new primary links function.
*/
function new_secondary_links() {
 
$msm = variable_get('menu_secondary_menu', 0);
  if (
$msm == 0) {
    return
NULL;
  }

  if (
$msm == variable_get('menu_primary_menu', 0)) {
    return
new_primary_links(2, $msm);
  }

  return
new_primary_links(1, $msm);
}
?>

And that's it. Pretty simple really.

I'm not sure if my problem

sa001 - May 31, 2008 - 08:34

I'm not sure if my problem was the same, but all I did was override theme_menu_item_link() by adding the following function to my template.php file (replacing THEMENAME with the name of the theme):

<?php
function THEMENAME_menu_item_link($link) {
  if (
$link['in_active_trail'])
   
$link['options']['attributes']['class'] .=  ' active';
 
  return
phptemplate_menu_item_link($link);
}
?>

This is for a D6 Zen subtheme, I don't know if it would work for other cases.

Both worked for me on my 6.3

Rob T - July 18, 2008 - 03:06

Both worked for me on my 6.3 install. I chose to go with the smaller sa001 solution.

I used the Basic theme as a starting point (which was based on Zen somewhat). There was already an existing "function THEMENAME_menu_item_link", so I just popped in these 2 lines into the template.php function...

if ($link['in_active_trail'])
    $link['options']['attributes']['class'] .=  ' active';

This solution works great,

kulfi - August 18, 2008 - 09:28

This solution works great, except if the menu link points to <front>. (@sa001 this didn't work for me in D5)

I can't believe this is so hard

hansBKK@drupal.org - July 22, 2008 - 10:23

To clarify version issues: this node is currently tagged as applying to D5, and the OP seems to indicate this explicitly above. Rob T indicates that the original snippet also worked for D5. sa001's snippet seems to apply to D6 exclusively, and did not work for me in D5 (zen starterkit).

The original snippet doesn't work directly in zen because the main (parent theme) template.php is already using _phptemplate_variables, when sub-theming they want you to use the D6-style themename_preprocess_page function.

I may come back and try to convert the logic from one format to the other, but I'm no coder. So at this point, I'm electing to look for something either simpler or more functional.

What I'm really looking for is something independent of the menu system that will highlight (set .active class) any link on the page that is a "parent" of the current page based on the URL.

In other words, that will work even if A the link isn't being generated by the menuing system and/or B the current page doesn't have a menu link itself.

Example: visiting example.com/about/facilities/sports/tennis (could be a view, term listing, whatever) highlights (sets "active") any links pointing to:

about (most likely in primary but not necessarily)
about/facilities (ditto for secondary)
about/facilities/sports (maybe a sidebar menu, eg html in a block)

This may result in my breadcrumb trail having a mix of links classed active and not, but I can handle that with more specific CSS rules.

Any pointers would be most welcome, TIA.

Update

hansBKK@drupal.org - July 22, 2008 - 11:50

Found a simpler fix overriding theme_links that works with D5 and Zen, but of course this only applies to theme-generated menu links (at least it takes care of primary/secondary menus).

Cleaned-up version in my comment toward the end:

http://drupal.org/node/140491

Still looking for the more functional solution (based on the URL path rather than the menuing path as described above), any help welcome!

Direct link to hansBKK's

kulfi - July 22, 2008 - 14:54

URLs are specific to sites, and not generic enough to be more accurate than menu_in_active_trail_in_submenu.

Direct link to hansBKK's post: http://drupal.org/node/140491#comment-931790

This is my solution with the

Starnox - September 12, 2008 - 15:24

This is my solution with the problem.

<?php
function zen_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;
           
// START NEW
           
if (substr($class,-6,6) == "active") {
               
$key = $class.' active';
            }
// END NEW   
      // Automatically add a class to each link and also to each LI
     
if (isset($link['attributes']) && isset($link['attributes']['class'])) {
       
$link['attributes']['class'] .= ' ' . $key;
      }
      else {
       
$link['attributes']['class'] = $key;
      }

     
// Add first and last classes to the list of links to help out themers.
     
$extra_class = '';
      if (
$i == 1) {
       
$extra_class .= 'first ';
      }
      if (
$i == $num_links) {
       
$extra_class .= 'last ';
      }
     
$output .= '<li '. drupal_attributes(array('class' => $extra_class . $class)) .'>';

     
// Is the title HTML?
     
$html = isset($link['html']) && $link['html'];

     
// Initialize fragment and query variables.
     
$link['query'] = isset($link['query']) ? $link['query'] : NULL;
     
$link['fragment'] = isset($link['fragment']) ? $link['fragment'] : NULL;

      if (isset(
$link['href'])) {
       
$output .= l($link['title'], $link['href'], $link['attributes'], $link['query'], $link['fragment'], FALSE, $html);
      }
      else if (
$link['title']) {
       
//Some links are actually not links, but we wrap these in <span> for adding title and class attributes
       
if (!$html) {
         
$link['title'] = check_plain($link['title']);
        }
       
$output .= '<span'. drupal_attributes($link['attributes']) .'>'. $link['title'] .'</span>';
      }

     
$i++;
     
$output .= "</li>\n";
    }

   
$output .= '</ul>';
  }

  return
$output;
}
?>

Just put this function in your template.php file. Just added three lines.

Little Modification for complete solution

rtandon - September 17, 2008 - 21:56

Hi Starnox,

Just a small addition to above modification.

Starnox, your solution is elegant, however there was a little shortcoming. The code did add another class 'active' to the element 'a' (i.e. the link). However, it did not add the class 'active' as attribute to the element LI.

I just added one more line and it's now Okay. It ads 'active' to attribute class, both for element A as well as to element LI.

The final solution is here.

I just used the following fragment in template.php of my zen sub-theme directory.

// File: template.php

function zen_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;

// START CUSTOMIZATION
// Ref: http://drupal.org/node/248522#comment-1008024
// This is to add active class to li and a href elements of links
// specifically, suited for primary links

            if (substr($class,-6,6) == "active") {
                $key = $class.' active';
            }
    $class = $key;             // this line added by me.
// END CUSTOMIZATION
// Rest of the code is same as in original zen theme's template.php

feel free to post correction / improvement here.

Regards,

Another Solution

tbartels - January 14, 2009 - 06:55

As long as your theme is using theme('links', $primary_links) to theme your primary menu(or any menu, as most do) you should be able to use the snippet below to add the "active" class to the li element

<?php
// add this to template.php
function phptemplate_links($links, $attributes = array('class' => 'links')) {
  foreach (
$links as $key => $link) {
    list(
$name, $startlevel, $count, $pid, $isactive) = explode('-', $key);

    if (
$isactive) {
     
$tmp = array_splice($links, 0, $count - 1);
     
array_shift($links);
     
$links = array_merge($tmp, array($key .' active' => $link), $links);
    }
  }

  return
theme_links($links, $attributes);
}
?>

Can this be used to add a <span> to item

cbarilla - July 22, 2009 - 18:57

@tbartels
I am using your code and it is working perfectly. But for my menu to work I need to output the item with a <span> tag around it.

EX.

<div id="nav_menu" class="menu withprimary ">
          <div id="sbmenu" >
            <ul class="links">
<li class="menu-646 active-trail first active"><a href="/" title="" class="active"><span>Home</span></a></li>
<li class="menu-647"><a href="/?q=contact" title="Use this page to send us an email."><span>Contact</span></a></li>
<li class="menu-648 last"><a href="/?q=content/about" title="About"><span>About</span></a></li>
</ul>         
</div>
        </div> <!-- /nav_menu -->

I am stilling trying to find another work around with the css style, but adding the span tag would be easier...I know it works because I tested it by adding the tag manually via firebug.

 
 

Drupal is a registered trademark of Dries Buytaert.