¿Cómo detectar si se presionan varias teclas a la vez usando JavaScript?


173

Estoy tratando de desarrollar un motor de juego JavaScript y me encontré con este problema:

  • Cuando presiono SPACEel personaje salta.
  • Cuando presiono el personaje se mueve hacia la derecha.

El problema es que cuando presiono hacia la derecha y luego presiono la barra espaciadora, el personaje salta y luego deja de moverse.

Uso la keydownfunción para presionar la tecla. ¿Cómo puedo verificar si hay varias teclas presionadas a la vez?


3
Aquí hay una demostración de una página web que imprime automáticamente una lista de todas las teclas que se presionan: stackoverflow.com/a/13651016/975097
Anderson Green el

Respuestas:


327

Nota: keyCode ahora está en desuso.

La detección de pulsaciones múltiples es fácil si comprende el concepto

La forma en que lo hago es así:

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 */
}

Este código es muy simple: dado que la computadora solo pasa una pulsación de tecla a la vez, se crea una matriz para realizar un seguimiento de varias teclas. La matriz se puede usar para verificar una o más claves a la vez.

Solo para explicar, digamos que presiona Ay B, cada uno dispara un keydownevento que se establece map[e.keyCode]en el valor de e.type == keydown, que se evalúa como verdadero o falso . Ahora ambos map[65]y map[66]están configurados en true. Cuando lo suelta A, el keyupevento se dispara, causando que la misma lógica determine el resultado opuesto para map[65](A), que ahora es falso , pero dado que map[66](B) todavía está "inactivo" (no ha activado un evento de keyup), Sigue siendo cierto .

La mapmatriz, a través de ambos eventos, se ve así:

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

Hay dos cosas que puedes hacer ahora:

A) Se puede crear un registrador de claves ( ejemplo ) como referencia para más adelante cuando desee descubrir rápidamente uno o más códigos clave. Suponiendo que haya definido un elemento html y lo haya señalado con la variable element.

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

Nota: Puede agarrar fácilmente un elemento por su idatributo.

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

Esto crea un elemento html al que se puede hacer referencia fácilmente en javascript con element

alert(element); // [Object HTMLDivElement]

Ni siquiera tiene que usarlo document.getElementById()o $()agarrarlo. Pero en aras de la compatibilidad, el uso de jQuery $()es más ampliamente recomendado.

Solo asegúrese de que la etiqueta del script venga después del cuerpo del HTML. Consejo de optimización : la mayoría de los sitios web de renombre colocan la etiqueta del script después de la etiqueta del cuerpo para la optimización. Esto se debe a que la etiqueta del script bloquea la carga de elementos adicionales hasta que el script termina de descargarse. Ponerlo por delante del contenido permite que el contenido se cargue de antemano.

B (que es donde radica su interés) Puede verificar una o más claves a la vez donde /*insert conditional here*/estaba, tome este ejemplo:

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');
}

Editar : ese no es el fragmento más legible. La legibilidad es importante, por lo que podría intentar algo como esto para que sea más fácil para la vista:

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;
}

Uso:

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

¿Es esto mejor?

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 la edición)


Este ejemplo cheques por CtrlShiftA, CtrlShiftByCtrlShiftC

Es tan simple como eso :)

Notas

Seguimiento de KeyCodes

