Divisez wp_nav_menu avec un marcheur personnalisé

16

J'essaie de créer un menu qui affiche un maximum de 5 éléments. S'il y a plus d'articles, il doit les envelopper dans un autre <ul>élément pour créer une liste déroulante.

5 articles ou moins:

Menu déroulant

6 Articles ou plus

Menu déroulant

Je sais que ce type de fonctionnalité pourrait facilement être créé avec un déambulateur qui compte les éléments de menu et les encapsule s'il y a plus de 5 éléments restants dans un élément séparé <ul> . Mais je ne sais pas comment créer ce déambulateur.

Le code qui montre mon menu en ce moment est le suivant:

<?php wp_nav_menu( array( 'theme_location' => 'navigation', 'fallback_cb' => 'custom_menu', 'walker' =>new Custom_Walker_Nav_Menu ) ); ?>

J'ai remarqué que si le menu n'est pas défini par l'utilisateur et qu'il utilise la fonction de repli, le déambulateur n'a aucun effet. J'en ai besoin pour fonctionner dans les deux cas.

Boule de neige
la source
1
Le marcheur de menu personnalisé est une classe qui s'étend Walker_Nav_Menuet il y a un exemple dans le codex . Que voulez-vous dire par "Je ne sais pas comment créer le Walker"?
cybmeta
Btw, +1 parce que l'idée est en fait assez géniale. Comment êtes-vous tombé dessus? Avez-vous un article source ou quelque chose? Si oui, je serais heureux de lire cela. Merci d'avance.
kaiser
@kaiser juste une idée de conception désagréable :) aucun message source, c'est pourquoi je demande.
Snowball
@cybmeta Je sais créer le marcheur et aussi qu'il y a un exemple dans le codex, mais il n'y a pas d'exemple pour ce problème spécifique. Je ne sais donc pas comment créer un déambulateur personnalisé qui me donne une solution
Snowball
Vous devriez demander aux gars UX.SE à propos de cette idée et vérifier s'il y a des problèmes avec cela pour l'utilisateur. UX est un site vraiment génial qui apporte une assez bonne vérification de la réalité sur la convivialité / l'expérience et des réponses et des problèmes régulièrement bien pensés. Vous pourriez aussi bien revenir et nous affinerons cette idée ensemble. (ce serait vraiment génial!).
kaiser

Réponses:

9

En utilisant un Walker personnalisé, la start_el()méthode a accès à $depthparam: quand il s'agit de 0l'elemnt est un top, et nous pouvons utiliser ces informations pour maintenir un compteur interne.

Lorsque le compteur atteint une limite, nous pouvons utiliser DOMDocumentpour obtenir à partir de la sortie HTML complète le dernier élément ajouté, l'envelopper dans un sous-menu et l'ajouter à nouveau au HTML.


Éditer

Lorsque le nombre d'éléments est exactement le nombre dont nous avions besoin + 1, par exemple, nous avons demandé que 5 éléments soient visibles et que le menu en ait 6, cela n'a aucun sens de diviser le menu, car les éléments seront 6 de toute façon. Le code a été modifié pour y remédier.


Voici le code:

class SplitMenuWalker extends Walker_Nav_Menu {

  private $split_at;
  private $button;
  private $count = 0;
  private $wrappedOutput;
  private $replaceTarget;
  private $wrapped = false;
  private $toSplit = false;

  public function __construct($split_at = 5, $button = '<a href="#">&hellip;</a>') {
      $this->split_at = $split_at;
      $this->button = $button;
  }

  public function walk($elements, $max_depth) {
      $args = array_slice(func_get_args(), 2);
      $output = parent::walk($elements, $max_depth, reset($args));
      return $this->toSplit ? $output.'</ul></li>' : $output;
  }

