Instantly share code, notes, and snippets.
Last active
October 10, 2021 23:50
-
Save yaharga/443b97a1889a0ab20c2c29b276cdcd10 to your computer and use it in GitHub Desktop.
This is a custom detailed navigational menu walker I worked on that adds various classes to identify li's, links, and ul's, with the code being collected from various walkers shared by other users.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Detailed Navigational Menu Walker | |
* | |
* Gives navigational menu detailed classes depending on position. | |
* Includes numbering positions, first and last classes, depth dependant classes, even and odd classes depending on position and depth, and number of children being even or odd. | |
* | |
* @class Detailed Navigational Menu Walker | |
* @version 1.0.0 | |
* @package Yolk | |
* @category Class | |
* @author YAHARGA | |
*/ | |
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly | |
class detailed_navigational_menu_walker extends Walker_Nav_Menu { | |
/** | |
* Code coppied from https://roots.io/plugins/soil/ | |
* Cleans up navigational menu and keeps parent class. | |
*/ | |
private $cpt; // Boolean, is current post a custom post type | |
private $archive; // Stores the archive page for current URL | |
public function __construct() { | |
add_filter('nav_menu_css_class', array($this, 'classes'), 10, 2); | |
$cpt = get_post_type(); | |
$this->cpt = in_array($cpt, get_post_types(array('_builtin' => false))); | |
$this->archive = get_post_type_archive_link($cpt); | |
} | |
/** | |
* Make a URL relative | |
*/ | |
function root_relative_url($input) { | |
if (is_feed()) { | |
return $input; | |
} | |
$url = parse_url($input); | |
if (!isset($url['host']) || !isset($url['path'])) { | |
return $input; | |
} | |
$site_url = parse_url(network_home_url()); // Falls back to home_url. | |
if (!isset($url['scheme'])) { | |
$url['scheme'] = $site_url['scheme']; | |
} | |
$hosts_match = $site_url['host'] === $url['host']; | |
$schemes_match = $site_url['scheme'] === $url['scheme']; | |
$ports_exist = isset($site_url['port']) && isset($url['port']); | |
$ports_match = ($ports_exist) ? $site_url['port'] === $url['port'] : true; | |
if ($hosts_match && $schemes_match && $ports_match) { | |
return wp_make_link_relative($input); | |
} | |
return $input; | |
} | |
/** | |
* Compare URL against relative URL | |
*/ | |
function url_compare($url, $rel) { | |
$url = trailingslashit($url); | |
$rel = trailingslashit($rel); | |
return ((strcasecmp($url, $rel) === 0) || $this->root_relative_url($url) == $rel); | |
} | |
public function classes($classes, $item) { | |
// Fix core `active` behavior for custom post types | |
if ($this->cpt) { | |
$classes = str_replace('current_page_parent', '', $classes); | |
if ($this->url_compare($this->archive, $item->url)) { | |
$classes[] = 'active'; | |
} | |
} | |
// Remove most core classes | |
$classes = preg_replace('/(current(-menu-|[-_]page[-_])(item|parent|ancestor))/', 'active', $classes); | |
$classes = preg_replace('/^((menu|page)[-_\w+]+)+/', '', $classes); | |
// Re-add core `menu-item-has-children` class on parent elements | |
if ($item->is_subitem) { | |
$classes[] = 'parent'; | |
} | |
$classes = array_unique($classes); | |
$classes = array_map('trim', $classes); | |
return array_filter($classes); | |
} | |
/* | |
* Add classes to sub menus. | |
* Starts the list before the elements are added. | |
* The $args parameter holds additional values that may be used with the child class methods. | |
* This method is called at the start of the output list. | |
* Add depth, even and odd depth, and sub-sub classes. | |
*/ | |
function start_lvl( &$output, $depth = 0, $args = array() ) { | |
$indent = ( $depth > 0 ? str_repeat( "\t", $depth ) : '' ); // Indenting the code. | |
$display_depth = ( $depth + 1 ); // It counts the first submenu as 0. | |
$classes = array( | |
'sub', // Add sub class if it's a sub menu. | |
( $display_depth % 2 ? 'odd-depth' : 'even-depth' ), // Add even or odd depth class. | |
( $display_depth >=2 ? 'sub-sub' : '' ), // Add sub-sub if it's a sub-sub menu, no matter how deep. | |
'depth-' . $display_depth // Add depth number of sub menu. | |
); | |
$class_names = implode( ' ', $classes ); // Seperate the classes using spaces. | |
// Build the html. | |
$output .= "\n" . $indent . '<ul class="' . $class_names . ' toggleable hide">' . "\n"; // Added toggleable and hide classes for checkbox dropdown hack. | |
} | |
/* | |
* Add main/sub classes to li's and links. | |
* Start the element output. | |
* The $args parameter holds additional values that may be used with the child class methods. | |
* Includes the element output also. | |
* Add children count and even or odd count. | |
*/ | |
function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) { | |
$indent = ( $depth > 0 ? str_repeat( "\t", $depth ) : '' ); // Indenting the code. | |
if ( $item->is_subitem ) { | |
$locations = get_nav_menu_locations(); // Getting the locations of the nav menus array. | |
$menu = wp_get_nav_menu_object( $locations[$args->theme_location] ); // Getting the menu calling the walker from the array. | |
$menu_items = wp_get_nav_menu_items( $menu->term_id ); // Getting the menu item objects array from the menu. | |
$menu_item_parents = array_map( function( $o ) { return $o->menu_item_parent; }, $menu_items ); // Getting the parent ids in an array by looping through the menu item objects array. | |
$children_count = array_count_values( $menu_item_parents )[$item->ID]; // Get number of children menu item has. | |
// Children count classes. | |
$children_classes = array( | |
( $item->is_subitem ? 'children-' . $children_count : '' ), // Add children count. | |
( $children_count % 2 ? 'odd-children' : 'even-children' ) // Add odd or even children class. | |
); | |
} | |
$children_classe_names = empty($children_classes) ? '' : esc_attr( implode( ' ', $children_classes ) ); | |
// Passed classes from item. | |
$classes = empty( $item->classes ) ? array() : (array) $item->classes; | |
$class_names = esc_attr( implode( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item ) ) ); | |
// Build the html. | |
$output .= $indent . '<li id="nav-menu-item-'. $item->ID . '" class="menu-item ' . $children_classe_names . ' ' . $class_names . '">'; | |
// Link attributes to link items. | |
$attributes = ' class="menu-link"'; | |
$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 ) .'"' : ''; | |
// Add trigger to open up dropdowns. | |
if ( $item->is_subitem ) { | |
$args->after = sprintf( '<button class="dropdown-toggle script-dependant">+</button>', $item->ID ); | |
} else { | |
$args->after = null; | |
} | |
// Building the link outputs. | |
$item_output = sprintf( '%1$s<a%2$s>%3$s%4$s%5$s</a>%6$s', | |
$args->before, | |
$attributes, | |
$args->link_before, | |
apply_filters( 'the_title', $item->title, $item->ID ), | |
$args->link_after, | |
$args->after | |
); | |
// Build the html. | |
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args, $id ); | |
} | |
/** | |
* Traverse elements to create list from elements. | |
* | |
* Display one element if the element doesn't have any children otherwise, | |
* display the element and its children. Will only traverse up to the max | |
* depth and no ignore elements under that depth. It is possible to set the | |
* max depth to include all depths, see walk() method. | |
*/ | |
public function display_element($element, &$children_elements, $max_depth, $depth = 0, $args, &$output) { | |
$element->is_subitem = ((!empty($children_elements[$element->ID]) && (($depth + 1) < $max_depth || ($max_depth === 0)))); | |
if ($element->is_subitem) { | |
foreach ($children_elements[$element->ID] as $child) { | |
if ($child->current_item_parent || $this->url_compare($this->archive, $child->url)) { | |
$element->classes[] = 'active'; | |
} | |
} | |
} | |
$element->is_active = (!empty($element->url) && strpos($this->archive, $element->url)); | |
if ($element->is_active) { | |
$element->classes[] = 'active'; | |
} | |
parent::display_element($element, $children_elements, $max_depth, $depth, $args, $output); | |
} | |
/* | |
* Display array of elements hierarchically. | |
* Does not assume any existing order of elements. | |
* $max_depth = -1 means flatly display every element. | |
* $max_depth = 0 means display all levels. | |
* $max_depth > 0 specifies the number of display levels. | |
*/ | |
public function walk( $elements, $max_depth ) { | |
$args = array_slice( func_get_args(), 2 ); | |
$output = ''; | |
// Invalid parameter or nothing to walk. | |
if ( $max_depth < -1 || empty( $elements ) ) { | |
return $output; | |
} | |
$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 ( empty( $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; | |
} | |
} | |
/* | |
* Add number of positions per hierarchy using arrays from earlier at the top of the function. | |
* One for top level elements and the other for child elements. | |
*/ | |
foreach ( $top_level_elements as $i => $e ) { // Loop to add classes to top level elements loop. | |
array_push( $e->classes, ( $i+1 ) % 2 ? 'odd' : 'even' ); // Add odd and even classes based on position. | |
// Add [before | after]-parent classes to element. | |
if ($i <> count( $top_level_elements ) - 1 ) { // If it is not the last element. | |
if (array_key_exists($top_level_elements[$i + 1 ]->ID, $children_elements)) { // If next element is a parent. | |
array_push($e->classes, 'before-parent'); // Add before-parent class. | |
} | |
} | |
if ($i <> 0) { // If it is not the first element. | |
if (array_key_exists($top_level_elements[$i - 1 ]->ID, $children_elements)) { // If previous element is a parent. | |
array_push($e->classes, 'after-parent'); // Add after-parent class. | |
} | |
} | |
// Add first and last classes to items. | |
if ( $i == 0 ) { | |
array_push( $e->classes, 'first' ); // Add first class to first item. | |
} elseif ( $i == ( count( $top_level_elements ) - 1 ) ) { | |
array_push( $e->classes, 'last' ); // Add last class to last item. | |
} | |
} | |
foreach ( $children_elements as $children ) { // Loop to add classes to child level elements loop. | |
foreach ( $children as $i => $e ) { | |
array_push( $e->classes, ( $i+1 ) % 2 ? 'odd' : 'even' ); // Add odd and even classes based on position. | |
// Add [before | after]-parent classes to element. | |
if ($i <> count( $children ) - 1 ) { // If it is not the last element. | |
if (array_key_exists($children[$i + 1 ]->ID, $children_elements)) { // If next element is a parent. | |
array_push($e->classes, 'before-parent'); // Add before-parent class. | |
} | |
} | |
if ($i <> 0) { // If it is not the first element. | |
if (array_key_exists($children[$i - 1 ]->ID, $children_elements)) { // If previous element is a parent. | |
array_push($e->classes, 'after-parent'); // Add after-parent class. | |
} | |
} | |
if ( $i == 0 ) { | |
array_push( $e->classes, 'first' ); // Add first class to first item. | |
}if ( $i == ( count( $children ) - 1 ) ) { | |
array_push( $e->classes, 'last' ); // Add last class to last item. | |
} | |
} | |
} | |
foreach ( $top_level_elements as $e ) | |
$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; | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment