Question

I have a menu defined in WP Admin that looks like this:

alt text

I want to be able to display all the child links on the sidebar whenever I am at a parent page. For example, if the user is on my "About Us" page, I want a list of the 4 links highlighted in green to appear on the sidebar.

I looked at the documentation for wp_nav_menu() and it doesn't appear to have any built-in way to specify a particular node of a given menu to use as the starting point when generating the links.

I created a solution for a similar situation which relied on the relationships created by the page parent, but I am looking for one which uses the menu system specifically. Any help would be appreciated.

Was it helpful?

Solution

This was still on my mind so I revisited it and put together this solution, that does not rely on context that much:

add_filter( 'wp_nav_menu_objects', 'submenu_limit', 10, 2 );

function submenu_limit( $items, $args ) {

    if ( empty( $args->submenu ) ) {
        return $items;
    }

    $ids       = wp_filter_object_list( $items, array( 'title' => $args->submenu ), 'and', 'ID' );
    $parent_id = array_pop( $ids );
    $children  = submenu_get_children_ids( $parent_id, $items );

    foreach ( $items as $key => $item ) {

        if ( ! in_array( $item->ID, $children ) ) {
            unset( $items[$key] );
        }
    }

    return $items;
}

function submenu_get_children_ids( $id, $items ) {

    $ids = wp_filter_object_list( $items, array( 'menu_item_parent' => $id ), 'and', 'ID' );

    foreach ( $ids as $id ) {

        $ids = array_merge( $ids, submenu_get_children_ids( $id, $items ) );
    }

    return $ids;
}

Usage

$args = array(
    'theme_location' => 'slug-of-the-menu', // the one used on register_nav_menus
    'submenu' => 'About Us', // could be used __() for translations
);

wp_nav_menu( $args );

OTHER TIPS

@goldenapples: Your Walker Class does not work. But the idea is really good. I created a walker based on your idea:

class Selective_Walker extends Walker_Nav_Menu
{
    function walk( $elements, $max_depth) {

        $args = array_slice(func_get_args(), 2);
        $output = '';

        if ($max_depth < -1) //invalid parameter
            return $output;

        if (empty($elements)) //nothing to walk
            return $output;

        $id_field = $this->db_fields['id'];
        $parent_field = $this->db_fields['parent'];

        // flat display
        if ( -1 == $max_depth ) {
            $empty_array = array();
            foreach ( $elements as $e )
                $this->display_element( $e, $empty_array, 1, 0, $args, $output );
            return $output;
        }

        /*
         * need to display in hierarchical order
         * separate elements into two buckets: top level and children elements
         * children_elements is two dimensional array, eg.
         * children_elements[10][] contains all sub-elements whose parent is 10.
         */
        $top_level_elements = array();
        $children_elements  = array();
        foreach ( $elements as $e) {
            if ( 0 == $e->$parent_field )
                $top_level_elements[] = $e;
            else
                $children_elements[ $e->$parent_field ][] = $e;
        }

        /*
         * when none of the elements is top level
         * assume the first one must be root of the sub elements
         */
        if ( empty($top_level_elements) ) {

            $first = array_slice( $elements, 0, 1 );
            $root = $first[0];

            $top_level_elements = array();
            $children_elements  = array();
            foreach ( $elements as $e) {
                if ( $root->$parent_field == $e->$parent_field )
                    $top_level_elements[] = $e;
                else
                    $children_elements[ $e->$parent_field ][] = $e;
            }
        }

        $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' );  //added by continent7
        foreach ( $top_level_elements as $e ){  //changed by continent7
            // descend only on current tree
            $descend_test = array_intersect( $current_element_markers, $e->classes );
            if ( !empty( $descend_test ) ) 
                $this->display_element( $e, $children_elements, 2, 0, $args, $output );
        }

        /*
         * if we are displaying all levels, and remaining children_elements is not empty,
         * then we got orphans, which should be displayed regardless
         */
         /* removed by continent7
        if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
            $empty_array = array();
            foreach ( $children_elements as $orphans )
                foreach( $orphans as $op )
                    $this->display_element( $op, $empty_array, 1, 0, $args, $output );
         }
        */
         return $output;
    }
}

Now you can use:

<?php wp_nav_menu( 
   array(
       'theme_location'=>'test', 
       'walker'=>new Selective_Walker() ) 
   ); ?>

The output is a list containing the current root element and it's children (not their children). Def: Root element := The top level menu item that corresponds to the current page or is parent of a current page or a parent of a parent ...

