Funny, I just ran into this same conundrum a few months back...
Background
I found it quite bizarre that Wordpress applies all manner of CSS classes to menu-items in order to distinguish the current item and its ancestors, but then completely ignores the sub-menus and their relationship with the current item. I wanted a .current-sub
CSS class so that I could hide all .sub-menu
s except .current-sub
s.
Theory
After much research and wading through source, the solution that I ended up embracing was to extend the Walker_Nav_Menu
class and modify its behavior at two distinct points in order to obtain my imagined .current-sub
CSS class:
- When processing a menu-item ( see
Walker_Nav_Menu->start_el()
), examine the classes that Wordpress assigned to the menu-item in order to determine if it corresponds to the current page or is a menu ancestor of the current page.
- When processing a new menu-lvl (a.k.a "sub-menu"; see
Walker_Nav_Menu->start_lvl()
), if the previous menu-item processed was either the current menu-item or an ancestor of it, apply a new class to the sub-menu element in order to distinguish it as a current submenu.
Aside
Note that I've emboldened menu ancestor above. In my situation, it turned out that I didn't actually care about posts', pages', or categories' hierarchy or relationships with one another (i.e. it didn't matter whether or not page B was a child of page A). The only thing that I needed to worry about was the hierarchy of their corresponding menu-item
s.
After the Walker was implemented, all I needed was a new call in a template file to wp_nav_menu()
with an instance of my new menu Walker as an argument.
Implementation
For convenience and brevity I've provided my implementation in a procedural-style plugin. For the most part the code is identical to the native Walker_Nav_Menu
- my comments denote my additions.
//Register a new theme location for a custom menu
function after_setup_theme_97226() {
register_nav_menu( 'menu-97226', 'Primary Navigation (uses Menu_Walker_97226)' );
}
add_action( 'after_setup_theme', 'after_setup_theme_97226' );
//Queue up CSS modifications
function enqueue_styles_97226() {
wp_enqueue_style( 'style-97226', plugins_url('style.css', __FILE__) );
}
add_action( 'wp_enqueue_scripts', 'enqueue_styles_97226' );
//Now walk it out.
class Menu_Walker_97226 extends Walker_Nav_Menu {
const CURRENT_SUBMENU_CLASS = 'current-sub'; //The CSS class to apply to the active submenu
var $is_current_submenu = FALSE; //Whether or not the next sub-menu to be processed is related to the current-menu-item.
function start_lvl( &$output, $depth = 0, $args = array() ) {
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
//If we've begun processing an "active" submenu, add a class to indicate as much.
$current_sub_class = ( $this->is_current_submenu ) ? Menu_Walker_97226::CURRENT_SUBMENU_CLASS : '';
//Add the new sub-menu ul element to the markup. Always give sub-menus the standard Wordpress "sub-menu" class.
$output .= "\n$indent<ul class=\"sub-menu $current_sub_class\">\n";
//We've now processed the sub-menu; reset the flag
$this->is_current_submenu = FALSE;
}
function start_el( &$output, $item, $depth = 0, $args = array() ) {
//Reset the flag such that "active" menu-items without sub-menus don't pass their "current" status on to the sub-menus of their siblings
$this->is_current_submenu = FALSE;
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$class_names = $value = '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
//Check to see if this element is "current" or a menu ancestor of "current" so that the "current" status may be passed on to any immediate sub-menu of the item.
//You should add or remove conditions here in order to fit your application.
//Also see "Final Notes" section below regarding performance
if (strpos($class_names, 'current-menu-item')
|| strpos($class_names, 'current-menu-parent')
|| strpos($class_names, 'current-menu-ancestor')
|| strpos($class_names, 'current_page_parent'))
$this->is_current_submenu = TRUE;
$id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
$output .= $indent . '<li' . $id . $value . $class_names .'>';
$attributes = ! empty( $item->attr_title ) ? ' title="' . esc_attr( $item->attr_title ) .'"' : '';
$attributes .= ! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) .'"' : '';
$attributes .= ! empty( $item->xfn ) ? ' rel="' . esc_attr( $item->xfn ) .'"' : '';
$attributes .= ! empty( $item->url ) ? ' href="' . esc_attr( $item->url ) .'"' : '';
$item_output = $args->before;
$item_output .= '<a'. $attributes .'>';
$item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
}
The entire contents of the referenced style.css
file are as follows:
ul.sub-menu {
display:none;
}
ul.current-sub {
display:block;
}
And finally, placing a call to wp_nav_menu()
within a template file where the menu is to be displayed in order to make use of the new Walker (see the documentation for additional arguments):
wp_nav_menu( array(
'theme_location' => 'menu-97226',
'walker' => new Menu_Walker_97226()
) );
Final Notes
- I have not run benchmarks, but it is entirely possible that may be quicker to check a menu-item's classes using
in_array()
calls before the $classes
array is joined rather than using strpos()
calls afterwards. There's also the possibility of using array_flip()
and subsequently using isset( $classes[ $class_name ] )
, though I suspect that the flip would defeat any performance gained from using isset()
.
- Some part of my implementation was heavily inspired by a blog post that I've since lost track of in the months between. If I manage to locate it I will return to link it.
- If any menu-items appear multiple times in your menu you may run into obscure behavior as all occurrences of a "current" menu item will maintain "current" status.
- I've used the terms "current" and "active" somewhat interchangeably to refer to menu-items and sub-menus that should be displayed... My terminology is somewhat fuzzy and my head muddled at the moment - I'll try to return later to clean it up ;)
This may not be the exact solution you were looking for (and there may well exist better approaches to the issue), but I hope it's enough to set you on the right path!