Aller au contenu

[Tuto] Comprendre et utiliser les évènements clavier en Javascript

De nos jour, les raccourcis claviers sont de plus en plus utilisés dans des applications web. Si Google Doc était l'un de ceux qui ont popularisé cette pratique, elle n'est aujourd'hui plus seulement restreinte au traitement de texte.

Nous allons voir aujourd'hui les mécanismes qui entrent en jeu pour capturer les raccourcis saisis par l'utilisateur, et comment les employer. Cet article peut sembler un peu long, mais il vise à expliquer le maximum de notions, qui sont trop souvent oubliées ou simplifiées dans les tutoriels sur le sujet.

Je parle avant tout de raccourcis claviers, mais sachez que tout ceci peut vous être utile dans d'autres cadres, comme la validation de champs de formulaire au fil de la saisie, par exemple.

  1. Les évènements
  2. L'objet KeyboardEvent
  3. Exemple d'exécution
  4. Remplacer un raccourci navigateur
  5. Méthode de contournement pour Firefox
  6. Conclusion

Les évènements

Il existe, en Javascript, 3 évènements différents générés par l'utilisation d'une touche de votre clavier :

  1. keydown / onkeydown : comme son nom l'indique, il est déclenché lorsque la touche est enfoncée.
  2. keypress / onkeypress : on dirait un synonyme de keydown. Il est d'ailleurs déclenché juste après, toujours quand on enfonce la touche.
  3. keyup / onkeyup : c'est le fait de retirer son doigt de la touche qui le déclenche.

Quelle est l'utilité d'avoir 2 évènements différents (keydown et keypress) pour la même chose, me direz-vous ?

Comme l'explique ce blogueur, il faut avant tout distinguer les touches servant à écrire des caractères (lettres, chiffres, caractères spéciaux, etc...) des autres touches (Ctrl, Maj, Alt, Windows/Commande, etc...). Les évènements keydown et keyup expriment le fait d'enfoncer ou relâcher une touche de votre clavier, quelle que soit sa fonction. Par contre keypress exprime théoriquement le fait d'écrire un caractère.

Je dis "théoriquement" car tous les navigateurs n'ont pas exactement le même comportement à ce propos. Tout ce qu'il faut retenir pour le moment, c'est que l'évènement keypress pourra être déclenché par l'appui sur la touche "A", mais pas par l'appui sur la touche "Contrôle".

L'objet KeyboardEvent

Habituellement, en Javascript, quand on réalise une fonction appelée sur un évènement, celle-ci prend en paramètre un objet de type Event. C'est la variable ev dans les exemples ci-dessous :

<script>
window.onload = function(ev) {
    ev.preventDefault();
}

document.getElementById('myId').addEventListener('click', function (ev) {
    console.log( ev.target.className );
});
</script>

Dans le cadre d'évènement issus des touches clavier, cet objet sera de type KeyboardEvent. C'est lui qui indique quelle touche (ou combinaison de touches) a été enfoncée. Voici une liste de ses attributs qui pourront nous être utiles :

  • keyCode (entier) : code numérique représentant de façon unique chaque caractère. La table des codes peut différer d'un navigateur/système à un autre (tableau comparatif entre les navigateurs)
  • Attributs indiquant si d'autres touches étaient enfoncées avant que celle-ci le soit aussi :
    • altKey (booléen) : indique si la touche Alt était enfoncée ou non
    • ctrlKey (booléen) : indique si la touche Ctrl était enfoncée ou non
    • metaKey (booléen) : indique si la touche Meta (Windows sur Windows, Commande sur Mac) était enfoncée ou non
    • shiftKey (booléen) : indique si la touche Majuscule était enfoncée ou non

Parmi les attributs non cités ci-dessus, on retrouve char, charCode et which, qui sont dépréciés, et doivent être remplacés par key, qui n'est pas actuellement implémenté (un comble !). En fait, même keyCode est déprécié, et sera remplacé par key au final (ils ont des fonctionnements très proches), mais en attendant, on continue à l'utiliser. Pour une liste exhaustive des attributs, RTFM.

Exemple d'exécution