  public function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0 ) {
      $this->count += $depth === 0 ? 1 : 0;
      parent::start_el($output, $item, $depth, $args, $id);
      if (($this->count === $this->split_at) && ! $this->wrapped) {
          // split at number has been reached generate and store wrapped output
          $this->wrapped = true;
          $this->replaceTarget = $output;
          $this->wrappedOutput = $this->wrappedOutput($output);
      } elseif(($this->count === $this->split_at + 1) && ! $this->toSplit) {
          // split at number has been exceeded, replace regular with wrapped output
          $this->toSplit = true;
          $output = str_replace($this->replaceTarget, $this->wrappedOutput, $output);
      }
   }

   private function wrappedOutput($output) {
       $dom = new DOMDocument;
       $dom->loadHTML($output.'</li>');
       $lis = $dom->getElementsByTagName('li');
       $last = trim(substr($dom->saveHTML($lis->item($lis->length-1)), 0, -5));
       // remove last li
       $wrappedOutput = substr(trim($output), 0, -1 * strlen($last));
       $classes = array(
         'menu-item',
         'menu-item-type-custom',
         'menu-item-object-custom',
         'menu-item-has-children',
         'menu-item-split-wrapper'
       );
       // add wrap li element
       $wrappedOutput .= '<li class="'.implode(' ', $classes).'">';
       // add the "more" link
       $wrappedOutput .= $this->button;
       // add the last item wrapped in a submenu and return
       return $wrappedOutput . '<ul class="sub-menu">'. $last;
   }
}

L'utilisation est assez simple:

// by default make visible 5 elements
wp_nav_menu(array('menu' => 'my_menu', 'walker' => new SplitMenuWalker()));

// let's make visible 2 elements
wp_nav_menu(array('menu' => 'another_menu', 'walker' => new SplitMenuWalker(2)));

// customize the link to click/over to see wrapped items
wp_nav_menu(array(
  'menu' => 'another_menu',
  'walker' => new SplitMenuWalker(5, '<a href="#">more...</a>')
));
gmazzap
la source
Fonctionne très bien! Super travail Giuseppe. La grande chose à ce sujet est qu'il fonctionne aussi bien s'il y a un sous-menu dans les 5 premiers éléments du menu. Et qu'il n'emballe pas un seul point de menu dans un sous-menu s'il n'est pas nécessaire. Juste une petite chose: par défaut, il affiche 6 éléments en tant que $split_at = 5mais l' $countindex commence à 0.
Snowball
Merci @Snowball J'ai résolu ce problème mineur, maintenant le menu affiche le nombre exact passé en $split_atargument, 5 par défaut.
gmazzap
10

Il existe même un moyen de rendre cela possible avec CSS seul. Cela a quelques limites, mais j'ai toujours pensé que cela pourrait être une approche intéressante:

Limites

  • Vous devez coder en dur la largeur de la liste déroulante
  • Navigateur-Support. Vous avez essentiellement besoin de sélecteurs CSS3 . Mais tout à partir d'IE8 devrait fonctionner, même si je n'ai pas testé cela.
  • Il s'agit plus d'une preuve de concept. Il y a plusieurs inconvénients comme ne fonctionner que s'il n'y a pas de sous-éléments.

Approche

Bien que je n'utilise pas vraiment "Quantity Queries", l'utilisation créative de :nth-childet ~j'ai lu dans les récentes requêtes de quantité pour CSS est ce qui m'a conduit à cette solution.