This does not exactly answer the original question but almost, since there is still the top level item. This is fine for me, because I want the top level element as a headline of the sidebar. If you want to get rid of this, you might have to override display_element or use a HTML-Parser.

Hi @jessegavin:

Nav Menus are stored in a combination of custom post types and custom taxonomies. Each menu is stored as a Term (i.e. "About Menu", found in wp_terms) of a Custom Taxonomy (i.e. nav_menu, found in wp_term_taxonomy.)

Each Nav Menu Item is stored as a post of post_type=='nav_menu_item' (i.e. "About the Firm", found in wp_posts) with it's attributes stored as post meta (in wp_postmeta) using a meta_key prefix of _menu_item_* where _menu_item_menu_item_parent is the ID of your menu item's parent Nav Menu item post.

The relationship between menus and menu items is stored in wp_term_relationships where object_id relates to the $post->ID for the Nav Menu Item and the $term_relationships->term_taxonomy_id relates to the menu defined collectively in wp_term_taxonomyand wp_terms.

I'm pretty sure it would be possible to hook both 'wp_update_nav_menu' and 'wp_update_nav_menu_item' to create actual menus in wp_terms and a parallel set of relations in wp_term_taxonomy and wp_term_relationships where every Nav Menu Item that has sub-Nav Menu items also becomes it's own Nav Menu.

You'd also want to hook 'wp_get_nav_menus' (which I suggested be added to WP 3.0 based on some similar work I was doing a few months ago) to ensure that your generated Nav Menus are not displayed for manipulation by the user in the admin, otherwise they'd get out of sync really fast and then you'd have a data nightmare on your hand.

Sounds like a fun and useful project, but it is a little bit more code and testing than I can afford to tackle right now in part because anything that synchronizes data tends to be a PITA when it comes to ironing out all the bugs (and because paying clients are pressing me to get things done. :) But armed with the above info I'm pretty a motivated WordPress plugin developer could code it if they wanted to.

Of course you do realize now if you do code it you are obligated to post it back here so we can all benefit from your largesse! :-)

This is a walker extension which should do what you're looking for:

class Selective_Walker extends Walker_Nav_Menu
{

    function walk( $elements, $max_depth) {

        $args = array_slice(func_get_args(), 2);
        $output = '';

        if ($max_depth < -1) //invalid parameter
            return $output;

        if (empty($elements)) //nothing to walk
            return $output;

        $id_field = $this->db_fields['id'];
        $parent_field = $this->db_fields['parent'];

        // flat display
        if ( -1 == $max_depth ) {
            $empty_array = array();
            foreach ( $elements as $e )
                $this->display_element( $e, $empty_array, 1, 0, $args, $output );
            return $output;
        }

        /*
         * need to display in hierarchical order
         * separate elements into two buckets: top level and children elements
         * children_elements is two dimensional array, eg.
         * children_elements[10][] contains all sub-elements whose parent is 10.
         */
        $top_level_elements = array();
        $children_elements  = array();
        foreach ( $elements as $e) {
            if ( 0 == $e->$parent_field )
                $top_level_elements[] = $e;
            else
                $children_elements[ $e->$parent_field ][] = $e;
        }

        /*
         * when none of the elements is top level
         * assume the first one must be root of the sub elements
         */
        if ( empty($top_level_elements) ) {

            $first = array_slice( $elements, 0, 1 );
            $root = $first[0];

            $top_level_elements = array();
            $children_elements  = array();
            foreach ( $elements as $e) {
                if ( $root->$parent_field == $e->$parent_field )
                    $top_level_elements[] = $e;
                else
                    $children_elements[ $e->$parent_field ][] = $e;
            }
        }

        $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' );

        foreach ( $top_level_elements as $e ) {

            // descend only on current tree
            $descend_test = array_intersect( $current_element_markers, $e->classes );
            if ( empty( $descend_test ) )  unset ( $children_elements );

            $this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
        }

        /*
         * if we are displaying all levels, and remaining children_elements is not empty,
         * then we got orphans, which should be displayed regardless
         */
        if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
            $empty_array = array();
            foreach ( $children_elements as $orphans )
                foreach( $orphans as $op )
                    $this->display_element( $op, $empty_array, 1, 0, $args, $output );
         }

         return $output;
    }

}

Based loosely on mfields' code I referenced in my comment earlier. All it does is check when walking the menu to see whether the current element is (1) the current menu item, or (2) an ancestor of the current menu item, and expands the subtree below it only if either of those conditions is true. Hope this works for you.

