Skip to content

A few issues I have noticed #408

Open
@Sammaye

Description

@Sammaye

This is mostly a sum up of: understrap/understrap#884

As a disclaimer: I have only done a cursory glance of the file, I have not actually downloaded and ran it. I did test the regex out though to reproduce that problem.

  1. Multiple sub menus will have the incorrect ID associated with them due to the regex here https://github.com/wp-bootstrap/wp-bootstrap-navwalker/blob/master/class-wp-bootstrap-navwalker.php#L73 always picking the first sub menu.
  2. It does not skip depths deeper than 1, I noticed in my own testing that the WP nav walker will trigger the start_el and end_el for deeper levels even if you don't have them, I believe it does n+1 levels (I have not double checked that) and this can cause broken HTML
  3. I have also noticed that support for the active sr-only item is not there
  4. Use WP's own $this->has_children instead of overriding this: https://github.com/wp-bootstrap/wp-bootstrap-navwalker/blob/master/class-wp-bootstrap-navwalker.php#L317
  5. Fix up coding standards such as https://github.com/wp-bootstrap/wp-bootstrap-navwalker/blob/master/class-wp-bootstrap-navwalker.php#L119 this isn't like super OMG since PHP does not differentiate between static and dynamic methods but it will still throw a strict error here.
  6. Since you are closing the tags here https://github.com/wp-bootstrap/wp-bootstrap-navwalker/blob/master/class-wp-bootstrap-navwalker.php#L281 this causes end_el to still call and print a </li> which will produce incorrect HTML

I think that's enough to start, there are maybe a few other places but I'll leave it at that for the minute :)

I have attached something I've cooked up as well, this produces valid HTML for most menus, but it lacks some features this plugin possesses, for example: icon type detection and icon only menu items; also it is super unclean and dirty; essentially it is my first draft of converting the BS3 version to BS4:

<?php

/**
 * Class Name: wp_bootstrap_navwalker
 * GitHub URI: https://github.com/twittem/wp-bootstrap-navwalker
 * Description: A custom WordPress nav walker class to implement the Bootstrap 3 navigation style in a custom theme using the WordPress built in menu manager.
 * Version: 2.0.4
 * Author: Edward McIntyre - @twittem
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 */

class Understrap_Walker_Nav_Menu extends Walker_Nav_Menu {

	public $item;

	public $sub_menu_tag = 'div';

