Zdarzenia

Zdarzenia to czynności, które użytkownik wykonuje podczas odwiedzania naszej strony. Przykładowymi zdarzeniami mogą być np. przesunięcie kursora na obrazek (mouseover), kliknięcie jakiegoś linka (click), wysłanie formularza (submit), naciśnięcie klawisza (keypress) itp.

Większość zdarzeń wywoływana jest przez użytkownika. Użytkownik kliknie to, użytkownik zrobi tamto itp. Istnieją też zdarzenia, które nie są bezpośrednio spowodowane przez użytkownika - np. zdarzenie load, które zachodzi np. gdy całkowicie załaduje się strona.


Javascript udostępnia kilkanaście typów zdarzeń:

Zdarzenie:Opis:
blurodpalane, gdy obiekt przestał być aktywny (np. input)
changeodpalane, gdy obiekt zmienił swoją zawartość (np. pole tekstowe)
clickodpalane, gdy obiekt został kliknięty (np. input)
dblClickodpalane, gdy podwójnie klikniemy na obiekt (np. input)
focusodpalane, gdy obiekt stał się wybrany (np. pole tekstowe)
keyDownodpalane, gdy został naciśnięty klawisz na klawiaturze
inputpodobne do powyższego, ale odpalane synchronicznie w czasie trzymania klawisza (np. przytrzymanie klawisza A w polu tekstowym)
keyUpodpalane gdy puścimy klawisz na klawiaturze
loadodpalane, gdy obiekt został załadowany (np. cała strona)
mouseOverodpalane, gdy kursor znalazł się na obiekcie
mouseOutodpalane, gdy kursor opuścił obiekt
resizeodpalane, rozmiar okna przeglądarki jest zmieniany
selectodpalane, gdy zawartość obiektu została zaznaczona
submitodpalane, gdy formularz został wysłany
unloadodpalane, gdy użytkownik opuszcza dana stronę

Powyższa lista zawiera tylko najczęściej używane zdarzenia. Takich zdarzeń jest o wiele, wiele więcej, chociaż prawdę mówiąc większości z nich i tak się nie używa.

Zanim zaczniemy

Wspominałem to już w dziale o hierarchii elementów na stronie, ale warto to sobie przypomnieć. Za chwilę zaczniemy podpinać zdarzenia do elementów na stronie. Aby nasz skrypt sie nie wykrzaczył, w chwili odpalenia skryptu musimy zapewnić, że te elementy są już stworzone (został wczytany html i zostalo stworzone drzewo dokumentu). Aby mieć pewność, że elementy już istnieją użyjemy jednej z dwóch metod: wstawimy nasz skrypt na końcu strony (najlepiej tuż przed tagiem </body>) lub wykryjemy czy dokument został już w całości wczytany.

Aby wykryć czy dokument został wczytany, skorzystamy z jednej z metod:


//metoda starsza
document.onreadystatechange = function () {
    if (document.readyState == "complete") {
        console.log("DOM został wczytany");
    }
}

//metoda zalecana, którą stosujemy w całym kursie
document.addEventListener("DOMContentLoaded", function(event) {
    console.log("DOM został wczytany");
});

Rejestrowanie zdarzenia bezpośrednio w kodzie HTML

Aby zdarzenie było dostępne dla danego obiektu, musimy je dla niego zarejestrować. Istnieje kilka metod na rejestrację zdarzenia dla obiektu.

Metoda inline (jako atrybut elementu) przypisywania zdarzeń polega na określeniu zdarzenia wewnątrz znacznika:


<a href="jakasStrona.html" onclick="alert('Kliknąłeś')"> kliknij </a>

