Você tem o poder sobre sua mente e não sobre eventos externos. Perceba isso e você vai encontrar resistência.
Marcus Aurelius, Meditations
Alguns programas funcionam com entradas direta do usuário, tais como a interação de mouse e teclado. O tempo e a ordem de tal entrada não pode ser previsto com antecedência. Isso requer uma abordagem diferente para controlar o fluxo do que utilizamos até agora.
Imaginem uma interface onde a única maneira de descobrir se uma tecla está sendo pressionada é ler o estado atual dessa chave. Para ser capaz de reagir às pressões de teclas você teria que ler constantemente o estado da chave para pegá-lo antes que ele fique liberado novamente. Seria perigoso executar outros cálculos demoradas pois você pode perder uma tecla.
É assim que tal atributo foi tratado em máquinas primitivas. A um passo para o hardware o sistema operacional deve notificar qual a tecla pressionada e colocá-lo em uma fila. Um programa pode então, verificar periodicamente a fila para novos eventos e reagir para o que se encontra lá.
É claro que ele tem sempre de olhar para a fila, e executa-las muitas vezes, porque a qualquer momento entre a teclas que está sendo pressionado o programa vai perceber que o evento fará com que o software sinta-se que não esta tendo resposta. Esta abordagem é chamada de polling
. A maioria dos programadores tenta evitar sempre que possível.
A melhor mecanismo para o sistema subjacente é dar ao nosso código a chance de reagir a eventos que ocorrerem. Os browsers podem fazerem isto por que nos permite registrar funções como manipuladores para eventos específicos.
<p>Click this document to activate the handler.</p>
<script>
addEventListener("click", function() {
console.log("You clicked!");
});
</script>
A função addEventListener
registra seu segundo argumento a ser chamado sempre que o evento descrito por seu primeiro argumento ocorre.
Cada navegador tem seu manipulador de eventos registrado em um contexto. Quando você chamar addEventListener
como mostramos anteriormente você estara chamando um método em todo window
, no navegador o escopo global é equivalente ao objeto window
. Cada elemento DOM tem seu próprio método addEventListener
que permite ouvir eventos especificamente para cada elemento.
<button>Click me</button>
<p>No handler here.</p>
<script>
var button = document.querySelector("button");
button.addEventListener("click", function() {
console.log("Button clicked.");
});
</script>
O exemplo atribuiu um manipulador para um nó de botão. Assim quando existir um clique no botão o manipulador sera executado, enquanto no resto do documento não.
Dar um nó um atributo onclick tem um efeito similar. Mas um nó tem apenas um atributo onclick, para que você possa registrar apenas um manipulador por nó dessa forma. O método addEventListener
permite que você adicione qualquer número de manipuladores, para que você não substitua acidentalmente um manipulador que já foi registrado.
O método removeEventListener
quando chamado com argumentos semelhantes assim como o addEventListener
remove um manipulador.
<button>Act-once button</button>
<script>
var button = document.querySelector("button");
function once() {
console.log("Done.");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>
Para ser capaz de cancelar um registro manipulador de uma função, precisamos dar-lhe um nome para que possamos utilizar tanto para addEventListener
quanto para removeEventListener
.
Embora tenhamos ignodo os exemplos anteriores, as funções manipuladoras de eventos são passados via argumento, chamamos de objeto de evento. Este objeto nos dá informações adicionais sobre o evento. Por exemplo, se queremos saber qual botão do mouse que foi pressionado podemos observar as propriedades do objeto de evento.
<button>Click me any way you want</button>
<script>
var button = document.querySelector("button");
button.addEventListener("mousedown", function(event) {
if (event.which == 1)
console.log("Left button");
else if (event.which == 2)
console.log("Middle button");
else if (event.which == 3)
console.log("Right button");
});
</script>
As informações armazenadas em um objeto de evento são diferentes dependendo do tipo de evento. Vamos discutir vários tipos mais adiante neste capítulo. Propriedade de tipo do objeto sempre detém uma cadeia que identifica o evento(por exemplo, "clique" ou "mousedown").
Os manipuladores de eventos registrados em nós com seus filhos também receberão alguns eventos que ocorrem nos filhos. Se um botão dentro de um parágrafo é clicado, manipuladores de eventos no parágrafo também vai receber o evento clique.
Mas se tanto o parágrafo e o botão tem um manipulador, o manipulador mais específico é o do botão e sera chamado primeiro. O evento foi feito para propagar para o exterior, a partir do nó onde aconteceu ate o nó pai do nó raiz do documento. Finalmente, depois de todos os manipuladores registrados em um nó específico tiveram sua vez, manipuladores registrados em toda a janela tem a chance de responder ao evento.
A qualquer momento um manipulador de eventos pode chamar o método stopPropagation
no objeto de evento para evitar que os manipuladores "mais acima" possam receberem o evento. Isso pode ser útil quando, por exemplo, se você tem um botão dentro de outro elemento clicável e você não quer o clique no botão aconteça se houver algum compartamento de clique no elemento exterior.
O exemplo a seguir registra manipuladores "mousedown" em ambos um botão e do parágrafo em torno dele. Quando clicado com o botão direito do mouse , o manipulador do botão chama stopPropagation
, o que impedirá o manipulador no parágrafo de execução. Quando o botão é clicado com outro botão do mouse os dois manipuladores seram executados.
<p>A paragraph with a <button>button</button>.</p>
<script>
var para = document.querySelector("p");
var button = document.querySelector("button");
para.addEventListener("mousedown", function() {
console.log("Handler for paragraph.");
});
button.addEventListener("mousedown", function(event) {
console.log("Handler for button.");
if (event.which == 3)
event.stopPropagation();
});
</script>
A maioria dos objetos de evento tem uma propriedade de destino que se refere ao nó onde eles se originaram. Você pode usar essa propriedade para garantir que você não está lidando com algo que acidentalmente propagou a partir de um nó que você não queira lidar.
Também é possível usar uma propriedade de destino para lançar uma ampla rede para um tipo específico de evento. Por exemplo, se você tem um nó que contém uma longa lista de botões, pode ser mais conveniente registrar um único manipulador de clique para o nó do exterior e que ele use a propriedade de destino para descobrir se um botão foi clicado, ao invés de se registrar manipuladores individuais sobre todos os botões.
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", function(event) {
if (event.target.nodeName == "BUTTON")
console.log("Clicked", event.target.textContent);
});
</script>
Muitos eventos têm sua ação padrão que lhes estão associados. Se você clicar em um link, você será levado para outra página. Se você pressionar a seta para baixo, o navegador vai rolar a página para baixo. Se você clicar com o botão direito, você tera um menu. E assim por diante.
Para a maioria dos tipos de eventos, os manipuladores de eventos de JavaScript são chamados antes do comportamento padrão. Se o condutor não quer que o comportamento normal aconteça, pode chamar o método preventDefault
no objeto de evento.
Isso pode ser usado para implementar seus próprios atalhos de teclado ou menus. Ele também pode ser utilizado para interferir com o comportamento desagradavelmente que os utilizadores esperaram. Por exemplo, aqui está um link que não podem ser clicável:
<a href="https://developer.mozilla.org/">MDN</a>
<script>
var link = document.querySelector("a");
link.addEventListener("click", function(event) {
console.log("Nope.");
event.preventDefault();
});
</script>
Tente não fazer tais coisas, a menos que você tem uma boa razão para isso. Para as pessoas que usam sua página isso pode ser desagradável quando o comportamento que eles esperam são quebrados.
Dependendo do navegador , alguns eventos não podem ser interceptados. No Chrome, por exemplo, os atalhos de teclado para fechar a aba atual (Ctrl- W ou Command-W) não pode ser manipulado por JavaScript.
Quando uma tecla do teclado é pressionado, o seu browser dispara um evento "keydown"
. Quando ele é liberado, um evento de "keyup"
é emitido.
<p>This page turns violet when you hold the V key.</p>
<script>
addEventListener("keydown", function(event) {
if (event.keyCode == 86)
document.body.style.background = "violet";
});
addEventListener("keyup", function(event) {
if (event.keyCode == 86)
document.body.style.background = "";
});
</script>
Apesar do nome "keydown"
é acionado, não só quando a tecla é empurrada para baixo fisicamente.
Quando uma tecla é pressionada e mantida, o evento é disparado novamente toda vez que se repete a tecla. Às vezes, por exemplo, se você quiser aumentar a aceleração de um personagem do jogo, quando uma tecla de seta é pressionado é diminuido somente quando a tecla é liberada, você tem que ter cuidado para não aumentá-lo novamente toda vez que se repete a tecla ou vai acabar com os valores involuntariamente enormes.
O exemplo anterior nos atentou para a propriedade keyCode
do objeto de evento. Isto é como você pode identificar qual tecla está sendo pressionada ou solta. Infelizmente, não é sempre óbvio traduzir o código numérico para uma tecla.
Para as teclas de letras e números, o código da tecla associado será o código de caracteres Unicode associado as letras maiúsculas ou número impresso na tecla. O método charCodeAt
em String
nos dá uma maneira de encontrar este código.
console.log("Violet".charCodeAt(0));
// → 86
console.log("1".charCodeAt(0));
// → 49
Outras teclas têm códigos previsíveis. A melhor maneira de encontrar os códigos que você precisa é geralmente experimentar o registo de um manipulador de eventos de tecla que registra os códigos de chave que ela recebe quando pressionado a tecla que você está interessado.
Teclas modificadoras como Shift, Ctrl, Alt e Command(no Mac) geram eventos de teclas apenas como teclas normais. Mas quando se olha para as combinações de teclas, você também pode descobrir se essas teclas são pressionadas por olhar para o shiftKey
, propriedades ctrlKey
, altKey
e metakey
de eventos de teclado e mouse.
<p>Press Ctrl-Space to continue.</p>
<script>
addEventListener("keydown", function(event) {
if (event.keyCode == 32 && event.ctrlKey)
console.log("Continuing!");
});
</script>
Os "KeyDown"
e eventos "KeyUp"
dão informações sobre a tecla física que está sendo pressionado. Mas e se você está interessado no próprio texto que está sendo digitado?
Conseguir o texto a partir de códigos de tecla é estranho.
Em vez disso, existe um outro evento, "keypress"
, que é acionado logo após "keydown"
(repetido junto com "keydown"
quando a tecla é solta), mas apenas para as teclas que produzem entrada de caracteres. A propriedade charCode
no objeto do evento contém um código que pode ser interpretado como um código de caracteres Unicode
. Podemos usar a função String.fromCharCode
para transformar esse código em uma verdadeira cadeia de caracteres simples.
<p>Focus this page and type something.</p>
<script>
addEventListener("keypress", function(event) {
console.log(String.fromCharCode(event.charCode));
});
</script>
O nó DOM
, onde um evento de tecla se origina depende do elemento que tem o foco quando a tecla for pressionada. Nós normais não podem ter o foco(a menos que você de um atributo tabindex
), mas podem as coisas como links, botões e campos de formulário. Voltaremos para formar campos no Capítulo 18. Quando nada em particular tem foco, document.body
é o um dos principais eventos dos destinos principais.
Pressionar o botão do mouse também provoca uma série de eventos para ser emitido. O "mousedown" e "mouseup" são semelhantes aos "keydown" e "keyup" e são acionados quando o botão é pressionado e liberado. Estes irão acontecer no DOM que estão abaixo do ponteiro do mouse quando o evento ocorrer.
Após o evento "mouseup", um evento "click" é acionado no nó mais específico que continha tanto ao pressionar e liberar o botão. Por exemplo, se eu pressionar o botão do mouse em um parágrafo e, em seguida, movo o ponteiro para outro parágrafo e solto o botão, o evento de "click" acontecerá no elemento que contém esses dois parágrafos.
Se dois cliques acontecem juntos, um "dblclick"(clique duplo) evento é emitido também após o segundo evento de clique.
Para obter informações precisas sobre o local onde aconteceu um evento do mouse, você pode olhar para as suas propriedades pageX
e pageY
, que contêm as coordenadas do evento(em pixels) em relação ao canto superior esquerdo do documento.
A seguir veja a implementação de um programa de desenho primitivo. Toda vez que você clique no documento ele acrescenta um ponto sob o ponteiro do mouse. Veja o Capítulo 19 um programa de desenho menos primitivo.
<style>
body {
height: 200px;
background: beige;
}
.dot {
height: 8px; width: 8px;
border-radius: 4px; /* rounds corners */
background: blue;
position: absolute;
}
</style>
<script>
addEventListener("click", function(event) {
var dot = document.createElement("div");
dot.className = "dot";
dot.style.left = (event.pageX - 4) + "px";
dot.style.top = (event.pageY - 4) + "px";
document.body.appendChild(dot);
});
</script>
As propriedades clientX
e clientY
são semelhantes aos pageX
e pageY
mas em relação à parte do documento que está sendo rolado. Estes podem ser úteis quando se compara a coordena do mouse com as coordenadas retornados por getBoundingClientRect
que também retorna coordenadas relativas da viewport
.
Toda vez que o ponteiro do mouse se move, um eventos de "mousemove" é disparado. Este evento pode ser usado para controlar a posição do mouse. Uma situação comum em que isso é útil é ao implementar algum tipo de funcionalidade de arrastar o mouse.
Como exemplo, o seguinte programa exibe uma barra e configura os manipuladores de eventos para que ao arrastar para a esquerda ou direita a barra se torna mais estreita ou mais ampla:
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
var lastX; // Tracks the last observed mouse X position
var rect = document.querySelector("div");
rect.addEventListener("mousedown", function(event) {
if (event.which == 1) {
lastX = event.pageX;
addEventListener("mousemove", moved);
event.preventDefault(); // Prevent selection
}
});
function moved(event) {
if (event.which != 1) {
removeEventListener("mousemove", moved);
} else {
var dist = event.pageX - lastX;
var newWidth = Math.max(10, rect.offsetWidth + dist);
rect.style.width = newWidth + "px";
lastX = event.pageX;
}
}
</script>
Note que o controlador "mousemove" é registrado em toda a janela. Mesmo que o mouse vai para fora da barra durante o redimensionamento, nós ainda queremos atualizar seu tamanho e parar de arrastar quando o mouse é liberado.
Sempre que o ponteiro do mouse entra ou sai de um nó, um "mouseover" ou "mouseout" evento é disparado. Esses dois eventos podem ser usados, entre outras coisas, para criar efeitos de foco, mostrando ou denominando algo quando o mouse está sobre um determinado elemento.
Infelizmente, a criação de um tal efeito não é tão simples de ativar o efeito em "mouseover" e acabar com ela em "mouseout". Quando o mouse se move a partir de um nó em um dos seus filhos, "mouseout" é acionado no nó pai, embora o mouse não chegou a deixar extensão do nó. Para piorar as coisas, esses eventos se propagam assim como outros eventos, portanto você também receberá eventos "mouseout" quando o mouse deixa um dos nós filhos do nó em que o manipulador é registrado.
Para contornar este problema, podemos usar a propriedade relatedTarget
dos objetos de eventos criados para esses eventos. Diz-nos que, no caso de "mouseover", o elemento o ponteiro acabou antes e, no caso de "mouseout", o elemento que vai. Nós queremos mudar o nosso efeito hover
apenas quando o relatedTarget
está fora do nosso nó de destino. Só nesse caso é que este evento realmente representam um cruzamento de fora para dentro do nó(ou o contrário).
<p>Hover over this <strong>paragraph</strong>.</p>
<script>
var para = document.querySelector("p");
function isInside(node, target) {
for (; node != null; node = node.parentNode)
if (node == target) return true;
}
para.addEventListener("mouseover", function(event) {
if (!isInside(event.relatedTarget, para))
para.style.color = "red";
});
para.addEventListener("mouseout", function(event) {
if (!isInside(event.relatedTarget, para))
para.style.color = "";
});
</script>
A função isInside
percorre os links pai do nó dado até ele atingir o topo do documento(quando for nulo), ou encontrar o pai que está procurando.
Devo acrescentar que um efeito hover como isso pode ser muito mais facilmente alcançado utilizando o pseudo selector em CSS :hover
, como o exemplo a seguir mostra. Mas quando o seu efeito hover envolve fazer algo mais complicado do que mudar um estilo no nó de destino, você deve usar o truque com "mouseover" e eventos "mouseout".
<style>
p:hover { color: red }
</style>
<p>Hover over this <strong>paragraph</strong>.</p>