Ta strona używa ciasteczek (cookies), dzięki którym nasz serwis może działać lepiej. Dowiedz się więcej OK, rozumiem
WebHelp.pl Warsztat Artykuły Obsługa zdarzeń w przeglądarkach

Warsztat / Artykuły i tutoriale

Obsługa zdarzeń w przeglądarkach

Rafał Kukawski 3 lutego 2011 komentarze ()

Wydanie Internet Explorera 9 będzie oznaczać początek końca udręk, kiedy konieczny był dodatkowy kod aby obsługiwać zdarzenia w tej przeglądarce. Biorąc jednak pod uwagę modę na wspieranie do 2 a nawet 3 wersji IE wstecz, etap przejściowy będzie trwał jeszcze parę lat. Dlatego nadal warto poznać specyfikę obsługi zdarzeń w przeglądarce Microsoftu.

Aby móc rozmawiać o Internet Explorerze, należy poznać podstawy standardowego modelu obsługi zdarzeń. Najważniejszym elementem całego modelu są oczywiście zdarzenia. Zdarzenia są reprezentowane w przeglądarkach przez obiekt, który opisuje to co zaszło w przeglądarce, czy to za sprawą akcji użytkownika, czy jako efekt pracy samej przeglądarki. Zdarzeń w przeglądarce generowanych jest wiele. Każde kliknięcie myszą, nawet każde poruszenie kursorem myszy czy wciśnięcie klawisza klawiatury, generuje nowe zdarzenie.

Model W3C

Obiekt zdarzenia

Obiekt zdarzenia w modelu W3C

Wszystkie typy zdarzeń dziedziczą wspólny zestaw cech. Ponadto każdy typ zdarzenia definiuje specyficzne dla siebie właściwości. Dla przykładu, zdarzenia związane z myszą informować będą o współrzędnych względem dokumentu, gdzie zaszło zdarzenie. Zdarzenia klawiaturowe informować będą o wciśniętym klawiszu. Poniżej przedstawiam najważniejsze i najczęściej wykorzystywane cechy zdarzeń.

Typ zdarzenia

Pole type obiektu zdarzenia przechowuje informację o jego typie. Jest to zwykłe pole tekstowe przechowujące nazwę zdarzenia, np. click, mouseover, keydown, itd.

Cel zdarzenia

Celem zdarzenia jest zwykle element, na którym zdarzenie zostało wywołane. Może to być dowolny element HTMLowy (np. jakiś DIV, czy odnośnik) lub dowolny obiekt implementujący interfejs EventTarget (przykładowo w niektórych przeglądarkach obiekt XMLHttpRequest implementuje wspomniany interfejs). Cel zdarzenia znajdziemy pod nazwą target.

Faza zdarzenia

Pole eventPhase informuje o fazie, w jakiej znajduje się w danym momencie zdarzenie.

Graficzne przedstawienie faz przechwytywania i bąblowania

Model W3C wyróżnia 3 fazy. Pierwszym etapem jest faza przechwytywania (capturing phase). W fazie tej obiekt zdarzenia wędruje od dokumentu po każdym elemencie w dół hierarchii aż osiągnie element docelowy. Osiągając element docelowy, zdarzenie wchodzi w drugą fazę, tj. at target. Następnie przechodzimy do fazy bąblowania, gdzie obiekt zdarzenia wędruje z powrotem w górę hierarchii, aż osiągnie obiekt dokumentu.

Nie wszystkie zdarzenia bąblują. Niektóre z nich kończą swoją podróż po dokumencie na drugiej fazie. Przykładowym niebąblującym zdarzeniem jest focus na kontrolkach formularzy.

Czy zdarzenie bąbluje?

Informacji na temat tego, czy zdarzenie bąbluje dostarcza kolejne pole o nazwie bubbles.

Czy można zablokować domyślną akcję?

Każde zdarzenie, gdy skończy swoją wędrówkę po dokumencie wymusza na przeglądarce wykonanie domyślnej akcji.

