Gelegentlich möchte ein CSS Dowpdown-Menü um eine kleine Animation erweitert werden, die bei der Gelegenheit auch den Suckerfish für den IE6 übernimmt. Natürlich macht man sowas mit jQuery, wenn man das sowieso eingebunden hat, in etwa so (natürlich alles innerhalb von $(document).ready()):

$('#mainMenu>li').hover(
  function(){
    $(this).find('>ul').slideDown(150);
  },
  function(){
    $(this).find('>ul').slideUp(50);
  }
);

Nun kommt es dabei immer wieder zu den selben Problemen:

Fährt man mit der Maus schnell mehrmals über einen Punkt, werden alle nötigen Animationen in eine Warteschlange gepackt und nach und nach gemütlich abgearbeitet. Das lässt sich noch leicht und logisch beheben, indem man jeweils mit einem stop() vor slideDown() und SlideUp() die Animation erst einmal stoppt, bevor man das Gegenstück ausführt. Das gleiche passiert auch bei animate() und vergleichbaren Sachen und lässt sich da auf die gleiche Weise beheben. Soweit kein Problem und nach ein paar Sekunden Google-Recherche gefunden.

Ein weiteres Problem ist das Handling der Animation bei der ersten Berührung mit der Maus: Scheinbar wird das jQuery hover Event erst bei der zweiten Berührung genutzt, bei der ersten klappt das Menü ganz normal mit der im CSS definierten Methode aus. Auch hier hilft eine Google-Recherche schnell weiter und bringt die Lösung frisch auf den Tisch: Vor der ersten Berührung (also zweckmäßiger Weise in $(document).ready()) müssen die UL-Elemente der Untermenüs mit einem beherzten Aufruf von hide() versteckt werden, auch wenn sie durch das CSS eigentlich schon versteckt sind. OK, das muss man wissen und kommt nicht von alleine drauf.

Perfide ist das dritte Problem bei Animationen mit den Ausmaßen der Untermenüs, also slideDown()/slideUp() und animate() in Kombination mit height() oder width(): Nutzt man diese innerhalb der hover() Funktion und verlässt das Element noch während die Animation läuft, speichert jQuery den zu diesem Zeitpunkt aktuellen Wert der animierten Abmessungen zwischen und animiert fortan nur noch bis zu diesem Maximalwert. Verlässt man den Menüpunkt also sofort wieder, wird das Menü bis zum Seitenreload nie wieder erscheinen, denn es ist ja nur noch etwa einen Pixel hoch und wird entsprechend auch nur bis zu einem Pixel animiert. Hier habe ich keine Lösung bei Google gefunden, weil ich nicht wusste, wonach ich suchen sollte. Also habe ich mich mit dem Firebug auf die Lauer gelegt und das Problem in der beschriebenen Form analysieren können. Ich weiß nicht, ob das ein Bug ist oder volle Absicht, aber ich kann mir nicht wirklich eine Situation vorstellen, wo dieses Verhalten gewünscht wäre. Die Lösung dafür liegt aber auf der Hand: Ein unscheinbares height('auto') vor der ausklappenden Animation behebt das Problem sehr zuverlässig.

So sieht nun also das komplette Menüscript aus, das sich endlich so verhält, wie man es auch erwartet:

$('#mainMenu>li>ul').hide(); // needed to prevent CSS-only behaviour on first contact
$('#mainMenu>li').hover(
  function() {
    $(this).find('>ul').stop().height('auto').slideDown(150); // the height('auto') prevents the menu from memorizing an incorrect height-value forever when leaving the menu while the animation is running
  },
  function() {
    $(this).find('>ul').stop().slideUp(50);
  }
);

An anderer Stelle half mir height('auto') aber nicht weiter, was also tun? Die Lösung lag zunächst auch hier auf der Hand: Beim Laden der Seite müssen die initialen Werte des Elements in einer Variablen gespeichert werden, zu denen die Animation dann jeweils zurückkehren kann. Das schien mir immens unelegant, weil ich nicht den globalen Scope mit solcherlei Variablen vollmüllen wollte. Mein Bruder brachte dann den entscheidenden Tipp: Man kann die Werte prima als Attribute des Elements im DOM ablegen. Beliebige Attribute sind in HTML zwar nicht erlaubt, aber wenn das Dokument erst mal ins DOM eingelesen wurde, sind die Limitierungen von HTML völlig gleichgültig. Kurz gesagt: Ist das Ding im DOM, ist es kein HTML mehr. Die Denke muss man sich erst mal klar machen. Gut, also schreibe ich die Werte in $(document).ready() fix als Attribute ins DOM und alles wird gut. Doch da hatte ich nicht mit der strengen, aber korrekten Auslegung des ready()-Events in Opera gerechnet: Dort stehen zu diesem Zeitpunkt die gewünschten Werte nicht zur Verfügung, weil es sofort gefeuert wird, sobald das DOM fertig eingelesen wurde, aber eben noch bevor die Engine irgendetwas rendern konnte. Abmessungen sind also nicht bekannt. Die Lösung lautet hier $(window).load(). Dieses Event wird gefeuert, sobald die Seite gerendert wurde, mithin also auch alle Abmessungen bereit stehen. So sind die Events spezifiziert, aber Opera scheint der einzige Browser zu sein, der sie auch so handhabt. Die komplette Lösung für dieses Problem lautet also in etwa so:

$(window).load(function(){
  $("#elementId").attr('origHeight', $("#elementId").height());
});

Ausgelesen wird auf die gleiche Weise, also $("#elementId").attr('origHeight').

Nachtrag 05.07.2010: Wo ich den alten Beitrag gerade noch mal lese, fällt mir auf, dass Doku lesen oft hilft. Der letzte Punkt wird natürlich viel eleganter gelöst, wenn man einfach die data()-Methode von jQuery benutzt. Also wie immer: Augen auf und Hirn an.

Nachtrag 19.07.2011: In den Kommentaren wurde ich darauf hingewiesen, dass ein .stop(true, true) die genannten Probleme löst. Fantastisch. Ist das neu dazu gekommen oder bin ich nur zu blöd, Dokumentationen zu lösen?

Nachtrag 04.11.2011: jQuery 1.7 nimmt sich zumindest bei toggling animations des Problems an, dass ein Abbruch auf halber Höhe weitere Klappvorgänge nur noch bis dort hin durchführt. Ich zitiere aus dem Release-Post zu jQuery 1.7:

In previous versions of jQuery, toggling animations such as .slideToggle() or .fadeToggle() would not work properly when animations were stacked on each other and a previous animation was terminated with .stop(). This has been fixed in 1.7 so that the animation system remembers the elements’ initial values and resets them in the case where a toggled animation is terminated prematurely.

Ich habe das noch nicht ausprobiert und wenn das nur bei Toggle funktioniert, braucht es eine der oben genannten Methoden in anderen Fällen weiterhin, aber trotzdem eine gute Nachricht.

Noch keine Kommentare

Die Kommentarfunktion wurde vom Besitzer dieses Blogs in diesem Eintrag deaktiviert.