Evenimente in JavaScript


Am vorbit data trecuta (Manipularea DOM cu JavaScript) despre cum putem modifica ierarhia DOM atunci cand avem de proiectat o interfata web, prin adaugarea, stergerea si modificarea elementelor din DOM. Totusi, in general, aceasta ierarhie nu se va modifica decat la interactiunea cu utilizatorul, de exemplu atunci cand facem click sau dublu click pe un buton, link sau imagine, sau cand completam un camp text. Chiar si atunci cand folosim AJAX pentru a adauga si mai multe functionalitati interfetei noastre, in general vom face cereri AJAX tot la interactiunea cu utilizatorul.

Exact pentru aceasta interactiune cu utilizatorul, ne ajuta ascultatorii de evenimente (eng. event handlers sau event listeners) in JavaScript. Exista doua cai de a adauga ascultatori de evenimente: calea cea veche, si calea cea noua. Sa le luam pe rand.

Calea cea veche de a adauga evenimente, a fost introdusa de Netscape Communications inainte sa se puna chiar problema standardizarii acestora, a devenit populara si a fost adoptata de toate browserele importante, astfel ca in ziua de azi browserele inca o suporta. Cel mai des vom intalni aceasta cale numita DOM Level 0 event model, intrucat a aparut inainte ca W3C sa creeze prima specificatie DOM, numita DOM Level 1.

Lista cu toate tipurile de evenimente este disponibila pe W3Schools, si adaugarea de ascultatori pentru fiecare urmeaza acelasi tipar. Putem adauga evenimentele inline (la fel cum puteam adauga CSS inline) folosind atributul cu acelasi nume cu al evenimentului:

   1: <a href="/cale/catre/pagina"
   2: onclick="alert('Actiunea nu va duce niciunde'); return false;">
   3: Apasa-ma</a>

sau manipuland DOM-ul folosind proprietatea cu acelasi nume cu al evenimentului (daca tineti minte din articolul trecut, puteam schimba valorile atributelor folosind proprietatea elementului din DOM corespunzator unui element HTML, care avea acelasi nume cu atributul). Acestei proprietati ii dam ca valoare o functie, care poate primi ca unic parametru, evenimentul care a declansat actiunea (vom vorbi despre evenimente un pic mai tarziu):

   1: <a id="un_link" href="/cale/">Link</a>
   1: var a = document.getElementById('un_link');
   2: a.onclick = function(event) { alert("Ati facut click"); };

Cu toate ca in exemplul de mai sus am folosit o functie anonima, putem folosi si functii cu nume (mai multe despre functii anonime si functii cu nume in articolul Introducere in JavaScript), si sunt de preferat cand vrem sa adaugam acelasi comportament pentru mai multe elemente din pagina. De exemplu, daca dorim sa dezactivam toate link-urile cu clasa disabled:

   1: function disableLink(event) {
   2:     return false;
   3: }
   4:
   5: var links = document.getElementsByTagName('a');
   6: var link = null;
   7: for (var i=0, total=links.length; i<total; i++) {
   8:     link = links[i];
   9:     if (link['class'] == 'disabled' || link['className'] == 'disabled')
  10:         link.onclick = disableLink;
  11: }
  12:

Notati ca la atribuire nu am folosit paranteze dupa disableLink; nu este o greseala, daca va amintiti din articolul Obiecte in JavaScript, spuneam ca daca am adauga si paranteze dupa disableLink, am fi atribuit de fapt valoarea returnata de functie, si nu functia insasi (deci practic link.onclick = disableLink() ar fi echivalent cu link.onclick = false, din moment ce disableLink returneaza intotdeauna false).

Sa vorbim un pic despre parametrul trimis acestor functii. Acesta va fi intotdeauna o instanta a unui obiect de tip Event, care contine cateva informatii despre evenimentul care a fost capturat. Insa, ca de obicei, browserul nostru preferat, Internet Explorer, vrea sa fie diferit si nu transmite acest parametru. In schimb, face disponibile informatiile despre eveniment, intr-o variabila globala cu numele event, astfel ca daca vreodata vom avea nevoie de acest parametru, va trebui sa adaugam o linie suplimentara de cod:

   1: function capteazaEveniment(event) {
   2:     if (!event) // cu alte cuvinte, avem de-a face cu IE
   3:         event = window.event;
   4:         // cu alte cuvinte, atribuim lui event, valoarea variabilei globale event
   5:     // cod care lucreaza cu variabila event
   6: }