W przypadku kliknięcia w odnośnik, domyślną akcją jest przekierowanie pod nowy adres. W przypadku zdarzeń klawiaturowych będzie to dodanie nowego znaku do pola tekstowego.

Niektórym zdarzeniom można zablokować domyślną akcję. Aby sprawdzić, czy zdarzenie można zablokować, należy odczytać wartość pola cancelable.

Aby zablokować wykonanie domyślnej akcji, należy na obiekcie zdarzenia wywołać metodę preventDefault.

Kod: Zaznacz cały
event.preventDefault();

Nie trzeba sprawdzać, czy zdarzenie można zablokować, zanim wywołamy preventDefault. Zgodnie ze specyfikacją, przeglądarka powinna zignorować wywołanie tej funkcji. Nie zostanie rzucony żaden wyjątek.

Przerywanie spaceru

Jak zostało wyżej napisane, każde zdarzenie wędruje po wybranych elementach dokumentu, przechodząc przez każdą z faz. Jeśli chcemy przerwać ten spacer, wystarczy wywołać funkcję stopPropagation.

Kod: Zaznacz cały
event.stopPropagation();
Jak nasłuchiwać na zdarzenia?

Poznaliśmy jak wygląda typowy obiekt zdarzenia. Teraz zobaczymy jak można nasłuchiwać na zdarzenia.

Aby nasłuchiwać na zdarzenia, potrzebujemy obiektu, który implementuje interfejs EventTarget. Większość elementów HTMLowych implementuje ten interfejs.

Wspomniany interfejs definiuje z kolei funkcję o nazwie addEventListener. Tą funkcją - jak nazwa wskazuje - będziemy rejestrować funkcje nasłuchujące na zdarzenia. Przykładowe wywołanie na elemencie document może wyglądać następująco:

Kod: Zaznacz cały
function clickHandler (event) {
   console.log('You clicked something', event);
}
document.addEventListener('click', clickHandler, false);

Pierwszy parametr funkcji definiuje typ (nazwę) zdarzenia, które chcemy obsłużyć. Drugi argument to funkcja wywołana w momencie wystąpienia zdarzenia. Ostatni parametr definiuje, czy chcemy nasłuchiwać na zdarzenie w fazie przechwytywania czy bąblowania. Przekazanie wartości true oznacza pierwszą z wymienionych faz. Domyślną wartością, w celu zachowania kompatybilności ze starym modelem z HTML 4, jest false. Metoda ta nie zwraca żadnej wartości.

Obecność obiektu zdarzenia

Gdy zostanie wywołana funkcja obsługująca dane zdarzenie, w pierwszym jej argumencie znajdziemy referencję na obiekt zdarzenia.

this

"Zmienna" this w przypadku event handlerów domyślnie wskazuje na obiekt, na którym nasłuchiwaliśmy na zdarzenie.

Jak wyrejestrować funkcję nasłuchującą?

Jeśli nie chcemy nasłuchiwać już na zdarzenie z poziomu danego elementu, należy wywołać funkcję removeEventListener z identycznymi argumentami jak addEventListener.

Kod: Zaznacz cały
document.removeEventListener('click', clickHandler, false);

Na czym polegają różnice w IE?

Model zdarzeniowy Internet Explorera zupełnie odbiega od definicji W3C. Oczywiście widać pewne podobieństwa, ale dotyczą one raczej drobnych detali, a nie większych fragmentów specyfikacji.

Rejestrowanie i usuwanie listenerów

Najbardziej zauważalny jest brak funkcji addEventListener i removeEventListener, bo z tymi elementami mamy najczęściej do czynienia.

Internet Explorer definiuje swoje odpowiedniki o nazwach attachEvent i detachEvent.

Sygnatury tych funkcji też się różnią od ustandaryzowanych odpowiedników.

attachEvent i detachEvent oczekują tylko dwóch argumentów, gdzie drugim jest funkcja, która zostanie wywołana, gdy zajdzie zdarzenie.

