web-dev-qa-db-fra.com

Comment détecter si plusieurs touches sont enfoncées simultanément à l'aide de JavaScript?

J'essaie de développer un moteur de jeu JavaScript et j'ai rencontré ce problème:

  • Quand j'appuie SPACE le personnage saute.
  • Quand j'appuie  le personnage bouge bien.

Le problème est que, lorsque j'appuie à droite puis sur Espace, le personnage saute puis cesse de bouger.

J'utilise la fonction keydown pour obtenir la touche enfoncée. Comment puis-je vérifier si plusieurs touches sont enfoncées simultanément?

151
Cristy

Remarque: le code de clé est maintenant obsolète.

La détection de frappe multiple est facile si vous comprenez le concept

Voici comment je le fais:

var map = {}; // You could also use an array
onkeydown = onkeyup = function(e){
    e = e || event; // to deal with IE
    map[e.keyCode] = e.type == 'keydown';
    /* insert conditional here */
}

Ce code est très simple: étant donné que l’ordinateur ne transmet qu’une frappe à la fois, un tableau est créé pour garder trace de plusieurs clés. Le tableau peut ensuite être utilisé pour rechercher une ou plusieurs clés à la fois.

Juste pour expliquer, disons que vous appuyez sur A et B, chacun déclenche un événement keydown qui définit map[e.keyCode] sur la valeur de e.type == keydown, dont le résultat est true ou faux . Maintenant, map[65] et map[66] sont tous deux réglés sur true. Lorsque vous relâchez A, l'événement keyup se déclenche, obligeant la même logique à déterminer le résultat opposé pour map[65] (A), qui est maintenant false , mais puisque map[66] (B) est toujours "en panne" (il n'a pas déclenché d'événement keyup), il reste vrai .

Le tableau map, à travers les deux événements, ressemble à ceci:

// keydown A 
// keydown B
[
    65:true,
    66:true
]
// keyup A
// keydown B
[
    65:false,
    66:true
]

Il y a deux choses que vous pouvez faire maintenant:

A) Un enregistreur de clé ( exemple ) peut être créé en tant que référence pour plus tard, lorsque vous souhaitez déterminer rapidement un ou plus de codes clés. En supposant que vous ayez défini un élément html et que vous y pointez avec la variable element.

element.innerHTML = '';
var i, l = map.length;
for(i = 0; i < l; i ++){
    if(map[i]){
        element.innerHTML += '<hr>' + i;
    }
}

Remarque: vous pouvez facilement saisir un élément par son attribut id.

<div id="element"></div>

Cela crée un élément html qui peut être facilement référencé en javascript avec element

alert(element); // [Object HTMLDivElement]

Vous n'avez même pas besoin d'utiliser document.getElementById() ou $() pour le récupérer. Mais pour des raisons de compatibilité, l’utilisation de $() de jQuery est plus largement recommandée.

Assurez-vous simplement que la balise du script vient après le corps du code HTML. Astuce d’optimisation : La plupart des sites Web portant un nom important placent la balise de script après le balise body pour l'optimisation. En effet, la balise de script empêche le chargement d'éléments supplémentaires jusqu'à la fin du téléchargement du script. Le faire passer avant le contenu permet au contenu de se charger à l'avance.