Deci, cu ce ne ajuta acest parametru? Raspunsul este ca in membrii acestei variabile gasim informatii despre elementul care a generat evenimentul, pozitia mouse-ului in acel moment si tasta care a fost apasata la declansarea evenimentului (daca este cazul). Insa IE nu vrea sa ne faca viata usoara, si deci diferentele fata de celelalte browsere nu se opresc aici. Atributul target din Event ar trebui sa ne dea o referinta la elementul care a declansat evenimentul. Insa in Internet Explorer, numele acestui atribut nu este target, ci srcElement, astfel ca daca avem nevoie de aceasta referinta, va trebui sa mai adaugam si urmatoarea linie de cod:

   1: var sursa = (event.target) ? event.target : event.srcElement;

Aceasta nu este singura diferenta intre Event-ul din Internet Explorer si Event-ul W3C, dar in cea mai mare parte exista proprietati/metode echivalente pentru a obtine informatii cross-browser despre evenimentul declansat (Event-ul W3C contine mai multe informatii decat cel IE), si gasiti o lista cu proprietatile/metodele lui Event din IE si W3C, precum si echivalente intre cele doua tipuri, aici.

Mai exista o caracteristica interesanta a evenimentelor JavaScript. Ganditi-va ca avem 3 elemente fiecare, al doilea fiind descendent al primului, iar al treilea, descendent al celui de-al doilea. In plus, sa presupunem ca fiecare dintre ele are atasat un ascultator de evenimente la click:

   1: <div id="div1" style="background-color: red; padding: 10px">
   2:     <div id="div2" style="padding: 10px; background-color: yellow">
   3:         <a id="link" href="#" style="background-color: white">Click me!</a>
   4:     </div>
   5: </div>
   1: function demo(nume, eu) {
   2:     return function(event) {
   3:         if (!event) event=window.event;
   4:         var mesaj = "Eu sunt "+nume+" si ";
   5:         var sursa = (event.target)?(event.target):event.srcElement;
   6:         if (sursa == eu)
   7:             mesaj += "sunt sursa click-ului";
   8:         else
   9:             mesaj += "nu sunt sursa click-ului";
  10:         alert(mesaj);
  11:     }
  12: }
  13:
  14: var div1 = document.getElementById('div1');
  15: div1.onclick = demo('div1', div1);
  16: var div2 = document.getElementById('div2');
  17: div2.onclick = demo('div2', div2);
  18: var link = document.getElementById('link');
  19: link.onclick = demo('link', link);

Sa vedem ce se intampla daca facem click pe link-ul “Click me”. Vom primi urmatoarele 3 alerte:

Eu sunt link si sunt sursa click-ului
Eu sunt div2 si nu sunt sursa click-ului
Eu sunt div3 si nu sunt sursa click-ului

Ce se intampla de fapt este ca evenimentul se propaga in ierarhia DOM din parinte in parinte, pana la radacina ierarhiei DOM (adica window). Daca pagina noastra ar fi continut in body doar elementele de mai sus, practic evenimentul se propaga astfel: #link->#div2->#div1->body->html->document->window, cu mentiunea ca de la html pana la window, nu toate elementele din calea de mai sus primesc evenimentul. De notat ca nu este nevoie ca toate elementele din aceasta cale sa aiba ascultatori la evenimentul propagat, dar evenimentul se va propaga oricum. Puteti vedea scriptul de mai sus in actiune la acest link. Acest fenomen se numeste event bubbling, si daca va intrebati daca foloseste la ceva, raspunsul este ca da. Ganditi-va ca vrem sa monitorizam toate click-urile dintr-o pagina (poate vrem sa cream o aplicatie de web analytics). Atunci este suficient sa adaugam un ascultator la click pentru html:

   1: var html = document.body.parentNode;
   2: html.onclick = function(event) {
   3:     //monitorizare aici
   4: };

Dar poate ca nu intotdeauna vrem sa lasam evenimentul sa se propage, si atunci este suficient sa setam event.cancelBubble la true in IE, respectiv sa apelam event.stopPropagation() in celelalte browsere, pentru a impiedica propagarea de la un anumit element in sus in ierarhia DOM.

Ce a mai ramas de clarificat este cum oprim ascultatorii la un anumit tip de eveniment. Simplu: atribuim valoarea null proprietatii onclick (sau oricare alta proprietate care ataseaza ascultatori de evenimente) a elementului DOM corespunzator. De exemplu:

   1: document.getElementById("element").onclick = null;