Pierwszy argument stanowi nazwę zdarzenia, ale trzeba pamiętać, żeby poprzedzić ją prefiksem on, np.

Kod: Zaznacz cały
document.attachEvent('onclick', function () {});

Metoda ta zwraca true lub false, zależnie od tego, czy udało się zarejestrować listenera czy nie.

Brak trzeciego argumentu uniemożliwia nasłuchiwanie na zdarzenia w fazie przechwytywania.

Kilkukrotne rejestrowanie tej samej funkcji

Kolejna istotna różnica polega na tym, że model W3C nie rejestruje kilku listenerów obsługiwanych przez tę samą funkcję.

Kod: Zaznacz cały
function handleEvent () { console.log("event triggered"); }
element.addEventListener('click', handleEvent, false);
element.addEventListener('click', handleEvent, false);
element.addEventListener('click', handleEvent, false);

Dla powyższego kodu, w momencie wystąpienia zdarzenia, w konsoli zobaczymy tylko jeden wpis.

W IE, wywołanie

Kod: Zaznacz cały
function handleEvent () { alert("event triggered"); }
element.attachEvent('onclick', handleEvent);
element.attachEvent('onclick', handleEvent);
element.attachEvent('onclick', handleEvent);

skutkować będzie pojawieniem się 3 komunikatów.

Obiekt zdarzenia nieobecny w event handlerze

Gdy korzystamy ze standardowego modelu, obiekt zdarzenia jest przekazywany jako pierwszy argument funkcji obsługującej zdarzenie. W przypadku modelu Microsoftu, obiektu zdarzenia należy szukać w zbiorze zmiennych globalnych.

Kod: Zaznacz cały
document.attachEvent('onclick', function () {
   var event = window.event;
});
this

Kolejnym problemem, na który natrafimy korzystając z modelu IE jest "zmienna" this. Niestety, nie wskazuje ona na element na którym zarejestrowaliśmy event listenera. Zamiast tego dostajemy referencję na globalny obiekt, który jest nam kompletnie niepotrzebny.

Obiekt zdarzenia

Obiekt zdarzenia też różni się od ustandaryzowanego odpowiednika. W zasadzie jedynym wspólnym elementem jest pole type przechowujące nazwę zdarzenia.

Pole target nosi nazwę srcElement. Pola eventPhase, timeStamp i currentTarget są nieobecne.

Blokowanie bąblowania

W IE również można przedwcześnie zakończyć fazę bąblowania. Nie robimy jednak tego poprzez wywołanie funkcji stopPropagation, która nie istnieje w Microsoftowym modelu, tylko poprzez przypisanie wartości true do pola cancelBubble.

Kod: Zaznacz cały
window.event.cancelBubble = true;
Blokowanie domyślnej akcji

Nie ma też problemu z zablokowaniem domyślnej akcji przeglądarki. Jednak podobnie jak z bąblowaniem, w obiekcie zdarzenia nie znajdziemy metody preventDefault. Jej odpowiednikiem jest pole returnValue. Jeśli przypiszemy false, zablokujemy domyślną akcję.

Kod: Zaznacz cały
window.event.returnValue = false;

Jak sobie z tym wszystkim radzić?

Temat obsługi zdarzeń w przeglądarkach był wiele razy porzuszany w Sieci. Powstało wiele implementacji rozwiązujących część lub wszystkie problemy.

Aby jedną funkcją obsługiwać zdarzenie w każdej przeglądarce zwykło się stosować poniższy zapis w pierwszych liniach event handlera.

Kod: Zaznacz cały
function clickHandler (event) {
   event = event || window.event;
}

lub

Kod: Zaznacz cały
function clickHandler (event) {
   if (!event) {
       event = window.event;
   }
}

W ten sposób w zmiennej event zawsze będziemy mieli referencję na obiekt zdarzenia.

W prosty sposób można naprawić też brak obsługi stopPropagation i preventDefault. Wystarczy zdefiniować sobie funkcję, która uzupełni obiekt o te metody.

