Webprogramozás

Interaktív programok készítése a böngészőben: eseménykezelés

Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu

Ismétlés

Ismétlés – nyelvi elemek

  • Dinamikusan típusos
  • Interpretált nyelv
  • Szintaxis C++-hoz hasonló
    • vezérlési szerkezetek
    • operátorok
  • Adatszerkezetek
    • elemi típusok
    • összetett típusok: tömb és objektum
  • Funkcionális aspektus
  • OOP-s aspektus

Ismétlés – DOM

  • HTML elemek belső ábrázolása
  • programozási interfész (API)
  • bemeneti-kimeneti interfész

Ismétlés – DOM

  • Elemek kiválasztása
    • document.querySelector('css selector')
    • document.querySelectorAll('css selector')
  • Elem (JavaScript objektum) tulajdonságai
    • Analógia: Webfejlesztés → Webprogramozás
    • írás/olvasás
    • tulajdonságok (pl. innerHTML)
    • metódusok
  • Eseménykezelés
    • Eseménytípusok
    • elem.addEventListener(típus, fgv)

Ismétlés – DOM

<input type="text" id="nev"> <br>
<input type="button" value="Köszönj!" id="gomb"> <br>
<span id="kimenet"></span>
<script src="hello.js"></script>
// hello.js
function nevbolUdvozles(nev) {
  return `Hello ${nev}!`;
}
function hello() {
  const nev = document.querySelector('#nev').value;
  const udvozles = nevbolUdvozles(nev);
  document.querySelector('#kimenet').innerHTML = udvozles;
}
document.querySelector('#gomb').addEventListener('click', hello);

Eseménykezelés

Interaktív programok

Fogalmak

  • interaktív programok
  • felhasználói tevékenység
  • esemény
  • eseménykezelő függvények
  • –> eseményvezérelt programozás

Eseménykezelők mint mini programok

function esemenykezelo() {
  // beolvasás
  // feldolgozás
  // kiírás
}

Események

  • click: egérkattintás
  • mousemove: egérmozgatás
  • mousedown: egér gombjának lenyomása
  • mouseup: egér gombjának felenegedése
  • input: input mező értékének megváltozása
  • keydown: billentyűzet gombjának lenyomása
  • keyup: billentyűzet gombjának felengedése
  • keypress: billentyűzet gombjának megnyomása
  • submit: űrlap elküldése
  • scroll: görgetés az oldalon

Események referenciája

Eseménykezelő függvények regisztrálása

// egyszerűbb esetekben
elem.addEventListener(esemény_típusa, eseménykezelő_függvény);
elem.removeEventListener(esemény_típusa, eseménykezelő_függvény);

// például
gomb.addEventListener("click", kattintas);
gomb.removeEventListener("click", kattintas);

function kattintas() {
  // mi történjen kattintáskor
}

// helyben definiálva
elem.addEventListener(esemény_típusa, function () {
    // eseménykezelő kód
});

Eseménykezelő függvények regisztrálása

Általánosan

target.addEventListener(type, listener[, options]);
// options object
// - capture: Boolean (elkapás iránya)
// - once: Boolean (egyszeri hívás, majd eltávolítás)
// - passive: Boolean (nincs preventDefault() hívás)

target.removeEventListener(type, listener[, options]);
// options object
// - capture: Boolean

// Csak a capture flag számít az eseménykezelő azonosításában 
// a type és listener mellett
// Névtelen függvényt nem lehet eltávolítani (nincs rá referencia), kivéve: once flag

// például
target.addEventListener('click', onClick, { once: true });

Eseménykezelő függvények regisztrálása

// Egy elem egy eseményéhez több eseménykezelő függvény is kapcsolható.
gomb.addEventListener("click", kattintas1);
gomb.addEventListener("click", kattintas2);

// Több eseményhez ugyanaz a függvény is kapcsolható
gomb1.addEventListener("click", kattintas);
gomb2.addEventListener("click", kattintas);

