Teilen Sie wp_nav_menu mit benutzerdefiniertem Walker auf

16

Ich versuche, ein Menü zu erstellen, das maximal 5 Elemente enthält. Wenn weitere Elemente vorhanden sind, sollten diese in ein anderes <ul>Element eingeschlossen werden, um ein Dropdown-Menü zu erstellen.

5 Artikel oder weniger:

Dropdown-Liste

6 Artikel oder mehr

Dropdown-Liste

Ich weiß, dass diese Art von Funktionalität leicht mit einem Walker erstellt werden kann, der die Menüelemente zählt und sie umschließt, wenn mehr als 5 in einem separaten Bereich verbleiben <ul>. Aber ich weiß nicht, wie ich diesen Walker erschaffen soll.

Der Code, der mein Menü im Moment anzeigt, ist der folgende:

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

Mir ist aufgefallen, dass der Benutzer keine Auswirkung hat, wenn das Menü nicht vom Benutzer definiert wurde und stattdessen die Fallback-Funktion verwendet wird. Ich brauche es in beiden Fällen zu arbeiten.

Schneeball
quelle
1
Die benutzerdefinierte Menüführung ist eine Klasse, die erweitert wird, Walker_Nav_Menuund es gibt ein Beispiel im Codex . Was meinst du mit "Ich weiß nicht, wie man den Walker erstellt"?
Cybmeta
Übrigens +1, weil die Idee eigentlich ganz toll ist. Wie bist du darauf gestoßen? Hast du einen Quellpost oder so? Wenn ja, würde ich das gerne lesen. Danke im Voraus.
Kaiser
@kaiser nur eine böse Designidee :) kein Quellpost, deshalb frage ich.
Schneeball
@cybmeta Ich weiß, dass ich den Walker erstellen muss und auch, dass es ein Beispiel im Codex gibt, aber es gibt kein Beispiel für dieses spezielle Problem. Ich weiß also nicht, wie ich einen benutzerdefinierten Walker erstellen soll, der mir eine Lösung bietet
Schneeball
Sie sollten die UX.SE-Mitarbeiter nach dieser Idee fragen und überprüfen, ob für den Benutzer Probleme damit vorliegen . UX ist eine wirklich großartige Website, die eine ziemlich gute Überprüfung der Benutzerfreundlichkeit / Erfahrung und regelmäßig gut durchdachte Antworten und Probleme bietet. Sie können auch zurückkommen, und wir alle entwickeln diese Idee gemeinsam weiter. (Das wäre wirklich großartig!).
Kaiser

Antworten:

9

Mit einem benutzerdefinierten Walker kann der start_el() Methode Zugriff auf $depthparam: Wenn es sich um ein 0Element handelt, das zu den Top-Werten gehört, können wir diese Informationen verwenden, um einen internen Zähler zu verwalten.

Wenn der Zähler ein Limit erreicht, können wir verwenden DOMDocument aus der vollständigen HTML-Ausgabe nur das zuletzt hinzugefügte Element abrufen, es in ein Untermenü einschließen und es erneut zu HTML hinzufügen.


Bearbeiten

Wenn die Anzahl der Elemente genau der Anzahl entspricht, die wir + 1 benötigen, z. B., dass 5 Elemente sichtbar sind und das Menü 6 enthält, ist es nicht sinnvoll, das Menü zu teilen, da die Elemente in beide Richtungen 6 sind. Der Code wurde bearbeitet, um dies zu beheben.


Hier ist der 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;
   }
}

Die Verwendung ist ziemlich einfach:

// 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
quelle
Funktioniert hervorragend! Tolle Arbeit, Giuseppe. Das Tolle daran ist, dass es auch funktioniert, wenn in den ersten 5 Menüelementen ein Untermenü vorhanden ist. Und dass es nicht nur einen Menüpunkt in ein Untermenü einwickelt, wenn es nicht benötigt wird. Nur eine Kleinigkeit: Standardmäßig werden 6 Elemente angezeigt, $split_at = 5aber der $countIndex beginnt bei 0.
Schneeball
Dank @Snowball habe ich dieses kleinere Problem behoben. Das Menü zeigt nun die genaue Zahl, die als $split_atArgument übergeben wurde (standardmäßig 5).
gmazzap
10

Es gibt sogar eine Möglichkeit, dies nur mit CSS zu ermöglichen. Dies hat einige Einschränkungen, aber ich dachte immer noch, dass es ein interessanter Ansatz sein könnte:

Einschränkungen

  • Sie müssen die Breite des Dropdowns fest codieren
  • Browser-Unterstützung. Sie benötigen grundsätzlich CSS3-Selektoren . Aber alles ab IE8 sollte funktionieren, obwohl ich das nicht getestet habe.
  • Dies ist eher ein Proof-of-Concept. Es gibt mehrere Nachteile, wie nur zu arbeiten, wenn es keine Unterelemente gibt.

Ansatz

Obwohl ich nicht wirklich "Mengenabfragen" verwende, ist die kreative Verwendung von :nth-childund ~ich habe in letzter Zeit gelesen Mengenabfragen für CSS zu dieser Lösung geführt.

