10 gennaio 2022

Tastiera: keydown e keyup

Prima di scendere nei dettagli della tastiera, è bene ricordare che nei dispositivi moderni esistono tanti modi per “inserire qualche dato”. Per esempio, è doveroso citare l’uso del riconoscimento vocale (specialmente sui dispositivi mobile) o il copia/incolla tramite il mouse.

Quindi, se vogliamo tenere traccia di qualunque input dentro un campo <input>, allora gli eventi della tastiera non saranno sufficienti. Esiste però un altro evento chiamato input per tenere traccia delle modifiche degli <input>, indipendentemente dalla modalità di inserimento. Questa potrebbe essere la scelta corretta per questo tipo di attività. L’argomento verrà affrontato più avanti nel capitolo Eventi: change, input, cut, copy, paste.

Gli eventi da tastiera dovrebbero essere usati per gestire, appunto, azioni da tastiera (comprese quelle virtuali). Ad esempio, per reagire ai tasti freccia Up e Down, oppure per l’uso delle scorciatoie e/o tasti di scelta rapida (includendo quindi combinazioni di tasti).

Banco di test

Per capire meglio gli eventi da tastiera, possiamo usare il seguente banco di test.

Proviamo diverse combinazioni di tasti nel campo di testo.

Risultato
script.js
style.css
index.html
kinput.onkeydown = kinput.onkeyup = kinput.onkeypress = handle;

let lastTime = Date.now();

function handle(e) {
  if (form.elements[e.type + 'Ignore'].checked) return;

  area.scrollTop = 1e6;

  let text = e.type +
    ' key=' + e.key +
    ' code=' + e.code +
    (e.shiftKey ? ' shiftKey' : '') +
    (e.ctrlKey ? ' ctrlKey' : '') +
    (e.altKey ? ' altKey' : '') +
    (e.metaKey ? ' metaKey' : '') +
    (e.repeat ? ' (repeat)' : '') +
    "\n";

  if (area.value && Date.now() - lastTime > 250) {
    area.value += new Array(81).join('-') + '\n';
  }
  lastTime = Date.now();

  area.value += text;

  if (form.elements[e.type + 'Stop'].checked) {
    e.preventDefault();
  }
}
#kinput {
  font-size: 150%;
  box-sizing: border-box;
  width: 95%;
}

#area {
  width: 95%;
  box-sizing: border-box;
  height: 250px;
  border: 1px solid black;
  display: block;
}