Eseménykezelő példa

<input id="nev">
<button id="gomb">Üdvözöl</button>
<span id="kimenet"></span>

<script>
  document.querySelector("#gomb").addEventListener("click", kattintas);
  function kattintas() {
    // beolvasás
    const nev = document.querySelector("#nev").value;
    // feldolgozás
    const udvozles = `Hello ${nev}!`;
    // kiírás
    document.querySelector("#kimenet").innerHTML = udvozles;
  }
</script>

Példa

  • Adott: hivatkozások felsorolása
  • Cél: SHIFT gomb nyomva tartásával kattintva a konzolra kiírjuk a megnyitandó oldal URL-jét

Kérdések

  • Honnan tudjuk, hogy a SHIFT billentyű le van-e nyomva?
  • Hogyan tiltom le a hivatkozás követését?
  • Hogyan skálázható a megoldás a hivatkozások számával?
<ul>
  <li><a href="http://www.elte.hu">ELTE</a></li>
  <li><a href="http://www.inf.elte.hu">ELTE Informatika Kar</a></li>
  <li><a href="http://www.inf.elte.hu/mot">Médiainformatikai Tanszék</a></li>
</ul>

Az eseményobjektum

Az eseménykezelő függvények első paraméterként automatikusan megkapják

function esemenykezelo(e) {
  console.log(e);
}

Az eseményobjektum

Tartalma az eseménytől függ:

  • közös tulajdonságok: pl. type, target
  • egéresemény-tulajdonságok: clientX, clientY, screenX, screenY, button, buttons
  • billentyűzetesemény-tulajdonságok: key, code, altKey, shiftKey, ctrlKey

Referencia

Billentyűzetesemény tulajdonságai

key, code

Egéresemény tulajdonságai

clientX, clientY, screenX, screenY

Eseményobjektum – példa

const link = document.querySelector('ul > li:nth-of-type(1) > a')
function kattintas(e) {
  if (e.shiftKey) {
    const href = link.href;
    console.log(href);
    alert('stop');
  }
}
link.addEventListener('click', kattintas);

Alapértelmezett művelet megakadályozása

  • Alapértelmezett műveletek:
    • Linkre kattintás → oldal betöltése
    • Submit gombra kattintás → űrlap elküldése
    • Beviteli mezőbe gépelés → karakterek beíródnak
  • Megakadályozása
    • eseményobjektum preventDefault() metódusa
function esemenykezelo(e) {
  e.preventDefault();
}

Alapértelmezett művelet megakadályozása – példa

const link = document.querySelector('ul > li:nth-of-type(1) > a')
function kattintas(e) {
  if (e.shiftKey) {
    e.preventDefault();
    const href = link.href;
    console.log(href);
  }
}
link.addEventListener('click', kattintas);

Hatékonysági megfontolások

<ul>
  <li><a href="http://www.elte.hu">ELTE</a></li>
  <li><a href="http://www.inf.elte.hu">ELTE Informatika Kar</a></li>
  <li><a href="http://www.inf.elte.hu/mot">Médiainformatikai Tanszék</a></li>
</ul>
const li1 = document.querySelector('ul > li:nth-of-type(1)')
function kattintas1(e) { /* ... */ }
li1.addEventListener('click', kattintas1);

const li2 = document.querySelector('ul > li:nth-of-type(2)')
function kattintas2(e) { /* ... */ }
li2.addEventListener('click', kattintas2);

// ...

Hatékonysági megfontolások

this az eseményre figyelő objektum (kezelő objektum)

const link1 = document.querySelector('ul > li:nth-of-type(1) > a')
const link2 = document.querySelector('ul > li:nth-of-type(2) > a')
link1.addEventListener('click', kattintas);
link2.addEventListener('click', kattintas);

function kattintas(e) { 
  if (e.shiftKey) {
    e.preventDefault();
    const href = this.href;
    console.log(href);
  }
}

