Custom wp_nav_menu output (displaying all child elements of top menu element in current branch)

I would like to create a walker function, or modify somehow current wp_nav_menus so it can produce a custom output.
Let’s imagine this structure:

menu item 1
menu item 2
 submenu item a
 submenu item b
menu item 3
 submenu item c
 submenu item d
 submenu item e

The easiest part is to show in one place only main menu items (main menu 1,2,3), and it can be done with wp_nav_menus, so we don’t need to code it.

Read More

The problematic part is to show in other place submenu items of currently main item…
So if user is on ‘menu item 1’ page, nothing shows.
If user is on ‘menu item 2’ page, our “new” custom menu shows:

submenu item a
submenu item b

Same menu items renders when user is on either of two above pages (menu items).

When user clicks ‘menu item 3’ and visits its target page, he sees submenu items c,d,e, same as after clicking on any of those submenu items.

If the menu has 3rd level elements (and deeper), custom menu should display all child elements of current most top menu element, beside that top element itself (which will be listed in top part of the site described in first place)

Is there a way of creating such functionality?

The idea is close to:
Display a portion/branch of menu
just it needs to be dynamic and display child elements of current menu branch/all child elements of top menu branch element if they exist.

AFAIK I can’t use get_ancestors, because it works only with hierarchical taxonomies, and here we are not talking about menu created from post/pages hierarchical structure, but with use of menu editor.

SOLVED:
Seems I was able to create correct functions kombining few methods:

/**
 * The API function, just a wrap for wp_nav_menu adding filter first and removing after
 */ 
function filtered_nav_menu( $args = array() ) {
    $echo = isset($args['echo']) ? (bool)$args['echo'] : true;
    $args['echo'] = false;
    add_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
    $menu = wp_nav_menu( $args );
    remove_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
    if ( $echo ) echo $menu;
    else return $menu;
}

/**
 * The filter callback, return the filtered elements
 */
function gmfnv_filter( $items ) {
    $found_top_parent_ID = false;
    foreach ($items as $item) {
        if ( ($item->menu_item_parent == 0 && $item->current_item_ancestor == 1) || ($item->menu_item_parent == 0 && $item->current == 1) ) {
            $found_top_parent_ID = $item->ID;
        }
    }
    $children  = submenu_get_children_ids( $found_top_parent_ID, $items );
    foreach ( $items as $key => $item ) {
        if ( ! in_array( $item->ID, $children ) )
            unset($items[$key]);
    }
    return $items;
}

/**
 * Helper function: return children of an element using wp_filter_object_list
 */
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;
}

What it does – it search for current branch top ancestor by going through each $items element and checking against:

($item->menu_item_parent == 0 && $item->current_item_ancestor == 1) || ($item->menu_item_parent == 0 && $item->current == 1)

This conditional needs to be 2 part, because top level menu element might also be current one.

If top level $item is found, then function submenu_get_children_ids is used to list all its child elements.

Related posts

1 comment

  1. The idea

    Instead of create a custom walker I thought was easier filter the items using the filter wp_nav_menu_objects hook.

    This hook is defined in /wp-includes/nav-menu-templates.php and whe is fired, it pass to functions hooking into it an array $sorted_menu_items that contains all the elements of the menu being printed, if the function alter that array, adding or removing items, the resulting menu will be altered.

    The method

    If the filter to wp_nav_menu_objects is applyed direclty, all menus will be filtered, so I thought was better create a function that wrap wp_nav_menu adding the filter before calling wp_nav_menu and remove it after: in this way only the wanted menu is filtered.

    The filter workflow

    1. Cycle all items passed by wp_nav_menu_objects filter hook
    2. Create 2 helper arrays: one only of parent ids, one with items like $itemid => $parentid
    3. While looping items, also check it item url match the current url
    4. if url not match return only parent items
    5. if url match, using the helper arrays cretaed, return the wanted elements

    The code

    The code I wrote for that, make use of 5 functions, so I create a plugin to contains all that, here the code:

    <?php
    /**
     * Plugin Name: Filtered Nav Menus
     * Author: Giuseppe  Mazzapica
     * Plugin URI: http://wordpress.stackexchange.com/questions/118720/
     * Author URI: http://wordpress.stackexchange.com/users/35541/
     */
    
    /**
     * The API function, just a wrap for wp_nav_menu adding filter first and removing after
     */ 
    function filtered_nav_menu( $args = array() ) {
      $echo = isset($args['echo']) ? (bool)$args['echo'] : true;
      $args['echo'] = false;
      add_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
      $menu = wp_nav_menu( $args );
      remove_filter( 'wp_nav_menu_objects', 'gmfnv_filter', 999 );
      if ( $echo ) echo $menu;
      else return $menu;
    }
    
    /**
     * The filter callback, return the filtered elements
     */
    function gmfnv_filter( $items ) {
      $found = false;
      $parents = $items_tree = $allowed = array();
      $all_items = $items;
      while ( ! empty( $items ) ) {
        $item = array_shift( $items );
        $items_tree[$item->ID] = $item->menu_item_parent;
        if ( (int) $item->menu_item_parent == 0 ) $parents[] = $item->ID;
        if ( isset($item->current) && $item->current ) $found = $item->ID;
      }
      if ( ! $found ) {
        $ids = $parents;
      } else {
        $tree = gmfnv_get_tree( $found, $all_items, $items_tree );
        $ids = array_merge( $parents, $tree );
      }
      foreach ( $all_items as $item ) {
        if ( in_array( $item->ID, $ids ) ) $allowed[] = $item;
      }
      return $allowed;
    }
    
    
    /**
     * Helper function: take the matched element if and the helper array and
     * return the item ancestors by gmfnv_get_parents,
     * and the children of these ancestors returned by gmfnv_get_parents
     * using gmfnv_get_parents
     */
    function gmfnv_get_tree( $test, $items, $tree ) {
      $parents = gmfnv_get_parents( $test, $items );
      $parents[] = $test;
      $n = array();
      foreach ( $parents as $parent ) {
        $n = array_merge( $n, gmfnv_get_childrens( $parent, $tree ) );
      }
      return array_unique( $n );
    }
    
    
    /**
     * Helper function: return ancestors of an element using the helper array
     */
    function gmfnv_get_parents( $test, $items ) {
      $parents = array();
      foreach( $items as $item ) {
          if (
            (isset($item->current_item_ancestor) && $item->current_item_ancestor)
            || (isset($item->current_item_ancestor) && $item->current_item_ancestor)
          ) $parents[] = $item->ID;
      }
      return $parents;
    }
    
    
    /**
     * Helper function: return children of an element using the helper array
     */
    function gmfnv_get_childrens( $test, $tree ) {
      $children = array();
      foreach ( $tree as $child => $parent ) {
        if ( $parent == $test ) $children[] = $child;
      }
      return $children;
    }
    

    How To

    Create a file containing this plugin put in plugins folder and activate.

    When you want filter a menu as required, instead using wp_nav_menu( $args ) use

    filtered_nav_menu( $args );
    

    Disclaimer

    Code provided as is, no warranty, but just quickly tested on PHP 5.4, WP 3.7 with twentytherteen theme active, and no other plugins: it worked in this case.

Comments are closed.