Suddividi wp_nav_menu con walker personalizzato


16

Sto cercando di creare un menu che mostra un massimo di 5 voci. Se ci sono più elementi, è necessario avvolgerli in un altro <ul>elemento per creare un menu a discesa.

5 articoli o meno:

Cadere in picchiata

6 articoli o più

Cadere in picchiata

So che questo tipo di funzionalità potrebbe essere facilmente creato con un walker che conta le voci di menu e si avvolge se ci sono più di 5 il resto in un separato <ul>. Ma non so come creare questo walker.

Il codice che mostra il mio menu al momento è il seguente:

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

Ho notato che se il menu non è definito dall'utente e utilizza la funzione di fallback invece il walker non ha alcun effetto. Ne ho bisogno per funzionare in entrambi i casi.


1
Il walker di menu personalizzato è una classe che si estende Walker_Nav_Menue c'è un esempio nel codice . Cosa intendi con "Non so come creare il Walker"?
cybmeta,

A proposito, +1 perché l'idea è in realtà abbastanza fantastica. Come ti sei imbattuto in esso? Hai un post di origine o qualcosa del genere? Se è così, sarei felice di leggerlo. Grazie in anticipo.
Kaiser

@kaiser solo una brutta idea di design :) nessun post di fonte, ecco perché lo sto chiedendo.
Snowball,

@cybmeta So di creare il walker e anche che c'è un esempio nel codice, ma non esiste un esempio per questo problema specifico. Quindi non so come creare un walker personalizzato che mi dia una soluzione
Snowball,

Dovresti chiedere ai ragazzi di UX.SE di questa idea e verificare se ci sono problemi con questo per l'utente. UX è un sito davvero fantastico che offre un ottimo controllo della realtà sull'usabilità / esperienza e risposte e problemi regolarmente ben ponderati. Potresti anche tornare e tutti perfezioniamo l'idea insieme. (sarebbe davvero fantastico!).
Kaiser,

Risposte:


9

Usando un Walker personalizzato, il start_el()metodo ha accesso a $depthparam: quando è 0elemnt è uno dei primi, e possiamo usare queste informazioni per mantenere un contatore interno.

Quando il contatore raggiunge un limite, possiamo usare DOMDocumentper ottenere dall'output HTML completo solo l'ultimo elemento aggiunto, inserirlo in un sottomenu e aggiungerlo di nuovo all'HTML.


modificare

Quando il numero di elementi è esattamente il numero richiesto + 1, ad esempio abbiamo richiesto che 5 elementi fossero visibili e il menu ne avesse 6, non ha senso dividere il menu, poiché gli elementi saranno 6 in entrambi i modi. Il codice è stato modificato per risolverlo.


Ecco il codice:

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'utilizzo è piuttosto semplice:

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

Funziona alla grande! Ottimo lavoro Giuseppe. La cosa grandiosa è che funziona anche se c'è un sottomenu nei primi 5 elementi del menu. E che non avvolge solo un singolo punto del menu in un sottomenu se non ce n'è bisogno. Solo una piccola cosa: per impostazione predefinita mostra 6 elementi come $split_at = 5ma l' $countindice inizia a 0.
Snowball

Grazie a @Snowball ho risolto quel piccolo problema, ora il menu mostra il numero esatto passato come $split_atargomento, 5 per impostazione predefinita.
gmazzap

10

C'è anche un modo per renderlo possibile solo con CSS. Questo ha alcune limitazioni, ma ho ancora pensato che potesse essere un approccio interessante:

limitazioni

  • Devi codificare a fondo la larghezza del menu a discesa
  • Browser-Support. Fondamentalmente hai bisogno di selettori CSS3 . Ma tutto da IE8 in su dovrebbe funzionare, anche se non l'ho provato.
  • Questa è più una prova di concetto. Ci sono molti inconvenienti come lavorare solo se non ci sono sotto-elementi.

Approccio

Anche se non sto realmente usando "Query quantitative", l'uso creativo di :nth-childe ~ho letto nelle recenti Query quantitative per CSS è stato ciò che mi ha portato a questa soluzione.

L'approccio è sostanzialmente questo:

  1. Nascondi tutti gli elementi dopo il 4 °
  2. Aggiungi ...punti usando uno beforepseudo-elemento.
  3. Quando passi con il mouse i punti (o uno qualsiasi degli elementi nascosti) mostra gli oggetti extra, ovvero il sottomenu.