To use it, just add a "walker" argument when you call the menu, ie:

<?php wp_nav_menu( 
   array(
       'theme_location'=>'test', 
       'walker'=>new Selective_Walker() ) 
   ); ?>

Update: I made this into a plugin. Download here.


I needed to solve this myself and eventually wound up writing a filter on the results of the menu lookup. It lets you use wp_nav_menu as normal, but choose a sub-section of the menu based on the title of the parent element. Add a submenu parameter to the menu like so:

wp_nav_menu(array(
  'menu' => 'header',
  'submenu' => 'About Us',
));

You can even go several levels deep by putting slashes in:

wp_nav_menu(array(
  'menu' => 'header',
  'submenu' => 'About Us/Board of Directors'
));

Or if you prefer with an array:

wp_nav_menu(array(
  'menu' => 'header',
  'submenu' => array('About Us', 'Board of Directors')
));

It uses a slug version of the title, which should make it forgiving of things like capitals and punctuation.

I put together the following class for myself. It will find the top nav parent of the current page, or you can give it a target top nav ID in the walker constructor.

class Walker_SubNav_Menu extends Walker_Nav_Menu {
    var $target_id = false;

    function __construct($target_id = false) {
        $this->target_id = $target_id;
    }

    function walk($items, $depth) {
        $args = array_slice(func_get_args(), 2);
        $args = $args[0];
        $parent_field = $this->db_fields['parent'];
        $target_id = $this->target_id;
        $filtered_items = array();

        // if the parent is not set, set it based on the post
        if (!$target_id) {
            global $post;
            foreach ($items as $item) {
                if ($item->object_id == $post->ID) {
                    $target_id = $item->ID;
                }
            }
        }

        // if there isn't a parent, do a regular menu
        if (!$target_id) return parent::walk($items, $depth, $args);

        // get the top nav item
        $target_id = $this->top_level_id($items, $target_id);

        // only include items under the parent
        foreach ($items as $item) {
            if (!$item->$parent_field) continue;

            $item_id = $this->top_level_id($items, $item->ID);

            if ($item_id == $target_id) {
                $filtered_items[] = $item;
            }
        }

        return parent::walk($filtered_items, $depth, $args);
    }

    // gets the top level ID for an item ID
    function top_level_id($items, $item_id) {
        $parent_field = $this->db_fields['parent'];

        $parents = array();
        foreach ($items as $item) {
            if ($item->$parent_field) {
                $parents[$item->ID] = $item->$parent_field;
            }
        }

        // find the top level item
        while (array_key_exists($item_id, $parents)) {
            $item_id = $parents[$item_id];
        }

        return $item_id;
    }
}

Nav call:

wp_nav_menu(array(
    'theme_location' => 'main_menu',
    'walker' => new Walker_SubNav_Menu(22), // with ID
));

@davidn @hakre Hi, i have an ugly solution without an HTML-Parser or overriding display_element.

 class Selective_Walker extends Walker_Nav_Menu
    {
        function walk( $elements, $max_depth) {

            $args = array_slice(func_get_args(), 2);
            $output = '';

            if ($max_depth < -1) //invalid parameter
                return $output;

            if (empty($elements)) //nothing to walk
                return $output;

            $id_field = $this->db_fields['id'];
            $parent_field = $this->db_fields['parent'];

            // flat display
            if ( -1 == $max_depth ) {
                $empty_array = array();
                foreach ( $elements as $e )
                    $this->display_element( $e, $empty_array, 1, 0, $args, $output );
                return $output;
            }

            /*
             * need to display in hierarchical order
             * separate elements into two buckets: top level and children elements
             * children_elements is two dimensional array, eg.
             * children_elements[10][] contains all sub-elements whose parent is 10.
             */
            $top_level_elements = array();
            $children_elements  = array();
            foreach ( $elements as $e) {
                if ( 0 == $e->$parent_field )
                    $top_level_elements[] = $e;
                else
                    $children_elements[ $e->$parent_field ][] = $e;
            }

            /*
             * when none of the elements is top level
             * assume the first one must be root of the sub elements
             */
            if ( empty($top_level_elements) ) {

                $first = array_slice( $elements, 0, 1 );
                $root = $first[0];

                $top_level_elements = array();
                $children_elements  = array();
                foreach ( $elements as $e) {
                    if ( $root->$parent_field == $e->$parent_field )
                        $top_level_elements[] = $e;
                    else
                        $children_elements[ $e->$parent_field ][] = $e;
                }
            }

            $current_element_markers = array( 'current-menu-item', 'current-menu-parent', 'current-menu-ancestor' );  //added by continent7
            foreach ( $top_level_elements as $e ){  //changed by continent7
                // descend only on current tree
                $descend_test = array_intersect( $current_element_markers, $e->classes );
                if ( !empty( $descend_test ) ) 
                    $this->display_element( $e, $children_elements, 2, 0, $args, $output );
            }

            /*
             * if we are displaying all levels, and remaining children_elements is not empty,
             * then we got orphans, which should be displayed regardless
             */
             /* removed by continent7
            if ( ( $max_depth == 0 ) && count( $children_elements ) > 0 ) {
                $empty_array = array();
                foreach ( $children_elements as $orphans )
                    foreach( $orphans as $op )
                        $this->display_element( $op, $empty_array, 1, 0, $args, $output );
             }
            */

/*added by alpguneysel  */
                $pos = strpos($output, '<a');
            $pos2 = strpos($output, 'a>');
            $topper= substr($output, 0, $pos).substr($output, $pos2+2);
            $pos3 = strpos($topper, '>');
            $lasst=substr($topper, $pos3+1);
            $submenu= substr($lasst, 0, -6);

        return $submenu;
        }
    }