form label {
  display: inline;
  white-space: nowrap;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <form id="form" onsubmit="return false">

    Prevent default for:
    <label>
      <input type="checkbox" name="keydownStop" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
    <label>
      <input type="checkbox" name="keyupStop" value="1"> keyup</label>

    <p>
      Ignore:
      <label>
        <input type="checkbox" name="keydownIgnore" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
      <label>
        <input type="checkbox" name="keyupIgnore" value="1"> keyup</label>
    </p>

    <p>Focus on the input field and press a key.</p>

    <input type="text" placeholder="Press keys here" id="kinput">

    <textarea id="area" readonly></textarea>
    <input type="button" value="Clear" onclick="area.value = ''" />
  </form>
  <script src="script.js"></script>


</body>
</html>

Keydown e keyup

L’evento keydown scaturisce alla pressione di un tasto; keyup, invece, quando viene rilasciato.

event.code e event.key

La proprietà key dell’oggetto evento permette di ottenere il carattere, mentre la proprietà code ci restituisce il “codice fisico del tasto”.

Ad esempio, a parità di Z, quest’ultimo potrebbe essere stato premuto con o senza il Shift, cosa che potrebbe restituirci due differenti caratteri: z minuscolo oppure Z maiuscolo.

event.key è esattamente il carattere, che può essere diverso secondo alcuni criteri. Invece event.code, a parità di tasto, restituisce sempre lo stesso valore:

Key event.key event.code
Z z (minuscolo) KeyZ
Shift+Z Z (maiuscolo) KeyZ

Prendendo questo tasto come riferimento, se un utente facesse uso di diverse lingue nello stesso sistema operativo, il passaggio ad un’altra lingua potrebbe portare ad avere tutt’altro carattere rispetto a "Z". Quest’ultimo sarebbe il valore di event.key, mentre event.code sarebbe sempre "KeyZ".

“KeyZ” e altri codici tasto

Ogni tasto ha un codice che dipende dalla sua posizione sulla tastiera. Questi codici vengono descritti nelle specifiche dei codici Evento UI.

Per esempio:

  • I tasti lettera hanno dei codici: "Key<letter>": "KeyA", "KeyB" etc.
  • I tasti numerici hanno dei codici: "Digit<number>": "Digit0", "Digit1" etc.
  • I tasti speciali sono codificati con i loro nomi: "Enter", "Backspace", "Tab" etc.

Esiste una grande varietà di layout di tastiera, e le specifiche danno un codice per ognuno di essi.

Per avere informazioni sui vari codici, fare riferimento alla sezione alfanumerica delle specifiche, oppure, è sufficiente premere un tasto nel banco di test precedente.

La distinzione tra maiuscolo e minuscolo è importante: è "KeyZ", e non "keyZ"

Sembra ovvio, ma le persone sbagliano ancora.

Bisogna evitare di scrivere in modo errato: la dicitura corretta è KeyZ, e non keyZ. Di conseguenza, un controllo scritto così event.code=="keyZ" non funziona: la prima lettera di "Key" deve essere maiuscola.

Cosa succederebbe se un tasto non restituisse nessun carattere? Per esempio, Shift oppure F1 o altri ancora. Per questi tasti il valore di event.key è, con buona approssimazione, lo stesso di event.code:

Key event.key event.code
F1 F1 F1
Backspace Backspace Backspace
Shift Shift ShiftRight or ShiftLeft

È importante sottolineare che event.code specifica esattamente il tasto premuto. Per esempio, la maggioranza delle tastiere ha due tasti Shift: uno nel lato sinistro e uno nel lato destro. event.code ci dice esattamente quale dei due viene premuto, event.key è invece responsabile del “significato” del tasto: cosa è (cioè uno “Shift”).

Mettiamo il caso che volessimo gestire una scorciatoia: Ctrl+Z (o Cmd+Z su Mac). La maggior parte degli editor di testo associa a questa combinazione, l’azione “Undo”. A quel punto potremmo impostare un listener sul keydown e controllare quale tasto venga premuto.

Ma qui ci troveremo di fronte a un dilemma: in questo listener, cosa dovremmo controllare? Il valore di event.key oppure quello di event.code?

Da una parte, il valore di event.key è un carattere, e cambia a seconda del linguaggio. Se il visitatore gestisce più lingue nel suo sistema operativo e passa da una all’altra, lo stesso tasto restituirebbe caratteri differenti. Quindi ha senso controllare event.code, che è sempre lo stesso.

Ecco un esempio:

document.addEventListener('keydown', function(event) {
  if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
    alert('Undo!')
  }
});

D’altra parte, c’è un problema anche con event.code, dato che per layout di tastiera differenti, possono corrispondere caratteri differenti.

Per esempio, qui abbiamo un layout americano (“QWERTY”) e sotto di esso un layout Tedesco (“QWERTZ”) (da Wikipedia):

A parità di tasto, sul layout americano corrisponderà il carattere “Z”, invece per quello tedesco sarà “Y” (le lettere sono scambiate tra di loro).

Letteralmente, event.code equivale a KeyZ per gli utenti con il layout tedesco quando premono Y.

Se andiamo a controllare event.code == 'KeyZ' nel nostro codice, per gli utenti con il layout tedesco, il test passerà alla pressione del tasto Y.

Questo può sembrare strano, ma è così. Le specifiche menzionano in modo esplicito questo comportamento.

