Construire une timeline horizontale avec CSS et JavaScript

Dans un précédent article, je vous ai montré comment créer un scénario vertical réactif à partir de zéro. Aujourd’hui, je couvrirai le processus de création des horizontal chronologie.

Comme d'habitude, pour avoir une première idée de ce que nous allons construire, jetez un œil à la démo correspondante de CodePen (consultez la version plus grande pour une meilleure expérience):

Nous avons beaucoup à couvrir, alors commençons!

1. Balisage HTML

Le balisage est identique au balisage que nous avons défini pour la chronologie verticale, à l'exception de trois petites choses:

  • Nous utilisons une liste ordonnée au lieu d'une liste non ordonnée, car c'est sémantiquement correct.
  • Il y a un élément de liste supplémentaire (le dernier) qui est vide. Dans une prochaine section, nous discuterons de la raison.
  • Il y a un élément supplémentaire (i.e. .flèches) Responsable de la navigation dans la timeline.

Voici le balisage requis:

  1. Un peu de contenu ici

L'état initial de la timeline ressemble à ceci:

2. Ajouter les styles CSS initiaux

Après quelques styles de police de base, styles de couleur, etc. que j'ai omis ici par souci de simplicité, nous spécifions quelques règles CSS structurelles:

.timeline white-space: nowrap; débordement-x: caché;  .timeline ol taille de la police: 0; largeur: 100vw; rembourrage: 250px 0; transition: tous les 1;  .timeline ol li position: relative; affichage: inline-block; type de style de liste: aucun; largeur: 160px; hauteur: 3px; background: #fff;  .timeline ol li: last-child width: 280px;  .timeline ol li: not (: premier-enfant) margin-left: 14px;  .timeline ol li: not (: last-child) :: after content: "; position: absolute; haut: 50%; gauche: calc (100% + 1px); bas: 0; largeur: 12px; hauteur: 12px; transformation: translation (-50%); bordure-rayon: 50%; arrière-plan: # F45B69;

Le plus important ici, vous remarquerez deux choses:

  • Nous affectons de larges marges au haut et au bas de la liste. Encore une fois, nous expliquerons pourquoi cela se produit dans la section suivante. 
  • Comme vous le constaterez dans la démo suivante, nous ne pouvons pas voir tous les éléments de la liste car cette liste a largeur: 100vw et son parent a overflow-x: caché. Cela «masque» efficacement les éléments de la liste. Grâce à la navigation dans la timeline, nous pourrons naviguer ultérieurement dans les éléments..

Avec ces règles en place, voici l'état actuel de la timeline (sans contenu réel, pour que tout soit clair):

3. Styles d'éléments de la timeline

À ce stade, nous appelons la div éléments (nous les appellerons désormais "éléments de chronologie") qui font partie des éléments de la liste ainsi que leurs ::avant pseudo-éléments.

De plus, nous utiliserons le : nième enfant (impair) et : nième enfant (pair) Pseudo-classes CSS pour différencier les styles des divs pairs et impairs.

Voici les styles courants pour les éléments de scénario:

.ligne de temps ol li div position: absolute; à gauche: calc (100% + 7px); largeur: 280px; rembourrage: 15px; taille de police: 1rem; espace blanc: normal; la couleur noire; fond blanc;  .timeline ol li div :: before content: "; position: absolute; top: 100%; left: 0; width: 0; height: 0; border-style: solid;

Puis quelques styles pour les plus étranges:

.ligne de temps ol: nth-child (impair) div top: -16px; transformer: traductionY (-100%);  .timeline ol li: nth-child (impair) div :: before top: 100%; largeur de bordure: 8px 8px 0 0; couleur de bordure: blanc transparent transparent transparent; 

Et enfin quelques styles pour les paires:

.timeline ol li: nth-child (pair) div top: calc (100% + 16 pixels);  .timeline ol li: n-enfant (pair) div :: before top: -8px; largeur de la bordure: 8px 0 0 8px; couleur de bordure: transparent transparent blanc transparent; 

Voici le nouvel état de la timeline, avec du contenu ajouté à nouveau:

Comme vous l'avez probablement remarqué, les éléments de la chronologie sont absolument positionnés. Cela signifie qu'ils sont supprimés du flux de documents normal. Dans cet esprit, afin de garantir l’apparition de l’ensemble de la chronologie, nous devons définir des valeurs de remplissage supérieures et inférieures importantes pour la liste. Si nous n'appliquons aucun rembourrage, la chronologie sera recadrée:

4. Styles de navigation dans la timeline

Il est maintenant temps de styliser les boutons de navigation. Rappelez-vous que par défaut nous désactivons la flèche précédente et lui donnons la classe de désactivée.

Voici les styles CSS associés:

.timeline .arrows display: flex; justifier-contenu: centre; marge inférieure: 20 px;  .timeline .arrows .arrow__prev margin-right: 20px;  .timeline .disabled opacity: .5;  .timeline .arrows img width: 45px; hauteur: 45px; 

Les règles ci-dessus nous donnent cette chronologie:

5. Ajouter de l'interactivité

La structure de base de la timeline est prête. Ajoutons-y de l'interactivité!

Variables

Tout d'abord, nous mettons en place un ensemble de variables que nous utiliserons plus tard. 

const timeline = document.querySelector (". timeline ol"), elH = document.querySelectorAll (". timeline li> div"), flèches = document.querySelectorAll (". timeline .arrows .arrow"), arrowPrev = document.querySelector (".timeline .arrows .arrow__prev"), arrowNext = document.querySelector (". timeline .arrows .arrow__next"), firstItem = document.querySelector (". timeline li: premier-enfant"), lastItem = document.querySelector ( ".timeline li: last-child"), xScrolling = 280, disabledClass = "disabled";

Initialisation des choses

Lorsque tous les éléments de la page sont prêts, le init la fonction s'appelle.

window.addEventListener ("load", init);

Cette fonction déclenche quatre sous-fonctions:

fonction init () setEqualHeights (elH); animateTl (xScrolling, flèches, chronologie); setSwipeFn (timeline, arrowPrev, arrowNext); setKeyboardFn (arrowPrev, arrowNext); 

Comme nous le verrons dans un moment, chacune de ces fonctions accomplit une certaine tâche.

Eléments de la timeline à hauteur égale

Si vous revenez à la dernière démo, vous remarquerez que les éléments de la timeline n'ont pas la même hauteur. Cela n'affecte pas les fonctionnalités principales de notre scénario, mais vous pouvez le préférer si tous les éléments ont la même hauteur. Pour ce faire, nous pouvons leur donner soit une hauteur fixe via CSS (solution facile), soit une hauteur dynamique qui correspond à la hauteur de l'élément le plus haut via JavaScript..

La deuxième option est plus souple et stable, voici donc une fonction qui implémente ce comportement:

fonction setEqualHeights (el) let compteur = 0; pour (soit 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';  

Cette fonction récupère la hauteur du plus haut élément de scénario et la définit comme hauteur par défaut pour tous les éléments..

Voici à quoi ressemble la démo:

6. Animation de la chronologie

Passons maintenant à l'animation de la timeline. Nous allons construire la fonction qui implémente ce comportement étape par étape.

Tout d'abord, nous enregistrons un écouteur d'événements de clic pour les boutons de la timeline:

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

Chaque fois qu'un bouton est cliqué, nous vérifions l'état désactivé des boutons de la timeline et s'ils ne le sont pas, nous les désactivons. Cela garantit que les deux boutons ne seront cliqués qu'une fois jusqu'à la fin de l'animation.

Ainsi, en termes de code, le gestionnaire de clics contient initialement ces lignes:

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

Les prochaines étapes sont les suivantes:

  • Nous vérifions si c'est la première fois que nous avons cliqué sur un bouton. Encore une fois, gardez à l’esprit que le précédent bouton est désactivé par défaut, le seul bouton sur lequel vous pouvez cliquer initialement est le bouton suivant un.
  • Si en effet c'est la première fois, nous utilisons le transformer propriété pour déplacer la timeline 280px vers la droite. La valeur de la xScrolling variable détermine la quantité de mouvement. 
  • Au contraire, si nous avons déjà cliqué sur un bouton, nous récupérons le courant transformer valeur de la ligne de temps et ajoutez ou supprimez à cette valeur la quantité de mouvement souhaitée (c'est-à-dire 280 pixels). Donc, tant que nous cliquons sur le précédent bouton, la valeur de la transformer propriété diminue et la timeline est déplacée de gauche à droite. Cependant, lorsque le suivant le bouton est cliqué, la valeur du transformer la propriété augmente et la timeline est déplacée de droite à gauche.

Le code qui implémente cette fonctionnalité est le suivant:

laisser compteur = 0; pour (soit 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++; ); 

Bon travail! Nous venons de définir un moyen d'animer la chronologie. Le prochain défi consiste à déterminer quand cette animation doit s'arrêter. Voici notre approche:

  • Lorsque le premier élément de scénario devient pleinement visible, cela signifie que nous avons déjà atteint le début du scénario et nous désactivons donc le précédent bouton. Nous nous assurons également que le suivant bouton est activé.
  • Lorsque le dernier élément devient pleinement visible, cela signifie que nous avons déjà atteint la fin de la chronologie et nous désactivons donc le suivant bouton. Nous veillons également à ce que le précédent bouton est activé.

N'oubliez pas que le dernier élément est un élément vide dont la largeur est égale à la largeur des éléments de scénario (c'est-à-dire 280 pixels). Nous lui donnons cette valeur (ou une valeur supérieure) car nous voulons nous assurer que le dernier élément de scénario sera visible avant de désactiver le suivant bouton.

Pour détecter si les éléments cibles sont entièrement visibles ou non dans la fenêtre d'affichage actuelle, nous allons tirer parti du même code que celui utilisé pour la timeline verticale. Le code requis qui provient de ce thread Stack Overflow est le suivant:

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

Au-delà de la fonction ci-dessus, nous définissons un autre assistant:

fonction setBtnState (el, indicateur = vrai) if (indicateur) el.classList.add (disabledClass);  else if (el.classList.contains (disabledClass)) el.classList.remove (disabledClass);  el.disabled = false; 

Cette fonction ajoute ou supprime le désactivée classe d'un élément basé sur la valeur de la drapeau paramètre. De plus, il peut changer l'état désactivé pour cet élément.

Compte tenu de ce que nous avons décrit ci-dessus, voici le code que nous définissons pour vérifier si l'animation doit s'arrêter ou non:

pour (soit 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); // autre code ici); 

Notez qu'il y a un délai de 1,1 seconde avant l'exécution de ce code. Pourquoi cela arrive-t-il?

Si nous revenons à notre CSS, nous verrons cette règle:

.timeline ol transition: tous les 1; 

Ainsi, l'animation de la chronologie nécessite 1 seconde. Tant que la procédure est terminée, nous attendons 100 millisecondes, puis nous effectuons nos vérifications..

Voici la chronologie avec des animations:

7. Ajout du support de balayage

Jusqu'à présent, la chronologie ne répond pas aux événements tactiles. Ce serait bien si nous pouvions ajouter cette fonctionnalité. Pour ce faire, nous pouvons écrire notre propre implémentation JavaScript ou utiliser l’une des bibliothèques associées (par exemple Hammer.js, TouchSwipe.js) qui existent déjà..

Pour notre démo, nous allons garder cela simple et utiliser Hammer.js, donc d’abord, nous incluons cette bibliothèque dans notre stylo:

Ensuite, nous déclarons la fonction associée:

fonction setSwipeFn (tl, précédente, suivante) const hammer = new Hammer (tl); hammer.on ("swipeleft", () => next.click ()); hammer.on ("swiperight", () => prev.click ()); 

Dans la fonction ci-dessus, nous procédons comme suit:

  • Créer une instance de Hammer. 
  • Enregistrer les gestionnaires pour le swipeleft et swiperight événements. 
  • Lorsque nous glissons sur la timeline dans la direction de gauche, nous déclenchons un clic sur le bouton suivant. La timeline est ainsi animée de droite à gauche..
  • Lorsque nous glissons sur la timeline dans la bonne direction, nous déclenchons un clic sur le bouton précédent. La timeline est animée de gauche à droite..

La chronologie avec prise en charge du balayage:

Ajout de la navigation au clavier

Améliorons encore l'expérience utilisateur en fournissant un support pour la navigation au clavier. Nos buts:

  • Quand le la gauche ou touche flèche droite est enfoncé, le document doit défiler en haut de la timeline (si une autre section de la page est actuellement visible). Cela garantit que toute la chronologie sera visible.
  • Plus précisément, lorsque le flèche gauche est pressé, la timeline devrait être animée de gauche à droite.
  • De même, lorsque le touche flèche droite est pressé, la timeline devrait être animée de droite à gauche.

La fonction associée est la suivante:

fonction 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 (); sinon if ( e.which === 39) next.click ();); 

