Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I would like a sub-menu in which the user visits a page, post, or category (in this example, "info") and is then displayed a list of children categories/pages. Not a dropdown menu, but a menu that appears only when the user is already in the parent category.

I need a code which continues to display the parent's children even when on that child page, post, or category. Maybe there is a way to do this with WP menus that I am unaware of?

The code needs to be separate from the main menu and be assigned the traditional CSS classes which indicate if the user is on the current page. The code also needs to automatically recognize the children of the parent, as opposed to assigning the children and calling multiple menus throughout the site. I'm positive this is possible because I used to utilize such a code. However, I can't remember what that code was!

Below is an illustrated example of what such a menu may look like: Classic menu example

share|improve this question

2 Answers 2

up vote 4 down vote accepted
+50

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-menus except .current-subs.


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:

  1. 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.
  2. 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-items.

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!

share|improve this answer
    
Thanks for the solution, I found a code which is a bit more nimble but requires some further changes which I'm going to post about - but your solution is also very helpful. –  AndrettiMilas May 2 '13 at 18:46
    
Glad to hear it! I look forward to seeing what you came up with :) –  boscho May 2 '13 at 18:47

I'm using the following code on one of my websites:

    if ( // Maybe you want different conditionals?
        (is_page() && ! is_front_page())
        || is_category()
    ) {
        $id = get_the_ID();
        $ancestors = get_ancestors($id, 'page');
        if (! empty($ancestors)) $id = $ancestors[count($ancestors)-1];
        $subpages = wp_list_pages('title_li=&child_of='.$id.'&depth=1&echo=0');
        if ('' !== $subpages) {
            echo '<nav id="subpages" role="navigation">'.PHP_EOL
                .'<ul>'.PHP_EOL
                .$subpages
                .'</ul>'.PHP_EOL
                .'</nav><!-- #subpages -->'.PHP_EOL;
        }
    }

Please try this out and check for your desired features. If it doesn't (exactly) do what you want, please let me know and I will try to update the code.

share|improve this answer
    
Thank you, this code works for pages - but I'm also looking for a code that produces the same results for categories, including when a user is in the sub-category and viewing a post. –  AndrettiMilas Apr 30 '13 at 19:34
    
Well, okay, then please give me a bit more information. How should this work for categories? Do you have category links in your menu? Or do you want to display the children of the parent category archive page, when displaying a subcategory archive page? Phew, that was a complex sentence. ;) Or did I still not get you at all? –  t f May 1 '13 at 11:53
    
It is fairly well-explained in my original question. I simply want to output the child categories of the current category including while I'm on a post within that category and sub-categories. I'd also like a class to be added to the current sub-category. –  AndrettiMilas May 1 '13 at 14:14

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.