Dividir wp_nav_menu con walker personalizado

16

Estoy tratando de crear un menú que muestre un máximo de 5 elementos. Si hay más elementos, debe envolverlos en otro <ul>Elemento para crear un menú desplegable.

5 artículos o menos:

Desplegable

6 artículos o más

Desplegable

Sé que este tipo de funcionalidad podría crearse fácilmente con un andador que cuenta los elementos del menú y se ajusta si hay más de 5 la continuación en un separado <ul>. Pero no sé cómo crear este andador.

El código que muestra mi menú en este momento es el siguiente:

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

Me di cuenta de que si el usuario no define el menú y utiliza la función alternativa, el andador no tiene ningún efecto. Lo necesito para trabajar en ambos casos.

Bola de nieve
fuente
1
El menú personalizado walker es una clase que se extiende Walker_Nav_Menuy hay un ejemplo en el códice . ¿Qué quieres decir con "No sé cómo crear el Walker"?
cybmeta
Por cierto, +1 porque la idea es realmente impresionante. ¿Cómo te topaste con eso? ¿Tienes una publicación fuente o algo así? Si es así, me encantaría leer eso. Gracias por adelantado.
kaiser
@kaiser solo una idea de diseño desagradable :) sin publicación de origen, es por eso que estoy preguntando.
Snowball
@cybmeta Sé crear el andador y también que hay un ejemplo en el códice, pero no hay ningún ejemplo para este problema específico. Así que no sé cómo crear un andador personalizado que me brinde una solución
Snowball
Debería preguntarles a los chicos de UX.SE sobre esta idea y verificar si hay problemas con eso para el usuario. UX es un sitio realmente increíble que ofrece una muy buena verificación de la realidad de usabilidad / experiencia y respuestas y problemas regularmente bien pensados. También podría volver y todos refinaremos esa idea juntos. (¡eso sería realmente genial!).
kaiser

Respuestas:

9

Usando un Walker personalizado, el start_el()método tiene acceso a $depthparam: cuando es 0el elemento es uno de los mejores, y podemos usar esta información para mantener un contador interno.

Cuando el contador alcanza un límite, podemos usar DOMDocumentpara obtener de la salida HTML completa solo el último elemento agregado, envolverlo en un submenú y agregarlo nuevamente al HTML.


Editar

Cuando el número de elementos es exactamente el número que requerimos + 1, por ejemplo, requerimos que 5 elementos estén visibles y el menú tiene 6, no tiene sentido dividir el menú, porque los elementos serán 6 de cualquier manera. El código fue editado para abordar eso.


Aquí está el 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;
   }
}

El uso es bastante 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
fuente
Funciona excelente! Impresionante trabajo Giuseppe. Lo bueno de esto es que funciona tan bien si hay un submenú en los primeros 5 elementos del menú. Y que no incluye un solo punto de menú en un submenú si no es necesario. Solo una cosa menor: de manera predeterminada, muestra 6 elementos, $split_at = 5pero el $countíndice comienza en 0.
Snowball
Gracias a @Snowball Arregle ese problema menor, ahora el menú muestra el número exacto pasado como $split_atargumento, 5 por defecto.
gmazzap
10

Incluso hay una manera de hacer esto posible solo con CSS. Esto tiene algunas limitaciones, pero todavía pensé que podría ser un enfoque interesante:

Limitaciones

  • Necesitas codificar el ancho del menú desplegable
  • Soporte de navegador. Básicamente necesitas selectores CSS3 . Pero todo, desde IE8 en adelante, debería funcionar, aunque no lo he probado.
  • Esto es más una prueba de concepto. Hay varios inconvenientes, como solo trabajar si no hay subelementos.

Acercarse

Aunque realmente no estoy usando "Consultas de cantidad", el uso creativo de :nth-childy ~he leído en las Consultas de cantidad recientes para CSS fue lo que me llevó a esta solución.

