Dividir wp_nav_menu com andador personalizado

16

Estou tentando criar um menu que mostre no máximo 5 itens. Se houver mais itens, ele deve agrupá-los em outro <ul>elemento para criar um menu suspenso.

5 itens ou menos:

Suspenso

6 itens ou mais

Suspenso

Eu sei que esse tipo de funcionalidade pode ser facilmente criado com um andador que conta os itens de menu e envolve se houver mais de 5 o restante em um separado <ul>. Mas não sei como criar esse andador.

O código que mostra meu menu no momento é o seguinte:

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

Notei que, se o menu não for definido pelo usuário e ele usar a função de fallback, o andador não terá efeito. Eu preciso que ele funcione nos dois casos.

Bola de neve
fonte
1
O menu walker personalizado é uma classe que se estende Walker_Nav_Menue há um exemplo no codex . O que você quer dizer com "Eu não sei como criar o Walker"?
cybmeta
Entre, +1 porque a ideia é realmente incrível. Como você tropeçou nisso? Você tem uma publicação de origem ou algo assim? Se assim for, eu ficaria feliz em ler isso. Desde já, obrigado.
kaiser
@ Kaiser apenas uma idéia de design desagradável :) nenhuma fonte post, é por isso que estou perguntando.
Snowball
@cybmeta Eu sei criar o walker e também que existe um exemplo no codex, mas não há exemplo para esse problema específico. Então eu não sei como criar um costume walker que me dá uma solução
Snowball
Você deve perguntar ao pessoal do UX.SE sobre essa idéia e verificar se há problemas com isso para o usuário. O UX é um site realmente incrível que traz uma verificação muito boa da realidade sobre usabilidade / experiência e respostas e problemas regularmente bem pensados. Você também pode voltar e todos nós refinamos essa ideia juntos. (isso seria realmente incrível!).
Kaiser #

Respostas:

9

Usando um Walker personalizado, o start_el()método tem acesso a $depthparam: quando ele é 0o elemento principal, e podemos usar essas informações para manter um contador interno.

Quando o contador atingir um limite, podemos usar DOMDocumentpara obter da saída HTML completa apenas o último elemento adicionado, envolvê-lo em um submenu e adicioná-lo novamente ao HTML.


Editar

Quando o número de elementos é exatamente o número que solicitamos + 1, por exemplo, solicitamos que 5 elementos estejam visíveis e o menu tenha 6, não faz sentido dividir o menu, porque os elementos serão 6 de qualquer maneira. O código foi editado para resolver isso.


Aqui está o código:

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;
   }
}

O uso é bem simples:

// 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
fonte
Funciona excelente! Impressionante trabalho Giuseppe. O melhor de tudo é que funciona tão bem se houver um submenu nos 5 primeiros elementos do menu. E que não envolve apenas um único ponto de menu em um submenu, se não houver necessidade. Apenas uma coisa menor: Por padrão, ele mostra 6 elementos como $split_at = 5, mas $countcomeça de índice em 0.
Snowball
Obrigado @Snowball Corrigi esse pequeno problema, agora o menu mostra o número exato passado como $split_atargumento, 5 por padrão.
gmazzap
10

Existe até uma maneira de tornar isso possível apenas com CSS. Isso tem algumas limitações, mas eu ainda pensei que poderia ser uma abordagem interessante:

Limitações

  • Você precisa codificar a largura da lista suspensa
  • Suporte ao navegador. Você basicamente precisa de seletores CSS3 . Mas tudo, desde o IE8 até, deve funcionar, embora eu não testei isso.
  • Isso é mais uma prova de conceito. Existem várias desvantagens, como trabalhar apenas se não houver subitens.

Abordagem

Embora eu não esteja realmente usando "Consultas de quantidade", o uso criativo :nth-childe ~li nas recentes Consultas de quantidade para CSS foram o que me levou a essa solução.

A abordagem é basicamente esta:

  1. Ocultar todos os itens após o quarto
  2. Adicione ...pontos usando um beforepseudoelemento.
  3. Ao passar o mouse sobre os pontos (ou qualquer um dos elementos ocultos), mostre os itens extras, também conhecidos como submenus.