Quindi, event.code può corrispondere a un carattere errato su layout non contemplati. A parità di lettera, per layout differenti potrebbero essere mappati tasti fisici differenti, portando ad avere codici differenti. Fortunatamente, questo avviene solo con alcuni di questi, ad esempio keyA, keyQ, keyZ (come abbiamo visto), ma non avviene con i tasti speciali come Shift. Si può vedere la lista nelle specifiche.

Per il tracciamento affidabile dei caratteri che sono dipendenti dal layout, event.key potrebbe essere la soluzione migliore.

Di contro, event.code ha il beneficio di essere sempre lo stesso, legato alla posizione fisica del tasto, anche se il visitatore dovesse modificare la lingua. E le scorciatoie ad esso relative funzioneranno bene anche in caso di cambio lingua.

Vogliamo gestire dei tasti dipendenti dal layout? Ecco che event.key è quello che fa per noi.

Oppure, vogliamo una scorciatoia che funzioni anche modificando la lingua? Allora event.code sarebbe più adatto.

Auto-repeat

Se un tasto viene premuto abbastanza a lungo, si instaura l’“auto-repeat”: l’evento keydown scaturisce ancora e ancora e, alla fine, quando viene rilasciato, otteniamo un evento keyup. Quindi è abbastanza normale avere molti keydown ed un solo keyup.

Per eventi generati da auto-repeat, l’oggetto evento coinvolto avrà la proprietà event.repeat impostata a true.

Azioni default

Le azioni di default possono essere tante e variegate, dal momento che sono tante le cose che possono essere attivate tramite la tastiera.

Per esempio:

  • Appare un nuovo carattere sullo schermo (lo scenario più frequente).
  • Viene cancellato un carattere (tasto Delete).
  • Si scrolla la pagina (tasto PageDown).
  • Il browser apre la finestra di dialogo “Salva la pagina” (Ctrl+S)
  • …e così via.

Prevenire le azioni di default sul keydown può annullare la maggioranza di esse, con l’eccezione delle combinazioni di tasti del sistema operativo. Per esempio, su Windows Alt+F4 chiude la finestra attuale del browser. E non c’è modo per prevenire questa azione predefinita attraverso JavaScript.

Ora un esempio: il seguente campo <input> si aspetta un numero di telefono, quindi non accetta tasti che non siano numeri, +, () o -:

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') || ['+','(',')','-'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Inserire un numero di telefono" type="tel">

Qui il gestore onkeydown utilizza checkPhoneKey per controllare i testi premuti. Se sono validi (da 0..9 o uno tra +-()), allora ritorna true, altrimenti false.

Come sappiamo, il valore false restituito da un gestore di evento, assegnato usando un attributo o una proprietà DOM, come in questo caso, previene l’azione default, quindi non appare nulla in <input> per i tasti che non passano il tets. (Il valore true restituito non influenza nulla, conta solo la restituzione di false)

È interessante notare che i tasti speciali, come Backspace, Left, Right, Ctrl+V, non funzionano nel campo input. Questo è un effetto collaterale delle restrizioni del filtro checkPhoneKey.

Rendiamolo un po’ più rilassato permettendo i tasti freccia Left, Right e Delete, Backspace::

Please note that special keys, such as Backspace, Left, Right, do not work in the input. That’s a side-effect of the strict filter checkPhoneKey. These keys make it return false.