El enfoque es básicamente este:

  1. Ocultar todos los elementos después del 4to.
  2. Agregue ...puntos usando un beforepseudo-elemento.
  3. Al desplazar los puntos (o cualquiera de los elementos ocultos) muestra los elementos adicionales, también conocido como el submenú.

Aquí está el código CSS para un marcado de menú predeterminado de WordPress. He comentado en línea.

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

También he creado un jsfiddle para mostrarlo en acción: http://jsfiddle.net/jg6pLfd1/

Si tiene más preguntas sobre cómo funciona esto, deje un comentario, me complacerá aclarar más el código.

Kraftner
fuente
Gracias por tu acercamiento. Ya pensé en hacerlo con CSS, pero creo que es más limpio hacerlo directamente desde php. Además, esta solución coloca el quinto punto del menú en un submenú, tampoco es necesario si solo hay cinco elementos del menú.
Snowball
Bueno, solo habilitarlo para más de 5 artículos probablemente podría repararse. De todos modos, soy consciente de que esto no es perfecto y un enfoque PHP podría ser más limpio. Pero todavía me pareció lo suficientemente interesante como para incluirlo por completo. Otra opción siempre es agradable. :)
kraftner
2
Por supuesto. Por cierto. si agrega otro submenú a esto, también se rompe
Snowball
1
Seguro. Esto es más una prueba de concepto hasta ahora. Se agregó una advertencia.
kraftner
8

Puedes usar wp_nav_menu_itemsfiltro. Acepta resultados de menú y argumentos que contienen atributos de menú, como babosa de menú, contenedor, 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
fuente
1
He editado un par de problemas menores, sin embargo, esto solo funciona si todos los elementos del menú no tienen elementos secundarios. Porque Regex no reconoce la jerarquía. Pruébelo: si alguno de los primeros 4 elementos del menú contiene un elemento secundario, el menú está bastante destruido.
gmazzap
1
Es verdad. En ese caso DOMDocumentse puede usar. Sin embargo, en esta pregunta no hay submenús, por lo tanto, la respuesta es correcta para este caso específico. DOMDocument sería una solución "universal" pero no tengo tiempo en este momento. Puede investigar;) recorrer los elementos de LI, si uno tiene un hijo UL omitirlo, sería una solución pero necesita una versión escrita :)
mjakic
1
(a) no sabe si hay un submenú en OP. Los submenús aparecen cuando el mouse se acaba, así que ... (b) Sí, DOMDocument puede funcionar, pero en ese caso necesita hacer un bucle recursivo de elementos para hacer la verificación de interno ul. WordPress ya recorre los elementos del menú en el menú de Walker Ya es una operación lenta per se , agregando y un ciclo adicional, creo que no es la solución correcta, por el contrario, un andador personalizado sería una solución mucho más limpia y eficiente.
gmazzap
Gracias chicos, pero @gmazzap es cierto, existe la posibilidad de que los otros puntos del menú (los primeros 4 o los otros) contengan otro submenú. Entonces esta alma no funcionará.
Snowball
También puede colocar dos menús, uno principal y uno "oculto". Agregue un botón con estilo con tres puntos "..." y al hacer clic o al pasar el mouse, muestre el segundo menú. Debería ser súper fácil.
mjakic
5

Tiene una función de trabajo, pero no estoy seguro de si es la mejor solución.

Usé un 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 );

  }
}

La función que muestra el menú real es la siguiente:

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

Declaré la variable global $ menu_items y la usé para mostrar el cierre <li>y <ul>-tags. Probablemente sea posible hacer eso también dentro del andador personalizado, pero no encontré dónde y cómo.

Dos problemas: 1. Si solo hay 5 elementos en el menú, también envuelve el último elemento en un pensamiento, aunque no es necesario.

  1. Simplemente funciona si el usuario realmente asignó un menú a theme_location, el andador no se activa si wp_nav_menu muestra la función de respaldo
Bola de nieve
fuente
¿Has probado qué sucede si alguno de los primeros 4 elementos tiene algunos submenús? Consejo: substr_count($output,'<li')estará == 4en el lugar equivocado ...
gmazzap