Ecco il codice CSS per un markup del menu WordPress predefinito. Ho commentato in linea.

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

Ho anche creato un jsfiddle per mostrarlo in azione: http://jsfiddle.net/jg6pLfd1/

Se hai ulteriori domande su come funziona, lascia un commento, sarei felice di chiarire ulteriormente il codice.


Grazie per il tuo approccio. Ho già pensato di farlo con CSS ma penso che sia più pulito farlo direttamente all'interno di php. Inoltre questa soluzione inserisce il quinto punto del menu in un sottomenu, inoltre non è necessario se ci sono solo cinque voci di menu.
Snowball,

Bene, abilitandolo solo per 5+ elementi potrebbe probabilmente essere risolto. Comunque sono consapevole che questo non è perfetto e un approccio PHP potrebbe essere più pulito. Ma l'ho trovato ancora abbastanza interessante da includerlo per completezza. Un'altra opzione è sempre piacevole. :)
Kraftner

2
Ovviamente. Btw. se aggiungi un altro sottomenu, anche questo si rompe
Snowball,

1
Sicuro. Questa è più di una prova di concetto fino ad ora. Aggiunto un avviso.
Kraftner,

8

Puoi usare il wp_nav_menu_itemsfiltro. Accetta l'output del menu e gli argomenti che contengono gli attributi del menu, come la barra dei menu, il contenitore, ecc.

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

1
Ho modificato un paio di problemi minori, tuttavia questo funziona solo se tutte le voci di menu non hanno voci secondarie. Perché Regex non riconosce la gerarchia. Provalo: se una delle prime 4 voci di menu contiene una voce figlio, il menu è piuttosto distrutto.
gmazzap

1
È vero. In tal caso DOMDocumentpuò essere utilizzato. Tuttavia, in questa domanda non ci sono sottomenu, quindi la risposta è corretta per questo caso specifico. DOMDocument sarebbe una soluzione "universale" ma non ho tempo in questo momento. Puoi indagare;) scorrere tra gli oggetti LI, se uno ha un bambino UL lo salta, sarebbe una soluzione ma necessita di una versione scritta :)
mjakic

1
(a) non sai se ci sono sottomenu in OP. I sottomenu vengono visualizzati quando il mouse è finito, quindi ... (b) Sì, DOMDocument potrebbe funzionare, ma in tal caso è necessario ricorrere in modo ricorsivo agli elementi per eseguire il controllo degli interni ul. WordPress esegue già il loop delle voci di menu nel menu a comparsa. È già un'operazione lenta di per sé , l'aggiunta e il loop aggiuntivo penso che non sia la soluzione giusta, in campagna, un deambulatore personalizzato sarebbe una soluzione molto più pulita ed efficiente.
gmazzap

Grazie ragazzi, ma @gmazzap è vero, c'è la possibilità che gli altri punti del menu (sia i primi 4 che gli altri) contengano un altro sottomenu. Quindi questa anima non funzionerà.
Snowball,

È inoltre possibile posizionare due menu, uno principale e uno "nascosto". Aggiungi un pulsante in stile con tre punti "..." e al clic o passa con il mouse mostra il secondo menu. Dovrebbe essere super facile.
mjakic,

5

Ha una funzione funzionante, ma non sono sicuro che sia la soluzione migliore.

Ho usato un walker personalizzato:

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 funzione che mostra il menu attuale è la seguente:

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

Ho dichiarato la variabile globale $ menu_items e l'ho usata per mostrare la chiusura <li> e i <ul>tag. Probabilmente è possibile farlo anche all'interno del walker personalizzato, ma non ho trovato dove e come.

Due problemi: 1. Se ci sono solo 5 elementi nel menu, avvolge anche l'ultimo elemento in un sebbene non sia necessario.

  1. Funziona solo se l'utente ha effettivamente assegnato un menu a theme_location, il walker non si attiva se wp_nav_menu mostra la funzione di fallback

Hai provato cosa succede se uno dei primi 4 elementi ha dei sottomenu? Suggerimento: substr_count($output,'<li')sarà == 4nel posto sbagliato ...
gmazzap
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.