Como regla general, es una buena práctica documentar el código, especialmente cosas como los códigos clave (como // CTRL+ENTER) para que pueda recordar cuáles eran.

También debe colocar los códigos clave en el mismo orden que la documentación ( CTRL+ENTER => map[17] && map[13], NO map[13] && map[17]). De esta manera, nunca se confundirá cuando necesite volver y editar el código.

Un gotcha con cadenas if-else

Si busca combinaciones de diferentes cantidades (como CtrlShiftAltEntery CtrlEnter), coloque combinaciones más pequeñas después de combinaciones más grandes, de lo contrario, las combinaciones más pequeñas anularán las combinaciones más grandes si son lo suficientemente similares. Ejemplo:

// 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: "Este combo de teclas se sigue activando aunque no presione las teclas"

Cuando se trata de alertas o cualquier cosa que se enfoca desde la ventana principal, es posible que desee incluir map = []para restablecer la matriz después de que se cumpla la condición. Esto se debe a que algunas cosas, comoalert() , alejan el foco de la ventana principal y hacen que el evento 'keyup' no se active. Por ejemplo:

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: valores predeterminados del navegador

Aquí hay una cosa molesta que encontré, con la solución incluida:

Problema: dado que el navegador generalmente tiene acciones predeterminadas en combinaciones de teclas (como CtrlDactiva la ventana de marcadores o CtrlShiftCactiva skynote en maxthon), es posible que también desee agregar return falsedespués map = [], para que los usuarios de su sitio no se sientan frustrados cuando el "Duplicar archivo" la función, que se pone CtrlD, marca la página como marcador

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

Sin return false, la ventana Marca haría pop-up, para el disgusto del usuario.

La declaración de devolución (nuevo)

Bien, entonces no siempre quieres salir de la función en ese punto. Es por eso que la event.preventDefault()función está ahí. Lo que hace es establecer un indicador interno que le dice al intérprete que no permita que el navegador ejecute su acción predeterminada. Después de eso, la ejecución de la función continúa (mientras returnque inmediatamente saldrá de la función).

Comprenda esta distinción antes de decidir si usar return falseoe.preventDefault()

event.keyCode es obsoleto

El usuario SeanVieira señaló en los comentarios que event.keyCodeestá en desuso.

Allí, dio una excelente alternativa: event.keyque devuelve una representación en cadena de la tecla que se está presionando, como "a"for Ao "Shift"for Shift.

Seguí adelante y preparé una herramienta para examinar dichas cadenas.

element.onevent vs element.addEventListener

Los controladores registrados con addEventListenerse pueden apilar y se llaman en el orden de registro, mientras que la configuración .oneventdirecta es bastante agresiva y anula todo lo que tenía anteriormente.

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 .oneventpropiedad parece anular todo y el comportamiento de ev.preventDefault()y return false;puede ser bastante impredecible.

En cualquier caso, los controladores registrados a través de addEventlistenerparecen ser más fáciles de escribir y razonar.

También existe attachEvent("onevent", callback)una implementación no estándar de Internet Explorer, pero esto está más allá de obsoleto y ni siquiera pertenece a JavaScript (pertenece a un lenguaje esotérico llamado JScript ). Le conviene evitar el código políglota tanto como sea posible.

Una clase auxiliar

Para abordar la confusión / quejas, he escrito una "clase" que hace esta abstracción ( enlace de 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();
}

Esta clase no hace todo y no manejará todos los casos de uso imaginables. No soy un chico de la biblioteca. Pero para uso interactivo general, debería estar bien.

Para usar esta clase, cree una instancia y apúntela al elemento con el que desea asociar la entrada del teclado:

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

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

Lo que esto hará es adjuntar un nuevo oyente de entrada al elemento con #txt(supongamos que es un área de texto) y establecer un punto de observación para el combo de teclas Ctrl+5. Cuando ambos Ctrly 5están abajo, la función de devolución de llamada que ha pasado en (en este caso, una función que añade "FIVE "al área de texto) se llamará. La devolución de llamada está asociada con el nombre print_5, por lo que para eliminarlo, simplemente use:

input_txt.unwatch("print_5");

Para separarse input_txtdel txtelemento:

input_txt.detach();

De esta manera, la recolección de basura puede recoger el objeto ( input_txt), en caso de que se deseche, y no quedará un viejo oyente de eventos zombie.

Para mayor detalle, aquí hay una referencia rápida a la API de la clase, presentada en estilo C / Java para que sepa qué devuelven y qué argumentos esperan.

Boolean  key_down (String key);

Devuelve truesi keyestá abajo, falso de lo contrario.

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

Devuelve truesi todas las teclas key1 .. keyNestán abajo, falso de lo contrario.

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

Crea un "punto de observación" tal que presionar todo keyNactivará la devolución de llamada

void     unwatch (String name);

Elimina dicho punto de observación a través de su nombre.

void     clear (void);

Limpia el caché de "teclas abajo". Equivalente a lo map = {}anterior

void     detach (void);

Separa el ev_kdowny ev_kupoyentes del elemento padre, por lo que es posible deshacerse de manera segura de la instancia

Actualización 2017-12-02 En respuesta a una solicitud para publicar esto en github, he creado una esencia .

Actualización 2018-07-21 He estado jugando con programación de estilo declarativo durante un tiempo, y de esta manera ahora es mi favorito personal: violín , pastebin

En general, funcionará con los casos que realmente desea (ctrl, alt, shift), pero si necesita golpear, digamos, a+wal mismo tiempo, no sería demasiado difícil "combinar" los enfoques en un Búsqueda de teclas múltiples.


Espero que este mini-blog explicado a fondo haya sido útil :)