The nav menu output includes lots of classes for current item, current item ancestor, etc. In some situations, I have been able to do what you want to do by letting the entire nav tree output, and then using css to pare it down to only children of the current page, etc.

I made a modified walker that should help! Not perfect - it leaves a few empty elements, but it does the trick. The modification is basically those $current_branch bits. Hope it helps someone!

class Kanec_Walker_Nav_Menu extends Walker {
/**
 * @see Walker::$tree_type
 * @since 3.0.0
 * @var string
 */
var $tree_type = array( 'post_type', 'taxonomy', 'custom' );

/**
 * @see Walker::$db_fields
 * @since 3.0.0
 * @todo Decouple this.
 * @var array
 */
var $db_fields = array( 'parent' => 'menu_item_parent', 'id' => 'db_id' );

/**
 * @see Walker::start_lvl()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param int $depth Depth of page. Used for padding.
 */
function start_lvl(&$output, $depth) {
    $indent = str_repeat("\t", $depth);
    $output .= "\n$indent<ul class=\"sub-menu\">\n";
}

/**
 * @see Walker::end_lvl()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param int $depth Depth of page. Used for padding.
 */
function end_lvl(&$output, $depth) {
    global $current_branch;
    if ($depth == 0) $current_branch = false;
    $indent = str_repeat("\t", $depth);
    $output .= "$indent</ul>\n";
}

/**
 * @see Walker::start_el()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param object $item Menu item data object.
 * @param int $depth Depth of menu item. Used for padding.
 * @param int $current_page Menu item ID.
 * @param object $args
 */
function start_el(&$output, $item, $depth, $args) {
    global $wp_query;
    global $current_branch;

    // Is this menu item in the current branch?
    if(in_array('current-menu-ancestor',$item->classes) ||
    in_array('current-menu-parent',$item->classes) ||
    in_array('current-menu-item',$item->classes)) {
        $current_branch = true; 
    }

    if($current_branch && $depth > 0) {
        $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 ) );
        $class_names = ' class="' . esc_attr( $class_names ) . '"';

        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
        $id = strlen( $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 );
    }

}

/**
 * @see Walker::end_el()
 * @since 3.0.0
 *
 * @param string $output Passed by reference. Used to append additional content.
 * @param object $item Page data object. Not used.
 * @param int $depth Depth of page. Not Used.
 */
function end_el(&$output, $item, $depth) {
    global $current_branch;
    if($current_branch && $depth > 0) $output .= "</li>\n";
    if($depth == 0) $current_branch = 0;
}

}

Check out the code in my plugin or use it for your purpose ;)

This plugin adds enhanced "Navigation Menu" widget. It offers many options which could be set to customize the output of the custom menu through the widget.

Features include:

  • Custom hierarchy - "Only related sub-items" or "Only strictly related sub-items".
  • Starting depth and maximum level to display + flat display.
  • Display all menu items starting with the selected one.
  • Display only direct path to current element or only children of
    selected item (option to include the parent item).
  • Custom class for a widget block.
  • And almost all the parameters for the wp_nav_menu function.

http://wordpress.org/extend/plugins/advanced-menu-widget/

Licensed under: CC-BY-SA with attribution
Not affiliated with wordpress.stackexchange
scroll top