O Modelo de Objeto de Documentos (DOM)

Que pena! A mesma velha história! Depois que você termina de construir sua casa, percebe que acabou aprendendo algo que realmente deveria saber—antes de começar.

Friedrich Nietzsche, Beyond Good and Evil
Ilustração mostrando uma árvore com letras, imagens e engrenagens penduradas em seus galhos

Quando você abre uma página web, seu navegador recupera o texto HTML da página e o analisa, de forma muito parecida com como nosso parser do Capítulo 12 analisava programas. O navegador constrói um modelo da estrutura do documento e usa esse modelo para desenhar a página na tela.

Essa representação do documento é um dos brinquedos que um programa JavaScript tem disponível em seu sandbox. Trata-se de uma estrutura de dados que você pode ler ou modificar. Ela funciona como uma estrutura de dados viva: quando é modificada, a página na tela é atualizada para refletir as mudanças.

Estrutura do documento

Você pode imaginar um documento HTML como um conjunto aninhado de caixas. Tags como <body> e </body> envolvem outras tags, que por sua vez contêm outras tags ou texto. Aqui está o documento de exemplo do capítulo anterior:

<!doctype html>
<html>
  <head>
    <title>My home page</title>
  </head>
  <body>
    <h1>My home page</h1>
    <p>Hello, I am Marijn and this is my home page.</p>
    <p>I also wrote a book! Read it
      <a href="http://eloquentjavascript.net">here</a>.</p>
  </body>
</html>

Essa página tem a seguinte estrutura:

Diagrama mostrando um documento HTML como um conjunto de caixas aninhadas. A caixa externa é rotulada 'html' e contém duas caixas chamadas 'head' e 'body'. Dentro delas há mais caixas, sendo que algumas das mais internas contêm o texto do documento.

A estrutura de dados que o navegador usa para representar o documento segue esse formato. Para cada caixa, existe um objeto com o qual podemos interagir para descobrir coisas como qual tag HTML ele representa e quais caixas e textos ele contém. Essa representação é chamada de Document Object Model, ou DOM.

A variável global document nos dá acesso a esses objetos. Sua propriedade documentElement refere-se ao objeto que representa a tag <html>. Como todo documento HTML tem um head (cabeça) e um body (corpo), ele também possui propriedades head e body que apontam para esses elementos.

Árvores

Lembre-se por um momento das árvore sintáticas do Capítulo 12. Suas estruturas são surpreendentemente semelhantes à estrutura de um documento no navegador. Cada pode se referir a outros nós, chamados filhos, que por sua vez podem ter seus próprios filhos. Esse formato é típico de estruturas aninhadas, em que elementos podem conter subelementos semelhantes a eles mesmos.

Chamamos uma estrutura de dados de árvore quando ela tem uma estrutura ramificada, sem ciclos (um nó não pode conter a si mesmo, direta ou indiretamente), e uma única raiz bem definida. No caso do DOM, document.documentElement serve como a raiz.

Árvores aparecem muito na ciência da computação. Além de representar estruturas recursivas como documentos HTML ou programas, elas são frequentemente usadas para manter conjuntos de dados ordenados, pois os elementos geralmente podem ser encontrados ou inseridos com mais eficiência em uma árvore do que em um array simples.

Uma árvore típica tem diferentes tipos de nós. A árvore sintática da linguagem Egg tinha identificadores, valores e nós de aplicação. Nós de aplicação podem ter filhos, enquanto identificadores e valores são folhas, ou nós sem filhos.

O mesmo vale para o DOM. Nós de elementos, que representam tags HTML, determinam a estrutura do documento. Esses podem ter nó filhos. Um exemplo desse tipo de nó é document.body. Alguns desses filhos podem ser nó folhas, como pedaços de texto ou nós de comentário.

Cada objeto de nó do DOM tem uma propriedade nodeType, que contém um código (número) que identifica o tipo de nó. Elementos têm código 1, também definido como a constante Node.ELEMENT_NODE. Nós de texto, que representam uma seção de texto no documento, recebem o código 3 (Node.TEXT_NODE). Comentários têm código 8 (Node.COMMENT_NODE).

Outra forma de visualizar nossa árvore de documento é a seguinte:

Diagrama mostrando o documento HTML como uma árvore, com setas dos nós pai para os nós filhos