B (c'est ce qui vous intéresse) Vous pouvez rechercher une ou plusieurs clés à la fois où /*insert conditional here*/ était, prenons l'exemple suivant:

if(map[17] && map[16] && map[65]){ // CTRL+SHIFT+A
    alert('Control Shift A');
}else if(map[17] && map[16] && map[66]){ // CTRL+SHIFT+B
    alert('Control Shift B');
}else if(map[17] && map[16] && map[67]){ // CTRL+SHIFT+C
    alert('Control Shift C');
}

Edit : Ce n'est pas l'extrait le plus lisible. La lisibilité est importante, vous pouvez donc essayer quelque chose comme ceci pour rendre les yeux plus faciles:

function test_key(selkey){
    var alias = {
        "ctrl":  17,
        "shift": 16,
        "A":     65,
        /* ... */
    };

    return key[selkey] || key[alias[selkey]];
}

function test_keys(){
    var keylist = arguments;

    for(var i = 0; i < keylist.length; i++)
        if(!test_key(keylist[i]))
            return false;

    return true;
}

Usage:

test_keys(13, 16, 65)
test_keys('ctrl', 'shift', 'A')
test_key(65)
test_key('A')

Est-ce mieux?

if(test_keys('ctrl', 'shift')){
    if(test_key('A')){
        alert('Control Shift A');
    } else if(test_key('B')){
        alert('Control Shift B');
    } else if(test_key('C')){
        alert('Control Shift C');
    }
}

(fin de montage)


Cet exemple vérifie CtrlShiftACtrlShiftB, et CtrlShiftC

C'est aussi simple que ça :)

Remarques

Garder une trace des codes clés

En règle générale, il est recommandé de documenter le code, en particulier des éléments tels que les codes de clé (tels que // CTRL+ENTER) afin que vous puissiez vous en souvenir.

Vous devez également placer les codes de clé dans le même ordre que la documentation (CTRL+ENTER => map[17] && map[13], NOT map[13] && map[17]). De cette façon, vous ne serez jamais dérouté lorsque vous devrez revenir en arrière et modifier le code.

Un piège avec des chaînes if-else

Si vous recherchez des combinaisons de quantités différentes (comme CtrlShiftAltEnter et CtrlEnter), mettez des combos plus petits après des combos plus grands, sinon les combos plus petits remplaceront les combos plus grands s'ils sont suffisamment similaires. Exemple:

// Correct:
if(map[17] && map[16] && map[13]){ // CTRL+SHIFT+ENTER
    alert('Whoa, mr. power user');
}else if(map[17] && map[13]){ // CTRL+ENTER
    alert('You found me');
}else if(map[13]){ // ENTER
    alert('You pressed Enter. You win the prize!')
}

// Incorrect:
if(map[17] && map[13]){ // CTRL+ENTER
    alert('You found me');
}else if(map[17] && map[16] && map[13]){ // CTRL+SHIFT+ENTER
    alert('Whoa, mr. power user');
}else if(map[13]){ // ENTER
    alert('You pressed Enter. You win the prize!');
}
// What will go wrong: When trying to do CTRL+SHIFT+ENTER, it will
// detect CTRL+ENTER first, and override CTRL+SHIFT+ENTER.
// Removing the else's is not a proper solution, either
// as it will cause it to alert BOTH "Mr. Power user" AND "You Found Me"

Gotcha: "Cette combinaison de touches continue de s'activer même si je n'appuie pas sur les touches"

Lorsque vous traitez avec des alertes ou avec tout ce qui prend le focus depuis la fenêtre principale, vous pouvez inclure map = [] pour réinitialiser le tableau une fois la condition remplie. En effet, certaines choses, comme alert(), détournent le focus de la fenêtre principale et empêchent le déclenchement de l'événement 'keyup'. Par exemple:

if(map[17] && map[13]){ // CTRL+ENTER
    alert('Oh noes, a bug!');
}
// When you Press any key after executing this, it will alert again, even though you 
// are clearly NOT pressing CTRL+ENTER
// The fix would look like this:

if(map[17] && map[13]){ // CTRL+ENTER
    alert('Take that, bug!');
    map = {};
}
// The bug no longer happens since the array is cleared

Gotcha: paramètres par défaut du navigateur

Voici une chose ennuyeuse que j'ai trouvée, avec la solution incluse:

Problème: le navigateur ayant généralement des actions par défaut sur les combinaisons principales (comme CtrlD active la fenêtre de signet, ou CtrlShiftC active skynote sur maxthon), vous pouvez également ajouter return false après map = [], afin que les utilisateurs de votre site ne soient pas frustrés lorsque la fonction "Fichier en double" est activée. CtrlD, signets la page à la place.

if(map[17] && map[68]){ // CTRL+D
    alert('The bookmark window didn\'t pop up!');
    map = {};
    return false;
}

Sans return false, la fenêtre Signet apparaîtrait , à la consternation de l'utilisateur.

La déclaration de retour (nouvelle)

Bon, vous ne voulez donc pas toujours quitter la fonction à ce stade. C'est pourquoi la fonction event.preventDefault() est là. Cela active un indicateur interne qui indique à l'interprète de ne pas autoriser le navigateur à exécuter l'action par défaut. Après cela, l'exécution de la fonction continue (alors que return quittera immédiatement la fonction).

Comprenez cette distinction avant de décider d’utiliser return false ou e.preventDefault()

event.keyCode est obsolète

L'utilisateur SeanVieira a signalé dans les commentaires que event.keyCode est obsolète.

Là, il a donné une excellente alternative: event.key, qui retourne une représentation sous forme de chaîne de la touche enfoncée, comme "a" pour A, ou "Shift" pour Shift.

Je suis allé de l'avant et ai cuit un outil pour examiner lesdites chaînes.

element.onevent vs element.addEventListener

Les gestionnaires enregistrés avec addEventListener peuvent être empilés et sont appelés dans l'ordre d'enregistrement, tandis que le réglage de .onevent est plutôt agressif et remplace tout ce que vous aviez auparavant.

document.body.onkeydown = function(ev){
    // do some stuff
    ev.preventDefault(); // cancels default actions
    return false; // cancels this function as well as default actions
}

document.body.addEventListener("keydown", function(ev){
    // do some stuff
    ev.preventDefault() // cancels default actions
    return false; // cancels this function only
});

La propriété .onevent semble tout écraser et le comportement de ev.preventDefault() et return false; peut être assez imprévisible.

Dans les deux cas, les gestionnaires enregistrés via addEventlistener semblent plus faciles à écrire et à raisonner.

Il y a aussi attachEvent("onevent", callback) de l'implémentation non standard d'Internet Explorer, mais elle est au-delà de obsolète et ne concerne même pas JavaScript (elle concerne un langage ésotérique appelé JScript ). Il serait dans votre intérêt d'éviter autant que possible le code polyglotte.

Une classe d'assistance

Pour faire face aux confusions/plaintes, j'ai écrit une "classe" qui fait cette abstraction ( lien Pastebin ):

function Input(el){
    var parent = el,
        map = {},
        intervals = {};

    function ev_kdown(ev)
    {
        map[ev.key] = true;
        ev.preventDefault();
        return;
    }

    function ev_kup(ev)
    {
        map[ev.key] = false;
        ev.preventDefault();
        return;
    }

    function key_down(key)
    {
        return map[key];
    }

    function keys_down_array(array)
    {
        for(var i = 0; i < array.length; i++)
            if(!key_down(array[i]))
                return false;

        return true;
    }

    function keys_down_arguments()
    {
        return keys_down_array(Array.from(arguments));
    }

    function clear()
    {
        map = {};
    }

    function watch_loop(keylist, callback)
    {
        return function(){
            if(keys_down_array(keylist))
                callback();
        }
    }

    function watch(name, callback)
    {
        var keylist = Array.from(arguments).splice(2);

        intervals[name] = setInterval(watch_loop(keylist, callback), 1000/24);
    }

    function unwatch(name)
    {
        clearInterval(intervals[name]);
        delete intervals[name];
    }

    function detach()
    {
        parent.removeEventListener("keydown", ev_kdown);
        parent.removeEventListener("keyup", ev_kup);
    }

    function attach()
    {
        parent.addEventListener("keydown", ev_kdown);
        parent.addEventListener("keyup", ev_kup);
    }

    function Input()
    {
        attach();

        return {
            key_down: key_down,
            keys_down: keys_down_arguments,
            watch: watch,
            unwatch: unwatch,
            clear: clear,
            detach: detach
        };
    }

    return Input();
}

Cette classe ne fait pas tout et elle ne gérera pas tous les cas d'utilisation imaginables. Je ne suis pas un gars de la bibliothèque. Mais pour une utilisation interactive générale, cela devrait aller.

Pour utiliser cette classe, créez une instance et pointez-la sur l'élément auquel vous souhaitez associer une entrée au clavier:

var input_txt = Input(document.getElementById("txt"));

input_txt.watch("print_5", function(){
    txt.value += "FIVE ";
}, "Control", "5");

Cela va attacher un nouvel écouteur d’entrée à l’élément avec #txt (supposons que c’est une zone de texte), et définir un point de contrôle pour le combo clé Ctrl+5. Lorsque Ctrl et 5 sont tous deux désactivés, la fonction de rappel que vous avez transmise (dans ce cas, une fonction qui ajoute "FIVE " à la zone de texte) sera appelée. Le rappel est associé au nom print_5, alors pour le supprimer, vous utilisez simplement:

input_txt.unwatch("print_5");

Pour détacher input_txt de l'élément txt:

input_txt.detach();

De cette façon, un ramasse-miettes peut récupérer l'objet (input_txt), s'il devait être jeté, et vous n'aurez plus aucun écouteur d'événements zombie.

Pour des raisons de minutie, voici une référence rapide à l'API de la classe, présentée en style C/Java pour que vous sachiez ce qu'elles retournent et quels arguments elles attendent.

Boolean  key_down (String key);

Retourne true si key est en panne, false sinon.

Boolean  keys_down (String key1, String key2, ...);

Retourne true si toutes les clés key1 .. keyN sont en panne, false sinon.

void     watch (String name, Function callback, String key1, String key2, ...);

Crée un "point de contrôle" tel que le fait d'appuyer sur keyN déclenchera le rappel

void     unwatch (String name);

Supprime ledit point de contrôle via son nom

void     clear (void);

Efface le cache "clés". Équivalent à map = {} ci-dessus

void     detach (void);

Détache les écouteurs ev_kdown et ev_kup de l'élément parent, ce qui permet de supprimer l'instance en toute sécurité

Mise à jour 2017-12-02 En réponse à une demande de publication sur github, j'ai créé un Gist .

Mise à jour 2018-07-21 Cela fait un moment que je joue à la programmation déclarative, et cette façon est maintenant ma préférée: violon , Pastebin

Généralement, cela fonctionnera avec les cas que vous souhaiteriez de façon réaliste (ctrl, alt, shift), mais si vous devez appuyer sur, disons, a+w en même temps, il ne sera pas trop difficile de "combiner" "les approches dans une recherche multi-clé.


J'espère Cela réponse bien expliquée mini-blog était utile :)

287
Braden Best

Vous devez utiliser l'événement keydown pour garder trace des touches enfoncées. et , vous devez utiliser les éléments événement keyup à suivre lorsque les touches sont relâchées.

Voir cet exemple: http://jsfiddle.net/vor0nwe/mkHsU/

(Mise à jour: je reproduis le code ici, au cas où jsfiddle.net échouerait :) Le code HTML:

<ul id="log">
    <li>List of keys:</li>
</ul>

... et le Javascript (avec jQuery):

var log = $('#log')[0],
    pressedKeys = [];

$(document.body).keydown(function (evt) {
    var li = pressedKeys[evt.keyCode];
    if (!li) {
        li = log.appendChild(document.createElement('li'));
        pressedKeys[evt.keyCode] = li;
    }
    $(li).text('Down: ' + evt.keyCode);
    $(li).removeClass('key-up');
});

$(document.body).keyup(function (evt) {
    var li = pressedKeys[evt.keyCode];
    if (!li) {
       li = log.appendChild(document.createElement('li'));
    }
    $(li).text('Up: ' + evt.keyCode);
    $(li).addClass('key-up');
});

Dans cet exemple, j’utilise un tableau pour savoir quelles touches sont enfoncées. Dans une application réelle, vous souhaiterez peut-être delete chaque élément une fois la clé associée libérée.

Notez que bien que jQuery ait été simplifié pour moi dans cet exemple, le concept fonctionne tout aussi bien lorsque vous utilisez du code Javascript "brut".

29
Martijn
document.onkeydown = keydown; 

function keydown (evt) { 

    if (!evt) evt = event; 

    if (evt.ctrlKey && evt.altKey && evt.keyCode === 115) {

        alert("CTRL+ALT+F4"); 

    } else if (evt.shiftKey && evt.keyCode === 9) { 

        alert("Shift+TAB");

    } 

}
13

J'ai utilisé cette méthode (il fallait vérifier où l'on appuyait sur Maj + Ctrl):

// create some object to save all pressed keys
var keys = {
    shift: false,
    ctrl: false
};

$(document.body).keydown(function(event) {
// save status of the button 'pressed' == 'true'
    if (event.keyCode == 16) {
        keys["shift"] = true;
    } else if (event.keyCode == 17) {
        keys["ctrl"] = true;
    }
    if (keys["shift"] && keys["ctrl"]) {
        $("#convert").trigger("click"); // or do anything else
    }
});

$(document.body).keyup(function(event) {
    // reset status of the button 'released' == 'false'
    if (event.keyCode == 16) {
        keys["shift"] = false;
    } else if (event.keyCode == 17) {
        keys["ctrl"] = false;
    }
});
6
Array

pour qui a besoin du code d'exemple complet. Right + Left ajouté

var keyPressed = {};
document.addEventListener('keydown', function(e) {

   keyPressed[e.key + e.location] = true;

    if(keyPressed.Shift1 == true && keyPressed.Control1 == true){
        // Left shift+CONTROL pressed!
        keyPressed = {}; // reset key map
    }
    if(keyPressed.Shift2 == true && keyPressed.Control2 == true){
        // Right shift+CONTROL pressed!
        keyPressed = {};
    }

}, false);

document.addEventListener('keyup', function(e) {
   keyPressed[e.key + e.location] = false;

   keyPressed = {};
}, false);
2
Reza Ramezanpour

Faites en sorte que le raccourci clavier appelle même plusieurs fonctions, chacune vérifiant une clé spécifique et y répondant correctement.

document.keydown = function (key) {

    checkKey("x");
    checkKey("y");
};
2
AnonymousGuest

Si l'une des touches appuyée est Alt/Ctrl/Shift, vous pouvez utiliser cette méthode:

document.body.addEventListener('keydown', keysDown(actions) );

function actions() {
   // do stuff here
}

// simultaneous pressing Alt + R
function keysDown (cb) {
  return function (zEvent) {
    if (zEvent.altKey &&  zEvent.code === "KeyR" ) {
      return cb()
    }
  }
}
1
Michael Lester

Je voudrais essayer d'ajouter un gestionnaire keypressEvent sur keydown. Par exemple:

window.onkeydown = function() {
    // evaluate key and call respective handler
    window.onkeypress = function() {
       // evaluate key and call respective handler
    }
}

window.onkeyup = function() {
    window.onkeypress = void(0) ;
}

Ceci est juste destiné à illustrer un modèle; Je n'entrerai pas dans les détails ici (surtout pas dans l'enregistrement du niveau2 + Event spécifique au navigateur).

Poster de retour s'il vous plaît si cela aide ou non.

0
FK82
    $(document).ready(function () {
        // using ascii 17 for ctrl, 18 for alt and 83 for "S"
        // ctr+alt+S
        var map = { 17: false, 18: false, 83: false };
        $(document).keyup(function (e) {
            if (e.keyCode in map) {
                map[e.keyCode] = true;
                if (map[17] && map[18] && map[83]) {
                    // Write your own code here, what  you want to do
                    map[17] = false;
                    map[18] = false;
                    map[83] = false;
                }
            }
            else {
                // if u press any other key apart from that "map" will reset.
                map[17] = false;
                map[18] = false;
                map[83] = false;
            }
        });

    });
0
Prosun Chakraborty
case 65: //A
jp = 1;
setTimeout("jp = 0;", 100);

if(pj > 0) {
ABFunction();
pj = 0;
}
break;

case 66: //B
pj = 1;
setTimeout("pj = 0;", 100);

if(jp > 0) {
ABFunction();
jp = 0;
}
break;

Pas le meilleur moyen, je sais.

0
Anonymous