Ten model rejestrowania zdarzeń jest zły, bo miesza JS z kodem HTML. Dla ciebie autorze takie krótkie wywołania mogą wydawać się oczywiste, ale jeżeli za kilka tygodni będziesz chciał zmienić taki skrypt, mogę ci życzyć tylko powodzenia, szczególnie, gdy będziesz musiał dokonać szybkiej podmiany na 400 plikach ftp :) (patrz też ten problem)
Jako super bohaterowie będziemy korzystać tylko z dobrych metod, dlatego pozostawiamy ten sposób w domu starców i przechodzimy do ciekawszych sposobów obsługi zdarzeń.

Pisanie kodu js wewnątrz znaczników stwarza same problemy. Po pierwsze mieszanie kilku języków nigdy nie jest dobrym pomysłem, a szukanie wplecionych w html skryptów jest mordęgą.
Po drugie podpinanie zdarzeń pod naście elementów jest bardzo uciążliwe. Zamiast wykonać jedną prostą pętlę po elementach na stronie musimy dłubać w html-js zupie. Nic przyjemnego.
Po trzecie nie możemy trzymać naszego kodu js w oddzielnych plikach.
Same problemy.

Dlatego właśnie o wiele lepszym pomysłem jest oddzielenie skryptów od html dzięki podpinaniu zdarzeń do elementów bezpośrednio w skryptach.

Zdarzenie jako właściwość obiektu

Pierwsza, starsza metoda przypisywania zdarzeń polega na ustawieniu zdarzenia jako właściwość danego obiektu:


function showText() {
    console.log('Kliknięto przycisk na stronie');
}

var element = document.getElementById('przycisk');
element.onclick = showText;

Zauważyłeś, że przy podpinaniu funkcji do zdarzeń pomijamy nawiasy? Robimy tak dlatego, ponieważ nie chcemy odpalać funkcji, a tylko ją podpiąć pod dane zdarzenie.

Do zdarzenia możemy też podpinać funkcję anonimową (czyli taką, którą tworzymy bezpośrednio przy deklaracji zdarzenia):


document.getElementById('przycisk').onclick = function() {
    console.log('Kliknięto przycisk na stronie');
}

Kolejny przykład pokazuje jak możemy przypisywać zdarzenia do wielu obiektów. Wystarczy zastosować pętlę:


var p = document.querySelectorAll('p');

for (i=0; i<p.length; i++) {
    p[i].onclick = function () {
        if (this.className.indexOf(' mark ') !== -1) {
            this.className += ' mark ';
        }
    }
}

Od tej pory gdy klikniemy na którykolwiek akapit na stronie, dodamy mu klasę mark, która zrobi z nim same cuda.

Aby usunąć wcześniej przypisane zdarzenie, wystarczy pod daną właściwość podstawić null:

obiekt.onclick = null;

Poza rejestrowaniem zdarzeń dla elementów, możemy też takie zdarzenia wywołać - np. onclick:

element.click()

Powyższy kod wywoła "kliknięcie" na element. Z resztą zdarzeń jest analogicznie.

Nowy model rejestracji zdarzeń

Problem z tradycyjnym modelem polega na tym, że do jednego elementu możemy podpiąć tylko jedną funkcję dla jednego rodzaju zdarzenia. Rejestrując nową funkcję do tego samego typu zdarzenia nadpisujemy starą.
Możemy to oczywiście obejść (wspólna funkcja odpalająca kilka funkcji), jednak jest to mało logiczne i przysparza kłopotów z późniejszym odrejestrowaniem takich zdarzeń.

Problemów takich nie mamy korzystając z "nowego" modelu rejestrowania zdarzeń opierającego się na metodzie addEventListener().

Przyjmuje ona 3 argumenty: typ zdarzenia, funkcja do wywołania, oraz trzeci opcjonalny argument, który włącza lub wyłącza (true/false) bąbelkowe zachowanie zdarzeń Kto programował choć przez chwilę w Action Script 3 - nie będzie miał żadnych problemów ze zrozumieniem tego typu obsługi zdarzań:


var element = document.getElementById('guzik');

//rejestrujemy 3 zdarzenia click dla elementu
element.addEventListener('click', showMe);
element.addEventListener('click', showSomething)
element.addEventListener('click', function() {
    this.style.color = 'red';
});