Der Ansatz ist im Grunde:

  1. Verstecke alle Gegenstände nach dem 4.
  2. Fügen Sie ...mit a Punkte hinzubefore Pseudoelement.
  3. Wenn Sie mit dem Mauszeiger über die Punkte (oder eines der ausgeblendeten Elemente) fahren, werden die zusätzlichen Elemente angezeigt, die auch als Untermenü bezeichnet werden.

Hier ist der CSS-Code für ein Standard-WordPress-Menü-Markup. Ich habe inline kommentiert.

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

Ich habe auch eine jsfiddle erstellt, um sie in Aktion zu zeigen: http://jsfiddle.net/jg6pLfd1/

Wenn Sie weitere Fragen haben, hinterlassen Sie bitte einen Kommentar, ich würde mich freuen, den Code weiter zu klären.

Kraftner
quelle
Vielen Dank für Ihre Annäherung. Ich dachte schon, es mit CSS zu machen, aber ich denke, es ist sauberer, es direkt in PHP zu machen. Zusätzlich bringt diese Lösung den 5. Menüpunkt in ein Untermenü, auch bei nur fünf Menüpunkten ist dies nicht erforderlich.
Schneeball
Nun, es könnte wahrscheinlich behoben werden, wenn es nur für 5+ Gegenstände aktiviert wird. Jedenfalls ist mir bewusst, dass dies nicht perfekt ist und ein PHP-Ansatz sauberer sein könnte. Aber ich fand es trotzdem interessant genug, es der Vollständigkeit halber aufzunehmen. Eine andere Option ist immer schön. :)
Kraftner
2
Natürlich. Btw. Wenn Sie ein weiteres Untermenü hinzufügen, wird es ebenfalls unterbrochen
Schneeball
1
Sicher. Dies ist bisher eher ein Proof of Concept. Warnung hinzugefügt.
Kraftner
8

Sie können wp_nav_menu_itemsFilter verwenden. Es akzeptiert Menüausgaben und Argumente, die Menüattribute enthalten, wie Menü-Slug, Container usw.

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
quelle
1
Ich habe ein paar kleinere Probleme bearbeitet, dies funktioniert jedoch nur, wenn alle Menüelemente keine Unterelemente enthalten. Weil Regex keine Hierarchie erkennt. Testen Sie es: Wenn eines der ersten 4 Menüelemente ein untergeordnetes Element enthält, ist das Menü ziemlich zerstört.
gmazzap
1
Das ist richtig. In diesem Fall DOMDocumentkann verwendet werden. In dieser Frage gibt es jedoch keine Untermenüs, daher ist die Antwort für diesen speziellen Fall richtig. DOMDocument wäre eine "universelle" Lösung, aber ich habe momentan keine Zeit. Sie können untersuchen;) die LI-Elemente durchlaufen, wenn ein UL-Kind es überspringen hat, wäre das eine Lösung, aber eine schriftliche Version
erforderlich
1
(a) Sie wissen nicht, ob es ein Untermenü in OP gibt. Untermenüs werden angezeigt, wenn der Mauszeiger nach oben zeigt. (B) Ja, DOMDocument funktioniert möglicherweise. In diesem Fall müssen Sie jedoch eine rekursive Schleife für Elemente ausführen, um die Prüfung auf Inneres durchzuführen ul. WordPress schleift bereits Menüelemente in der Menüführung. Es ist bereits ein langsamer Vorgang per se , das Hinzufügen und zusätzliche Schleife denke ich , ist nicht die richtige Lösung, auf der countrary, eine benutzerdefinierte walker viel sauberer und effiziente Lösung wäre.
gmazzap
Danke Leute, aber @gmazzap ist wahr, es besteht die Möglichkeit, dass die anderen Menüpunkte (sowohl die ersten 4 als auch die anderen) ein anderes Untermenü enthalten. Also wird diese Lösung nicht funktionieren.
Schneeball
Sie können auch zwei Menüs platzieren, ein Hauptmenü und ein "verstecktes" Menü. Fügen Sie eine gestaltete Schaltfläche mit drei Punkten "..." hinzu und klicken Sie auf oder bewegen Sie den Mauszeiger, um das zweite Menü anzuzeigen. Sollte super einfach sein.
mjakic
5

Ich habe eine funktionierende Funktion, bin mir aber nicht sicher, ob es die beste Lösung ist.

Ich habe einen Custom Walker benutzt:

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

  }
}

Die Funktion, die das aktuelle Menü anzeigt, ist die folgende:

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

Ich habe die globale Variable $ menu_items deklariert und damit das Closing <li>und -tags angezeigt <ul>. Möglicherweise ist dies auch im Custom-Walker möglich, aber ich habe nicht herausgefunden, wo und wie.

Zwei Probleme: 1. Wenn das Menü nur 5 Elemente enthält, wird auch das letzte Element in einen Gedanken eingeschlossen, der nicht erforderlich ist.

  1. Es funktioniert nur, wenn der Benutzer dem theme_location tatsächlich ein Menü zugewiesen hat. Der Walker wird nicht ausgelöst, wenn im wp_nav_menu die Fallback-Funktion angezeigt wird
Schneeball
quelle
Haben Sie versucht, was passiert, wenn eines der ersten 4 Elemente Untermenüs enthält? Tipp: substr_count($output,'<li')Wird == 4am falschen Ort sein ...
gmazzap