	/**
	 * Starts the list before the elements are added.
	 *
	 * @since 3.0.0
	 *
	 * @see Walker::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 ( 1 < $depth ) {
			return;
		}

		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$indent = str_repeat( $t, $depth );

		// Default class.
		$classes = array( 'dropdown-menu' );

		/**
		 * Filters the CSS class(es) applied to a menu list element.
		 *
		 * @since 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 = implode( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );

		$atts                    = array();
		$atts['class']           = $class_names;
		$atts['aria-labelledby'] = 'navbarDropdown-' . $this->item->ID;

		$attributes = $this->html_attributes( $atts );
		$attributes = $attributes ? ' ' . $attributes : '';

		$output .= "{$n}{$indent}<" . $this->sub_menu_tag . "$attributes>{$n}";
	}

	public function end_lvl( &$output, $depth = 0, $args = array() ) {
		if ( 1 < $depth ) {
			return;
		}

		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$indent = str_repeat( $t, $depth );
		$output .= "$indent</" . $this->sub_menu_tag . ">{$n}";
	}

	/**
	 * Starts the element output.
	 *
	 * @since 3.0.0
	 * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
	 *
	 * @see Walker::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 ( 1 < $depth ) {
			return;
		}

		$this->item = $item;

		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$indent =  $depth ? str_repeat( $t, $depth ) : '';

		/**
		 * Filters the arguments for a single nav menu item.
		 *
		 * @since 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 );

		$classes   = empty( $item->classes ) ? array() : (array) $item->classes;
		$classes[] = 'menu-item-' . $item->ID;
		if ( 0 === $depth ) {
			$classes[]       = 'nav-item';
			$this->item->tag = 'li';
		} else if ( ! empty( $item->title ) && strcasecmp( $item->title, 'divider' ) === 0 ) {
			$classes[]       = 'dropdown-divider';
			$this->item->tag = 'div';
		} else if ( ! empty( $item->attr_title ) && strcasecmp( $item->attr_title, 'header' ) === 0 ) {
			$classes[]       = 'dropdown-header';
			$this->item->tag = 'div';
		} else {
			$this->item->tag = 'a';
		}

		if ( $this->has_children && 0 === $depth ) {
			$classes[] = 'dropdown';
		}

		$current = false;
		foreach ( $item->classes as $class ) {
			if ( in_array( $class, [
				'current-menu-parent',
				'current-menu-ancestor',
				'current-menu-item',
				'current-page-item'
			] ) ) {
				$current   = true;
				$classes[] = 'active';
			}
		}

		/**
		 * Filters the CSS class(es) applied to a menu item's list item element.
		 *
		 * @since 3.0.0
		 * @since 4.1.0 The `$depth` parameter was added.
		 *
		 * @param array $classes The CSS classes that are 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.
		 */
		$classes = apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth );

		/**
		 * Filters the ID applied to a menu item's list item element.
		 *
		 * @since 3.0.1
		 * @since 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 = esc_attr( apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth ) );

		$atts           = array();
		$atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
		$atts['target'] = ! empty( $item->target ) ? $item->target : '';
		$atts['rel']    = ! empty( $item->xfn ) ? $item->xfn : '';
		$atts['href']   = ! empty( $item->url ) ? $item->url : '';
		$atts['class']  = [];

		if ( $this->has_children && 0 === $depth ) {
			$atts['id']            = 'navbarDropdown-' . $item->ID;
			$atts['role']          = 'button';
			$atts['href']          = '#';
			$atts['data-toggle']   = 'dropdown';
			$atts['class']         = [ 'nav-link', 'dropdown-toggle' ];
			$atts['aria-haspopup'] = 'true';
			$atts['aria-expanded'] = 'false';
		} else if ( 1 === $depth ) {
			$atts['id']    = $id;
			$atts['class'] = array_merge($atts['class'], ['dropdown-item'], $classes);

			if ( $current ) {
				$atts['class'][] = 'active';
			}
		} else {
			$atts['class']  = ['nav-link'];
		}

		$atts['class'] = esc_attr( implode(' ', $atts['class'] ) );

		$id          = $id ? ' id="' . $id . '"' : '';
		$class_names = $classes ? ' class="' . implode( ' ', $classes) . '"' : '';

		/**
		 * Filters the HTML attributes applied to a menu item's anchor element.
		 *
		 * @since 3.6.0
		 * @since 4.1.0 The `$depth` parameter was added.
		 *
		 * @param array $atts {
		 *     The HTML attributes applied to the menu item's `<a>` element, empty strings are ignored.
		 *
		 * @type string $title Title attribute.
		 * @type string $target Target attribute.
		 * @type string $rel The rel attribute.
		 * @type string $href The href attribute.
		 * }
		 *
		 * @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.
		 */
		$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );

		/** This filter is documented in wp-includes/post-template.php */
		$title = apply_filters( 'the_title', $item->title, $item->ID );

		$icon = '';
		if ( ! empty( $item->icon ) ) {
			$icon = '<span class="' . esc_attr( $item->icon ) . '"></span>&nbsp;';
		}

		/**
		 * Filters a menu item's title.
		 *
		 * @since 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 ( $this->item->tag === 'div' ) {
			$item_output = $args->before . $indent . '<div' . $id . $class_names . '>' . $args->after;

			/**
			 * Filters a menu item's starting output.
			 *
			 * The menu item's starting output only includes `$args->before`, the opening `<a>`,
			 * the menu item's title, the closing `</a>`, and `$args->after`. Currently, there is
			 * no filter for modifying the opening and closing `<li>` for a menu item.
			 *
			 * @since 3.0.0
			 *
			 * @param string $item_output The menu item's starting HTML output.
			 * @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.
			 */
			$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );

			return;
		}

		$item_output = $args->before;

		if ( 0 === $depth ) {
			$item_output .= $indent . '<li' . $id . $class_names . '>';
		}

		$attributes = '';
		foreach ( $atts as $attr => $value ) {
			if ( ! empty( $value ) ) {
				$value      = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
				$attributes .= ' ' . $attr . '="' . $value . '"';
			}
		}

		$item_output .= '<a' . $attributes . '>' . $icon;
		$item_output .= $args->link_before . $title . $args->link_after;

		if ( $current ) {
			$item_output .= '<span class="sr-only">(current)</span>';
		}

		if ( $this->has_children && 0 === $depth ) {
			$item_output .= ' <span class="caret"></span>';
		}

		if ( 0 === $depth ) {
			$item_output .= '</a>';
		}

		$item_output .= $args->after;

		/**
		 * Filters a menu item's starting output.
		 *
		 * The menu item's starting output only includes `$args->before`, the opening `<a>`,
		 * the menu item's title, the closing `</a>`, and `$args->after`. Currently, there is
		 * no filter for modifying the opening and closing `<li>` for a menu item.
		 *
		 * @since 3.0.0
		 *
		 * @param string $item_output The menu item's starting HTML output.
		 * @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.
		 */
		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}

	/**
	 * Ends the element output, if needed.
	 *
	 * @since 3.0.0
	 *
	 * @see Walker::end_el()
	 *
	 * @param string $output Used to append additional content (passed by reference).
	 * @param WP_Post $item Page data object. Not used.
	 * @param int $depth Depth of page. Not Used.
	 * @param stdClass $args An object of wp_nav_menu() arguments.
	 */
	public function end_el( &$output, $item, $depth = 0, $args = array() ) {
		if ( 1 < $depth ) {
			return;
		}

		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}

		$output .= '</' . $this->item->tag . ">{$n}";
	}

	private function html_attributes( array $atts ) {
		$attributes = array();

		foreach ( $atts as $k => $v ) {
			$attributes[] = $k . '="' . esc_attr( $v ) . '"';
		}

		return implode( ' ', $attributes );
	}
}

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions