Search code examples
jquerywordpresstwitter-bootstrapwp-nav-walker

Add additional button to Bootstrap navigation in WordPress custom walker


I'm using the following code of a custom navigation walker for WordPress to generate a bootstrap navigation:

<?php


class NavWalker extends Walker_Nav_Menu {
  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, 'cssClasses'), 10, 2);
    add_filter('nav_menu_link_attributes', array($this, 'filter_menu_link_atts'), 10, 3);
    add_filter('walker_nav_menu_start_el', array($this, 'filter_menu_item_output'), 10, 4);
    add_filter('nav_menu_item_id', '__return_null');
    $cpt           = get_post_type();
    $this->cpt     = in_array($cpt, get_post_types(array('_builtin' => false)));
    $this->archive = get_post_type_archive_link($cpt);
  }

  /**
  * @see Walker::start_lvl()
  * @since 3.0.0
  *
  * @param string $output Passed by reference. Used to append additional content.
  * @param int $depth Depth of page. Used for padding.
  */
  // @codingStandardsIgnoreStart
  public function start_lvl(&$output, $depth = 0, $args = array()) {
    parent::start_lvl($output, $depth, $args);
    $pos    = strrpos($output, '">', -1);
    $output = substr_replace($output, ' dropdown-menu depth'.$depth.'" role="menu">', $pos);
  }
  // @codingStandardsIgnoreEnd

  /**
   * 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.
   *
   * This method shouldn't be called directly, use the walk() method instead.
   *
   * @see Walker::start_el()
   * @since 2.5.0
   *
   * @param object $element Data object
   * @param array $children_elements List of elements to continue traversing.
   * @param int $max_depth Max depth to traverse.
   * @param int $depth Depth of current element.
   * @param array $args
   * @param string $output Passed by reference. Used to append additional content.
   * @return null Null on failure with no changes to parameters.
   */

  // @codingStandardsIgnoreStart
  public function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output) {
    if (!$element) {
      return;
    }

    $id_field = $this->db_fields['id'];
    $this->has_children = !empty($children_elements[$element->$id_field]);

    parent::display_element($element, $children_elements, $max_depth, $depth, $args, $output);
  }
  // @codingStandardsIgnoreEnd

  public function cssClasses($classes, $item) {
    if ($this->has_children) {
      $classes[] = 'dropdown';
    }

    if ($item->menu_item_parent != 0) {
      $classes[] = 'dropdown-item';
    }

    if ($item->menu_item_parent == 0) {
      $classes[] = 'nav-item';
    }

    if (strcasecmp($item->attr_title, 'header') == 0) {
      $classes[] = 'dropdown-header';
    }

    $slug = sanitize_title($item->title);

    if ($this->cpt) {
      $classes = str_replace('current_page_parent', '', $classes);
      if (strcasecmp($this->archive, $item->url) === 0) {
        $classes[] = 'active';
      }
    }

    //$classes = preg_replace('/(current(-menu-|[-_]page[-_])(item|parent|ancestor))/', '', $classes);
    $classes = preg_replace('/^((menu|page)[-_\w+]+)+/', '', $classes);
    $classes[] = 'menu-' . $slug;
    $classes = array_unique($classes);

    return array_filter($classes, function ($element) {
      $element = trim($element);
      return !empty($element);
    });
  }

  /*
   * This filter is used to customize the <li> attributes.
   */
   // @codingStandardsIgnoreStart
  public function filter_menu_link_atts($atts, $item, $args) {

    if (is_object($args->walker)) { // Filter if custom walker
      $classes   = array();
      $classes[] = 'nav-link';

	  /*
      if ($args->walker->has_children) {
        $atts['data-toggle']   = 'dropdown';
        $classes[]             = 'dropdown-toggle';
        $atts['aria-haspopup'] = 'true';
      }
      */

      if (strcasecmp($item->attr_title, 'disabled') == 0) {
        $classes[] = 'disabled';
      }

      if ($item->current == 1) {
        $classes[] = 'active';
      }

      if ($item->parent == 1) {
        $classes[] = 'active';
      }

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

    return $atts;
  }
  // @codingStandardsIgnoreEnd

  /*
   * This filter is used to customize the <li> final output.
   */
   // @codingStandardsIgnoreStart
  public function filter_menu_item_output($item_output, $item, $depth, $args) {
    $indent = str_repeat("\t", $depth);

    if (strcasecmp($item->attr_title, 'divider') == 0 && $depth === 1) {
      $item_output .= $indent . '<li role="presentation" class="dropdown-divider">';
    } else if ((strcasecmp($item->attr_title, 'header') == 0 && $depth === 1) && $depth === 1) {
      $item_output = $indent . '<h6>' . esc_attr($item->title) . '</h6>';
    }

    $args->link_before = '';
    $args->link_after = '';

    return $indent . $item_output;
  }
  // @codingStandardsIgnoreEnd
}

I'm also using the following JS code to open the dropdown in the navigation on mouse over:

 $('ul.nav li.dropdown.nav-item').hover(function() {
			  $(this).find('.sub-menu.dropdown-menu.depth0').stop(true, true).delay(200).fadeIn(500);
			}, function() {
			  $(this).find('.sub-menu.dropdown-menu.depth0').stop(true, true).delay(200).fadeOut(500);
			});

		    $('li.dropdown.dropdown-item').hover(function() {
			  $(this).find('.sub-menu.dropdown-menu.depth1').stop(true, true).delay(200).fadeIn(500);
			}, function() {
			  $(this).find('.sub-menu.dropdown-menu.depth1').stop(true, true).delay(200).fadeOut(500);
			});

In this case, the mobile user has no chance to open the dropdown. He will always click on the link. This is absolutly ok for me. I just need one more link for mobile users (arrow-icon on the right side) to let them open the dropdown.

I just don't understand where I have to add this link in the custom walker.


Solution

  • I've found a solution. Change this code:

    public function start_lvl(&$output, $depth = 0, $args = array()) {
         parent::start_lvl($output, $depth, $args);
         $pos    = strrpos($output, '">', -1);
         $output = substr_replace($output, ' dropdown-menu depth'.$depth.'" role="menu">', $pos);
    }
    

    to this:

    public function start_lvl(&$output, $depth = 0, $args = array()) {
        $indent = str_repeat("\t", $depth);
        $output .= "\n$indent<button class=\"btn btn-link btn-menu pull-xs-right hidden-lg-up\" data-toggle=\"dropdown\"><i class=\"icon-chevron-down\"></i></button>\n";
    
        parent::start_lvl($output, $depth, $args);
        $pos    = strrpos($output, '">', -1);
        $output = substr_replace($output, ' dropdown-menu depth'.$depth.'" role="menu">', $pos);
     }