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

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.

Leave a Comment