Let’s relax the filter a little bit by allowing arrow keys Left, Right and Delete, Backspace:

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') ||
    ['+','(',')','-','ArrowLeft','ArrowRight','Delete','Backspace'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">

Adesso le frecce e il tasto cancella funzionano.

Anche se abbiamo il filtro chiave, è comunque possibile inserire qualsiasi cosa utilizzando il mouse e facendo clic con il pulsante destro del mouse + Incolla. I dispositivi mobili forniscono altri mezzi per immettere valori. Quindi il filtro non è affidabile al 100%.

L’approccio alternativo sarebbe quello di tenere traccia dell’evento oninput, che si attiva dopo qualsiasi modifica. Lì possiamo controllare il nuovo input.value e modificarlo/evidenziare <input> quando non è valido. Oppure possiamo usare entrambi i gestori di evento insieme.

Eredità

In passato, c’era l’evento keypress, ed anche le proprietà keyCode, charCode, which dell’oggetto evento.

C’erano tante di quelle incompatibilità tra i vari browser, anche durante lo sviluppo delle specifiche da parte degli sviluppatori che cercavano di implementarle, che l’unica soluzione fu quella di deprecarli tutti, e creare degli eventi nuovi e moderni (descritti sopra in questo capitolo). Il codice vecchio funziona ancora, dal momento che i browser continuano a supportarli, ma non c’è assolutamente nessuna ragione per continuare a farlo.

Tastiere dei dispositivi mobile

Usando le tastiere virtuali dei dispositivi mobile, conosciute formalmente come IME (Input-Method Editor), lo standard W3C ha stabilito che per quanto riguarda il KeyboardEvent, il e.keyCode dovrebbe essere 229 ed il e.key dovrebbe essere "Unidentified".

Mentre alcune tastiere potrebbero usare il valore corretto per e.key, e.code, e.keyCode… premendo certi tasti come le frecce o lo barra spaziatrice, non esistono garanzie di aderenza alle specifiche, quindi la logica della tastiera potrebbe non funzionare sui dispositivi mobile.

Riepilogo

La pressione di una tasto genera sempre un evento da tastiera, che sia un tasto simbolo o un tasto speciale come Shift, Ctrl e così via. L’unica eccezione è rappresentata dal tasto Fn che a volte è presente nelle tastiere dei portatili. Per questo tasto non ci sono eventi, perché spesso il funzionamento di questo tasto è implementato a un livello più basso del sistema operativo.

Eventi da tastiera:

  • keydown premendo il tasto (auto-repeat se il tasto viene tenuto premuto a lungo),
  • keyup rilasciando il tasto.

Le principali proprietà degli eventi da tastiera sono:

  • code il “codice del tasto” ("KeyA", "ArrowLeft" e così via), specifico della posizione fisica del tasto sulla tastiera.
  • key – il carattere ("A", "a" e così via), per tasti che non rappresentano caratteri, come Esc, solitamente ha lo stesso valore di code.

In passato, gli eventi da tastiera erano talvolta usati per tenere traccia degli input dell’utente nei campi dei form, cosa non molto affidabile perché l’input può avvenire in vari modi. Per gestire qualunque tipo di input abbiamo input e change (affrontati più avanti nel capitolo Eventi: change, input, cut, copy, paste). Questi scaturiscono da qualunque tipo di input, inclusi il copia-incolla o il riconoscimento vocale.

In generale, dovremmo usare gli eventi da tastiera solamente quando vogliamo usare, appunto, la tastiera. Ad esempio per scorciatoie o tasti speciali.

Esercizi

importanza: 5

Create una funzione runOnKeys(func, code1, code2, ... code_n) che esegue func quando vengono premuti contemporaneamente i tasti con i codici code1, code2, …, code_n.

Ad esempio, il seguente codice mostra un alert quando vengono premuti "Q" e "W" insieme (in qualunque lingua, con o senza il CapsLock)

runOnKeys(
  () => alert("Hello!"),
  "KeyQ",
  "KeyW"
);

Demo in una nuova finesta

Qui dobbiamo usare due gestori: document.onkeydown e document.onkeyup.

Andiamo ad impostare pressed = new Set() per memorizzare i tasti attualmente premuti.

Il primo gestore lo aggiunge, mentre il secondo lo rimuove. Ad ogni keydown controlliamo se abbiamo abbastanza tasti premuti, ed in caso affermativo la funzione verrà eseguita.

Apri la soluzione in una sandbox.

Mappa del tutorial