Sper ca din ce am prezentat pana acum v-ati dat seama ca evenimentele sunt destul de importante atunci cand vrem sa cream o interfata web evoluata. Totusi exista un mic neajuns la acest model de a atasa evenimentele: la un moment dat, nu putem sa avem decat un singur ascultator la un anumit tip de eveniment, pentru un element dat. De exemplu:

   1: element.onclick = function(event) { // cod }
   2: // alte instructiuni
   3: element.onclick = function(event) { // alt cod }
   4: // ascultatorul precedent a fost sters!

Putem desigur sa improvizam adaugarea de evenimente multiple, pastrand o referinta la vechiul ascultator si adaugand nou cod atunci cand atasam inca un ascultator, dar ne complicam degeaba. In plus, ar trebui sa avem si o modalitate de a sterge un anumit ascultatori dintre toti ascultatorii unui element.

Pentru a ne usura munca, exista DOM Level 2 model, care printre altele ne permite tocmai asta: sa atasam mai multi ascultatori la un anumit tip de eveniment, si sa stergem selectiv dintre ascultatori. Insa sa nu ne bucuram prea tare: IE nu implementeaza DOM Level 2 model. Insa nu disperati: ne ofera totusi o modalitate de a obtine ce ne doream, doar ca va trebui, pentru a obtine acelasi efect in toate browserele, sa scriem separat cod pentru IE si cod pentru browserele care suporta DOM Level 2 model.

Pentru browserele non-IE, sintaxa este urmatoarea:

   1: element.addEventListener(tip, functie[, tip_de_propagare])

unde:

  • tip este un string care spune tipul evenimentului. valoarea acestuia ar trebui sa fie aceeasi cu a atributelor de care vorbeam la DOM Level 0, fara on in fata. astfel, daca pentru a adauga un ascultator la click foloseam element.onclick = function() { //... }, acum vom folosi element.addEventListener('click', function() { //... });
  • functie este functia care se apeleaza la producerea evenimentului, care primeste la fel ca la DOM 0 un parametru optional de tip Event. In plus, spre deosebire de functiile de la DOM 0, in acest caz functia va avea in this o referinta la elementul al carui ascultator este functia
  • tip_de_propagare este o variabila optionala de tip boolean. Daca lipseste sau daca are valoarea false, evenimentul se va propaga folosind event bubbling. Daca insa este setata la true, evenimentul se va propaga folosind event capturing (adica se propaga exact in sens invers decat event bubbling). de obicei nu se foloseste event capturing, intrucat IE nu suporta acest tip de propagare. Daca vreti o demonstratie, aveti aici un demo (html-ul este acelasi ca la primul demo, insa de data aceasta am folosit
    addEventListener

    si event capturing. observati ca alertele de data aceasta sunt in ordine inversa decat la primul demo) pe care puteti sa-l rulati cu orice browser mai putin IE.

Pentru a detasa un ascultator de un anumit tip, sintaxa este asemanatoare:

   1: element.removeEventListener(tip, functie)

unde:

  • tip are aceeasi semnificatie ca mai sus,
  • iar
    functie trebuie sa fie aceeasi functie care a fost atasata cu addEvent Listener.

De ce am pus accentul mai sus pe aceeasi? Pentru ca e foarte tricky sa folosesti functii anonime ca sa atasezi evenimente, si sa folosesti tot functii anonime pentru a le sterge, insa acest lucru nu va functiona. De exemplu:

   1: element.addEventListener('click',
   2:     function() {
   3:         return false;
   4:     }
   5: );
   6: // cod
   7: element.removeEventListener('click',
   8:     function() {
   9:         return false;
  10:     }
  11: );

Cu toate ca cele doua functii au acelasi efect, ele nu sunt aceeasi functie. Este asemanator cu diferenta dintre variabile si valori din alte limbaje de programare. Daca in C am avea urmatorul cod:

   1: int * a = 1;
   2: int * b = 1;
   3: printf("%b", a == b); // false
   4: printf("%b", *a == *b); // true

observati ca e vorba de doi pointeri diferiti a si b, care se intampla sa aiba aceeasi valoare. La fel si in JavaScript, mai sus s-a intamplat ca cele doua functii sa aiba acelasi efect, dar ele nu sunt aceeasi functie. De aceea intotdeauna cand atasam ascultatori pe care mai tarziu vom dori sa-i stergem, trebuie sa folosim functii cu nume:

   1: function doNothing() {
   2:     return false;
   3: }
   4: element.addEventListener('click', doNothing);
   5: // cod
   6: element.removeEventListener('click', doNothing);
   7: // acum a functionat

Putem de asemenea sa declansam un anumit eveniment, chiar daca acesta nu a fost declansat de utilizator (de exemplu vrem sa se produca toate actiunile care s-ar fi produs daca utilizatorul ar fi facut click pe un anumit element, chiar daca utilizatorul nu a facut explicit click). Codul este simplu:

   1: element.dispatchEvent(eveniment);

unde eveniment este o instanta de Event, creata astfel:

   1: var eveniment = document.createEvent(nume_eveniment);
   2: eveniment.initEvent('click', true, true);
   3: // cei 3 parametri ai lui initEvent sunt:
   4: //  - tip
   5: //  - bubbles (true daca vrem ca evenimentul sa se propage)
   6: //  - cancelable (true daca evenimentul poate fi oprit)

Acesta e un Event generic, dar exista mai multe tipuri specifice de evenimente, care pot sa transmita mai multe informatii (de exemplu, daca am vrea sa lansam un eveniment de tip ‘keypress’, putem sa trimitem o anumita tasta). Aflati mai multe despre acestea din articolul JavaScript tutorial – DOM events.

Pentru IE, sintaxa este oarecum asemanatoare, dar, cum aminteam mai sus, nu putem sa setam ca un eveniment sa se propage ca event capturing. De asemenea, este valabil ce spuneam si la DOM 0 despre obiectul Event si instanta trimisa ca parametru ascultatorilor, si anume ca numele proprietatilor si metodelor nu sunt toate aceleasi cu cele ale browserelor non-IE, si ca nu este transmisa instanta evenimentului ca parametru pentru ascultator, ci aceasta este disponibila in variabila globala event (window.event).

Astfel, atasarea unui ascultator se face astfel:

   1: element.attachEvent(tip, functie)

Atentie insa, tipul de data aceasta contine si “on” astfel ca un la adaugarea unui ascultator pentru evenimente de tip click, tip va fi “onclick”, spre deosebire de browserele non-IE unde acesta era “click”. Al treilea parametru care exista la browserele non-IE nu exista in IE, deoarece, cum mentionam mai sus, la IE nu putem propaga evenimentele ca event capturing. In plus, this din functia trimisa ca al doilea parametru nu va fi o referinta la elementul cu care avem de-a face, ci la window.

Detasarea se face asemanator, si ramane valabila observatia importanta ca trebuie sa folosim functii cu nume.

   1: element.detatchEvent(tip, functie)

In fine, lansarea unui eveniment se face folosind

   1: element.fireEvent(tip, instantaEvent);

Crearea instantei Event se face astfel:

   1: var instantaEvent = document.createEventObject();
   2: instantaEvent.cancelBubble = true; // evenimentul nu se propaga
   3: // alte initializari ale membrilor lui instantaEvent

Acestea au fost cele doua metode de a atasa ascultatori elementelor din DOM. Dupa cum vedeti, este vorba de multa munca depusa pentru a obtine cod care sa functioneze corect in toate browserele. Ca best practices, in general e recomandat sa definim functiile pe care le atasam ca evenimente ca functii cu nume, in care sa tratam evenimentul (si in care cu siguranta vom avea nevoie de cod separat daca ne intereseaza instanta Event trimisa ca parametru) – in asa fel incat sa putem detasa evenimentele. Apoi, sa scriem o functie generica care sa ataseze un eveniment cat mai cross-browser cu putinta:

   1: function attachEvent(element, tip, functie) {
   2:     if (element.addEventListener)
   3:         element.addEventListener(tip, functie);
   4:     else
   5:         element.attachEvent("on"+tip, functie);
   6: }

Desigur, am mai putea trata mai multe cazuri in care difera implementarile IE / non-IE – cum ar fi faptul ca this in cadrul ascultatorilor este o referinta la elementul care a declansat evenimentul in browserele non-IE, si o referinta la window pentru IE. Insa asa cum vom vedea intr-un articol viitor, mare parte din aceste neajunsuri (plus multe altele) sunt reparate de framework-urile JavaScript.

Data viitoare, vom vorbi de AJAX.

Link-uri utile:

Creative Commons License
The Evenimente in JavaScript by Interfeţe Web, unless otherwise expressly stated, is licensed under a Creative Commons Attribution-Share Alike 3.0 Romania License.

, , , , , , , ,

blog comments powered by Disqus