L'approche est essentiellement la suivante:

  1. Masquer tous les éléments après le 4
  2. Ajoutez des ...points à l'aide d'un beforepseudo-élément.
  3. Lorsque vous survolez les points (ou l'un des éléments cachés), affichez les éléments supplémentaires, alias le sous-menu.

Voici le code CSS pour un balisage de menu WordPress par défaut. J'ai commenté en ligne.

/* Optional: Center the navigation */
.main-navigation {
    text-align: center;
}

.menu-main-menu-container {
    display: inline-block;
}

/* Float menu items */
.nav-menu li {
    float:left;
    list-style-type: none;
}

/* Pull the 5th menu item to the left a bit so that there isn't too
   much space between item 4 and ... */
.nav-menu li:nth-child(4) {
    margin-right: -60px;
}

/* Create a pseudo element for ... and force line break afterwards
   (Hint: Use a symbol font to improve styling) */
.nav-menu li:nth-child(5):before {
    content: "...\A";
    white-space: pre;
}

/* Give the first 4 items some padding and push them in front of the submenu */
.nav-menu li:nth-child(-n+4) {
    padding-right: 15px;
    position: relative;
    z-index: 1;
}

/* Float dropdown-items to the right. Hardcode width of dropdown. */
.nav-menu li:nth-child(n+5) {
    float:right;
    clear: right;
    width: 150px;
}

/* Float Links in dropdown to the right and hide by default */
.nav-menu li:nth-child(n+5) a{
    display: none;      
    float: right;
    clear: right;
}   

/* When hovering the menu, show all menu items from the 5th on */
.nav-menu:hover li:nth-child(n+5) a,
.nav-menu:hover li:nth-child(n+5) ~ li a{
    display: inherit;
}

/* When hovering one of the first 4 items, hide all items after it 
   so we do not activate the dropdown on the first 4 items */
.nav-menu li:nth-child(-n+4):hover ~ li:nth-child(n+5) a{
    display: none;
}

J'ai également créé un jsfiddle pour le montrer en action: http://jsfiddle.net/jg6pLfd1/

Si vous avez d'autres questions sur la façon dont cela fonctionne, veuillez laisser un commentaire, je serais heureux de clarifier davantage le code.

kraftner
la source
Merci pour votre approche. J'ai déjà pensé à le faire avec CSS mais je pense que c'est plus propre de le faire directement dans php. De plus, cette solution place le 5ème point de menu dans un sous-menu, il n'est également pas nécessaire s'il n'y a que cinq éléments de menu.
Snowball
Eh bien, l'activer uniquement pour 5+ éléments pourrait probablement être corrigé. Quoi qu'il en soit, je suis conscient que ce n'est pas parfait et qu'une approche PHP pourrait être plus propre. Mais je l'ai quand même trouvé suffisamment intéressant pour l'inclure par souci d'exhaustivité. Une autre option est toujours agréable. :)
kraftner
2
Bien sûr. Btw. si vous ajoutez un autre sous-menu à cela, il se casse également
Snowball
1
Sûr. Il s'agit plus d'une preuve de concept jusqu'à présent. Ajout d'un avertissement.
kraftner
8

Vous pouvez utiliser un wp_nav_menu_itemsfiltre. Il accepte la sortie de menu et les arguments qui contiennent des attributs de menu, comme le slug de menu, le conteneur, etc.

add_filter('wp_nav_menu_items', 'wpse_180221_nav_menu_items', 20, 2);

function wpse_180221_nav_menu_items($items, $args) {
    if ($args->menu != 'my-menu-slug') {
        return $items;
    }

    // extract all <li></li> elements from menu output
    preg_match_all('/<li[^>]*>.*?<\/li>/iU', $items, $matches);

    // if menu has less the 5 items, just do nothing
    if (! isset($matches[0][5])) {
        return $items;
    }

    // add <ul> after 5th item (can be any number - can use e.g. site-wide variable)
    $matches[0][5] = '<li class="menu-item menu-item-type-custom">&hellip;<ul>'
          . $matches[0][5];

    // $matches contain multidimensional array
    // first (and only) item is found matches array
    return implode('', $matches[0]) . '</ul></li>';
}
mjakic
la source
1
J'ai édité quelques problèmes mineurs, mais cela ne fonctionne que si tous les éléments de menu n'ont pas de sous-éléments. Parce que Regex ne reconnaît pas la hiérarchie. Testez-le: si l'un des 4 premiers éléments de menu contient un élément enfant, le menu est assez détruit.
gmazzap
1
C'est vrai. Dans ce cas, DOMDocumentpeut être utilisé. Cependant, dans cette question, il n'y a pas de sous-menus, donc la réponse est correcte pour ce cas spécifique. DOMDocument serait une solution "universelle" mais je n'ai pas le temps pour l'instant. Vous pouvez enquêter;) parcourir les éléments LI, si un enfant UL le saute, ce serait une solution mais nécessite une version écrite :)
mjakic
1
(a) vous ne savez pas s'il y a un sous-menu dans OP. Les sous-menus apparaissent lorsque la souris est terminée, donc ... (b) Oui, DOMDocument peut fonctionner, mais dans ce cas, vous devez boucler récursivement les éléments pour vérifier l'intérieur ul. WordPress boucle déjà les éléments de menu dans le menu walker. C'est déjà une opération lente en soi , l'ajout d'une boucle supplémentaire je pense que ce n'est pas la bonne solution, au contraire, un déambulateur personnalisé serait une solution beaucoup plus propre et efficace.
gmazzap
Merci les gars, mais @gmazzap est vrai, il y a la possibilité que les autres points de menu (les 4 premiers ou les autres) contiennent un autre sous-menu. Donc, cette soultion ne fonctionnera pas.
Snowball
Vous pouvez également placer deux menus, un principal et un "caché". Ajoutez un bouton stylisé avec trois points "..." et cliquez ou survolez pour afficher le deuxième menu. Ça devrait être super facile.
mjakic
5

