jQuery Stolperstein: hover() und slideUp()/slideDown()/animate()

21 06 2009

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.



Trackbacks


Keine Trackbacks

Kommentare

Ansicht der Kommentare: (Linear | Verschachtelt)
28 07 2009
#1 simon (Antwort)

wunderbar,
funktioniert auch astrein bei längeren listen im dokument.
hatte erst angefangen mit lock variablen zu arbeiten, was auch ging, allerdings gefällt mir deine lösung mit 2 Zeilen wesentlich besser.

vg,
simon
12 03 2010
#2 TYPO3 Agentur (Antwort)

Tolles Tutorial....
Funktioniert auch einwandfrei

Grüsse aus KS
07 04 2010
#3 Dominik (Antwort)

Super, genau das hab ich gesucht. Funktioiert perfekt.

Danke
17 04 2010
#4 Robert (Antwort)

Hi,

genau das hab ich gesucht. Vielen Dank!

Gruß,
Robert ...
29 07 2010
#5 Felix (Antwort)

Als Tip:
Für das Speichern der Variablen bietet sich $.data an:

$("#elementId").data('origHeight',$("#elementId").height());

auslesen: $("#elementId").data('origHeight');

... so müllt man weder das globale Scope noch das DOM zu und nutzt nebenbei noch die dafür vorgesehene jQuery Funktion ;)
29 07 2010
#5.1 Gregor Nathanael Meyer (Antwort)

Ja, war mir auch irgendwann aufgefallen, daher der Nachtrag vom 05.07.2010.
15 01 2011
#6 PHilipp (Antwort)

Vielen Dank.
Hatte schon mit komplizierten setTimeout gearbeitet, um das zu verhinden!
So geht das viel einfacher xD

Danke ^^

Gruß von einer der größten Spieleseiten Deutschlands ^^
13 03 2011
#7 Werner (Antwort)

Vielen Dank

Das ".height('auto')" brachte mich aber nicht weiter, da slideDown immer rd. 50px zu viel animierte, um dann auf die tatsächliche Höhe wieder nach oben korrigierte.

Habe es dann mit einer festen Höhenangabe probiert, also nicht mehr
".height('auto')" , sondern wie in meinem Fall ".height('65px')"

Hierbei ging teilweise die Animation verloren, da sofort mit 65px geöffnet wurde.
Habe es jetzt wie nachstehend gelöst, indem im die Höhe auch animiere und so langsam aufbaue.


$('#fig_aside_Video figcaption').hide();
$('#fig_aside_Video').hover(function() {
$(this).children('figcaption').stop().slideDown(1000).animate({height:'65px'},400);
}, function() {
$(this).children('figcaption').stop().slideUp(500);
});
13 04 2011
#8 Webagentur Goslar (Antwort)

Super!!! Funktioniert einwandfrei
15 05 2011
#9 Flo (Antwort)

Ja saugut... Hatte die Hoffnung schon bald aufgegeben das Problem mit dem "anhovern" und der Warteschlange in den Griff zu bekommen... Aber Du hast ja jetzt die optimale Lösung gefunden, vielen Dank :-)
19 07 2011
#10 flex (Antwort)

mit
.stop( true, true )
kann man sich den Umweg schenken.
http://api.jquery.com/stop/
19 07 2011
#10.1 Gregor Nathanael Meyer (Antwort)

Oh, fantastisch, ich ändere den Text gleich.

Kommentar schreiben


Die angegebene E-Mail-Adresse wird nicht dargestellt, sondern nur für eventuelle Benachrichtigungen verwendet.
BBCode-Formatierung erlaubt