Costruire una linea temporale orizzontale con CSS e JavaScript

In un post precedente, ti ho mostrato come costruire una timeline verticale reattiva da zero. Oggi parlerò del processo di creazione degli associati orizzontale sequenza temporale.

Come al solito, per avere un'idea iniziale di ciò che costruiremo, dai un'occhiata alla relativa demo CodePen (controlla la versione più grande per una migliore esperienza):

Abbiamo molto da coprire, quindi iniziamo!

1. Markup HTML

Il markup è identico al markup che abbiamo definito per la timeline verticale, a parte tre piccole cose:

  • Usiamo una lista ordinata invece di una lista non ordinata poiché è più semanticamente corretta.
  • C'è una voce di elenco in più (l'ultima) che è vuota. In una prossima sezione, ne discuteremo la ragione.
  • C'è un elemento in più (ad es. .frecce) Che è responsabile della navigazione nella timeline.

Ecco il markup richiesto:

  1. Alcuni contenuti qui

Lo stato iniziale della timeline è simile a questo:

2. Aggiunta di stili CSS iniziali

Dopo alcuni stili di carattere di base, stili di colore, ecc. Che ho omesso qui per motivi di semplicità, specifichiamo alcune regole CSS strutturali:

.timeline white-space: nowrap; overflow-x: hidden;  .timeline ol dimensione carattere: 0; larghezza: 100vw; imbottitura: 250px 0; transizione: tutti gli 1;  .timeline ol li position: relativo; display: blocco in linea; list-style-type: none; larghezza: 160 px; altezza: 3px; sfondo: #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;

Soprattutto qui, noterai due cose:

  • Assegniamo grandi padding in alto e in basso alla lista. Ancora una volta, spiegheremo perché ciò accade nella prossima sezione. 
  • Come noterai nella demo seguente, a questo punto non possiamo vedere tutte le voci dell'elenco perché l'elenco ha larghezza: 100vw e il suo genitore ha overflow-x: nascosto. Questo effettivamente "maschera" le voci dell'elenco. Grazie alla navigazione nella timeline, tuttavia, saremo in grado di navigare tra gli elementi in seguito.

Con queste regole in atto, ecco lo stato attuale della timeline (senza alcun contenuto effettivo, per tenere le cose chiare):

3. Stili degli elementi della timeline