Vous avez une fonction de travail, mais vous ne savez pas si c'est la meilleure solution.

J'ai utilisé un déambulateur personnalisé:

class Custom_Walker_Nav_Menu extends Walker_Nav_Menu {
function start_el(  &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
    global $wp_query;
    $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';

    $classes = empty( $item->classes ) ? array() : (array) $item->classes;
    $classes[] = 'menu-item-' . $item->ID;

    $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
    $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

    $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
    $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

    /**
     * This counts the $menu_items and wraps if there are more then 5 items the
     * remaining items into an extra <ul>
     */
    global $menu_items;
    $menu_items = substr_count($output,'<li');
    if ($menu_items == 4) {
      $output .= '<li class="tooltip"><span>...</span><ul class="tooltip-menu">';
    }

    $output .= $indent . '<li' . $id . $class_names .'>';

    $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 = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );

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

    $item_output = $args->before;
    $item_output .= '<a'. $attributes .'>';
    $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
    $item_output .= '</a>';
    $item_output .= $args->after;

    $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );

  }
}

La fonction qui affiche le menu réel est la suivante:

        <?php
        wp_nav_menu( array( 'container' => false, 'theme_location' => 'navigation', 'fallback_cb' => 'custom_menu', 'walker' =>new Custom_Walker_Nav_Menu ) );
        global $menu_items;
        // This adds the closing </li> and </ul> if there are more then 4 items in the menu
        if ($menu_items > 4) {
            echo "</li></ul>";
        }
        ?>

J'ai déclaré la variable globale $ menu_items et l'ai utilisée pour montrer la fermeture <li> et les <ul>balises. Il est probablement possible de le faire également dans le marcheur personnalisé, mais je n'ai pas trouvé où et comment.

Deux problèmes: 1. S'il n'y a que 5 éléments dans le menu, il enveloppe également le dernier élément dans une pensée qu'il n'est pas nécessaire.

  1. Cela ne fonctionne que si l'utilisateur a réellement alloué un menu à theme_location, le déambulateur ne se déclenche pas si wp_nav_menu affiche la fonction de secours
Boule de neige
la source
Avez-vous essayé ce qui se passe si l'un des 4 premiers éléments comporte des sous-menus? Astuce: substr_count($output,'<li')sera == 4au mauvais endroit ...
gmazzap