Kod: Zaznacz cały
function fixEvent (event) {
   if (!event.stopPropagation) {
       event.stopPropagation = function () {
           event.cancelBubble = true;
       };
   }

   if (!event.preventDefault) {
       event.preventDefault = function () {
           event.returnValue = false;
       }
   }

   if (!event.target && event.srcElement) {
       event.target = event.srcElement;
   }    
   return event;
}

a następnie skorzystanie z niej w event handlerze.

Kod: Zaznacz cały
function clickHandler (event) {
   event = fixEvent(event || window.event);
}

W tym momencie pozostała jeszcze sprawa funkcji addEventListener vs attachEvent. Tutaj najczęściej sobie radzono poprzez zdefiniowanie funkcji addEvent.

Kod: Zaznacz cały
function addEvent (element, event, fn) {
   if (element.addEventListener) {
       element.addEventListener(event, fn, false);
   } else if (element.attachEvent) {
       element.attachEvent('on' + event, fn);
   }
}

Funkcję wywołujemy według poniższego wzoru:

Kod: Zaznacz cały
addEvent(document, 'click', clickHandler);

W ten sposób ujednoliciliśmy podpinanie zdarzeń, ale nadal nie rozwiązaliśmy problemu "zmiennej" this. Sprawa okazuje się relatywnie prosta. Wystarczy, jeśli event handler zostanie wywołany za pomocą funkcji call z odpowiednim parametrem.

Kod: Zaznacz cały
function addEvent (element, event, fn) {
   if (element.addEventListener) {
       element.addEventListener(event, fn, false);
   } else if (element.attachEvent) {
       element.attachEvent('on' + event, function () {
           fn.call(element, fixEvent(window.event));
       });
   }
}

Jako bonus zafundowaliśmy sobie przekazywanie obiektu zdarzenia bezpośrednio do funkcji. W ten sposób pierwsza z napisanych poprawek staje się niepotrzebna.

Z powyższym kodem związany jest jeden poważny problem. Jako event handler nie jest rejestrowana przekazana funkcja, tylko pewna anonimowa funkcja pośrednicząca. Próba usunięcia listenera poniższym kodem się nie powiedzie.

Kod: Zaznacz cały
element.detachEvent('onclick', clickHandler);

Konieczna jest przeróbka funkcji, gdzie faktycznie rejestrowany event handler jest cache'owany w sposób łatwy w późniejszej identyfikacji.

Zadanie to było nawet elementem konkursu, w którym zwyciężyło rozwiązanie Johna Resiga. Zwycięzkie rozwiązanie okazało się mieć wady, że sam autor odradza jego stosowania. Na szczęście w Sieci można znaleźć wiele alternatywnych implementacji. Każda z nich jest na swój sposób specyficzna, każda z nich ma swoje wady i zalety.

Poniżej kolejna - bardzo podstawowa - alternatywa.