La timeline avec support du clavier:

8. Être réactif

Nous avons presque terminé! Dernier point mais non le moindre, faisons en sorte que le calendrier soit réactif. Lorsque la fenêtre d’affichage mesure moins de 600 pixels, la disposition empilée suivante doit apparaître:

Comme nous utilisons une approche "bureau d'abord", voici les règles CSS que nous devons écraser:

Écran @média et (largeur maximale: 599 pixels) . olimine., olimine. ol li largeur: auto;  .timeline ol padding: 0; transformer: aucun! important;  .timeline ol li display: block; hauteur: auto; fond: transparent;  .timeline ol li: premier-enfant margin-top: 25px;  .timeline ol li: not (: premier-enfant) margin-left: auto;  .timeline ol li div width: 94%; hauteur: auto! important; marge: 0 auto 25 px;  .timeline ol li: nth-child div position: static;  .timeline ol li: nième enfant (impair) div transform: none;  .timeline ol: nth-child (impair) div :: before,. timeline ol: nth-child (pair) div :: before Top 100%; transformer: translateX (-50%); bordure: aucune; bordure gauche: 1px blanc solide; hauteur: 25px;  .timeline avec li: last-child, .timeline avec li: nth-last-child (2) div :: before,. : aucun; 

Remarque: Pour deux des règles ci-dessus, nous avons dû utiliser le !important règle pour remplacer les styles en ligne associés appliqués via JavaScript. 

L'état final de notre chronologie:

Prise en charge du navigateur

La démo fonctionne bien dans tous les navigateurs et appareils récents. En outre, comme vous l'avez peut-être remarqué, nous utilisons Babel pour compiler notre code ES6 jusqu'à ES5..

Le seul petit problème que j'ai rencontré lors du test est la modification du rendu du texte qui survient lorsque la timeline est animée. Bien que j'aie essayé diverses approches proposées dans différents threads Stack Overflow, je n'ai pas trouvé de solution simple pour tous les systèmes d'exploitation et tous les navigateurs. Alors, gardez à l'esprit que vous pourriez rencontrer de petits problèmes de rendu des polices lorsque la timeline est animée..

Conclusion

Dans ce tutoriel assez important, nous avons commencé avec une simple liste ordonnée et créé un scénario horizontal réactif. Sans doute, nous avons couvert beaucoup de choses intéressantes, mais j'espère que vous avez aimé travailler pour le résultat final et que cela vous a aidé à acquérir de nouvelles connaissances..

Si vous avez des questions ou s'il y a quelque chose que vous n'avez pas compris, faites le moi savoir dans les commentaires ci-dessous!

Prochaines étapes

Si vous souhaitez améliorer ou prolonger ce délai, voici quelques solutions:

  • Ajouter un support pour glisser. Au lieu de cliquer sur les boutons de la timeline pour naviguer, nous pourrions simplement faire glisser la zone de la timeline. Pour ce comportement, vous pouvez utiliser l'API native Drag and Drop (qui malheureusement ne prend pas en charge les périphériques mobiles au moment de l'écriture) ou une bibliothèque externe telle que Draggable.js..
  • Améliorez le comportement de la timeline en redimensionnant la fenêtre du navigateur. Par exemple, lorsque nous redimensionnons la fenêtre, les boutons doivent être activés et désactivés en conséquence..
  • Organisez le code de manière plus gérable. Peut-être utiliser un modèle de conception JavaScript commun.