¡Acabo de hacer una gran actualización a esta respuesta! El ejemplo del keylogger es más coherente, actualicé el formato para que la sección de "notas" fuera más fácil de leer, y agregué una nueva nota sobre return falsevspreventDefault()
Braden Best

¿Qué sucede cuando presiona / mantiene presionada una tecla con el documento en foco, luego hace clic en el cuadro URL y luego suelta la tecla? keyup nunca se activa, pero la clave está activada, lo que hace que la lista sea incorrecta. También viceversa: presionar / mantener presionada la tecla en el cuadro de URL, nunca se activa el keydown, luego se enfoca en el documento y el estado del keydown no está en la lista. Básicamente, cuando el documento recupera el enfoque, nunca puede estar seguro del estado de la clave.
user3015682

3
NB: keyCodeestá en desuso: si cambia a key, obtendrá la representación de caracteres real de la clave, lo que puede ser agradable.
Sean Vieira

1
@SeanVieira Por otra parte, también puedes hacer cosas extrañas en C. Por ejemplo, ¿sabía que myString[5]es lo mismo 5[myString]y ni siquiera le dará una advertencia de compilación (incluso con -Wall -pedantic)? Se debe a que la pointer[offset]notación toma el puntero, agrega el desplazamiento y luego desreferencia el resultado, haciendo myString[5]lo mismo que *(myString + 5).
Braden Best

1
@inorganik ¿te refieres a la clase auxiliar? ¿Se pueden usar las esencias como repositorios? Sería tedioso hacer un repositorio completo para un pequeño fragmento de código. Claro, voy a hacer una idea. Dispararé por esta noche. Midnight mountain Time -ish
Braden Mejor

30

Debe usar el evento keydown para realizar un seguimiento de las teclas presionadas, y debe usar el evento keyup para realizar un seguimiento de cuándo se sueltan las teclas.

Vea este ejemplo: http://jsfiddle.net/vor0nwe/mkHsU/

(Actualización: estoy reproduciendo el código aquí, en caso de que jsfiddle.net salga :) El HTML:

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

... y el Javascript (usando 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');
});

En ese ejemplo, estoy usando una matriz para realizar un seguimiento de las teclas que se presionan. En una aplicación real, es posible que deseedelete cada elemento una vez que se haya liberado su clave asociada.

Tenga en cuenta que si bien he usado jQuery para facilitarme las cosas en este ejemplo, el concepto funciona igual de bien cuando se trabaja en Javascript 'sin procesar'.


Pero como he pensado, hay un error. Si mantiene presionado un botón, cambie a otra pestaña (o suelte el foco) mientras mantiene presionado el botón cuando vuelva a enfocar el cursor, esto mostrará que el botón está presionado, incluso si no lo está. : D
XCS

3
@Cristy: entonces también podría agregar un onblurcontrolador de eventos, que elimina todas las teclas presionadas de la matriz. Una vez que haya perdido el foco, tendría sentido tener que presionar todas las teclas nuevamente. Desafortunadamente, no hay JS equivalente a GetKeyboardState.
Martijn