Kod: Zaznacz cały
var Events = (function (unbindOnUnload) {
    var eventListenerList = [];
    
    function indexOfListener (element, type, callback) {
        var list = eventListenerList;
        
        for (var i = 0, l = list.length, h; i < l; i++) {
            h = list[i];
            if (h[0] === element && h[1] === type && h[2] === callback) {
                return i;
            }
        }
        
        return -1;
    }
    
    var api = {
        /**
         * Uzupełnia obiekt zdarzenia o brakujące pola i metody
         * 
         * @param {Event} event Obiekt zdarzenia
         * @returns Obiekt wejściowy z poprawkami
         * @type Event
         */
        fix: function (event) {
            if (!event.stopPropagation) {
                event.stopPropagation = function () {
                    event.cancelBubble = true;
                }
            }
            
            if (!event.preventDefault) {
                event.preventDefault = function () {
                    event.returnValue = false;
                }
            }
            
            if (!event.target) {
                event.target = event.srcElement;
            }
            
            return event;
        },
        
        /**
         * Dodaje do elementu funkcję obsługującą wystąpienie pewnego zdarzenia
         * 
         * @param {EventTarget} element Obiekt implementujący interfejs EventTarget
         *                              na którym chcemy nasłuchiwać zdarzenia
         * @param {String} type Nazwa zdarzenia, które chcemy obsługiwać
         * @param {Function} callback Funkcja obsługująca zdarzenie
         */
        bind: function (element, type, callback) {
            if (element.addEventListener) {
                element.addEventListener(type, callback, false);
            } else if (element.attachEvent) {
                var index = indexOfListener(element, type, callback);
                
                if (index === -1) {
                    var fn = function () {
                        callback.call(element, api.fix(window.event));
                    };
                    
                    eventListenerList.push([element, type, callback, fn]);
                    element.attachEvent('on' + type, fn);
                }
            }
            
        },
        
        /**
         * Usuwa funkcję obsługują zdarzenia na danym elemencie
         * 
         * @param {EventTarget} element Obiekt implementujący interfejs EventTarget
         *                              z którego chcemy usunąć funkcję nasłuchującą
         * @param {String} type Nazwa zdarzenia, dla którego chcemy usunąć funkcję nasłuchującą
         * @param {Function} callback Funkcja, którą chcemy wyrejestrować
         */
        unbind: function (element, type, callback) {
            if (element.removeEventListener) {
                element.removeEventListener(type, callback, false);
            } else if (element.detachEvent) {
                var index = indexOfListener(element, type, callback);
                
                if (index !== -1) {
                    element.detachEvent('on' + type, eventListenerList[index][3]);
                    eventHandlerList.splice(index, 1);
                }
            }
        }
    };

    /**
     * Usuwa wszystkie funkcje nasłuchujące na zdarzenia
     */
    function unbindAll () {
        for (var i = 0, list = eventListenerList, l = list.length; i < l; i++) {
            var h = list[i];
            h[0].detachEvent('on' + h[1], h[3]);
        }
            
        list = h = null;
        eventListenerList = [];
    }
    
    if (unbindOnUnload) {
        api.bind(window, 'unload', function () {
            unbindAll();
        });
    }
    
    return api;
}(true));

Korzystając z powyższego skryptu, obsługa zdarzeń w przeglądarkach sprowadza się do wywołania:

Kod: Zaznacz cały
Events.bind(document, 'click', clickHandler); // nasłuchiwanie na zdarzenie
Events.bind(document, 'click', clickHandler); // ta sama funkcja nie zostanie po raz drugi zarejestrowana
Events.unbind(document, 'click', clickHandler); // kończenie nasłuchiwania

Skrypt definiuje także funkcję o nazwie unbindAll, którą automatycznie wywoła w momencie zamykania dokumentu, żeby wyrejestrować wszystkie event listenery w Internet Explorerze. Warto o tym pamiętać, choćby przez to, że Internet Explorerowi niezbyt wychodziło sprzątanie po uruchomionych w nim skryptach. Jeśli nie chcesz, żeby skrypt sprzątał po sobie, usuń wartość true z ostatniej linii skryptu.

Jak pisałem wyżej, funkcja dotyczy wyłącznie Internet Explorera, nie wykona niczego w Firefoksie ani innych przeglądarkach zgodnych z modelem W3C. Można to zmienić. Poniżej nieco zmodyfikowana wersja, która zapamiętuje rejestrowane funkcje dla każdej przeglądarki. Przeniosłem funkcję z prywatnej przestrzeni do publicznego API.