A questo punto modelleremo il div elementi (li chiameremo "elementi della timeline" d'ora in poi) che fanno parte delle voci dell'elenco e delle loro ::prima pseudo-elementi.

Inoltre, useremo il : Nth-child (dispari) e : Nth-child (anche) CSS pseudo-classi per differenziare gli stili per le div varie e dispari.

Ecco gli stili comuni per gli elementi della timeline:

.timeline ol li div position: absolute; a sinistra: calc (100% + 7px); larghezza: 280 px; imbottitura: 15px; font-size: 1rem; spazio bianco: normale; colore nero; sfondo: bianco;  .timeline ol li div :: before content: "; position: absolute; top: 100%; left: 0; width: 0; height: 0; border-style: solid;

Quindi alcuni stili per quelli dispari:

.timeline ol li: nth-child (dispari) div sopra: -16px; transform: translateY (-100%);  .timeline ol li: nth-child (dispari) div :: before top: 100%; border-width: 8px 8px 0 0; border-color: trasparente trasparente trasparente bianco; 

E infine alcuni stili per quelli pari:

.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: trasparente trasparente bianco trasparente; 

Ecco il nuovo stato della timeline, con il contenuto aggiunto di nuovo:

Come probabilmente avrai notato, gli elementi della timeline sono posizionati in modo assoluto. Ciò significa che vengono rimossi dal normale flusso di documenti. Con questo in mente, al fine di garantire che l'intera timeline appaia, dobbiamo impostare grandi valori di riempimento superiore e inferiore per l'elenco. Se non applichiamo alcun paddings, la timeline verrà ritagliata:

4. Stili di navigazione timeline

È ora di modellare i pulsanti di navigazione. Ricorda che per impostazione predefinita disabilitiamo la freccia precedente e assegniamo la classe di Disabilitato.

Ecco gli stili CSS associati:

.linea temporale. frecce display: flex; justify-content: center; margin-bottom: 20px;  .timeline .arrows .arrow__prev margin-right: 20px;  .timeline .disabled opacity: .5;  .timeline .arrows img width: 45px; altezza: 45px; 

Le regole sopra ci danno questa linea temporale:

5. Aggiunta di interattività

La struttura di base della timeline è pronta. Aggiungiamo un po 'di interattività ad esso!

variabili

Per prima cosa, impostiamo un gruppo di variabili che useremo in seguito. 

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";

Inizializzare le cose

Quando tutte le risorse della pagina sono pronte, il dentro la funzione è chiamata.

window.addEventListener ("load", init);

Questa funzione attiva quattro funzioni secondarie:

function init () setEqualHeights (elH); animateTl (xScrolling, arrows, timeline); setSwipeFn (timeline, arrowPrev, arrowNext); setKeyboardFn (arrowPrev, arrowNext); 

Come vedremo tra un momento, ognuna di queste funzioni svolge un determinato compito.

Elementi di cronologia dell'altezza uguale

Se torni all'ultima demo, noterai che gli elementi della timeline non hanno altezze uguali. Ciò non influisce sulla funzionalità principale della nostra timeline, ma potresti preferirlo se tutti gli elementi avessero la stessa altezza. Per ottenere questo, possiamo dare loro un'altezza fissa tramite CSS (soluzione facile) o un'altezza dinamica che corrisponde all'altezza dell'elemento più alto tramite JavaScript.

La seconda opzione è più flessibile e stabile, quindi ecco una funzione che implementa questo 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 = '$counterpx';  

Questa funzione recupera l'altezza dell'elemento timeline più alto e lo imposta come altezza predefinita per tutti gli elementi.

Ecco come la demo sta guardando:

6. Animazione della linea temporale

Ora concentriamoci sull'animazione della timeline. Costruiremo la funzione che implementa questo comportamento passo dopo passo.

Innanzitutto, registriamo un listener di eventi click per i pulsanti della timeline:

funzione animateTl (scrolling, el, tl) for (let i = 0; i < el.length; i++)  el[i].addEventListener("click", function()  // code here );  

Ogni volta che si fa clic su un pulsante, controlliamo lo stato disabilitato dei pulsanti della timeline e, se non sono disattivati, li disabilitiamo. Ciò garantisce che entrambi i pulsanti vengano cliccati una sola volta fino al termine dell'animazione.

Quindi, in termini di codice, inizialmente il gestore di clic contiene queste righe:

if (! arrowPrev.disabled) arrowPrev.disabled = true;  if (! arrowNext.disabled) arrowNext.disabled = true; 

I prossimi passi sono i seguenti:

  • Controlliamo per vedere se è la prima volta che clicchiamo su un pulsante. Ancora una volta, tieni presente che il precedente il pulsante è disabilitato per impostazione predefinita, quindi l'unico pulsante su cui è possibile fare clic inizialmente è il Il prossimo uno.
  • Se davvero è la prima volta, usiamo il trasformare proprietà per spostare la timeline a 280 px a destra. Il valore del xScrolling variabile determina la quantità di movimento. 
  • Al contrario, se abbiamo già fatto clic su un pulsante, recuperiamo la corrente trasformare valore della timeline e aggiungere o rimuovere quel valore, la quantità di movimento desiderata (ad esempio 280px). Quindi, finché clicchiamo su precedente pulsante, il valore del trasformare la proprietà diminuisce e la timeline viene spostata da sinistra a destra. Tuttavia, quando il Il prossimo viene cliccato il pulsante, il valore di trasformare la proprietà aumenta e la timeline viene spostata da destra a sinistra.

Il codice che implementa questa funzionalità è il seguente:

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(-$scrollingpx)';  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($valuespx)';  counter++; ); 

Ottimo lavoro! Abbiamo appena definito un modo di animare la timeline. La prossima sfida è capire quando questa animazione dovrebbe fermarsi. Ecco il nostro approccio:

  • Quando il primo elemento della timeline diventa completamente visibile, significa che abbiamo già raggiunto l'inizio della timeline, e quindi disabilitiamo il precedente pulsante. Garantiamo inoltre che il Il prossimo il pulsante è abilitato.
  • Quando l'ultimo elemento diventa completamente visibile, significa che abbiamo già raggiunto la fine della timeline, e quindi disabilitiamo il Il prossimo pulsante. Inoltre, assicuriamo anche che il precedente il pulsante è abilitato.

Ricorda che l'ultimo elemento è vuoto con larghezza uguale alla larghezza degli elementi della timeline (ad esempio 280px). Diamo questo valore (o uno più alto) perché vogliamo essere sicuri che l'ultimo elemento della timeline sia visibile prima di disabilitare il Il prossimo pulsante.

Per scoprire se gli elementi target sono completamente visibili nella finestra corrente o no, trarremo vantaggio dallo stesso codice che abbiamo usato per la timeline verticale. Il codice richiesto che proviene da questo thread di overflow dello stack è il seguente:

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) ); 

