Nota: keyCode agora está obsoleto.
A detecção de várias teclas é fácil se você entender o conceito
O jeito que eu faço é assim:
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 */
}
Esse código é muito simples: como o computador passa apenas um pressionamento de tecla de cada vez, é criada uma matriz para controlar várias chaves. A matriz pode ser usada para verificar uma ou mais chaves ao mesmo tempo.
Apenas para explicar, digamos que você pressione Ae B, cada um dispara um keydown
evento que define map[e.keyCode]
o valor de e.type == keydown
, que é avaliado como verdadeiro ou falso . Agora ambos map[65]
e map[66]
estão definidos como true
. Quando você solta A
, o keyup
evento é acionado, causando a mesma lógica para determinar o resultado oposto para map[65]
(A), que agora é falso , mas como map[66]
(B) ainda está "inativo" (não disparou um evento de keyup), permanece verdadeiro .
A map
matriz, através dos dois eventos, fica assim:
// keydown A
// keydown B
[
65:true,
66:true
]
// keyup A
// keydown B
[
65:false,
66:true
]
Há duas coisas que você pode fazer agora:
A) Um registrador de chaves ( exemplo ) pode ser criado como referência para mais tarde, quando você quiser descobrir rapidamente um ou mais códigos de chave. Supondo que você tenha definido um elemento html e apontou para ele com a variável element
.
element.innerHTML = '';
var i, l = map.length;
for(i = 0; i < l; i ++){
if(map[i]){
element.innerHTML += '<hr>' + i;
}
}
Nota: Você pode facilmente pegar um elemento por seu id
atributo.
<div id="element"></div>
Isso cria um elemento html que pode ser facilmente referenciado em javascript com element
alert(element); // [Object HTMLDivElement]
Você nem precisa usar document.getElementById()
ou $()
agarrar. Mas, por uma questão de compatibilidade, o uso de jQuery$()
é mais amplamente recomendado.
Apenas verifique se a tag do script vem após o corpo do HTML. Dica de otimização : a maioria dos sites de grandes nomes coloca a tag de script após a tag body para otimização. Isso ocorre porque a tag de script impede que outros elementos sejam carregados até o término do download do script. Colocá-lo à frente do conteúdo permite que o conteúdo seja carregado com antecedência.
B (onde é o seu interesse) Você pode procurar uma ou mais chaves por vez, onde /*insert conditional here*/
está, veja este exemplo:
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 : esse não é o trecho mais legível. A legibilidade é importante, então você pode tentar algo assim para facilitar a visão:
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')
Isso é melhor?
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');
}
}
(fim da edição)
Este exemplo verifica para CtrlShiftA, CtrlShiftBeCtrlShiftC
É tão simples assim :)
Notas
Acompanhamento dos códigos-chave
Como regra geral, é uma boa prática documentar código, especialmente coisas como códigos-chave (como // CTRL+ENTER
), para que você possa se lembrar do que eram.
Você também deve colocar os códigos de chave na mesma ordem que a documentação ( CTRL+ENTER => map[17] && map[13]
, NÃOmap[13] && map[17]
). Dessa forma, você nunca ficará confuso quando precisar voltar e editar o código.
Uma pegadinha com cadeias if-else
Se verificar combos de quantidades diferentes (como CtrlShiftAltEntere CtrlEnter), coloque combos menores depois de combos maiores, caso contrário os combos menores substituirão os combos maiores se forem semelhantes o suficiente. Exemplo:
// 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: "Essa combinação de teclas continua sendo ativada mesmo que eu não esteja pressionando as teclas"
Ao lidar com alertas ou qualquer coisa que tenha foco na janela principal, convém incluir map = []
a redefinição da matriz após a conclusão da condição. Isso ocorre porque algumas coisas alert()
afastam o foco da janela principal e fazem com que o evento 'keyup' não seja acionado. Por exemplo:
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: Padrões do navegador
Aqui está uma coisa chata que eu achei, com a solução incluída:
Problema: como o navegador geralmente possui ações padrão em combinações de teclas (como CtrlDativa a janela de favoritos ou CtrlShiftCskynote no maxthon), convém adicionar return false
depois map = []
, para que os usuários do seu site não fiquem frustrados quando o "Arquivo duplicado" função, sendo colocadoCtrlD , marca a página.
if(map[17] && map[68]){ // CTRL+D
alert('The bookmark window didn\'t pop up!');
map = {};
return false;
}
Sem return false
, a janela do marcador seria exibida, para desgosto do usuário.
A declaração de retorno (nova)
Ok, então você nem sempre deseja sair da função nesse ponto. É por isso que a event.preventDefault()
função está lá. O que ele faz é definir um sinalizador interno que instrua o intérprete a não permitir que o navegador execute sua ação padrão. Depois disso, a execução da função continua (enquantoreturn
sairá imediatamente da função).
Entenda essa distinção antes de decidir se deve usar return false
ou nãoe.preventDefault()
event.keyCode
está obsoleto
O usuário SeanVieira apontou nos comentários que foram event.keyCode
preteridos.
Lá, ele ofereceu uma excelente alternativa event.key
:, que retorna uma representação em cadeia da tecla que está sendo pressionada, como "a"
para Aou "Shift"
paraShift .
Fui em frente e cozinhei uma ferramenta para examinar as ditas cordas.
element.onevent
vs element.addEventListener
Os manipuladores registrados com addEventListener
podem ser empilhados e são chamados na ordem do registro, enquanto a configuração .onevent
diretamente é bastante agressiva e substitui tudo o que você tinha 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
});
A .onevent
propriedade parece substituir tudo e o comportamento de ev.preventDefault()
ereturn false;
pode ser bastante imprevisível.
Nos dois casos, os manipuladores registrados via addEventlistener
parecem ser mais fáceis de escrever e raciocinar.
Também existe attachEvent("onevent", callback)
na implementação fora do padrão do Internet Explorer, mas isso está além de obsoleto e nem pertence ao JavaScript (refere-se a uma linguagem esotérica chamada JScript ). Seria do seu interesse evitar o máximo possível o código poliglota.
Uma classe auxiliar
Para resolver confusão / reclamações, escrevi uma "classe" que faz essa abstração ( link 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();
}
Essa classe não faz tudo e não lida com todos os casos de uso concebíveis. Eu não sou um cara da biblioteca. Mas, para uso interativo geral, tudo bem.
Para usar essa classe, crie uma instância e aponte para o elemento ao qual você deseja associar a entrada do teclado:
var input_txt = Input(document.getElementById("txt"));
input_txt.watch("print_5", function(){
txt.value += "FIVE ";
}, "Control", "5");
O que isso fará é anexar um novo ouvinte de entrada ao elemento #txt
(vamos supor que seja uma área de texto) e definir um ponto de controle para a combinação de teclas Ctrl+5
. Quando ambos Ctrl
e 5
estão inativos, a função de retorno de chamada que você passou (nesse caso, uma função que adiciona "FIVE "
à área de texto) será chamada. O retorno de chamada está associado ao nome print_5
; portanto, para removê-lo, basta usar:
input_txt.unwatch("print_5");
Para desanexar input_txt
do txt
elemento:
input_txt.detach();
Dessa forma, a coleta de lixo pode pegar o objeto ( input_txt
), caso ele seja jogado fora, e você não terá um ouvinte de evento zumbi antigo sobrando.
Para mais detalhes, aqui está uma referência rápida à API da classe, apresentada no estilo C / Java para que você saiba o que eles retornam e quais argumentos eles esperam.
Boolean key_down (String key);
Retorna true
se key
estiver inativo, caso contrário, false.
Boolean keys_down (String key1, String key2, ...);
Retorna true
se todas as chaves key1 .. keyN
estiverem inativas , caso contrário, false.
void watch (String name, Function callback, String key1, String key2, ...);
Cria um "ponto de controle", de modo que pressionar todos keyN
acionará o retorno de chamada
void unwatch (String name);
Remove o referido watchpoint pelo nome
void clear (void);
Limpa o cache "keys down". Equivalente a map = {}
acima
void detach (void);
Desanexa os ouvintes ev_kdown
e ev_kup
do elemento pai, tornando possível se livrar da instância com segurança
Atualização 2017-12-02 Em resposta a uma solicitação para publicá-la no github, criei uma essência .
Atualização 2018-07-21 Eu jogo com programação de estilo declarativo há um tempo e agora é o meu favorito: fiddle , pastebin
Geralmente, ele funciona com os casos que você deseja realisticamente (ctrl, alt, shift), mas se precisar pressionar, digamos, a+w
ao mesmo tempo, não seria muito difícil "combinar" as abordagens em um pesquisa com várias teclas.
Espero que esta resposta minimamente explicada seja útil :)