Kod: Zaznacz cały
var Events = {
    /**
     * Lista zarejestrowanych funkcji nasłuchujących na zdarzenia
     * 
     * @type Array
     * @private
     */
    _eventListenerList: [],
    
    /**
     * Dodaje do obiektu zdarzenia brakujące pola i metody.
     * Sprawdzana jest obecność target, stopPropagation, preventDefault
     * 
     * @param {Event} event Obiekt zdarzenia
     * @returns Obiekt wejściowy z poprawkami
     * @type Event
     */
    fix: function (event) {
        if (!event.stopPropagation) {
            event.stopPropagation = function () {
                event.cancelBubble = true;
            }
        }
        
        if (!event.preventDefault) {
            event.preventDefault = function () {
                event.returnValue = false;
            }
        }
        
        if (!event.target) {
            event.target = event.srcElement;
        }
        
        return event;
    },
    
    /**
     * Dodaje do elementu funkcję obsługującą wystąpienie pewnego zdarzenia
     * 
     * @param {EventTarget} element Obiekt implementujący interfejs EventTarget
     *                              na którym chcemy nasłuchiwać zdarzenia
     * @param {String} type Nazwa zdarzenia, które chcemy obsługiwać
     * @param {Function} callback Funkcja obsługująca zdarzenie
     */
    bind: function (element, type, callback) {
        var index = this._indexOfListener(element, type, callback),
            self = this, fn = callback;
        
        if (index === -1) {
            if (element.addEventListener) {
                element.addEventListener(type, callback, false);
            } else if (element.attachEvent) {
                fn = function () {
                    callback.call(element, self.fix(window.event));
                };
                element.attachEvent('on' + type, fn);
            }
            
            this._eventListenerList.push([element, type, callback, fn]);
        }
    },
    
    /**
     * Usuwa funkcję obsługują zdarzenia na danym elemencie
     * 
     * @param {EventTarget} element Obiekt implementujący interfejs EventTarget
     *                              z którego chcemy usunąć funkcję nasłuchującą
     * @param {String} type Nazwa zdarzenia, dla którego chcemy usunąć funkcję nasłuchującą
     * @param {Function} callback Funkcja, którą chcemy wyrejestrować
     */
    unbind: function (element, type, callback) {
        var index = this._indexOfListener(element, type, callback);
        
        if (index !== -1) {
            if (element.removeEventListener) {
                element.removeEventListener(type, callback, false);
            } else if (element.detachEvent) {
                element.detachEvent('on' + type, this._eventListenerList[index][3]);
            }
            this._eventListenerList.splice(index, 1);
        }
    },
    
    /**
     * Usuwa wszystkie funkcje nasłuchujące na zdarzenia
     * 
     * @returns void
     * @type void
     */
    unbindAll: function () {
        
        for (var i = 0, list = this._eventListenerList, l = list.length; i < l; i++) {
            var h = list[i],
                element = h[0];
                
            if (element.removeEventListener) {
                element.removeEventListener(h[1], h[3], false);
            } else if (element.detachEvent) {
                element.detachEvent('on' + h[1], h[3]);
            }
        }
        
        list = h = null;
        this._eventListenerList = [];
        
    },
    
    /**
     * Przeszukuje cache za funkcją zarejestrowaną dla podanego elementu i typu zdarzenia
     * 
     * @param {EvenTarget} element Obiekt dla którego szukamy event listenera
     * @param {String} type Nazwa poszukiwanego zdarzenia
     * @param {Function} callback Poszukiwana funkcja
     * 
     * @returns Indeks funkcji w pamięci podręcznej lub -1, gdy dana funkcja nie była rejestrowana
     * @type Number
     * @private
     */
    _indexOfListener: function (element, type, callback) {
        for (var i = 0, list = this._eventListenerList, l = list.length; i < l; i++) {
            var h = list[i];
            if (h[0] === element && h[1] === type && h[2] === callback) {
                return i;
            }
        }
        
        return -1;
    }
};

Events.bind(window, 'unload', function () {
    Events.unbindAll();
});

Przygotowałem kilka prostych przykładów, jak używać powyższego skryptu.

Masz pytania lub wątpliwości? Odwiedź nasze forum dyskusyjne.

Rafał Kukawski

Programista, webmaster. Szczególnie upodobał sobie JavaScript i technologie klienckie, choć strona serwera i bazy danych nie stanowią tajemnicy. Tworzy też aplikacje na urządzenia mobilne. kukawski.pl.


Komentarze


HTML CSS JavaScript PHP bazy danych MySQL Flash grafika framework hosting domeny pozycjonowanie wordpress Facebook