Dynamically exclude menu items from wp_nav_menu

I tried looking for info about how to exclude/remove nav menu items from custom menus, and the only thread I found did not have any answers that were useful to me.

1. Background:

I put together a Dock menu using WP custom menus (wp_nav_menu) and jqDock on my site. Since jqDock needs continuous images or image links to work its magic, I’ using a custom walker so the nav menu HTML output looks smth like this:

<div id="menu-first" class="nav">
<a><img src="http://path/to/image-1.png"/></a>
<a><img src="http://path/to/image-2.png"/></a>
<a><img src="http://path/to/image-3.png"/></a>
etc...
</div>

The code for my custom walker is:

class custom_nav_walker extends Walker_Nav_Menu 
{
    var $tree_type = array( 'post_type', 'taxonomy', 'custom' );
    var $db_fields = array( 'parent' => 'menu_item_parent', 'id' => 'db_id' );

    function start_lvl(&$output, $depth) {
        $indent = str_repeat("\t", $depth);
        $output .= "\n$indent<ul class=\"sub-menu\">\n";
    }

    function end_lvl(&$output, $depth) {
        $indent = str_repeat("\t", $depth);
        $output .= "$indent</ul>\n";
    }

    function start_el(&$output, $item, $depth, $args) {
        global $wp_query;
        $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="" . 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        ) .'"' : '';

        $description  = ! empty( $item->description ) ? esc_attr( strtolower( $item->description )) : '';
        $item_title   = ! empty( $item->attr_title )  ? esc_attr( $item->attr_title ) : '';

        if ( strpos($description, ';') !== false ) {
        $description_array = explode (';', $description);
            $image_name = $description_array[0];
            $image_alt = $description_array[1];
        } else {
            $image_name = $description;
            $image_alt = $item_title;
        }

        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before .'<img src="'.get_bloginfo('template_url').'/images/skin1/'.$image_name.'" alt="'.$image_alt.'" title="'.$item_title.'" />'.$args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;

        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }

    function end_el(&$output, $item, $depth) {
        $output .= "";
    }

}

The jqDock script then catches the menu ID (‘menu-first’) and replaces the wp_nav_menu output with the Dock menu’s. The Dock menu’s HTML output changes based on the options specified when loading jqDock.

2. The Question:

I would like to not display (i.e., exclude) certain menu items according to where the user is on the site. For example, I would like to only show the Home item when the user is not in the Home, and the Random post item only when he is.

3. Discarded solutions:

a. Muliple menus: Registering and creating multiple menus and then calling them conditionally could work; however, I don’t think this is an ideal nor a clean solution at all for many reasons. Also, multiple menus are not easy to maintain or update.

b. Regex Search and Replace: This might force me to change the needle parameter every time I change the jqDock options because the HTML output is modified.

c. CSS ‘display’ property: Hiding the items through the CSS display property works, but since it has to be applied to the jqDock menu output, it affects the visual rendering of the menu.

4. Failed Solutions:

a. Filter to wp_nav_menu_items: I tried catching the ‘$items’ variable (string) and assigning it different values through conditional tags with the following code:

function userf_dynamic_nav_menu ($items) {
    $items_array_home = explode('<a', $items);
    $items_array_nothome = $items_array_home;

    unset($items_array_home[1]);
    unset($items_array_nothome[2]);

    $items_home = implode('<a', $items_array_home);
    $items_nothome = implode('<a', $items_array_nothome);

    if ( is_home() ) {
        $items = $items_home;
    } else {
        $items = $items_nothome;
    }
    return $items;
}
add_filter('wp_nav_menu_first_items', 'userf_dynamic_nav_menu');

This works only partially, because the menu items do change, but the conditional tags are ignored. I guess this makes sense because of the moment in which the filter is applied.

b. Custom nav menu function: I tried creating my own custom nav menu function to be able to add an exclude argument to the $defaults array and to use this slightly modified code from wp_list_pages to populate the additional argument:

$exclude_array = ( $args->exclude ) ? explode(',', $args->exclude) : array();
$args->exclude = implode( ',', apply_filters('wp_nav_menu_excludes', $exclude_array) );

Any ideas?

2

Method 1

You can add a constructor to your custom Walker to store some additional exclusion arguments, like:

class custom_nav_walker extends Walker_Nav_Menu {
    function __construct( $exclude = null ) {
        $this->exclude = $exclude;
    }

    function skip( $item ) {
        return in_array($item->ID, (array)$this->exclude);
        // or
        return in_array($item->title, (array)$this->exclude);
        // etc.
    }

    // ...inside start_el, end_el
    if ( $this->skip( $item ) ) return;
}

Or drop the constructor and set its $exclude property before passing it in as a walker to wp_nav_menu() like so:

$my_custom_nav_walker = new custom_nav_walker;
$my_custom_nav_walker->exclude = array( ... );

Depending on what you’re excluding by, supply the correct form to the exclude.

Method 2

This is how you would go about doing this by hooking into the wp_get_nav_menu_items filter.

function wpse31748_exclude_menu_items( $items, $menu, $args ) {
    // Iterate over the items to search and destroy
    foreach ( $items as $key => $item ) {
        if ( $item->object_id == 168 ) unset( $items[$key] );
    }

    return $items;
}

add_filter( 'wp_get_nav_menu_items', 'wpse31748_exclude_menu_items', null, 3 );

Note: object_id is the object the menu points to, while ID is the menu ID, these are different.

Let me know your thoughts.

Leave a Comment