1
Tener un problema con Pegar en una Mac (Chrome). Obtiene exitosamente keydown 91 (comando), keydown 86 (v), pero luego solo keyups 91, dejando 86 abajo. Lista de teclas: Arriba: 91, Abajo: 86. Esto solo parece suceder cuando suelto la tecla de comando en segundo lugar; si la suelto primero, registrará correctamente la combinación de teclas en ambos.
James Alday

2
Parece que cuando presiona tres o más teclas a la vez, deja de detectar más teclas hasta que levanta una. (Probado con Firefox 22)
Qvcool

1
@JamesAlday Mismo problema. Aparentemente solo afecta la tecla Meta (OS) en Mac. Vea el número 3 aquí: bitspushedaround.com/…
Don McCurdy

20
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");

    } 

}

1
Esto fue todo lo que quería, la mejor respuesta
Randall Coding

7

Lo utilicé de esta manera (tenía que verificar dónde se presiona Shift + 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;
    }
});

5

para quien necesita un código de ejemplo completo. Derecha + Izquierda agregada

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);

3

Haga que el keydown incluso llame a múltiples funciones, cada función verificando una tecla específica y respondiendo adecuadamente.

document.keydown = function (key) {

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

2

Intentaría agregar un keypress Eventcontrolador sobre keydown. P.ej:

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) ;
}

Esto solo pretende ilustrar un patrón; No entraré en detalles aquí (especialmente no en el Eventregistro específico de level2 + del navegador ).

Publicar de nuevo por favor si esto ayuda o no.


1
Esto no funcionaría: pulsación de tecla no se activa en una gran cantidad de llaves que keydown y keyUp hacer gatillo. Además, no todos los navegadores activan repetidamente eventos keydown.
Martijn

Quirksmode dice que estás equivocado: quirksmode.org/dom/events/keys.html . Pero no discutiré eso ya que no probé mi propuesta.
FK82

Citado de esa página: "Cuando el usuario presiona teclas especiales como las teclas de flecha, el navegador NO debe disparar eventos de pulsación de teclas" . En cuanto a las repeticiones, enumera que Opera y Konqueror no lo hacen correctamente.
Martijn

2

Si una de las teclas presionadas es Alt / Crtl / Shift, puede usar este método:

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()
    }
  }
}

2
    $(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;
            }
        });

    });

Gracias por tu aporte. intente no solo publicar el código, agregue alguna explicación.
Tim Rutter

2

Este no es un método universal, pero es útil en algunos casos. Es útil para combinaciones como CTRL+ somethingo Shift+ somethingo CTRL+ Shift+ something, etc.

Ejemplo: cuando desea imprimir una página con CTRL+ P, la primera tecla presionada siempre va CTRLseguida de P. Lo mismo con CTRL+ S, CTRL+ Uy otras combinaciones.

document.addEventListener('keydown',function(e){
      
    //SHIFT + something
    if(e.shiftKey){
        switch(e.code){

            case 'KeyS':
                console.log('Shift + S');
                break;

        }
    }

    //CTRL + SHIFT + something
    if(e.ctrlKey && e.shiftKey){
        switch(e.code){

            case 'KeyS':
                console.log('CTRL + Shift + S');
                break;

        }
    }

});


1
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;

No es la mejor manera, lo sé.


-1
Easiest, and most Effective Method

//check key press
    function loop(){
        //>>key<< can be any string representing a letter eg: "a", "b", "ctrl",
        if(map[*key*]==true){
         //do something
        }
        //multiple keys
        if(map["x"]==true&&map["ctrl"]==true){
         console.log("x, and ctrl are being held down together")
        }
    }

//>>>variable which will hold all key information<<
    var map={}

//Key Event Listeners
    window.addEventListener("keydown", btnd, true);
    window.addEventListener("keyup", btnu, true);

    //Handle button down
      function btnd(e) {
      map[e.key] = true;
      }

    //Handle Button up
      function btnu(e) {
      map[e.key] = false;
      }

//>>>If you want to see the state of every Key on the Keybaord<<<
    setInterval(() => {
                for (var x in map) {
                    log += "|" + x + "=" + map[x];
                }
                console.log(log);
                log = "";
            }, 300);
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.