Események buborékolása és delegálása

  • Forrásobjektum: az eseményt kiváltó objektum (e.target)
  • Események buborékolása: az esemény a forrásobjektumtól kezdve sorban mindegyik szülőjén is bekövetkezik
  • Lehetséges magasabb szinten kezelni az eseményt, mint ahol bekövetkezik
  • Kezelőobjektum: az az objektum, amelyhez az eseménykezelő hozzá van rendelve (this)
forrásobjektum → szülő → szülő → ... → body → html → document → window

Események buborékolása és delegálása

  • Delegálás: az eseményt magasabb szinten kezeljük, de egy alacsonyabb szintű objektummal dolgozunk
  • Delegált objektum: az az objektum, amellyel az eseménykezelőben dolgozni szeretnénk
  • Buborékolás megakadályozása: e.stopPropagation()
  • Pontosabban
      1. Föntről lefefe (capture)
      1. Lentről felfele (bubbling)
forrásobjektum → szülő → szülő → ... → body → html → document → window

Esemény delegálása – példa

<html><body>
    <ul>
      <li><a href="http://www.elte.hu">ELTE</a></li>
      <li><a href="http://www.inf.elte.hu">ELTE Informatika Kar</a></li>
      <li><a href="http://www.inf.elte.hu/mot">Médiainformatikai Tanszék</a></li>
    </ul>
</body></html>
const ul = document.querySelector('ul')
ul.addEventListener('click', kattintas);

function kattintas(e) {
  if (e.target.matches('li a')) {
    if (e.shiftKey) {
      e.preventDefault();
      const href = e.target.href;
      console.log(href);
    }
  }
}

Esemény delegálása – példa

<ul>
  <li><a href="http://www.elte.hu"><span>ELTE</span></a></li>
  <li><a href="http://www.inf.elte.hu">ELTE <span>Informatika Kar</span></a></li>
  <li><a href="http://www.inf.elte.hu/mot">Médiainformatikai Tanszék</a></li>
</ul>

A delegált elem közbülső elem!

Esemény delegálása

function onClick(e) {
  const handlerElement = this;
  const sourceElement = e.target;
  const selector = '.alma';

  let element = sourceElement
  while (element !== handlerElement && !element.matches(selector)) {
    element = element.parentNode;
  }

  if (element !== handlerElement) {
    const delegatedElement = element;
    console.log(delegatedElement);
  }
}

Esemény delegálása

function kattintas(e) {
  const handlerElement = this;
  const sourceElement = e.target;
  const selector = '.alma';

  const closestElement = sourceElement.closest(selector);
  if (handlerElement.contains(closestElement)) {
    const delegatedElement = closestElement;
    console.log(delegatedElement);
  }
}

Esemény delegálása – példa

function kattintas(e) {
  const handlerElement = this;
  const sourceElement = e.target;
  const selector = 'li a';

  const closestElement = sourceElement.closest(selector);
  if (handlerElement.contains(closestElement)) {
    const delegatedElement = closestElement;
    if (e.shiftKey) {
      e.preventDefault();
      console.log(delegatedElement.href);
    }
  }
}

Esemény delegálása

Segédfüggvény

function delegate(parent, type, selector, fn) {

  function delegatedFunction(e) {
    const handlerElement = this;
    const sourceElement = e.target;

    const closestElement = sourceElement.closest(selector);
    if (handlerElement.contains(closestElement)) {
      const delegatedElement = closestElement;
      fn.call(delegatedElement, e)
    }
  }

  parent.addEventListener(type, delegatedFunction);
}

Esemény delegálása – példa

const ul = document.querySelector('ul')
delegate(ul, 'click', 'li a', kattintas);

function kattintas(e) {
  if (e.shiftKey) {
    e.preventDefault();
    console.log(this.href);
  }
}