Od tej pory po pojedynczym kliknięciu zostaną wywołane wszystkie trzy funkcje.

Do wyrejestrowania funkcji z danego zdarzenia służy metoda element.removeEventListener(), która przyjmuje dokładnie takie same argumenty jakie były użyte do zarejestrowania danego zdarzenie za pomocą addEventListener().


element.removeEventListener('click', showMe);
element.removeEventListener('click', showSomething);

Niestety nasza ostatnia funkcja jest funkcją anonimową - nie ma nazwy i nie jest podstawiona pod żadną zmienną. Nie jesteśmy więc w stanie przekazać dla removeEventListener drugiego parametru, tym samym nie jesteśmy w stanie odrejestrować tej funkcji.
Co więc zrobić? Nie bałaganić i nie iść na łatwiznę. Prawdziwie ładny kod powinien unikać funkcji anonimowych, a stosować funkcje z nazwami - zarówno przy podpinaniu ich jak i odpinaniu.

Ok. Wiem. To jest Javascript. Przy małych skryptach używanie funkcji anonimowych jest i wygodne i zwyczajnie się sprawdza. Warto przynajmniej o takich problemach jak powyższe wiedzieć. Czasami uratuje wam to życie. Na dziewczynach wrażenia może nie zrobicie, ale na programistach jak najbardziej.

Co jednak z przekazywaniem do takich funkcji parametrów? Jest na to bardzo proste rozwiązanie. Dane parametry nie powinny być wyssane z palca. Powinny być brane skądś - np z danego obiektu (lub np z wyniku jakiejś funkcji):


    function showSomething() {
        alert(this.tekst);
    }

    var element = document.getElementById('guzik');

    element.tekst = 'To jest tekst do wypisania';
    element.addEventListener('click', showSomething);

Na zakończenie tych wywodów kawałek kodu, który demonstruje użycie powyższych metod:


<div>
    <input type="button" value="Wpierw podepnij zdarzenie!" id="buttonTest" />
    <input type="button" value="Podepnij zdarzenie" id="buttonTestAdd" />
    <input type="button" value="Odepnij zdarzenie" id="buttonTestRemove" />
</div>

var buttonTest = document.getElementById('buttonTest');
    buttonTest.tekst = 'To jest tekst do wypisania';

var buttonAdd =  document.getElementById('buttonTestAdd');
var buttonRemove =  document.getElementById('buttonTestRemove');

function showText() {
    alert(this.tekst);
}

function addTestEvent() {
    buttonTest.value = 'No dobra. Można klikać!';
    buttonTest.addEventListener('click', showText);
}

function removeTestEvent() {
    buttonTest.value = 'Wpierw podepnij zdarzenie!';
    buttonTest.removeEventListener('click', showText);
}

document.addEventListener("DOMContentLoaded", function() {
    buttonAdd.addEventListener('click', addTestEvent, false);
    buttonRemove.addEventListener('click', removeTestEvent);
});

Problemy z nowym modelem rejestracji zdarzeń

Poniższy tekst jest nieaktualny. W dzisiejszych czasach nie ma co się przejmować staroświeckimi IE. Zostawiłem go jednak dlatego, bo może jakaś osoba będzie zmuszona do walki z nieaktualnymi przeglądarkami.