Imaginons que vous souhaitez effectuer une combinaison de touches (prenons par exemple Ctrl+S, comme quand vous voulez sauvegarder une page). Dans la pratique, voici dans l'ordre les évènements qui ont lieu, ainsi que les valeurs des attributs de l'objet KeyboardEvent dans chaque cas (testé sur Firefox) :

  1. keydown (on vient d'appuyer sur Contrôle)

    • keyCode = 17 (code correspondant à la touche Contrôle)
    • altKey = false
    • ctrlKey = true
  2. keydown (on vient d'appuyer sur S)

    • keyCode = 83 (code correspondant à la touche S)
    • altKey = false
    • ctrlKey = true
  3. keypress (on vient d'appuyer sur S, la touche Contrôle est toujours enfoncée)

    • keyCode = 83 (code correspondant à la touche S)
    • altKey = false
    • ctrlKey = true

On constate bien que keypress n'a pas lieu quand on appui sur "Contrôle". De même on constate que quand on appui sur S, les évènements keydown et keypress se suivent et comportent des informations identiques.

Mais du coup keydown nous suffit amplement, à quoi bon s'intéresser à keypress ?

Vous avez parfaitement raison ! En théorie cet évènement est dépriécié (comme le reste de DOM L3. La norme HTML5 apporte, comme l'indique la doc de Mozilla, un remplaçant dans l'évènement input).

Hélas, tout ceci n'est bien que cela : de la théorie. Vous allez pouvoir vous en rendre compte dans le chapitre suivant.

Remplacer un raccourci navigateur

Reprenons notre exemple de raccourci : Ctrl+S. Si vous l'employez sur une page web dans votre navigateur, ce dernier vous proposera probablement d'enregistrer une sauvegarde de la page courante (c'est le cas de Firefox et Chrome, et sans doute d'autres).

Imaginons que vous réalisez une application web de prise de note (au hasard, Jotter). Vous aurez besoin de laisser la possibilité à vos utilisateurs de sauvegarder ce qu'ils ont saisi vers le serveur de votre appli. Vous savez aussi que, sauf besoin très spécifique, ils n'auront pas besoin de sauvegarder le HTML de la page en cours vers leur ordinateur.

Naturellement, vous décider de remplacer le fonctionnement par défaut de votre navigateur par celui de votre application. Voici le code de base pour le réaliser :

document.addEventListener('keydown', function (e){
    if(e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) {
        e.preventDefault();
        //app-specific code goes here
    }
});

Décorticons ce code. On commence par vérifier si l'évènement est survenu sur la combinaison de touches Ctrl+S grâce aux attributs ctrlKey et keyCode. 'S'.charCodeAt(0) est une manière plus lisible d'indiquer le code de la lettre S (qui est 83 dans tous les navigateurs).

    if(e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) {

Ensuite, on demande d'annuler l'effet par défaut associé à cette combinaison de touches :

        e.preventDefault();

Il ne reste ensuite plus qu'à réaliser les actions spécifiques à notre application (comme par exemple faire une requête ajax pour enregistrer sur le serveur les données saisies par l'utilisateur). C'est ce que j'ai désigné par :

        //app-specific code

Si vous utilisez ce code, vous constaterez que, en fonction du code spécifique que vous ajoutez, parfois Firefox proposera malgré tout à l'utilisateur de sauvegarder la page sur son ordinateur. Et là, vous vous dites :

Pourquoi ?!?
... et vous avez parfaitement raison !

Explication fumeuse : Firefox se base sur keypress pour gérer les raccourcis clavier qu'il a de définis (ce qui n'est pas forcément le cas des autres navigateurs, à vérifier). Jusqu'ici tout va bien. Il respecte en théorie la norme qui dit que si l'évènement par défaut de keydown est stoppé (via .preventDefault()), alors celui de keypress devrait l'être aussi.

Et Firefox le fait, la plupart du temps. Mais il suffit que votre code "app-specific" contienne certains bouts de code (comme un bête alert()) pour que .preventDefault() ne suffise plus. En fait, même si vous utilisez l'un des deux bouts de code ci-dessous, rien n'y fera, et Firefox exécutera quand même le :

document.addEventListener('keydown', function (e){
    if(e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) {
        e.preventDefault();
        e.stopPropagation();
        //app-specific code
    }
});

Ou :

document.addEventListener('keydown', function (e){
    if(e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) {
        //app-specific code
        return false;
    }
});

Le pire est que dans ce cas précis, l'évènement keypress verra son Event.keyCode à 0 au lieu de 83 (pour S)...

Méthode de contournement pour Firefox

Du coup, la solution consiste à contrôler dès le keydown si on doit annuler l'évènement keypress, puisque ce dernier n'est même plus capable de savoir quelle touche a été enfoncée :

//must be accessible from both keydown and keypress events
var cancelKeypress = false;

document.addEventListener('keydown', function (e){
    if(e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) {
        //do this prior to anything else
        cancelKeypress = true;

        //might still be useful for other browsers
        e.preventDefault();

        //app-specific code goes here
    }
});

//workaround for Firefox:
document.addEventListener('keypress', function (e){
    //must probably be done prior to anything else
    if(cancelKeypress === true) {
        e.preventDefault();
        cancelKeypress = false;
    }
});

Je vous l'avais bien dit que ce satané keypress nous servirait à quelque chose.

Conclusion

Vous avez désormais une base vraiment solide pour gérer les évènements clavier en Javascript. Nous avons abordé les logiques générales, et le reste pourra généralement se trouver dans la doc.

Nous avons malgré tout pu constater que certaines spécificités des différents navigateurs rendaient ce sujet un peu plus complexe que prévu. Si ces spécificités sont parfois documentées, vous aurez sans doute souvent à farfouiller les forums ou expérimenter par vous même pour trouver réponse à vos questions.

J'ajouterai aussi que même si les raccourcis claviers dans une appli web, ça peut avoir la classe, n'oubliez pas que cela n'est pas toujours judicieux, et que si vous remplacez les comportements d'un navigateur par quelque chose de nouveau, cela peut déstabiliser vos utilisateurs, au risque de les rendre insatisfaits.

Keep typing & rock on!