Skip to content

Latest commit

 

History

History
452 lines (348 loc) · 25.9 KB

14-manipulando-eventos.md

File metadata and controls

452 lines (348 loc) · 25.9 KB

Manipulando eventos

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.

Os manipuladores de eventos

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.

Eventos e nós do DOM

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.

Os objetos de evento

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").

Propagação

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 todo window 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>

Ações padrão

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.

Evento de tecla

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.

Evento de mouse

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.

Movimento do mouse

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 no window. 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>

Evento de rolagem

Sempre que um elemento é rolado, um evento de scroll é disparado sobre ele. Isto tem vários usos, como saber o que o usuário está olhando(para desativar animações fora da tela ou o envio de relatórios de espionagem para o seu quartel general) ou apresentar alguma indicação de progresso(por destacar parte de uma tabela de conteúdo ou que mostra um número de página).

O exemplo a seguir desenha uma barra de progresso no canto superior direito do documento e atualiza a enchendo quando você rolar para baixo:

<style>
  .progress {
    border: 1px solid blue;
    width: 100px;
    position: fixed;
    top: 10px; right: 10px;
  }
  .progress > div {
    height: 12px;
    background: blue;
    width: 0%;
  }
  body {
    height: 2000px;
  }
</style>
<div class="progress"><div></div></div>
<p>Scroll me...</p>
<script>
  var bar = document.querySelector(".progress div");
  addEventListener("scroll", function() {
    var max = document.body.scrollHeight - innerHeight;
    var percent = (pageYOffset / max) * 100;
    bar.style.width = percent + "%";
  });
</script>

Um elemento com uma posição fixa é muito parecido com uma posição absoluta, mas também impede a rolagem junto com o resto do documento. O efeito é fazer com que nosso progresso bar pare no canto. Dentro dele existe outro elemento, que é redimensionada para indicar o progresso atual. Usamos %, em vez de px como unidade, definindo a largura de modo que quando o elemento é dimensionado em relação ao conjunto da barra.

A variável innerHeight nos dá a altura de window, devemos subtrair do total altura de sua rolagem para não manter a rolagem quando você chegar no final do documento.(Há também uma innerWidth que acompanha o innerHeight.) Ao dividir pageYOffset a posição de rolagem atual menos posição de deslocamento máximo multiplicando por 100, obtemos o percentual da barra de progresso .

Chamando preventDefault em um evento de rolagem não impede a rolagem de acontecer. Na verdade, o manipulador de eventos é chamado apenas após da rolagem ocorrer.

Evento de foco

Quando um elemento entra em foco, o navegador dispara um evento de "focus" nele. Quando se perde o foco, um eventos de "blur" é disparado.

Ao contrário dos eventos discutidos anteriormente, esses dois eventos não se propagam. Um manipulador em um elemento pai não é notificado quando um filho ganha ou perde o foco do elemento.

O exemplo a seguir exibe um texto de ajuda para o campo de texto que possui o foco no momento:

<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Age in years"></p>
<p id="help"></p>

<script>
  var help = document.querySelector("#help");
  var fields = document.querySelectorAll("input");
  for (var i = 0; i < fields.length; i++) {
    fields[i].addEventListener("focus", function(event) {
      var text = event.target.getAttribute("data-help");
      help.textContent = text;
    });
    fields[i].addEventListener("blur", function(event) {
      help.textContent = "";
    });
  }
</script>
</script>

O objeto window recebe o evento de "focus" e o evento de "blur" quando o usuário move-se para outra aba ou janela do navegador a qual o documento esta sendo mostrado.

Evento de load

Quando uma página termina de carregar, o evento "load" é disparado no window e os objetos do corpo do document. Isso é muitas vezes usado para programar ações de inicialização que exigem que todo o documento deve ter sido construído.

Lembre-se que o conteúdo de tags <script> é executado imediatamente quando o tag é encontrada. As vezes a tag é executado antes ou tal o conteúdo do <script> precisa fazer algo com algumas partes do document que ainda não apareceu após a tag.

Elementos como imagens e tags de script que carregam arquivo externo tem um evento de "load" para indica que os arquivos que eles fazem referência foram carregados . Eles são como os eventos de focus, pois não se propagam.

Quando uma página é fechada ou navegação é colocado em segundo plano um evento de "beforeunload" é acionado. O uso principal deste evento é para evitar que o usuário perca o trabalho acidentalmente por fechar um documento. Prevenir a página de descarga não é feito com o método preventDefault. Ele é feito através do envio de uma string a partir do manipulador . A seqüência será usado em uma caixa de diálogo que pergunta ao usuário se ele quer ficar na página ou deixá-la. Este mecanismo garante que um usuário seja capaz de deixar a página, mesmo se estiver sendo executado um script malicioso que prefere mantê-los para sempre, a fim de forçá-los a olhar para alguns anúncios que leva alguns segundos.

Cronograma do Script de execução

Há várias coisas que podem causar a inicialização da execução de um script. A leitura de um tag <script> é um exemplo disto. Um disparo de eventos é outra. No capítulo 13 discutimos a função requestAnimationFrame, que agenda uma função a ser chamada antes de redesenhar a próxima página. Essa é mais uma forma em que um script pode começar a correr.

É importante entender que, disparo de eventos podem ocorrer a qualquer momento, quando há dois scripts em um único documento eles nunca iram correr no mesmo tempo. Se um script já está em execução, os manipuladores de eventos e pedaços de código programados em outras formas tem que esperar pela sua vez. Esta é a razão pela qual um documento irá congelar quando um script é executado por um longo tempo. O navegador não pode reagir aos cliques e outros eventos dentro do documento, porque ele não pode executar manipuladores de eventos até que o script atual termine sua execução.

Alguns ambientes de programação permitem que múltiplas threads de execução se propaguem ao mesmo tempo.

Fazer várias coisas ao mesmo tempo torna um programa mais rápido. Mas quando você tem várias ações tocando nas mesmas partes do sistema, ao mesmo tempo, torna-se pelo menos uma ordem de magnitude mais difícil.

O fato de que os programas de JavaScript fazem apenas uma coisa de cada vez torna a nossa vida mais fácil. Para os casos em que você realmente quer fazer alguma coisa de muito tempo em segundo plano, sem o congelamento da página, os navegadores fornecem algo chamado de web workers. Um web workers é um ambiente isolado do JavaScript que funciona ao lado do principal programa para um documento e pode se comunicar com ele apenas por envio e recebimento de mensagens.

Suponha que temos o seguinte código em um arquivo chamado code/squareworker.js:

addEventListener("message", function(event) {
  postMessage(event.data * event.data);
});

Imagine que a multiplicação com os número seja pesado com uma computação de longa duração e queremos performance então colocamos em uma thread em segundo plano. Este código gera um worker, envia algumas mensagens e produz as respostas.

var squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", function(event) {
  console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);

A função postMessage envia uma mensagem o que causa um evento de "message" disparado ao receptor. O roteiro que criou o worker envia e recebe mensagens através do objeto Worker, ao passo que as conversações de worker para o script que o criou é enviado e ouvido diretamente sobre o seu âmbito global não compartilhada com o roteiro original.