Skip to content

Instantly share code, notes, and snippets.

@yaharga
Last active October 10, 2021 23:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yaharga/443b97a1889a0ab20c2c29b276cdcd10 to your computer and use it in GitHub Desktop.
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.
<?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