Em um post anterior, mostrei como construir uma linha do tempo vertical responsiva do zero. Hoje, abordarei o processo de criação do associado horizontal Linha do tempo.
Como de costume, para ter uma ideia inicial do que vamos construir, dê uma olhada na demonstração relacionada do CodePen (confira a versão maior para uma melhor experiência):
Temos muito a cobrir, então vamos começar!
1. Marcação HTML
A marcação é idêntica à marcação que definimos para a linha do tempo vertical, exceto por três pequenas coisas:
- Usamos uma lista ordenada em vez de uma lista não ordenada, pois isso é mais semanticamente correto.
- Há um item de lista extra (o último) que está vazio. Em uma próxima seção, discutiremos o motivo.
- Há um elemento extra (ou seja,
.arrows
) que é responsável pela navegação na linha do tempo.
Aqui está a marcação necessária:
Some content here
O estado inicial da linha do tempo é assim:
2. Adicionando estilos CSS iniciais
Depois de alguns estilos básicos de fonte, estilos de cores, etc. que omiti aqui para simplificar, especificamos algumas regras estruturais de CSS:
.timeline { white-space: nowrap; overflow-x: hidden; } .timeline ol { font-size: 0; width: 100vw; padding: 250px 0; transition: all 1s; } .timeline ol li { position: relative; display: inline-block; list-style-type: none; width: 160px; height: 3px; background: #fff; } .timeline ol li:last-child { width: 280px; } .timeline ol li:not(:first-child) { margin-left: 14px; } .timeline ol li:not(:last-child)::after { content: ''; position: absolute; top: 50%; left: calc(100% + 1px); bottom: 0; width: 12px; height: 12px; transform: translateY(-50%); border-radius: 50%; background: #F45B69; }
Mais importante aqui, você notará duas coisas:
- Atribuímos grandes preenchimentos superiores e inferiores à lista. Novamente, explicaremos por que isso acontece na próxima seção.
- Como você notará na demonstração a seguir, neste momento não podemos ver todos os itens da lista porque a lista tem
width: 100vw
e seu pai temoverflow-x: hidden
. Isso efetivamente “mascara” os itens da lista. Graças à navegação na linha do tempo, no entanto, poderemos navegar pelos itens mais tarde.
Com essas regras em vigor, aqui está o estado atual da linha do tempo (sem nenhum conteúdo real, para manter as coisas claras):
3. Estilos de elementos da linha do tempo
Neste ponto, vamos estilizar o div
elementos (vamos chamá-los de “elementos da linha do tempo” de agora em diante) que fazem parte dos itens da lista, bem como seus ::before
pseudo-elementos.
Além disso, usaremos o :nth-child(odd)
e :nth-child(even)
Pseudoclasses CSS para diferenciar os estilos das divs ímpares e pares.
Aqui estão os estilos comuns para os elementos da linha do tempo:
.timeline ol li div { position: absolute; left: calc(100% + 7px); width: 280px; padding: 15px; font-size: 1rem; white-space: normal; color: black; background: white; } .timeline ol li div::before { content: ''; position: absolute; top: 100%; left: 0; width: 0; height: 0; border-style: solid; }
Então alguns estilos para os estranhos:
.timeline ol li:nth-child(odd) div { top: -16px; transform: translateY(-100%); } .timeline ol li:nth-child(odd) div::before { top: 100%; border-width: 8px 8px 0 0; border-color: white transparent transparent transparent; }
E por fim alguns estilos para os pares:
.timeline ol li:nth-child(even) div { top: calc(100% + 16px); } .timeline ol li:nth-child(even) div::before { top: -8px; border-width: 8px 0 0 8px; border-color: transparent transparent transparent white; }
Aqui está o novo estado da linha do tempo, com conteúdo adicionado novamente:
Como você deve ter notado, os elementos da linha do tempo estão absolutamente posicionados. Isso significa que eles são removidos do fluxo normal de documentos. Com isso em mente, para garantir que toda a linha do tempo apareça, temos que definir grandes valores de preenchimento superior e inferior para a lista. Se não aplicarmos nenhum preenchimento, a linha do tempo será cortada:
4. Estilos de navegação da linha do tempo
Agora é hora de estilizar os botões de navegação. Lembre-se que por padrão desabilitamos a seta anterior e damos a ela a classe de disabled
.
Aqui estão os estilos CSS associados:
.timeline .arrows { display: flex; justify-content: center; margin-bottom: 20px; } .timeline .arrows .arrow__prev { margin-right: 20px; } .timeline .disabled { opacity: .5; } .timeline .arrows img { width: 45px; height: 45px; }
As regras acima nos dão esta linha do tempo:
5. Adicionando interatividade
A estrutura básica da linha do tempo está pronta. Vamos adicionar alguma interatividade a ele!
Variáveis
Primeiro de tudo, configuramos um monte de variáveis que usaremos mais tarde.
const timeline = document.querySelector(".timeline ol"), elH = document.querySelectorAll(".timeline li > div"), arrows = document.querySelectorAll(".timeline .arrows .arrow"), arrowPrev = document.querySelector(".timeline .arrows .arrow__prev"), arrowNext = document.querySelector(".timeline .arrows .arrow__next"), firstItem = document.querySelector(".timeline li:first-child"), lastItem = document.querySelector(".timeline li:last-child"), xScrolling = 280, disabledClass = "disabled";
Inicializando coisas
Quando todos os recursos da página estiverem prontos, o init
função é chamada.
window.addEventListener("load", init);
Esta função aciona quatro subfunções:
function init() { setEqualHeights(elH); animateTl(xScrolling, arrows, timeline); setSwipeFn(timeline, arrowPrev, arrowNext); setKeyboardFn(arrowPrev, arrowNext); }
Como veremos em breve, cada uma dessas funções realiza uma determinada tarefa.
Elementos da linha do tempo de altura igual
Se você voltar para a última demonstração, notará que os elementos da linha do tempo não têm alturas iguais. Isso não afeta a funcionalidade principal de nossa linha do tempo, mas você pode preferir que todos os elementos tenham a mesma altura. Para conseguir isso, podemos dar a eles uma altura fixa via CSS (solução fácil) ou uma altura dinâmica que corresponde à altura do elemento mais alto via JavaScript.
A segunda opção é mais flexível e estável, então aqui está uma função que implementa esse comportamento:
function setEqualHeights(el) { let counter = 0; for (let i = 0; i < el.length; i++) { const singleHeight = el[i].offsetHeight; if (counter < singleHeight) { counter = singleHeight; } } for (let i = 0; i < el.length; i++) { el[i].style.height = `${counter}px`; } }
Essa função recupera a altura do elemento da linha de tempo mais alto e a define como a altura padrão para todos os elementos.
Veja como está a demo:
6. Animando a linha do tempo
Agora vamos nos concentrar na animação da linha do tempo. Construiremos a função que implementa esse comportamento passo a passo.
Primeiro, registramos um ouvinte de evento de clique para os botões da linha do tempo:
function animateTl(scrolling, el, tl) { for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // code here }); } }
Cada vez que um botão é clicado, verificamos o estado desabilitado dos botões da linha do tempo e, se não estiverem desabilitados, nós os desabilitamos. Isso garante que ambos os botões sejam clicados apenas uma vez até que a animação termine.
Portanto, em termos de código, o manipulador de cliques contém inicialmente estas linhas:
if (!arrowPrev.disabled) { arrowPrev.disabled = true; } if (!arrowNext.disabled) { arrowNext.disabled = true; }
Os próximos passos são os seguintes:
- Verificamos se é a primeira vez que clicamos em um botão. Novamente, lembre-se de que o anterior botão está desabilitado por padrão, então o único botão que pode ser clicado inicialmente é o próximo 1.
- Se de fato é a primeira vez, usamos o
transform
propriedade para mover a linha do tempo 280px para a direita. O valor doxScrolling
variável determina a quantidade de movimento. - Pelo contrário, se já clicamos em um botão, recuperamos o atual
transform
valor da linha do tempo e adicionar ou remover a esse valor, a quantidade de movimento desejada (ou seja, 280px). Assim, desde que cliquemos no anterior botão, o valor dotransform
propriedade diminui e a linha do tempo é movida da esquerda para a direita. No entanto, quando o próximo botão é clicado, o valor dotransform
propriedade aumenta e a linha do tempo é movida da direita para a esquerda.
O código que implementa esta funcionalidade é o seguinte:
let counter = 0; for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // other code here const sign = (this.classList.contains("arrow__prev")) ? "" : "-"; if (counter === 0) { tl.style.transform = `translateX(-${scrolling}px)`; } else { const tlStyle = getComputedStyle(tl); // add more browser prefixes if needed here const tlTransform = tlStyle.getPropertyValue("-webkit-transform") || tlStyle.getPropertyValue("transform"); const values = parseInt(tlTransform.split(",")[4]) + parseInt(`${sign}${scrolling}`); tl.style.transform = `translateX(${values}px)`; } counter++; }); }
Bom trabalho! Acabamos de definir uma forma de animar a linha do tempo. O próximo desafio é descobrir quando essa animação deve parar. Aqui está nossa abordagem:
- Quando o primeiro elemento da linha do tempo fica totalmente visível, significa que já chegamos ao início da linha do tempo e, portanto, desabilitamos o anterior botão. Asseguramos também que o próximo botão está habilitado.
- Quando o último elemento fica totalmente visível, significa que já chegamos ao final da linha do tempo e, portanto, desabilitamos o próximo botão. Garantimos também, por isso, que a anterior botão está habilitado.
Lembre-se que o último elemento é um vazio com largura igual à largura dos elementos da linha de tempo (ou seja, 280px). Damos esse valor (ou um valor maior) porque queremos ter certeza de que o último elemento da linha do tempo estará visível antes de desabilitar o próximo botão.
Para detectar se os elementos de destino estão totalmente visíveis na janela de visualização atual ou não, aproveitaremos o mesmo código que usamos para a linha do tempo vertical. O código necessário que vem deste thread do Stack Overflow é o seguinte:
function isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }
Além da função acima, definimos outro auxiliar:
function setBtnState(el, flag = true) { if (flag) { el.classList.add(disabledClass); } else { if (el.classList.contains(disabledClass)) { el.classList.remove(disabledClass); } el.disabled = false; } }
Esta função adiciona ou remove o disabled
classe de um elemento com base no valor da flag
parâmetro. Além disso, pode alterar o estado desabilitado desse elemento.
Dado o que descrevemos acima, aqui está o código que definimos para verificar se a animação deve parar ou não:
for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // other code here // code for stopping the animation setTimeout(() => { isElementInViewport(firstItem) ? setBtnState(arrowPrev) : setBtnState(arrowPrev, false); isElementInViewport(lastItem) ? setBtnState(arrowNext) : setBtnState(arrowNext, false); }, 1100); // other code here }); }
Observe que há um atraso de 1,1 segundo antes de executar esse código. Por que isso acontece?
Se voltarmos ao nosso CSS, veremos esta regra:
.timeline ol { transition: all 1s; }
Portanto, a animação da linha do tempo precisa de 1 segundo para ser concluída. Enquanto estiver concluído, esperamos 100 milissegundos e, em seguida, realizamos nossas verificações.
Aqui está a linha do tempo com animações:
7. Adicionando suporte para furto
Até agora, a linha do tempo não responde a eventos de toque. Seria bom se pudéssemos adicionar essa funcionalidade. Para isso, podemos escrever nossa própria implementação JavaScript ou usar uma das bibliotecas relacionadas (por exemplo, Hammer.js, TouchSwipe.js) que existem por aí.
Para nossa demonstração, manteremos isso simples e usaremos Hammer.js, então primeiro incluímos esta biblioteca em nossa caneta:
Em seguida, declaramos a função associada:
function setSwipeFn(tl, prev, next) { const hammer = new Hammer(tl); hammer.on("swipeleft", () => next.click()); hammer.on("swiperight", () => prev.click()); }
Dentro da função acima, fazemos o seguinte:
- Crie uma instância de Hammer.
- Manipuladores de registro para o
swipeleft
eswiperight
eventos. - Quando deslizamos sobre a linha do tempo na direção esquerda, acionamos um clique para o próximo botão e, assim, a linha do tempo é animada da direita para a esquerda.
- Quando deslizamos sobre a linha do tempo na direção certa, acionamos um clique no botão anterior e, assim, a linha do tempo é animada da esquerda para a direita.
A linha do tempo com suporte para furto:
Adicionando navegação do teclado
Vamos aprimorar ainda mais a experiência do usuário fornecendo suporte para navegação pelo teclado. Nossos objetivos:
- Quando o deixei ou tecla de seta para a direita for pressionado, o documento deve ser rolado para a posição superior da linha do tempo (se outra seção da página estiver visível no momento). Isso garante que toda a linha do tempo seja visível.
- Especificamente, quando o tecla de seta para a esquerda for pressionado, a linha do tempo deve ser animada da esquerda para a direita.
- Da mesma forma, quando o tecla de seta para a direita for pressionado, a linha do tempo deve ser animada da direita para a esquerda.
A função associada é a seguinte:
function setKeyboardFn(prev, next) { document.addEventListener("keydown", (e) => { if ((e.which === 37) || (e.which === 39)) { const timelineOfTop = timeline.offsetTop; const y = window.pageYOffset; if (timelineOfTop !== y) { window.scrollTo(0, timelineOfTop); } if (e.which === 37) { prev.click(); } else if (e.which === 39) { next.click(); } } }); }
A linha do tempo com suporte para teclado:
8. Tornando-se responsivo
Estamos quase terminando! Por último, mas não menos importante, vamos tornar a linha do tempo responsiva. Quando a viewport for menor que 600px, ela deverá ter o seguinte layout empilhado:
Como estamos usando uma abordagem de desktop, aqui estão as regras de CSS que precisamos substituir:
@media screen and (max-width: 599px) { .timeline ol, .timeline ol li { width: auto; } .timeline ol { padding: 0; transform: none !important; } .timeline ol li { display: block; height: auto; background: transparent; } .timeline ol li:first-child { margin-top: 25px; } .timeline ol li:not(:first-child) { margin-left: auto; } .timeline ol li div { width: 94%; height: auto !important; margin: 0 auto 25px; } .timeline ol li:nth-child div { position: static; } .timeline ol li:nth-child(odd) div { transform: none; } .timeline ol li:nth-child(odd) div::before, .timeline ol li:nth-child(even) div::before { left: 50%; top: 100%; transform: translateX(-50%); border: none; border-left: 1px solid white; height: 25px; } .timeline ol li:last-child, .timeline ol li:nth-last-child(2) div::before, .timeline ol li:not(:last-child)::after, .timeline .arrows { display: none; } }
Observação: Para duas das regras acima, tivemos que usar o !important
regra para substituir os estilos inline relacionados aplicados por meio de JavaScript.
O estado final da nossa linha do tempo:
Suporte ao navegador
A demonstração funciona bem em todos os navegadores e dispositivos recentes. Além disso, como você deve ter notado, usamos Babel para compilar nosso código ES6 para ES5.
O único pequeno problema que encontrei durante o teste é a alteração de renderização de texto que acontece quando a linha do tempo está sendo animada. Embora eu tenha tentado várias abordagens propostas em diferentes threads do Stack Overflow, não encontrei uma solução direta para todos os sistemas operacionais e navegadores. Portanto, lembre-se de que você pode ver pequenos problemas de renderização de fonte enquanto a linha do tempo está sendo animada.
Conclusão
Neste tutorial bastante substancial, começamos com uma lista ordenada simples e criamos uma linha do tempo horizontal responsiva. Sem dúvida, cobrimos muitas coisas interessantes, mas espero que você tenha gostado de trabalhar para o resultado final e que tenha ajudado a adquirir novos conhecimentos.
Se você tiver alguma dúvida ou se houver algo que não tenha entendido, deixe-me saber nos comentários abaixo!
Próximos passos
Se você quiser melhorar ainda mais ou estender esse cronograma, aqui estão algumas coisas que você pode fazer:
- Adicione suporte para arrastar. Em vez de clicar nos botões da linha do tempo para navegar, poderíamos simplesmente arrastar a área da linha do tempo. Para esse comportamento, você pode usar a API nativa de arrastar e soltar (que infelizmente não suporta dispositivos móveis no momento da escrita) ou uma biblioteca externa como Draggable.js.
- Melhore o comportamento da linha do tempo à medida que redimensionamos a janela do navegador. Por exemplo, à medida que redimensionamos a janela, os botões devem ser habilitados e desabilitados de acordo.
- Organize o código de uma maneira mais gerenciável. Talvez, use um padrão de design JavaScript comum.
Originally posted 2022-06-24 02:31:33.