As folhas são nós de texto, e as setas indicam as relações de pai e filho entre os nós.

O padrão

Usar códigos numéricos enigmáticos para representar tipos de nó não é algo muito característico de JavaScript. Mais adiante neste capítulo, veremos que outras partes da interface do DOM também parecem pesadas e estranhas. Isso acontece porque a interface do DOM não foi projetada apenas para JavaScript. Na verdade, ela tenta ser uma interface neutra em relação à linguagem, que pode ser usada em outros sistemas também—não apenas para HTML, mas também para XML, que é um formato de dados genérico com sintaxe semelhante ao HTML.

Isso é um pouco infeliz. Padrões costumam ser úteis. Mas neste caso, a vantagem (consistência entre linguagens) não é tão convincente. Ter uma interface bem integrada com a linguagem que você está usando economiza mais tempo do que ter uma interface familiar entre linguagens.

Como exemplo dessa má integração, considere a propriedade childNodes que os nós de elemento no DOM possuem. Essa propriedade guarda um objeto semelhante a um array, com uma propriedade length e propriedades numeradas para acessar os nós filhos. Mas ele é uma instância do tipo NodeList, não um array real, então não possui métodos como slice e map.

Também há problemas que são simplesmente causados por um design ruim. Por exemplo, não há uma forma de criar um novo nó e imediatamente adicionar filhos ou atributos a ele. Em vez disso, você precisa primeiro criá-lo e depois adicionar os filhos e atributos um por um, usando efeitos colaterais. Código que interage muito com o DOM tende a ficar longo, repetitivo e feio.

Mas essas falhas não são fatais. Como JavaScript nos permite criar nossas próprias abstraçãos, é possível projetar maneiras melhores de expressar as operações que estamos realizando. Muitas bibliotecas voltadas para programação em navegador vêm com ferramentas desse tipo.

Percorrendo a árvore

Os nós do DOM contêm uma grande quantidade de ligaçãos para outros nós próximos. O diagrama a seguir ilustra isso:

Diagrama que mostra as ligações entre nós do DOM. O nó 'body' aparece como uma caixa, com uma seta 'firstChild' apontando para o nó 'h1' no início, uma seta 'lastChild' apontando para o último parágrafo, e uma seta 'childNodes' apontando para um array de ligações para todos os seus filhos. O parágrafo do meio tem uma seta 'previousSibling' apontando para o nó anterior, uma 'nextSibling' para o nó seguinte, e uma 'parentNode' apontando para o nó 'body'.

Embora o diagrama mostre apenas uma ligação de cada tipo, todo nó tem uma propriedade parentNode que aponta para o nó ao qual ele pertence, se houver. Da mesma forma, todo nó de elemento (tipo 1) tem uma propriedade childNodes que aponta para um objeto tipo array contendo seus filhos.

Em teoria, você poderia navegar por toda a árvore usando apenas essas ligações de pai e filho. Mas JavaScript também fornece várias ligações de conveniência. As propriedades firstChild e lastChild apontam para o primeiro e o último filho ou têm valor null para nós sem filhos. Da mesma forma, previousSibling e nextSibling apontam para nós adjacentes, que são nós com o mesmo pai que aparecem imediatamente antes ou depois do próprio nó. Para o primeiro filho, previousSibling será null, e para o último filho, nextSibling será null.

Há também a propriedade children, que é como childNodes, mas contém apenas filhos que são elementos (tipo 1), não outros tipos de nós. Isso pode ser útil quando você não está interessado em nós de texto.

Ao lidar com uma estrutura de dados aninhada como essa, funções recursivas costumam ser úteis. A função a seguir percorre um documento em busca de nó de textos contendo uma string específica e retorna true quando encontra um:

function talksAbout(node, string) {
  if (node.nodeType == Node.ELEMENT_NODE) {
    for (let child of node.childNodes) {
      if (talksAbout(child, string)) {
        return true;
      }
    }
    return false;
  } else if (node.nodeType == Node.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}

console.log(talksAbout(document.body, "book"));
// → true

A propriedade nodeValue de um nó de texto contém a string de texto que ele representa.

Encontrando elementos

Navegar por essas ligaçãos entre pais, filhos e irmãos costuma ser útil. Mas, se quisermos encontrar um nó específico no documento, alcançá-lo começando em document.body e seguindo um caminho fixo de propriedades é uma má ideia. Isso embute suposições no programa sobre a estrutura exata do documento—uma estrutura que você pode querer mudar depois. Outro fator complicador é que nós de texto são criados até mesmo para os espaços em branco entre nós. A tag <body> do documento de exemplo não tem apenas três filhos (<h1> e dois <p>), mas sete: esses três, mais os espaços antes, depois e entre eles.

Se quisermos obter o atributo href do link nesse documento, não queremos dizer algo como “pegue o segundo filho do sexto filho do corpo do documento”. Seria melhor poder dizer “pegue o primeiro link no documento”. E podemos.

let link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

Todos os nós de elemento têm um método getElementsByTagName, que coleta todos os elementos com o nome de tag dado que são descendentes (filhos diretos ou indiretos) daquele nó e os retorna como um objeto tipo array.

Para encontrar um nó único específico, você pode dar a ele um atributo id e usar document.getElementById.

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="img/ostrich.png"></p>

<script>
  let ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>

Um terceiro método semelhante é getElementsByClassName, que, assim como getElementsByTagName, percorre o conteúdo de um nó de elemento e recupera todos os elementos que têm a string dada em seu atributo class.

Alterando o documento

Quase tudo na estrutura de dados do DOM pode ser alterado. A forma da árvore do documento pode ser modificada alterando relações de pai e filho. Nós têm um método remove para removê-los de seu nó pai atual. Para adicionar um nó filho a um nó de elemento, podemos usar appendChild, que o coloca no final da lista de filhos, ou insertBefore, que insere o nó dado como primeiro argumento antes do nó dado como segundo argumento.

<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
  let paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

Um nó pode existir no documento em apenas um lugar. Assim, inserir o parágrafo Three antes do parágrafo One primeiro o removerá do final do documento e depois o inserirá no início, resultando em Three/One/Two. Todas as operações que inserem um nó em algum lugar irão, como efeito colateral, removê-lo de sua posição atual (se tiver uma).

O método replaceChild é usado para substituir um nó filho por outro. Ele recebe dois nós como argumentos: um novo nó e o nó a ser substituído. O nó substituído deve ser filho do elemento no qual o método é chamado. Note que tanto replaceChild quanto insertBefore esperam o nó novo como primeiro argumento.

Criando nós

Suponha que queremos escrever um script que substitua todas as imagems (tags <img>) no documento pelo texto contido em seus atributos alt, que especifica uma representação textual alternativa da imagem. Isso envolve não apenas remover as imagens, mas também adicionar um novo nó de texto para substituí-las.

<p>The <img src="img/cat.png" alt="Cat"> in the
  <img src="img/hat.png" alt="Hat">.</p>

<p><button onclick="replaceImages()">Replace</button></p>

<script>
  function replaceImages() {
    let images = document.body.getElementsByTagName("img");
    for (let i = images.length - 1; i >= 0; i--) {
      let image = images[i];
      if (image.alt) {
        let text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>

Dada uma string, createTextNode nos fornece um nó de texto que podemos inserir no documento para fazê-lo aparecer na tela.

O loop que percorre as imagens começa do fim da lista. Isso é necessário porque a lista de nós retornada por um método como getElementsByTagName (ou uma propriedade como childNodes) é viva. Ou seja, ela é atualizada conforme o documento muda. Se começássemos do início, remover a primeira imagem faria com que a lista perdesse seu primeiro elemento, de modo que na segunda repetição do loop, quando i fosse 1, ele pararia porque o tamanho da coleção agora também é 1.

Se você quiser uma coleção fixa de nós, em vez de uma viva, pode converter a coleção em um array real chamando Array.from.

let arrayish = {0: "one", 1: "two", length: 2};
let array = Array.from(arrayish);
console.log(array.map(s => s.toUpperCase()));
// → ["ONE", "TWO"]

Para criar nós de elemento, você pode usar o método document.createElement. Esse método recebe um nome de tag e retorna um novo nó vazio do tipo especificado.

O exemplo a seguir define um utilitário elt, que cria um nó de elemento e trata o restante de seus argumentos como filhos desse nó. Essa função é então usada para adicionar uma atribuição a uma citação.

<blockquote id="quote">
  No book can ever be finished. While working on it we learn
  just enough to find it immature the moment we turn away
  from it.
</blockquote>

<script>
  function elt(type, ...children) {
    let node = document.createElement(type);
    for (let child of children) {
      if (typeof child != "string") node.appendChild(child);
      else node.appendChild(document.createTextNode(child));
    }
    return node;
  }

  document.getElementById("quote").appendChild(
    elt("footer", "—",
        elt("strong", "Karl Popper"),
        ", preface to the second edition of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

Atributos

Alguns atributos de elemento, como href para links, podem ser acessados por meio de uma propriedade com o mesmo nome no objeto DOM do elemento. Esse é o caso da maioria dos atributos padrão mais usados.

O HTML permite que você defina qualquer atributo que quiser em nós. Isso pode ser útil porque permite armazenar informações extras em um documento. Para ler ou alterar atributos personalizados, que não estão disponíveis como propriedades normais do objeto, você deve usar os métodos getAttribute e setAttribute.

<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>

<script>
  let paras = document.body.getElementsByTagName("p");
  for (let para of Array.from(paras)) {
    if (para.getAttribute("data-classified") == "secret") {
      para.remove();
    }
  }
</script>

Recomenda-se prefixar os nomes desses atributos inventados com data- para garantir que não entrem em conflito com outros atributos.

Existe um atributo muito usado, class, que é uma palavra-chave na linguagem JavaScript. Por razões históricas—algumas implementações antigas de JavaScript não conseguiam lidar com nomes de propriedades que coincidiam com palavras-chave—a propriedade usada para acessar esse atributo é chamada className. Você também pode acessá-lo pelo seu nome real, "class", com os métodos getAttribute e setAttribute.

Layout

Você pode ter notado que diferentes tipos de elementos são organizados de maneiras diferentes. Alguns, como parágrafos (<p>) ou títulos (<h1>), ocupam toda a largura do documento e são renderizados em linhas separadas. Esses são chamados de elementos block. Outros, como links (<a>) ou o elemento <strong>, são renderizados na mesma linha que o texto ao redor. Esses são chamados de elementos inline.

Para qualquer documento, os navegadores são capazes de calcular um layout, que dá a cada elemento um tamanho e uma posição com base em seu tipo e conteúdo. Esse layout é então usado para realmente desenhar o documento.

O tamanho e a posição de um elemento podem ser acessados a partir do JavaScript. As propriedades offsetWidth e offsetHeight fornecem o espaço que o elemento ocupa em pixels. Um pixel é a unidade básica de medida no navegador. Tradicionalmente, corresponde ao menor ponto que a tela pode desenhar, mas em telas modernas, que conseguem desenhar pontos muito pequenos, isso pode não ser mais o caso, e um pixel do navegador pode abranger vários pontos físicos da tela.

Da mesma forma, clientWidth e clientHeight fornecem o tamanho do espaço dentro do elemento, ignorando a largura da borda.

<p style="border: 3px solid red">
  I'm boxed in
</p>

<script>
  let para = document.body.getElementsByTagName("p")[0];
  console.log("clientHeight:", para.clientHeight);
  // → 19
  console.log("offsetHeight:", para.offsetHeight);
  // → 25
</script>

A maneira mais eficaz de encontrar a posição precisa de um elemento na tela é o método getBoundingClientRect. Ele retorna um objeto com propriedades top, bottom, left e right, indicando as posições em pixels dos lados do elemento em relação ao canto superior esquerdo da tela. Se você quiser posições em pixels relativas ao documento inteiro, deve adicionar a posição atual de rolagem, que pode ser encontrada nas variáveis pageXOffset e pageYOffset.

Organizar o layout de um documento pode dar bastante trabalho. Por questão de velocidade, os motores de navegador não recalculam imediatamente o layout toda vez que você altera o documento, mas esperam o máximo possível antes de fazê-lo. Quando um programa JavaScript que alterou o documento termina de executar, o navegador terá que calcular um novo layout para desenhar o documento atualizado na tela. Quando um programa solicita a posição ou o tamanho de algo lendo propriedades como offsetHeight ou chamando getBoundingClientRect, fornecer essa informação também exige calcular o layout.

Um programa que alterna repetidamente entre ler informações de layout do DOM e modificar o DOM força muitos cálculos de layout e, consequentemente, será muito lento. O código a seguir é um exemplo disso. Ele contém dois programas diferentes que constroem uma linha de caracteres X com 2.000 pixels de largura e mede o tempo que cada um leva.

<p><span id="one"></span></p>
<p><span id="two"></span></p>

<script>
  function time(name, action) {
    let start = Date.now(); // Tempo atual em milissegundos
    action();
    console.log(name, "took", Date.now() - start, "ms");
  }

  time("naive", () => {
    let target = document.getElementById("one");
    while (target.offsetWidth < 2000) {
      target.appendChild(document.createTextNode("X"));
    }
  });
  // → naive took 32 ms

  time("clever", function() {
    let target = document.getElementById("two");
    target.appendChild(document.createTextNode("XXXXX"));
    let total = Math.ceil(2000 / (target.offsetWidth / 5));
    target.firstChild.nodeValue = "X".repeat(total);
  });
  // → clever took 1 ms
</script>

Estilo

Vimos que diferentes elementos HTML são desenhados de formas diferentes. Alguns são exibidos como blocos, outros inline. Alguns adicionam estilo—<strong> deixa seu conteúdo em negrito, e <a> o deixa azul e sublinhado.

A forma como uma tag <img> exibe uma imagem ou uma tag <a> faz com que um link seja seguido ao ser clicado está fortemente ligada ao tipo do elemento. Mas podemos alterar o estilo associado a um elemento, como a cor do texto ou o sublinhado. Aqui está um exemplo que usa a propriedade style:

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>

Um atributo de estilo pode conter uma ou mais declaraçãos, que são uma propriedade (como color) seguida de dois-pontos e um valor (como green). Quando há mais de uma declaração, elas devem ser separadas por ponto e vírgulas, como em "color: red; border: none".

Muitos aspectos do documento podem ser influenciados por estilo. Por exemplo, a propriedade display controla se um elemento é exibido como bloco ou inline.

This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.

A tag block ficará em sua própria linha, já que elemento de blocos não são exibidos inline com o texto ao redor. A última tag não é exibida—display: none impede que um elemento apareça na tela. Essa é uma forma de ocultar elementos. Muitas vezes é preferível a removê-los completamente do documento, pois facilita mostrá-los novamente depois.

Código JavaScript pode manipular diretamente o estilo de um elemento por meio da propriedade style do elemento. Essa propriedade contém um objeto com propriedades para todas as propriedades de estilo possíveis. Os valores dessas propriedades são strings, que podemos alterar para mudar aspectos específicos do estilo do elemento.

<p id="para" style="color: purple">
  Nice text
</p>

<script>
  let para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

Alguns nomes de propriedades de estilo contêm hífens, como font-family. Como esses nomes são inconvenientes em JavaScript (você teria que usar style["font-family"]), no objeto style os hífens são removidos e as letras seguintes são capitalizadas (style.fontFamily).

Estilos em cascata

O sistema de estilização do HTML é chamado CSS, de Cascading Style Sheets. Uma folha de estilo é um conjunto de regras sobre como estilizar elementos em um documento. Ela pode ser definida dentro de uma tag <style>.

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>Now <strong>strong text</strong> is italic and gray.</p>

O termo cascading no nome refere-se ao fato de que múltiplas regras são combinadas para produzir o estilo final de um elemento. No exemplo, o estilo padrão das tags <strong>, que define font-weight: bold, é sobreposto pela regra na tag <style>, que adiciona font-style e color.

Quando várias regras definem um valor para a mesma propriedade, a regra lida mais recentemente tem maior precedência e prevalece. Por exemplo, se a regra na tag <style> incluísse font-weight: normal, contradizendo a regra padrão, o texto ficaria normal, não em negrito. Estilos no atributo style aplicado diretamente ao nó têm a maior precedência e sempre vencem.

É possível direcionar coisas além de nomes de tag em regras CSS. Uma regra para .abc aplica-se a todos os elementos com "abc" no atributo class. Uma regra para #xyz aplica-se ao elemento com atributo id igual a "xyz" (que deve ser único no documento).

.subtle {
  color: gray;
  font-size: 80%;
}
#header {
  background: blue;
  color: white;
}
/* elementos p com id main e classes a e b */
p#main.a.b {
  margin-bottom: 20px;
}

A regra de precedência que favorece a regra mais recente aplica-se apenas quando as regras têm a mesma especificidade. A especificidade mede o quão precisamente uma regra descreve os elementos que ela seleciona, com base no número e tipo (tag, classe ou ID) dos aspectos exigidos. Por exemplo, uma regra que seleciona p.a é mais específica do que regras que selecionam p ou apenas .a, e portanto terá precedência sobre elas.

A notação p > a {…} aplica os estilos dados a todas as tags <a> que são filhas diretas de <p>. Da mesma forma, p a {…} aplica-se a todas as <a> dentro de <p>, sejam filhos diretos ou indiretos.

Seletores de consulta

Não usaremos muito folhas de estilo neste livro. Entendê-las é útil ao programar no navegador, mas são complexas o suficiente para justificar um livro próprio. O principal motivo de eu ter introduzido a sintaxe de seletor—a notação usada em folhas de estilo para determinar a quais elementos um conjunto de estilos se aplica—é que podemos usar essa mesma mini-linguagem como uma forma eficaz de encontrar elementos do DOM.

O método querySelectorAll, definido tanto no objeto document quanto em nós de elemento, recebe uma string de seletor e retorna um NodeList contendo todos os elementos que correspondem a ele.

<p>And if you go chasing
  <span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="character">hookah smoking
  <span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>

<script>
  function count(selector) {
    return document.querySelectorAll(selector).length;
  }
  console.log(count("p"));           // All <p> elements
  // → 4
  console.log(count(".animal"));     // Class animal
  // → 2
  console.log(count("p .animal"));   // Animal inside of <p>
  // → 2
  console.log(count("p > .animal")); // Direct child of <p>
  // → 1
</script>

Diferentemente de métodos como getElementsByTagName, o objeto retornado por querySelectorAll não é vivo. Ele não muda quando você altera o documento. Ainda assim, não é um array real, então você precisa chamar Array.from se quiser tratá-lo como um.

O método querySelector (sem o All) funciona de forma semelhante. Ele é útil quando você quer um único elemento específico. Retorna apenas o primeiro elemento correspondente, ou null quando nenhum elemento corresponde.

Posicionamento e animação

A propriedade de estilo position influencia o layout de forma poderosa. Ela tem valor padrão static, significando que o elemento fica em sua posição normal no documento. Quando definida como relative, o elemento ainda ocupa espaço no documento, mas agora as propriedades top e left podem ser usadas para movê-lo em relação a essa posição normal. Quando position é definida como absolute, o elemento é removido do fluxo normal do documento—ou seja, deixa de ocupar espaço e pode sobrepor outros elementos. Suas propriedades top e left podem ser usadas para posicioná-lo em relação ao canto superior esquerdo do elemento pai mais próximo cujo position não seja static, ou em relação ao documento se não houver tal elemento.

Podemos usar isso para criar uma animação. O documento a seguir exibe uma imagem de um gato que se move em uma elipse:

<p style="text-align: center">
  <img src="img/cat.png" style="position: relative">
</p>
<script>
  let cat = document.querySelector("img");
  let angle = Math.PI / 2;
  function animate(time, lastTime) {
    if (lastTime != null) {
      angle += (time - lastTime) * 0.001;
    }
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(newTime => animate(newTime, time));
  }
  requestAnimationFrame(animate);
</script>

Nossa imagem está centralizada na página e recebe position: relative. Vamos atualizar repetidamente os estilos top e left dessa imagem para movê-la.

O script usa requestAnimationFrame para agendar a execução da função animate sempre que o navegador estiver pronto para redesenhar a tela. A própria função animate chama novamente requestAnimationFrame para agendar a próxima atualização. Quando a janela (ou aba) do navegador está ativa, isso faz com que as atualizações ocorram cerca de 60 vezes por segundo, o que tende a produzir uma animação suave.

Se simplesmente atualizássemos o DOM em um loop, a página travaria e nada apareceria na tela. Navegadores não atualizam a exibição enquanto um programa JavaScript está em execução, nem permitem interação com a página nesse período. É por isso que precisamos de requestAnimationFrame—ele informa ao navegador que terminamos por agora, permitindo que ele atualize a tela e responda às ações do usuário.

A função de animação recebe o tempo atual como argumento. Para garantir que o movimento do gato por milissegundo seja estável, ela baseia a velocidade de mudança do ângulo na diferença entre o tempo atual e o tempo da última execução. Se simplesmente aumentasse o ângulo por uma quantidade fixa a cada passo, o movimento ficaria irregular quando, por exemplo, outra tarefa pesada impedisse a execução da função por uma fração de segundo.

Mover-se em círculos é feito usando as funções trigonométricas Math.cos e Math.sin. Para quem não está familiarizado com elas, vou apresentá-las brevemente, pois as usaremos ocasionalmente neste livro.

Math.cos e Math.sin são úteis para encontrar pontos que estão em um círculo ao redor do ponto (0, 0) com raio 1. Ambas interpretam seu argumento como a posição nesse círculo, onde 0 representa o ponto mais à direita, seguindo no sentido horário até 2π (cerca de 6,28), completando a volta inteira. Math.cos fornece a coordenada x do ponto correspondente, e Math.sin fornece a coordenada y. Posições (ou ângulos) maiores que 2π ou menores que 0 são válidas—a rotação se repete, de modo que a+2π representa o mesmo ângulo que a.

Essa unidade de medida de ângulos é chamada de radiano—um círculo completo tem 2π radianos, assim como tem 360 graus. A constante π está disponível como Math.PI em JavaScript.

Diagrama mostrando o uso de cosseno e seno para calcular coordenadas. Um círculo de raio 1 é mostrado com dois pontos nele. O ângulo desde o lado direito do círculo até o ponto, em radianos, é usado para calcular a posição de cada ponto usando 'cos(angle)' para a distância horizontal do centro e 'sin(angle)' para a vertical.

O código da animação do gato mantém um contador, angle, para o ângulo atual da animação e o incrementa a cada chamada da função animate. Ele então usa esse ângulo para calcular a posição atual da imagem. O estilo top é calculado com Math.sin multiplicado por 20, que é o raio vertical da nossa elipse. O estilo left é baseado em Math.cos multiplicado por 200, tornando a elipse muito mais larga do que alta.

Note que estilos geralmente precisam de unidades. Neste caso, precisamos adicionar "px" ao número para indicar ao navegador que estamos usando pixels (em vez de centímetros, “ems” ou outras unidades). Isso é fácil de esquecer. Usar números sem unidade fará com que o estilo seja ignorado—exceto quando o número é 0, que sempre significa a mesma coisa, independentemente da unidade.

Resumo

Programas JavaScript podem inspecionar e interferir no documento que o navegador está exibindo por meio de uma estrutura de dados chamada DOM. Essa estrutura representa o modelo do documento no navegador, e um programa JavaScript pode modificá-la para alterar o documento visível.

O DOM é organizado como uma árvore, onde os elementos são dispostos hierarquicamente de acordo com a estrutura do documento. Os objetos que representam elementos têm propriedades como parentNode e childNodes, que podem ser usadas para navegar por essa árvore.

A forma como um documento é exibido pode ser influenciada por estilo, tanto aplicando estilos diretamente aos nós quanto definindo regras que correspondem a certos nós. Existem muitas propriedades de estilo diferentes, como color ou display. Código JavaScript pode manipular diretamente o estilo de um elemento por meio de sua propriedade style.

Exercícios

Construindo uma tabela

Uma tabela HTML é construída com a seguinte estrutura de tags:

<table>
  <tr>
    <th>name</th>
    <th>height</th>
    <th>place</th>
  </tr>
  <tr>
    <td>Kilimanjaro</td>
    <td>5895</td>
    <td>Tanzania</td>
  </tr>
</table>

Para cada linha, a tag <table> contém uma tag <tr>. Dentro dessas <tr>, podemos colocar elementos de célula: células de cabeçalho (<th>) ou células comuns (<td>).

Dado um conjunto de dados de montanhas, um array de objetos com propriedades name, height e place, gere a estrutura DOM de uma tabela que enumere os objetos. Ela deve ter uma coluna por chave e uma linha por objeto, além de uma linha de cabeçalho com elementos <th> no topo listando os nomes das colunas.

Escreva isso de forma que as colunas sejam automaticamente derivadas dos objetos, usando os nomes das propriedades do primeiro objeto do conjunto de dados.

Mostre a tabela resultante no documento adicionando-a ao elemento que tem atributo id igual a "mountains".

Depois que isso estiver funcionando, alinhe à direita as células que contêm valores numéricos definindo sua propriedade style.textAlign como "right".

<h1>Mountains</h1>

<div id="mountains"></div>

<script>
  const MOUNTAINS = [
    {name: "Kilimanjaro", height: 5895, place: "Tanzania"},
    {name: "Everest", height: 8848, place: "Nepal"},
    {name: "Mount Fuji", height: 3776, place: "Japan"},
    {name: "Vaalserberg", height: 323, place: "Netherlands"},
    {name: "Denali", height: 6168, place: "United States"},
    {name: "Popocatepetl", height: 5465, place: "Mexico"},
    {name: "Mont Blanc", height: 4808, place: "Italy/France"}
  ];

  // Your code here
</script>
Mostrar dicas...

Você pode usar document.createElement para criar novos nós de elemento, document.createTextNode para criar nós de texto, e o método appendChild para inserir nós em outros nós.

Você vai querer percorrer os nomes das chaves uma vez para preencher a linha superior e depois novamente para cada objeto no array para construir as linhas de dados. Para obter um array com os nomes das propriedades do primeiro objeto, Object.keys será útil.

Para adicionar a tabela ao nó pai correto, você pode usar document.getElementById ou document.querySelector com "#mountains".

Elementos por nome de tag

O método document.getElementsByTagName retorna todos os elementos filhos com um determinado nome de tag. Implemente sua própria versão disso como uma função que recebe um nó e uma string (o nome da tag) como argumentos e retorna um array contendo todos os nós de elemento descendentes com esse nome de tag. Sua função deve percorrer o próprio documento. Ela não pode usar um método como querySelectorAll para fazer o trabalho.

Para encontrar o nome da tag de um elemento, use sua propriedade nodeName. Mas observe que ela retorna o nome da tag em letras maiúsculas. Use os métodos de string toLowerCase ou toUpperCase para lidar com isso.

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
  spans.</p>

<script>
  function byTagName(node, tagName) {
    // Your code here.
  }

  console.log(byTagName(document.body, "h1").length);
  // → 1
  console.log(byTagName(document.body, "span").length);
  // → 3
  let para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>
Mostrar dicas...

A solução é mais facilmente expressa com uma função recursiva, semelhante à função talksAbout definida anteriormente neste capítulo.

Você pode chamar byTagname recursivamente, concatenando os arrays resultantes para produzir a saída. Ou pode criar uma função interna que chama a si mesma recursivamente e que tem acesso a um array definido na função externa, ao qual pode adicionar os elementos correspondentes que encontrar. Não se esqueça de chamar a função interna uma vez a partir da função externa para iniciar o processo.

A função recursiva deve verificar o tipo do nó. Aqui estamos interessados apenas em nós do tipo 1 (Node.ELEMENT_NODE). Para esses nós, devemos percorrer seus filhos e, para cada filho, verificar se ele corresponde à busca, além de fazer uma chamada recursiva para inspecionar seus próprios filhos.

O chapéu do gato

Estenda a animação do gato definida anteriormente para que tanto o gato quanto seu chapéu (<img src="img/hat.png">) orbitem em lados opostos da elipse.

Ou faça o chapéu girar ao redor do gato. Ou altere a animação de alguma outra forma interessante.

Para facilitar o posicionamento de múltiplos objetos, você provavelmente vai querer usar posicionamento absoluto. Isso significa que top e left são medidos a partir do canto superior esquerdo do documento. Para evitar usar coordenadas negativas, que fariam a imagem sair da área visível, você pode adicionar um número fixo de pixels aos valores de posição.

<style>body { min-height: 200px }</style>
<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">

<script>
  let cat = document.querySelector("#cat");
  let hat = document.querySelector("#hat");

  let angle = 0;
  let lastTime = null;
  function animate(time) {
    if (lastTime != null) angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 40 + 40) + "px";
    cat.style.left = (Math.cos(angle) * 200 + 230) + "px";

    // Your extensions here.

    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>
Mostrar dicas...

Math.cos e Math.sin medem ângulos em radianos, onde um círculo completo é 2π. Para um dado ângulo, você pode obter o ângulo oposto adicionando metade disso, ou seja, Math.PI. Isso pode ser útil para posicionar o chapéu no lado oposto da órbita.