Problem z powyższym modelem rejestracji polega na tym, że starsze przeglądarki IE (<=8) stosują swój własny model :(. Aby zarejestrować zdarzenie dla starszych IE, skorzystamy z metody element.attachEvent('onclick', nazwaFunkcji).
Pierwszy atrybut to nazwa zdarzenia, poprzedzona przedrostkiem "on".
Drugi to nazwa funkcji, która zostanie wywołana. Model IE nie przewiduje wyłączania bąbelkowego zachowania się zdarzeń, więc trzeciego parametru nie ma.

Aby wyrejestrować zdarzenie dla IE, użyjemy metody element.detachEvent('onclick', nazwaFunkcji)

Z pewnością nie za bardzo będzie nam pasowało co chwile sprawdzanie, czy mamy do czynienia ze starymi IE, czy z normalnymi przeglądarkami, dlatego napiszmy funkcję dodającą zaawansowaną rejestrację zdarzeń dla wszystkich modeli:


function addEvent(o, zdarzenie, funkcja) {
    if (o.addEventListener) {
        o.addEventListener(zdarzenie, funkcja, false);
    } else if (o.attachEvent) {
        o.attachEvent("on" + zdarzenie, funkcja);
    } else {
        eval(o + ".on" + zdarzenie + "=" + funkcja + ";");
    }
}

Jeżeli przeglądarka obsługuje addEventListener, wtedy korzystamy z tego modelu. Jeżeli korzysta z attachEvent, korzystamy z tego modelu, dodając do nazwy zdarzenia przedrostek "on". Jeżeli oba modele nie zostały wykryte, wówczas wykonujemy funkcję eval, do której przekazujemy ciąg tworzący kod rejestrujące zdarzenie w modelem tradycyjnym - np akapit.onclick = wypisz;

Od tej chwili możemy rejestrować zdarzenia dla elementów w następujący sposób:


addEvent(button1, "click", showSomething);
addEvent(button2, "click", showSomething);

Zobacz też filmik na stronie http://net.tutsplus.com/tutorials/javascript-ajax/javascript-from-null-cross-browser-event-binding/, w którym bardzo fajnie jest omówiona nieco inna metoda obsługi wszystkich przeglądarek.

Który model wybrać?

Na pewno nie dziadka inline. Może na siłę udało by mi się wymyślić jakiś szczególny przykład, kiedy musiałbyś zastosować ten model (przykład, który byłby przeznaczony dla leniwców).
Osobiście zachęcam do stosowania nowoczesnego sposobu obsługi zdarzeń. Koniec kropka.

Słowo kluczowe this

Javascript udostępnia słowo kluczowe this, które wskazuje na obiekt, który wywołał daną funkcję. Słowo to jest bardzo użyteczne w przypadku rejestrowania zdarzeń dla obiektów.
Przykładowo chcielibyśmy zmienić kolor czcionki dowolnego obiektu, do którego przypiszemy zdarzenie onmouseover:


function changeColor() {
    this.style.color = '#CC0000';
}

document.addEventListener("DOMContentLoaded", function() {
    element.addEventListener('mouseoever', changeColor);
    element2.element.addEventListener('mouseoever', changeColor);
});

Lub to samo z parametrami:


function changeColor(jaki, element) {
    element.style.color = jaki;
}

document.addEventListener("DOMContentLoaded", function() {
    element.addEventListener('mouseoever', function() {
        changeColor('#CCFF33', this);
    });

    element2.addEventListener('mouseoever', function() {
        changeColor('#FF9966', this)
    });
});

Wkraczamy w głąb zdarzenia

Javascript udostępnia nam specjalne właściwości, dzięki którym możemy bardziej dokładnie badać każde zarejestrowane zdarzenie.

Aby odczytać właściwości zdarzenia musimy posłużyć się pseudo parametrem, który będziemy przekazywać do deklarowanej funkcji (w naszych przykładach taki parametr nazwiemy e).

 
element.document.addEventListener('click', function(e) {

    //...dzięki pseudo parametrowi "e" mamy dostęp do właściwości zdarzenia
    console.log(e);

});

Niestety IE nieco inaczej podchodzi do tego. Aby odczytać właściwości zdarzenia w tej przeglądarce, musimy skorzystać z obiektu window.event. Nie jest to jednak problem. Wystarczy w zadeklarowanej funkcji sprawdzić, czy pseudo parametr e został przekazany, a jeżeli nie istnieje podstawić pod niego window.event. Załatwiamy to jedną linijką:

if (!e) var e = window.event;

Od tej pory zaczynamy dłubać przy eventach. Możemy więc sprawdzić typ elementu, który wywołał dane zdarzenie. Możemy sprawdzić typ zdarzenia, możemy sprawdzić skąd dane zdarzenie się pojawiło itp. Możliwości jest multum.

Typ zdarzenia

Jakiego typu jest dane zdarzenie? Aby się tego dowiedzieć, wystarczy odczytać właściwość type każdego zdarzenia:


function checkEventType(e) {
    if (!e) {
        var e = window.event;
    }
    return e.type;
}

//...

document.getElementById('uberButton').addEventListener('click', function(e) {
    alert('Typ zdarzenia: ' + checkEventType(e));
});

Który klawisz został naciśnięty

Wartość naciśniętego klawisza jest przechowywana w właściwości keyCode zdarzenia. Wystarczy ją odczytać i przesłać do metody String.fromCharCode() aby uzyskać naciśnięty klawisz.


function showKey(e) {
    if (!e) {
        var e = window.event;
    }
    if (e.keyCode) return e.keyCode;            
}

document.addEventListener("DOMContentLoaded", function() {

    document.getElementById('textField').addEventListener('keydown', function (e) {
        document.getElementById('textarea').value += 'Klawisz: ' + String.fromCharCode(showKey(e)) + ', ';
    });

});

Wstrzymanie domyślnej akcji

Większość elementów na stronie wykonuje domyślne akcje. Linki przenoszą w jakieś miejsca, formularz się wysyłają itp. Po podpięciu zdarzeń pod obiekt będą ono wywoływane na początku, jednak zaraz po nich wykonana zostanie domyślna czynność.
Aby temu zapobiec skorzystamy z metody e.preventDefault(). Żeby mieć pewność, że dane zdarzenie zostanie zatrzymane, stosujmy e.preventDefault() na początku zdarzenia:


element.addEventListener('click', function(e) {
    e.preventDefault();
    alert('Ten link nigdzie nie przeniesie.');
});

Niektórych zdarzeń nie da się w ten sposób zatrzymać (np. load), o czym mówi nam właściwość e.cancelable.

Zatrzymanie nasłuchu innych zdarzeń

Po odpaleniu zdarzenia, domyślnie przechodzi ono po obiektach od dołu hierarchii do góry - dążąc do dokumentu.
Spójrzmy na poniższy przykład:


<div id="exampleDiv">
    <a id="exampleLink" href="">Kliknij mnie</a>
</div>

var div = document.getElementById('exampleDiv');
var a = document.getElementById('exampleLink');

div.addEventListener('click', function(e) {
    alert('Kliknięto div');
});

a.addEventListener('click',function(e) {
    alert('Kliknięto link');
});

Jeżeli teraz klikniemy na link zostanie wyświetlony komunikat "Kliknięto link". Równocześnie jednak zostanie wyświetlony komunikat dla div!. Dzieje się tak dlatego, że wszystkie zdarzenia zachowują się jak bąbelki. Po wywołaniu biegną one do góry hierarchii dokumentu. Nasze zdarzenie click dla linka zostaje odpalone, a następnie biegnie do początku hierarchii natrafiając po drodze na kolejny nasłuch wywołany przez div.

Aby przerwać tą wędrówkę oraz kolejne nasłuchy, skorzystamy z metody e.stopPropagation():

                
var div = document.getElementById('exampleDiv');
var a = document.getElementById('exampleLink');

div.addEventListener('click', function (e) {
    alert('Kliknięto div');
});

a.addEventListener('click', function (e) {
    e.preventDefault();
    e.stopPropagation();
    alert('Kliknięto link');
});

Od tej pory kliknięcie na link wywoła zdarzenie tylko dla tego linku.

Dodatkowe przykłady i informacje o bąbelkach możesz zobaczyć na stronie o obsłudze myszki.