Aqui está o código CSS para uma marcação de menu padrão do WordPress. Eu comentei em linha.

/* 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;
}

Também criei um jsfiddle para mostrá-lo em ação: http://jsfiddle.net/jg6pLfd1/

Se você tiver mais alguma dúvida sobre como isso funciona, deixe um comentário. Teremos prazer em esclarecer melhor o código.

kraftner
fonte
Obrigado pela sua abordagem. Eu já pensei em fazê-lo com CSS, mas acho que é mais fácil fazê-lo diretamente no php. Além disso, esta solução coloca o quinto ponto do menu em um submenu, também não há necessidade se houver apenas cinco itens de menu.
Snowball
Bem, apenas a ativação para mais de 5 itens provavelmente poderia ser corrigida. De qualquer forma, estou ciente de que isso não é perfeito e uma abordagem PHP pode ser mais limpa. Mas ainda achei interessante incluí-lo por uma questão de integridade. Outra opção é sempre agradável. :)
kraftner
2
Claro. Btw. se você adicionar outro submenu para esta quebra, bem
Snowball
1
Certo. Esta é mais uma prova de conceito até agora. Adicionado um aviso.
Kraftner # 6/15
8

Você pode usar wp_nav_menu_itemsfiltro. Ele aceita saída de menu e argumentos que contêm atributos de menu, como slug de menu, contêiner 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
fonte
1
Eu editei alguns problemas menores, mas isso funcionará apenas se todos os itens de menu não tiverem subitens. Porque o Regex não reconhece a hierarquia. Teste: se qualquer um dos 4 primeiros itens de menu contiver um item filho, o menu será bastante destruído.
gmazzap
1
Isso é verdade. Nesse caso, DOMDocumentpode ser usado. No entanto, nesta pergunta não há submenus, portanto, a resposta está correta para este caso específico. O DOMDocument seria uma solução "universal", mas não tenho tempo no momento. Você pode investigar;) looping através dos itens LI, se alguém tem UL criança ignorá-lo, essa seria a solução, mas precisa de escrita versão :)
mjakic
1
(a) você não sabe se há submenu no OP. Os submenus aparecem quando o mouse termina, então ... (b) Sim, o DOMDocument pode funcionar, mas nesse caso você precisa fazer um loop recursivo dos itens para fazer a verificação interna ul. O WordPress já faz loops de itens de menu no menu walker. Já é uma operação lenta, por si só , adicionando um loop adicional que eu acho que não é a solução certa. No país, um caminhante personalizado seria uma solução muito mais limpa e eficiente.
gmazzap
Obrigado pessoal, mas @gmazzap é verdade, existe a possibilidade de que os outros pontos do menu (os 4 primeiros ou os outros) contenham outro submenu. Então essa alma não funcionará.
Snowball
Você também pode colocar dois menus, o principal e o "oculto". Adicione um botão estilizado com três pontos "..." e clique ou passe o mouse para exibir o segundo menu. Deve ser super fácil.
mjakic
5

Tem uma função de trabalho, mas não tem certeza se é a melhor solução.

Eu usei um andador personalizado:

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 );

  }
}

A função que mostra o menu real é a seguinte:

        <?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>";
        }
        ?>

Declarei a variável global $ menu_items e a usei para mostrar o fechamento <li>e <ul>-tags. Provavelmente é possível fazer isso também dentro do andador personalizado, mas não encontrei onde e como.

Dois problemas: 1. Se houver apenas 5 itens no menu, ele envolverá o último item e, ainda assim, não será necessário.

  1. Funciona apenas se o usuário realmente alocou um menu para o theme_location, o walker não dispara se wp_nav_menu estiver mostrando a função de fallback
Bola de neve
fonte
Você já tentou o que acontece se algum dos 4 primeiros itens tiver alguns submenus? Dica: substr_count($output,'<li')estará == 4no lugar errado ...
gmazzap