2018-04-16 21:28:31 +00:00
< ? php
/**
* WP Bootstrap Navwalker
*
* @ package WP - Bootstrap - Navwalker
*/
2019-06-20 08:57:12 +00:00
// Exit if accessed directly.
defined ( 'ABSPATH' ) || exit ;
2018-09-10 21:59:04 +00:00
2018-04-16 21:28:31 +00:00
/*
* Class Name : WP_Bootstrap_Navwalker
* Plugin Name : WP Bootstrap Navwalker
* Plugin URI : https :// github . com / wp - bootstrap / wp - bootstrap - navwalker
* Description : A custom WordPress nav walker class to implement the Bootstrap 4 navigation style in a custom theme using the WordPress built in menu manager .
* Author : Edward McIntyre - @ twittem , WP Bootstrap , William Patton - @ pattonwebz
* Version : 4.1 . 0
* Author URI : https :// github . com / wp - bootstrap
* GitHub Plugin URI : https :// github . com / wp - bootstrap / wp - bootstrap - navwalker
* GitHub Branch : master
* License : GPL - 3.0 +
* License URI : http :// www . gnu . org / licenses / gpl - 3.0 . txt
*/
/* Check if Class Exists. */
2018-04-17 16:10:40 +00:00
if ( ! class_exists ( 'Understrap_WP_Bootstrap_Navwalker' ) ) {
2018-04-16 21:28:31 +00:00
/**
* WP_Bootstrap_Navwalker class .
*
* @ extends Walker_Nav_Menu
*/
2018-04-17 16:10:40 +00:00
class Understrap_WP_Bootstrap_Navwalker extends Walker_Nav_Menu {
2018-04-16 21:28:31 +00:00
/**
* Starts the list before the elements are added .
*
* @ since WP 3.0 . 0
*
* @ see Walker_Nav_Menu :: start_lvl ()
*
* @ param string $output Used to append additional content ( passed by reference ) .
* @ param int $depth Depth of menu item . Used for padding .
* @ param stdClass $args An object of wp_nav_menu () arguments .
*/
public function start_lvl ( & $output , $depth = 0 , $args = array () ) {
if ( isset ( $args -> item_spacing ) && 'discard' === $args -> item_spacing ) {
$t = '' ;
$n = '' ;
} else {
$t = " \t " ;
$n = " \n " ;
}
$indent = str_repeat ( $t , $depth );
// Default class to add to the file.
$classes = array ( 'dropdown-menu' );
/**
* Filters the CSS class ( es ) applied to a menu list element .
*
* @ since WP 4.8 . 0
*
* @ param array $classes The CSS classes that are applied to the menu `<ul>` element .
* @ param stdClass $args An object of `wp_nav_menu()` arguments .
* @ param int $depth Depth of menu item . Used for padding .
*/
$class_names = join ( ' ' , apply_filters ( 'nav_menu_submenu_css_class' , $classes , $args , $depth ) );
$class_names = $class_names ? ' class="' . esc_attr ( $class_names ) . '"' : '' ;
/**
* The `.dropdown-menu` container needs to have a labelledby
* attribute which points to it ' s trigger link .
*
* Form a string for the labelledby attribute from the the latest
* link with an id that was added to the $output .
*/
$labelledby = '' ;
// find all links with an id in the output.
preg_match_all ( '/(<a.*?id=\"|\')(.*?)\"|\'.*?>/im' , $output , $matches );
// with pointer at end of array check if we got an ID match.
if ( end ( $matches [ 2 ] ) ) {
// build a string to use as aria-labelledby.
$labelledby = 'aria-labelledby="' . end ( $matches [ 2 ] ) . '"' ;
}
$output .= " { $n } { $indent } <ul $class_names $labelledby role= \" menu \" > { $n } " ;
}
/**
* Starts the element output .
*
* @ since WP 3.0 . 0
* @ since WP 4.4 . 0 The { @ see 'nav_menu_item_args' } filter was added .
*
* @ see Walker_Nav_Menu :: start_el ()
*
* @ param string $output Used to append additional content ( passed by reference ) .
* @ param WP_Post $item Menu item data object .
* @ param int $depth Depth of menu item . Used for padding .
* @ param stdClass $args An object of wp_nav_menu () arguments .
* @ param int $id Current item ID .
*/
public function start_el ( & $output , $item , $depth = 0 , $args = array (), $id = 0 ) {
if ( isset ( $args -> item_spacing ) && 'discard' === $args -> item_spacing ) {
$t = '' ;
$n = '' ;
} else {
$t = " \t " ;
$n = " \n " ;
}
$indent = ( $depth ) ? str_repeat ( $t , $depth ) : '' ;
$classes = empty ( $item -> classes ) ? array () : ( array ) $item -> classes ;
// Initialize some holder variables to store specially handled item
// wrappers and icons.
$linkmod_classes = array ();
$icon_classes = array ();
/**
* Get an updated $classes array without linkmod or icon classes .
*
* NOTE : linkmod and icon class arrays are passed by reference and
* are maybe modified before being used later in this function .
*/
$classes = self :: seporate_linkmods_and_icons_from_classes ( $classes , $linkmod_classes , $icon_classes , $depth );
// Join any icon classes plucked from $classes into a string.
$icon_class_string = join ( ' ' , $icon_classes );
/**
* Filters the arguments for a single nav menu item .
*
* WP 4.4 . 0
*
* @ param stdClass $args An object of wp_nav_menu () arguments .
* @ param WP_Post $item Menu item data object .
* @ param int $depth Depth of menu item . Used for padding .
*/
$args = apply_filters ( 'nav_menu_item_args' , $args , $item , $depth );
// Add .dropdown or .active classes where they are needed.
if ( isset ( $args -> has_children ) && $args -> has_children ) {
$classes [] = 'dropdown' ;
}
if ( in_array ( 'current-menu-item' , $classes , true ) || in_array ( 'current-menu-parent' , $classes , true ) ) {
$classes [] = 'active' ;
}
// Add some additional default classes to the item.
$classes [] = 'menu-item-' . $item -> ID ;
$classes [] = 'nav-item' ;
// Allow filtering the classes.
$classes = apply_filters ( 'nav_menu_css_class' , array_filter ( $classes ), $item , $args , $depth );
// Form a string of classes in format: class="class_names".
$class_names = join ( ' ' , $classes );
$class_names = $class_names ? ' class="' . esc_attr ( $class_names ) . '"' : '' ;
/**
* Filters the ID applied to a menu item ' s list item element .
*
* @ since WP 3.0 . 1
* @ since WP 4.1 . 0 The `$depth` parameter was added .
*
* @ param string $menu_id The ID that is applied to the menu item ' s `<li>` element .
* @ param WP_Post $item The current menu item .
* @ param stdClass $args An object of wp_nav_menu () arguments .
* @ param int $depth Depth of menu item . Used for padding .
*/
$id = apply_filters ( 'nav_menu_item_id' , 'menu-item-' . $item -> ID , $item , $args , $depth );
$id = $id ? ' id="' . esc_attr ( $id ) . '"' : '' ;
$output .= $indent . '<li itemscope="itemscope" itemtype="https://www.schema.org/SiteNavigationElement"' . $id . $class_names . '>' ;
// initialize array for holding the $atts for the link item.
$atts = array ();
// Set title from item to the $atts array - if title is empty then
// default to item title.
if ( empty ( $item -> attr_title ) ) {
$atts [ 'title' ] = ! empty ( $item -> title ) ? strip_tags ( $item -> title ) : '' ;
} else {
$atts [ 'title' ] = $item -> attr_title ;
}
$atts [ 'target' ] = ! empty ( $item -> target ) ? $item -> target : '' ;
2019-06-04 10:55:00 +00:00
if ( '_blank' === $item -> target && empty ( $item -> xfn ) ) { // Thanks to LukaszJaro, see https://github.com/understrap/understrap/issues/973
$atts [ 'rel' ] = 'noopener noreferrer' ;
} else {
$atts [ 'rel' ] = $item -> xfn ;
}
2018-04-16 21:28:31 +00:00
// If item has_children add atts to <a>.
2018-11-23 16:25:38 +00:00
if ( isset ( $args -> has_children ) && $args -> has_children && 0 === $depth && $args -> depth !== 1 ) {
2018-04-16 21:28:31 +00:00
$atts [ 'href' ] = '#' ;
$atts [ 'data-toggle' ] = 'dropdown' ;
$atts [ 'aria-haspopup' ] = 'true' ;
$atts [ 'aria-expanded' ] = 'false' ;
$atts [ 'class' ] = 'dropdown-toggle nav-link' ;
$atts [ 'id' ] = 'menu-item-dropdown-' . $item -> ID ;
} else {
$atts [ 'href' ] = ! empty ( $item -> url ) ? $item -> url : '#' ;
// Items in dropdowns use .dropdown-item instead of .nav-link.
if ( $depth > 0 ) {
$atts [ 'class' ] = 'dropdown-item' ;
} else {
$atts [ 'class' ] = 'nav-link' ;
}
}
2019-11-09 10:20:38 +00:00
$atts [ 'aria-current' ] = $item -> current ? 'page' : '' ;
2018-04-16 21:28:31 +00:00
// update atts of this item based on any custom linkmod classes.
$atts = self :: update_atts_for_linkmod_type ( $atts , $linkmod_classes );
// Allow filtering of the $atts array before using it.
$atts = apply_filters ( 'nav_menu_link_attributes' , $atts , $item , $args , $depth );
// Build a string of html containing all the atts for the item.
$attributes = '' ;
foreach ( $atts as $attr => $value ) {
if ( ! empty ( $value ) ) {
$value = ( 'href' === $attr ) ? esc_url ( $value ) : esc_attr ( $value );
$attributes .= ' ' . $attr . '="' . $value . '"' ;
}
}
/**
* Set a typeflag to easily test if this is a linkmod or not .
*/
$linkmod_type = self :: get_linkmod_type ( $linkmod_classes );
/**
* START appending the internal item contents to the output .
*/
$item_output = isset ( $args -> before ) ? $args -> before : '' ;
/**
* This is the start of the internal nav item . Depending on what
* kind of linkmod we have we may need different wrapper elements .
*/
if ( '' !== $linkmod_type ) {
// is linkmod, output the required element opener.
$item_output .= self :: linkmod_element_open ( $linkmod_type , $attributes );
} else {
// With no link mod type set this must be a standard <a> tag.
$item_output .= '<a' . $attributes . '>' ;
}
/**
* Initiate empty icon var , then if we have a string containing any
* icon classes form the icon markup with an < i > element . This is
* output inside of the item before the $title ( the link text ) .
*/
$icon_html = '' ;
if ( ! empty ( $icon_class_string ) ) {
// append an <i> with the icon classes to what is output before links.
$icon_html = '<i class="' . esc_attr ( $icon_class_string ) . '" aria-hidden="true"></i> ' ;
}
/** This filter is documented in wp-includes/post-template.php */
$title = apply_filters ( 'the_title' , $item -> title , $item -> ID );
/**
* Filters a menu item ' s title .
*
* @ since WP 4.4 . 0
*
* @ param string $title The menu item ' s title .
* @ param WP_Post $item The current menu item .
* @ param stdClass $args An object of wp_nav_menu () arguments .
* @ param int $depth Depth of menu item . Used for padding .
*/
$title = apply_filters ( 'nav_menu_item_title' , $title , $item , $args , $depth );
/**
* If the . sr - only class was set apply to the nav items text only .
*/
if ( in_array ( 'sr-only' , $linkmod_classes , true ) ) {
$title = self :: wrap_for_screen_reader ( $title );
2019-11-09 11:12:25 +00:00
$keys_to_unset = array_keys ( $linkmod_classes , 'sr-only' , true );
2018-04-16 21:28:31 +00:00
foreach ( $keys_to_unset as $k ) {
unset ( $linkmod_classes [ $k ] );
}
}
// Put the item contents into $output.
$item_output .= isset ( $args -> link_before ) ? $args -> link_before . $icon_html . $title . $args -> link_after : '' ;
/**
* This is the end of the internal nav item . We need to close the
* correct element depending on the type of link or link mod .
*/
if ( '' !== $linkmod_type ) {
// is linkmod, output the required element opener.
$item_output .= self :: linkmod_element_close ( $linkmod_type , $attributes );
} else {
// With no link mod type set this must be a standard <a> tag.
$item_output .= '</a>' ;
}
$item_output .= isset ( $args -> after ) ? $args -> after : '' ;
/**
* END appending the internal item contents to the output .
*/
$output .= apply_filters ( 'walker_nav_menu_start_el' , $item_output , $item , $depth , $args );
}
/**
* 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 .
*
* This method should not be called directly , use the walk () method instead .
*
* @ since WP 2.5 . 0
*
* @ see Walker :: start_lvl ()
*
* @ param object $element Data object .
* @ param array $children_elements List of elements to continue traversing ( passed by reference ) .
* @ param int $max_depth Max depth to traverse .
* @ param int $depth Depth of current element .
* @ param array $args An array of arguments .
* @ param string $output Used to append additional content ( passed by reference ) .
*/
public function display_element ( $element , & $children_elements , $max_depth , $depth , $args , & $output ) {
if ( ! $element ) {
return ; }
$id_field = $this -> db_fields [ 'id' ];
// Display this element.
if ( is_object ( $args [ 0 ] ) ) {
$args [ 0 ] -> has_children = ! empty ( $children_elements [ $element -> $id_field ] ); }
parent :: display_element ( $element , $children_elements , $max_depth , $depth , $args , $output );
}
/**
* Menu Fallback
* =============
* If this function is assigned to the wp_nav_menu ' s fallback_cb variable
* and a menu has not been assigned to the theme location in the WordPress
* menu manager the function with display nothing to a non - logged in user ,
* and will add a link to the WordPress menu manager if logged in as an admin .
*
* @ param array $args passed from the wp_nav_menu function .
*/
public static function fallback ( $args ) {
if ( current_user_can ( 'edit_theme_options' ) ) {
/* Get Arguments. */
$container = $args [ 'container' ];
$container_id = $args [ 'container_id' ];
$container_class = $args [ 'container_class' ];
$menu_class = $args [ 'menu_class' ];
$menu_id = $args [ 'menu_id' ];
// initialize var to store fallback html.
$fallback_output = '' ;
if ( $container ) {
$fallback_output .= '<' . esc_attr ( $container );
if ( $container_id ) {
$fallback_output .= ' id="' . esc_attr ( $container_id ) . '"' ;
}
if ( $container_class ) {
$fallback_output .= ' class="' . esc_attr ( $container_class ) . '"' ;
}
$fallback_output .= '>' ;
}
$fallback_output .= '<ul' ;
if ( $menu_id ) {
$fallback_output .= ' id="' . esc_attr ( $menu_id ) . '"' ; }
if ( $menu_class ) {
$fallback_output .= ' class="' . esc_attr ( $menu_class ) . '"' ; }
$fallback_output .= '>' ;
2018-04-17 16:10:40 +00:00
$fallback_output .= '<li><a href="' . esc_url ( admin_url ( 'nav-menus.php' ) ) . '" title="' . esc_attr__ ( 'Add a menu' , 'understrap' ) . '">' . esc_html__ ( 'Add a menu' , 'understrap' ) . '</a></li>' ;
2018-04-16 21:28:31 +00:00
$fallback_output .= '</ul>' ;
if ( $container ) {
$fallback_output .= '</' . esc_attr ( $container ) . '>' ;
}
// if $args has 'echo' key and it's true echo, otherwise return.
if ( array_key_exists ( 'echo' , $args ) && $args [ 'echo' ] ) {
echo $fallback_output ; // WPCS: XSS OK.
} else {
return $fallback_output ;
}
}
}
/**
* Find any custom linkmod or icon classes and store in their holder
* arrays then remove them from the main classes array .
*
* Supported linkmods : . disabled , . dropdown - header , . dropdown - divider , . sr - only
* Supported iconsets : Font Awesome 4 / 5 , Glypicons
*
* NOTE : This accepts the linkmod and icon arrays by reference .
*
* @ since 4.0 . 0
*
* @ param array $classes an array of classes currently assigned to the item .
* @ param array $linkmod_classes an array to hold linkmod classes .
* @ param array $icon_classes an array to hold icon classes .
* @ param integer $depth an integer holding current depth level .
*
* @ return array $classes a maybe modified array of classnames .
*/
private function seporate_linkmods_and_icons_from_classes ( $classes , & $linkmod_classes , & $icon_classes , $depth ) {
// Loop through $classes array to find linkmod or icon classes.
foreach ( $classes as $key => $class ) {
// If any special classes are found, store the class in it's
// holder array and and unset the item from $classes.
if ( preg_match ( '/^disabled|^sr-only/i' , $class ) ) {
// Test for .disabled or .sr-only classes.
$linkmod_classes [] = $class ;
unset ( $classes [ $key ] );
} elseif ( preg_match ( '/^dropdown-header|^dropdown-divider|^dropdown-item-text/i' , $class ) && $depth > 0 ) {
// Test for .dropdown-header or .dropdown-divider and a
// depth greater than 0 - IE inside a dropdown.
$linkmod_classes [] = $class ;
unset ( $classes [ $key ] );
} elseif ( preg_match ( '/^fa-(\S*)?|^fa(s|r|l|b)?(\s?)?$/i' , $class ) ) {
// Font Awesome.
$icon_classes [] = $class ;
unset ( $classes [ $key ] );
} elseif ( preg_match ( '/^glyphicon-(\S*)?|^glyphicon(\s?)$/i' , $class ) ) {
// Glyphicons.
$icon_classes [] = $class ;
unset ( $classes [ $key ] );
}
}
return $classes ;
}
/**
* Return a string containing a linkmod type and update $atts array
* accordingly depending on the decided .
*
* @ since 4.0 . 0
*
* @ param array $linkmod_classes array of any link modifier classes .
*
* @ return string empty for default , a linkmod type string otherwise .
*/
private function get_linkmod_type ( $linkmod_classes = array () ) {
$linkmod_type = '' ;
// Loop through array of linkmod classes to handle their $atts.
if ( ! empty ( $linkmod_classes ) ) {
foreach ( $linkmod_classes as $link_class ) {
if ( ! empty ( $link_class ) ) {
// check for special class types and set a flag for them.
if ( 'dropdown-header' === $link_class ) {
$linkmod_type = 'dropdown-header' ;
} elseif ( 'dropdown-divider' === $link_class ) {
$linkmod_type = 'dropdown-divider' ;
} elseif ( 'dropdown-item-text' === $link_class ) {
$linkmod_type = 'dropdown-item-text' ;
}
}
}
}
return $linkmod_type ;
}
/**
* Update the attributes of a nav item depending on the limkmod classes .
*
* @ since 4.0 . 0
*
* @ param array $atts array of atts for the current link in nav item .
* @ param array $linkmod_classes an array of classes that modify link or nav item behaviors or displays .
*
* @ return array maybe updated array of attributes for item .
*/
private function update_atts_for_linkmod_type ( $atts = array (), $linkmod_classes = array () ) {
if ( ! empty ( $linkmod_classes ) ) {
foreach ( $linkmod_classes as $link_class ) {
if ( ! empty ( $link_class ) ) {
// update $atts with a space and the extra classname...
// so long as it's not a sr-only class.
if ( 'sr-only' !== $link_class ) {
$atts [ 'class' ] .= ' ' . esc_attr ( $link_class );
}
// check for special class types we need additional handling for.
if ( 'disabled' === $link_class ) {
// Convert link to '#' and unset open targets.
$atts [ 'href' ] = '#' ;
unset ( $atts [ 'target' ] );
} elseif ( 'dropdown-header' === $link_class || 'dropdown-divider' === $link_class || 'dropdown-item-text' === $link_class ) {
// Store a type flag and unset href and target.
unset ( $atts [ 'href' ] );
unset ( $atts [ 'target' ] );
}
}
}
}
return $atts ;
}
/**
* Wraps the passed text in a screen reader only class .
*
* @ since 4.0 . 0
*
* @ param string $text the string of text to be wrapped in a screen reader class .
* @ return string the string wrapped in a span with the class .
*/
private function wrap_for_screen_reader ( $text = '' ) {
if ( $text ) {
$text = '<span class="sr-only">' . $text . '</span>' ;
}
return $text ;
}
/**
* Returns the correct opening element and attributes for a linkmod .
*
* @ since 4.0 . 0
*
* @ param string $linkmod_type a sting containing a linkmod type flag .
* @ param string $attributes a string of attributes to add to the element .
*
* @ return string a string with the openign tag for the element with attribibutes added .
*/
private function linkmod_element_open ( $linkmod_type , $attributes = '' ) {
$output = '' ;
if ( 'dropdown-item-text' === $linkmod_type ) {
$output .= '<span class="dropdown-item-text"' . $attributes . '>' ;
} elseif ( 'dropdown-header' === $linkmod_type ) {
// For a header use a span with the .h6 class instead of a real
// header tag so that it doesn't confuse screen readers.
$output .= '<span class="dropdown-header h6"' . $attributes . '>' ;
} elseif ( 'dropdown-divider' === $linkmod_type ) {
// this is a divider.
$output .= '<div class="dropdown-divider"' . $attributes . '>' ;
}
return $output ;
}
/**
* Return the correct closing tag for the linkmod element .
*
* @ since 4.0 . 0
*
* @ param string $linkmod_type a string containing a special linkmod type .
*
* @ return string a string with the closing tag for this linkmod type .
*/
private function linkmod_element_close ( $linkmod_type ) {
$output = '' ;
if ( 'dropdown-header' === $linkmod_type || 'dropdown-item-text' === $linkmod_type ) {
// For a header use a span with the .h6 class instead of a real
// header tag so that it doesn't confuse screen readers.
$output .= '</span>' ;
} elseif ( 'dropdown-divider' === $linkmod_type ) {
// this is a divider.
$output .= '</div>' ;
}
return $output ;
}
}
}