Oltre la funzione sopra, definiamo un altro helper:

function setBtnState (el, flag = true) if (flag) el.classList.add (disabledClass);  else if (el.classList.contains (disabledClass)) el.classList.remove (disabledClass);  el.disabled = false; 

Questa funzione aggiunge o rimuove il Disabilitato classe da un elemento basato sul valore del bandiera parametro. Inoltre, può modificare lo stato disabilitato per questo elemento.

Considerato ciò che abbiamo descritto sopra, ecco il codice che definiamo per verificare se l'animazione debba fermarsi o meno:

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); // altro codice qui); 

Si noti che c'è un ritardo di 1,1 secondi prima di eseguire questo codice. Perché questo succede?

Se torniamo al nostro CSS, vedremo questa regola:

.timeline ol transition: all 1s; 

Quindi, l'animazione della timeline ha bisogno di 1 secondo per essere completata. Finché completa, aspettiamo 100 millisecondi e poi eseguiamo i nostri controlli.

Ecco la timeline con le animazioni:

7. Aggiunta del supporto Swipe

Finora, la cronologia non risponde agli eventi tattili. Sarebbe bello se potessimo aggiungere questa funzionalità. Per realizzarlo, possiamo scrivere la nostra implementazione JavaScript o utilizzare una delle librerie correlate (ad esempio Hammer.js, TouchSwipe.js) esistenti là fuori.

Per la nostra demo, manterremo tutto questo semplice e useremo Hammer.js, quindi per prima cosa includiamo questa libreria nella nostra penna:

Quindi dichiariamo la funzione associata:

function setSwipeFn (tl, prev, next) const hammer = new Hammer (tl); hammer.on ("swipeleft", () => next.click ()); hammer.on ("swiperight", () => prev.click ()); 

All'interno della funzione sopra, facciamo quanto segue:

  • Crea un'istanza di Hammer. 
  • Registrati gestori per scorrere verso sinistra e swiperight eventi. 
  • Quando scorri la timeline nella direzione sinistra, attiviamo un clic sul pulsante successivo, quindi la sequenza temporale viene animata da destra a sinistra.
  • Quando facciamo scorrere la timeline nella direzione corretta, attiviamo un clic sul pulsante precedente, quindi la timeline viene animata da sinistra a destra.

La timeline con supporto di scorrimento:

Aggiunta della navigazione tramite tastiera

