Ostatni projekt, którym się zajmuje, zawiera imitację flashowej reklamy z trzema rozwijanymi od dołu sloganami, chwalącymi zalety firmy. Implementacja tego rozwiązania, biorąc pod uwagę obecność efektów animacyjnych, nastręcza dużych kłopotów z dziwnym działaniem zdarzenia onmouseout. Postaram się więc przedstawić rozwiązanie problemu – tym razem oparte o mootools.
Dokument HTML
By rozpocząć naszą przygodę z omawianym tematem, przyjrzyjmy się najpierw szablonowi HTML, na którym będziemy pracować.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="pl-PL">
<head>
<title>Ruchome zakładki</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
* { margin: 0; padding: 0 }
body { font: 12px normal, 'Georgia'; }
div { width: 500px; margin: auto; }
div ul { display: block; width: 400px; height: 200px; background: #BAE4F0; list-style-type: none; overflow: hidden; }
div ul li { display: block; width: 120px; height: 100%; float: left; background: #A9D26A; margin-top: 150px; margin-left: 10px; text-align: center; }
div ul li h2 { margin-bottom: 30px; font-size: 20px; }
</style>
</head>
<body>
<div>
<ul>
<li><h2>fast</h2><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p></li>
<li><h2>cool</h2><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p></li>
<li><h2>best</h2><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p></li>
</ul>
</div>
</body>
</html>
Jak widać, każdy element listy nieuporządkowanej ma dość duży margines, który wypycha treść poza kontener. Zastosowanie overflow: hidden; skutecznie ukrywa ją.
Ogólne założenie co do skryptu jest takie, by po najechaniu myszką na element listy zmienić margin-top na 0px. Załóżmy przy tym, że nie stanie się to od razu poprzez ustawienie określonej wartości, np. style.marginTop = 0;. Aby całość prezentowała się schludnie, do rozwijania zakładki użyjemy efektów animacyjnych.
Zwijanie elementu, po opuszczeniu pola zakładki przez kursor myszki, będzie się odbywać analogicznie – ustawimy margin-top na domyślne 150px. Nie obędzie się bez ładnej animacji.
onmouseout
Przed przystąpieniem do działania, musimy omówić sobie istotę problemu, który został poruszony we wstępie. Otóż załóżmy, że mamy ustawione dwa zdarzenia (onmouseover i onmouseout) dla div#foobar:
<div id="foobar">
<p style="background: #cc0000; color: #fff; margin: 0;">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nam ultrices, dolor pellentesque dignissim volutpat, ante felis aliquet elit, nec ornare neque dolor quis eros. Cras auctor velit et magna. Quisque vitae pede eget justo vehicula lacinia. Aenean mauris. Nullam vitae velit a nisl aliquet dictum.</p>
<p style="background: #cc0000; color: #fff; margin: 0;">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nam ultrices, dolor pellentesque dignissim volutpat, ante felis aliquet elit, nec ornare neque dolor quis eros. Cras auctor velit et magna. Quisque vitae pede eget justo vehicula lacinia. Aenean mauris. Nullam vitae velit a nisl aliquet dictum.</p>
</div>
Ku naszemu zdziwieniu, zdarzenie onmouseout jest uruchamiane nie tylko po opuszczeniu obszaru diva, ale także podczas poruszania kursorem w jego obrębie. Dzieje się tak dlatego, że JS, i nie jest to wina żadnej z przeglądarek, interpretuje childNodes jako oddzielne elementy. Potwierdza to poniższy, pełny przykład:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="pl-PL">
<head>
<title>Ruchome zakładki</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript">
window.onload = function()
{
var foobar = document.getElementById('foobar');
foobar.onmouseover = function() { alert('over'); };
foobar.onmouseout = function() { alert('out'); };
}
</script>
</head>
<body>
<div id="foobar">
<p style="background: #cc0000; color: #fff; margin: 0;">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nam ultrices, dolor pellentesque dignissim volutpat, ante felis aliquet elit, nec ornare neque dolor quis eros. Cras auctor velit et magna. Quisque vitae pede eget justo vehicula lacinia. Aenean mauris. Nullam vitae velit a nisl aliquet dictum.</p>
<p style="background: #cc0000; color: #fff; margin: 0;">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nam ultrices, dolor pellentesque dignissim volutpat, ante felis aliquet elit, nec ornare neque dolor quis eros. Cras auctor velit et magna. Quisque vitae pede eget justo vehicula lacinia. Aenean mauris. Nullam vitae velit a nisl aliquet dictum.</p>
</div>
</body>
</html>
Ilość uruchomień funkcji alert jest niepokojąca duża.
Zasadniczo istnieją dwa sposoby na wyeliminowanie problemu. Jedna opiera się o funkcję z IE – contains, której implementację trzeba dodać do innych, nie obsługujących jej, przeglądarek. Drugą metodą jest żmudne sprawdzanie, z jakiego elementu został zwolniony kursor i na który najechał podczas wykonywania się zdarzenia onmouseout. Pierwszy zawiera elementy drugiego, więc omówmy go.
element.contains()
Funkcja ta powinna sprawdzać, czy element podany w argumencie jest dzieckiem elementu ze zmiennej element. Zanim dojdzie do jej kodowania, powiedzmy sobie parę ciekawych rzeczy.
Podczas wykonywania zdarzeń mamy do dyspozycji obiekt Event, który możemy przekazać do funkcji wykonującej dane zdarzenie:
element.onclick = function(e <-- o, tutaj) { };
Dla IE oczywiście pod e nic nie będzie i trzeba ratować się poniższym:
element.onclick = function(e) { if (!e) var e = window.event; };
Obiekt ten zawiera cenne informacje, jak na przykład z jakiego elementu wraca kursor myszki podczas onmouseover:
element.onmouseover = function(e) { alert(e.fromElement); };
bądź na jaki właśnie najechał podczas onmouseout:
element.onmouseout = function(e) { alert(e.toElement); };
Niestety, Event.toElement i Event.fromElement nie są zaimplementowane w IE, gdzie trzeba użyć zamiennego dla obydwu Event.relatedTarget:
Podobna linijka załatwia sprawę - jeśli przeglądarka nie obsługuje relatedTarget (Opera, Fx i inne), to do zmiennej element przypisana zostanie własność toElement. Warto też pamiętać, że każdy element drzewa DOM jest w JS reprezentowany przez klasę/obiekt HTMLElement. Klasami dziedziczącymi są np. HTMLParagraphElement, HTMLSpanElement i tak dalej. Jeśli więc zastosujemy coś takiego: Z reguły, w okienku powinny pojawić się nazwy obiektów, jak powyżej. Otóż tę wiedzę można wykorzystać do napisania funkcji contains. Będzie ona wywoływana np. tak: Zwróci prawdę, kiedy element o identyfikatorze parent zawierał będzie element o identyfikatorze child. Jak już wiemy, funkcja ta działa w IE. Trzeba więc uzupełnić o nią obiekt HTMLElement w każdej innej przeglądarce. Obiekty uzupełniać możemy poprzez funkcję prototype użytą w kontekście obiektu: Od tej pory możliwe jest używanie czegoś na wzór: Uzupełnijmy więc obiekt HTMLElement o funkcję contains: I tak, jeśli wskazany element DOM jest pusty - funkcja zwraca nieprawdę, jeśli obiekt typu HTMLElement zawiera node, zwraca prawdę. Jeśli niespełniony jest żaden z tych warunków, funkcja rekurencyjnie pobiera kolejnych rodziców node. Jeśli odnajdzie szukanego rodzica, zwróci prawdę. Przyjmijmy taki przykład: Pobieramy odnośnik do diva i szukamy w nim elementu o #small: Uruchamiana jest więc funkcja contains, która szuka elementu #small. Analizując krok po kroku: Cała funkcja opiera się na tautologii, mówiącej, że jeśli rodzic elementu należy do jakiegoś elementu, to również ten element do niego należy. Zajmijmy się teraz naszymi przykładami. Do obsłużenia przykładowego skryptu musimy posiadać całe komponenty Core, Native i Class oraz część Effects: Fx.Base, Fx.CSS i Fx.Style. Skrypt powinien rozpocząć się po załadowaniu DOM, więc: Potem pobieramy elementy li i nadajemy im stosowne zdarzenia. Tak działają selektory w mootools ;-). Oprócz zdarzeń, musimy stworzyć też nowe efekty animacyjne dla każdego elementu, opierając się na zmianie margin-top: Animacja powinna trwać 1000ms (duration: 1000). Analizując powyższy przykład stwierdzamy, że po najechaniu myszką na zakładkę, uruchamia się animacja, która ustawia na końcu wartość margin-top na 0px. Po zwolnieniu kursora, animacja dąży do wartości 150px. Dzieje się jednak coś dziwnego. Zapewne to przez wielokrotne uruchamianie onmouseout. Wykorzystajmy więc funkcję contains, która sprawdzi, czy element, do którego zawitał kursor myszy, to tak naprawdę element należący do zakładki, czy nie. Jeśli nie, pozwolimy włączyć animację, jeśli tak, zapomnijmy o tym. Jest ładnie, ale mogłoby być lepiej. Kiedy trwa animacja rozwijania jednej zakładki, a my wskazaliśmy na inną, dobrze byłoby zatrzymać jej pokazywanie, by rozwijanie zakładki nie kolidowało z jej zwijaniem. Używająć mootools możemy jeszcze bardziej uprościć sprawę - podpinając zamiast mouseover - zdarzenie mousenter oraz zamiast mouseout zdarzenie mouseleave. Nie musimy wtedy dbać o funkcję contains, bo zdarzenia te obsługują właśnie podobne do omawianych sytuacje. Nie zawsze jednak mamy do dyspozycji frameworki, więc dobrze znać rozwiązanie w czystym JS. Niezależnie od metody, popatrzmy na końcowy efekt:var element = e.relatedTarget || e.toElement;
element.onclick = function(e) { alert(this); };document.getElementById('parent').contains(document.getElementById('child'));obiekt.prototype.foobar = function () { alert('to jest nowa funkcja w obiekcie "obiekt"');obiekt.foobar();HTMLElement.prototype.contains = function(node)
{
if (node == null)
{
return false;
}
else if (node == this)
{
return true;
}
return this.contains(node.parentNode);
}<div id="div"><span><small id="small"></small></span></div>document.getElementById('div').contains(document.getElementById('small'));
else if (node == this)
{
return true;
}Moo Tools
window.addEvent('domready', function()
{
});window.addEvent('domready', function()
{
$$('div ul li').each(function(element)
{
element.addEvent('mouseover', function(e) {});
element.addEvent('mouseout', function(e) {});
});
});window.addEvent('domready', function()
{
$$('div ul li').each(function(element)
{
var efekt = new Fx.Style(element, 'margin-top', {duration: 1000 });
var efekt2 = new Fx.Style(element, 'margin-top', {duration: 1000 });
element.addEvent('mouseover', function() { efekt.start(0); });
element.addEvent('mouseout', function() { efekt2.start(150); });
});
});window.addEvent('domready', function()
{
$$('div ul li').each(function(element)
{
var efekt = new Fx.Style(element, 'margin-top', {duration: 1000 });
var efekt2 = new Fx.Style(element, 'margin-top', {duration: 1000 });
element.addEvent('mouseover', function() { efekt.start(0); });
element.addEvent('mouseout', function(e)
{
if (!element.contains(e.relatedTarget || e.toElement))
{
efekt2.start(150);
}
});
});
});window.addEvent('domready', function()
{
$$('div ul li').each(function(element)
{
var efekt = new Fx.Style(element, 'margin-top', {duration: 1000 });
var efekt2 = new Fx.Style(element, 'margin-top', {duration: 1000 });
element.addEvent('mouseover', function() { efekt2.stop(); efekt.start(0); });
element.addEvent('mouseout', function(e)
{
if (!element.contains(e.relatedTarget || e.toElement))
{
efekt.stop();
efekt2.start(150);
}
});
});
});element.addEvent('mouseenter', function() { efekt2.stop(); efekt.start(0); });
element.addEvent('mouseleave', function(e)
{
efekt.stop();
efekt2.start(150);
});<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="pl-PL">
<head>
<title>Ruchome zakładki</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="mootools-release-1.11.js"></script>
<script type="text/javascript">
window.addEvent('domready', function()
{
$$('div ul li').setStyle('margin-top', 150);
$$('div ul li').each(function(element)
{
var efekt = new Fx.Style(element, 'margin-top', {duration: 1000 });
var efekt2 = new Fx.Style(element, 'margin-top', {duration: 1000 });
element.addEvent('mouseenter', function() { efekt2.stop(); efekt.start(0); });
element.addEvent('mouseleave', function(e)
{
efekt.stop();
efekt2.start(150);
});
});
});
</script>
<style type="text/css">
* { margin: 0; padding: 0 }
body { font: 12px normal, 'Georgia'; }
div { width: 500px; margin: auto; }
div ul { display: block; width: 400px; height: 200px; background: #BAE4F0; list-style-type: none; overflow: hidden; }
div ul li { display: block; width: 120px; height: 100%; float: left; background: #A9D26A; margin-top: 150px; margin-left: 10px; text-align: center; }
div ul li h2 { margin-bottom: 30px; font-size: 20px; }
</style>
</head>
<body>
<div>
<ul>
<li><h2>fast</h2><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p></li>
<li><h2>cool</h2><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p></li>
<li><h2>best</h2><p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p></li>
</ul>
</div>
</body>
</html>


Komentarze
działa całkiem sympatycznie. Jednak z nieznanych mi przyczyn nie chciało odpalić za pierwszym razem (dopiero odświerzenie strony pomogło i skrypt zadziałał)
very nice :)
Myślę, że można rozwiązać inaczej ten problem. Zamiast używać mouseover i mouseout spróbuj użyć mootoolsowych eventów mouseenter i mouseleave – to są specyficzne dla tej biblioteki zdarzenia, które zapobiegają właśnie takim zachowaniom.
Racja, mój błąd, że o tym zapomniałem – zaktualizowałem już stosowną część artykułu. Dzięki.
Dobry człowieku – dzięki Ci! Szukam rozwiązania tego problemu od kilku dni :-)
Jakbyś się kiedyś znalazł w Krakowie to masz u mnie piwo!
Tak z innej beczki: wiesz może, jak sprawdzić, czy użytkownik ma zainstalowanego Firebuga? (GMail sprawdza) Wydaje mi się, że bależy wybadać istnienie obiektu console, ale samo sprawdzenie if(console) {} wywala się w Operze :(
Pozdrawiam
Huh, przekonales mnie tym piwem do odwiedzenia Krakowa ;-). Bede pamietal ;-)
Co do Firebuga, mysle, ze to pomoze:
var console = console || false;if (!console)
{
alert('Nie ma firebuga');
}
@ferrante: niestety, to nie co. Opera zwraca:
message: Statement on line 2: Reference to undefined variable: consoleW Fx (z Firebugiem) działa (znaczy – pokazuje komunikat).
Ale udało mi się to obejść wsadzająć kod w blok try {} catch() {}
Pozdrawiam
Dziwne, u mnie w Operze w porzadku…
Pozdro
XHTML 1.0 Transitional ? :-0
Broni Ci ktos użyć innego DOCTYPE?
Z doctype w przypadku MooTools trzeba uważać, bo np. niektóre efekty wymagają XHTML, no ale to już dzisiaj przecież standard i każdy używa :-)
Transitional nie krzyzuje tu nam planow, gdyby bylo inaczej na pewno bym o tym wspomnial ;-), nie mniej wowo, na pewno ktos kodujacy w mootools skorzysta na Twoim komentarzu.
Pozdrawiam
Ajjj – nie ma ostatniej poprawki w formie odnośnika :) – przydał by się plik moo_fourth.html tak dla porządku …
Interesuje mnie właśnie czy to przejście na zdarzenia mootools’owe zmieni coś widocznego w stosunku do moo_third.html
(patrze tutaj katalog: ruchome_zakladki/)
Nie zmieni, bo mouseleave to to samo co mouseout, tylko z wbudowana funkcja contains() ;-)
Problem opisany już tu: quirksmode.org
I żeby nie było :) JQuery również posiada rozwiązanie tego problemu – użyć należy .hover().
Pozdrawiam
Dario, hover juz nie ma w najnowszej wersji jQuery. Jest za to takze mouseenter i mouseleave.
dzięki za interesujący artykuł :) od wczoraj walczę właśnie z tym „mouseout” :P
Bardzo przydatny wpis :) dzięki
A co znaczy np taki zapis
$(„p:first”,this)