Események delegálása

  • Hatékony programozási minta
    • egy elem egy eseménykezelő
  • Sok elemnél
  • Dinamikusan beszúrt elemeknél
  • Viselkedés hozzárendelése elemekhez deklaratívan

Események delegálása – példa

<a href="http://www.elte.hu" class="info"><span>ELTE</span></a>
<a href="http://www.inf.elte.hu">ELTE <span>Informatika Kar</span></a>
<a href="http://www.inf.elte.hu/mot" class="info">Médiainformatikai Tanszék</a>
<script>
delegate(document, 'click', 'a.info', kattintas);
function kattintas(e) {
  if (e.shiftKey) {
    e.preventDefault();
    console.log(this.href);
  }
}
</script>

Események programozott kiváltása

Beépített események kiváltása

const event = new MouseEvent('click', {
  bubbles: true,
  clientX: tx,
  clientY: ty
})
elem.dispatchEvent(event)

// vagy
elem.dispatchEvent(new Event('change', {bubbles: true}))

Egyedi események kiváltása és figyelése

const event = new Event('valami');
// Feliratkozás az eseményre
elem.addEventListener('valami', function (e) { /* ... */ });
// Esemény kiváltása
elem.dispatchEvent(event);

// vagy

const event = new CustomEvent('valami', { detail: "adat", bubbles: true });
elem.addEventListener('valami', eventHandler);
function eventHandler(e) {
  console.log(e.detail); // "adat"
}

Referencia

Tesztelés

Jasmine

Példa

Előkészítés

<!-- keretrendszer betöltése -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/jasmine.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/jasmine.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/jasmine-html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/boot.min.js"></script>

<input type="text" id="nev"> <br>
<input type="button" value="Köszönj!" id="gomb"> <br>
<span id="kimenet"></span>

<!-- az alkalmazás és teszt betöltése -->
<script src="hello.js"></script>
<script src="hello.test.js"></script>

Tesztek

describe('felület működése', () => {
    // saját $ függvény
    function $(sel) { return document.querySelector(sel); }
    // minden teszt előtt készítsük elő a felületet
    beforeEach(() => {
        $('#nev').value = '';
        $('#kimenet').innerHTML = '';
    });
    // tesztek
    it(`Minden megjelenik`, () => {
        expect($('input#nev')).not.toBeNull();
        expect($('input#gomb')).not.toBeNull();
        expect($('span#kimenet')).not.toBeNull();
    });
    it(`Üresen hagyva Hello ! jelenik meg`, () => {
        $('#gomb').click(); // elvégez
        expect($('#kimenet').textContent).toBe('Hello !'); // ellenőriz
    });
    it(`Szöveget írva be helyes üdvözlés lesz`, () => {
        $('#nev').value = 'alma'; // előkészít
        $('#gomb').dispatchEvent(new Event('click')); // elvégez
        expect($('#kimenet').textContent).toBe('Hello alma!'); // ellenőriz
    });
});

Böngésző-függetlenség

Örökölt kód megértéséhez

Tulajdonság ellenőrzés

Inline módszer

  • HTML attribútumként jelenik meg az eseménykezelő
  • Történetileg az első módszer
  • Mindenhol működik
<!-- Általában -->
<elem ontípus="javascript kód">

<!-- Például -->
<input type="button" id="gomb" onclick="hello()">

Tradícionális módszer

  • Programozottan rendelhető eseménykezelő egy elem eseményéhez
  • Nem szabványos, de minden böngésző támogatja
  • Egy elemhez csak egy eseménykezelő köthető
elem.ontípus = függvény;

// És NEM:
elem.ontípus = függvény();

// Törlés
elem.ontípus = null;

// Például
document.querySelector('#gomb').onclick = hello;

Szabványos módszer

  • addEventListener, removeEventListener
  • Paraméterek
    • Esemény típusa
    • Eseménykezelő függvény
    • Elkapás iránya