Miglioriamo ulteriormente l'esperienza utente fornendo supporto per la navigazione da tastiera. I nostri obiettivi:

  • Quando il sinistra o tasto freccia destra viene premuto, il documento deve essere spostato nella posizione superiore della timeline (se una sezione di un'altra pagina è attualmente visibile). Ciò garantisce che l'intera timeline sia visibile.
  • In particolare, quando il tasto freccia sinistra viene premuto, la timeline dovrebbe essere animata da sinistra a destra.
  • Allo stesso modo, quando il tasto freccia destra viene premuto, la timeline dovrebbe essere animata da destra a sinistra.

La funzione associata è la seguente:

function setKeyboardFn (precedente, successiva) 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 ();); 

La timeline con supporto per tastiera:

8. Going Responsive

Abbiamo quasi finito! Ultimo ma non meno importante, rendiamo la timeline reattiva. Quando il viewport è inferiore a 600 px, dovrebbe avere il seguente layout impilato:

Poiché stiamo utilizzando un approccio desktop-first, ecco le regole CSS che dobbiamo sovrascrivere:

@media screen e (max-width: 599px) .timeline ol, .timeline ol li width: auto;  .timeline ol padding: 0; trasformare: nessuno! importante;  .timeline ol li display: block; altezza: auto; sfondo: trasparente;  .timeline ol li: first-child margin-top: 25px;  .timeline ol li: not (: first-child) margin-left: auto;  .timeline ol li div width: 94%; altezza: auto! importante; margine: 0 auto 25px;  .timeline ol li: nth-child div position: static;  .timeline ol li: nth-child (dispari) div transform: none;  .timeline ol li: nth-child (dispari) div :: before, .timeline ol li: nth-child (even) div :: before left: 50%; i primi 100%; transform: translateX (-50%); confine: nessuno; border-left: 1px solido bianco; altezza: 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 : nessuno; 

Nota: Per due delle regole sopra, abbiamo dovuto usare il !importante regola per sovrascrivere gli stili in linea correlati applicati tramite JavaScript. 

Lo stato finale della nostra cronologia:

Supporto del browser

La demo funziona bene in tutti i browser e dispositivi recenti. Inoltre, come forse avrete notato, usiamo Babel per compilare il nostro codice ES6 fino a ES5.

L'unico piccolo problema che ho riscontrato durante il test è la modifica del rendering del testo che si verifica quando la sequenza temporale viene animata. Sebbene abbia provato vari approcci proposti in diversi thread di Overflow dello stack, non ho trovato una soluzione semplice per tutti i sistemi operativi e browser. Quindi, tieni presente che potresti vedere piccoli problemi di rendering dei caratteri mentre la sequenza temporale viene animata.

Conclusione

In questo tutorial abbastanza sostanziale, abbiamo iniziato con una semplice lista ordinata e abbiamo creato una timeline orizzontale reattiva. Senza dubbio, abbiamo coperto molte cose interessanti, ma spero che ti sia piaciuto lavorare per il risultato finale e che ti ha aiutato ad acquisire nuove conoscenze.

Se hai domande o se c'è qualcosa che non hai capito, fammelo sapere nei commenti qui sotto!

Prossimi passi

Se vuoi migliorare ulteriormente o estendere questa linea temporale, qui ci sono alcune cose che puoi fare:

  • Aggiungi supporto per il trascinamento. Invece di fare clic sui pulsanti della timeline per navigare, potremmo semplicemente trascinare l'area della timeline. Per questo comportamento, è possibile utilizzare l'API di trascinamento e rilascio nativa (che purtroppo non supporta i dispositivi mobili al momento della scrittura) o una libreria esterna come Draggable.js.
  • Migliora il comportamento della timeline mentre ridimensioniamo la finestra del browser. Ad esempio, mentre ridimensioniamo la finestra, i pulsanti dovrebbero essere abilitati e disabilitati di conseguenza.
  • Organizza il codice in un modo più gestibile. Forse, usa un modello di progettazione JavaScript comune.