// Hozzáadás/regisztrálás
elem.addEventListener('típus', fuggveny, false);
// Elvétel/leiratkozás
elem.removeEventListener('típus', fuggveny, false);

// Például
let gomb = document.querySelector('#gomb');
gomb.addEventListener('click', hello, false);

Eseménykezelők regisztrációja

Tulajdonság ellenőrzés (feature detection)

function kezelotRegisztral(elem, tipus, kezelo) {
  //Szabványos módszer
  if (elem.addEventListener) {
    elem.addEventListener(tipus, kezelo, false);
  }
  //Microsoft módszer
  else if (elem.attachEvent) {
    elem.attachEvent('on' + tipus, function () {
      kezelo(window.event);
      //vagy
      // return kezelo.call(elem, window.event);
    });
  }
  //Tradicionális módszer
  else {
    elem['on' + tipus] = kezelo;
  }
}

// Használata
kezelotRegisztral($('#lista'), 'click', mutat);

Példák az eseménykezelésre

Csillagok

Rajzoljunk ki egy csillagot a képernyőnek azon pontjára, ahova kattintottunk!

Csillagok

<style>
  .csillag {
    position: fixed;
    list-style-type: none;
  }
</style>
<ul id="csillagok"></ul>
<script>
  document.addEventListener("click", kattintas);
  function kattintas(e) {
    // beolvasás
    const x = e.clientX;
    const y = e.clientY;
    // feldolgozás
    const csillag = `<li class="csillag" style="top: ${y}px; left: ${x}px;">*</li>`;
    // kiírás
    document.querySelector("#csillagok").innerHTML += csillag;
  }
</script>

Todo lista 1.

Egy listaelemre kattintva váltogassuk annak stílusosztályát!

Todo lista 1.

<style>
  .kesz:before {
    content: "✓ ";
  }
</style>
<ul class="lista">
  <li>első</li>
  <li>második</li>
  <li>harmadik</li>
</ul>
<script>
  const ul = document.querySelector("ul.lista")
  ul.addEventListener("click", listaKattintas);
  function listaKattintas(e) {
    if (e.target.matches("li")) {
      // beolvasás
      const li = e.target;
      // kiírás
      li.classList.toggle("kesz");
    }
  }
</script>

Todo lista 2.

Legyen lehetőség új elemet hozzáadni a listához!

Egy listaelemre kattintva váltogassuk annak stílusosztályát!

Todo lista 2.

<style>
.kesz:before {
  content: "✓ ";
}
</style>
Új elem:
<input>
<button>Hozzáad</button>
<ul>
  <li>első</li>
  <li>második</li>
  <li>harmadik</li>
</ul>
<script>
document.querySelector("button").addEventListener("click", ujElemKattintas);
function ujElemKattintas(e) {
  // beolvasás
  const ujelem = document.querySelector('input').value;
  // kiírás
  document.querySelector('ul').innerHTML += `<li>${ujelem}</li>`
}

document.querySelector("ul").addEventListener("click", listaKattintas);
function listaKattintas(e) {
  if (e.target.matches("li")) {
    // beolvasás
    const li = e.target;
    // kiírás
    li.classList.toggle("kesz");
  }
}
</script>

Megjelenítő

Ha egy gombra kattintunk, és annak van egy speciális attribútuma, akkor az attribútumban lévő elem megjelenését váltogassuk!

Megjelenítő

<button data-toggle-id="some-statement">
    A nyilatkozat mutatása
</button>
<div id="some-statement" hidden>
    Nyilatkozat
</div>
<script>
document.addEventListener('click', onTogglerClick)
function onTogglerClick(e) {
    const id = e.target.dataset.toggleId;
    if (!id) return;

    const elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
}
</script>

Összefoglalás

  • Eseménykezelés
    • elem.addEventListener(típus, fgv)
    • Interakció eszköze
    • Eseményobjektum
    • Alapértelmezett műveletek megakadályozása
    • Buborékolás
    • Delegálás