6. Cas concret : navigation par onglets▲
Avant d'aller plus loin dans l'étude de jQuery, nous allons mettre en place un exemple concret que nous allons faire évoluer dans les prochains chapitres.
La page d'exemple (il s'agit en fait d'un extrait de page, qui serait supposé être intégré à une page plus complète) montre la mise en place avec jQuery d'un système de navigation par onglets.
Le but est simple : plutôt que d'afficher toutes les informations sur la page, ce qui aurait un aspect un peu austère et pourrait rebuter certains visiteurs, nous choisissons de n'en afficher qu'une partie et de mettre des pseudos liens de navigation pour permettre de visualiser les autres.
La page se compose donc d'une barre de liens (ou d'onglets) et d'une partie contenu. Notez que l'ensemble du contenu est en fait présent sur la page, c'est uniquement le clic sur un onglet qui déterminera quelles parties seront réellement affichées ou non.
D'autre part, bien qu'il soit de nos jours fantaisiste de considérer qu'il existe une part significative de visiteurs qui choisissent intentionnellement de désactiver JavaScript, des impératifs d'accessibilité voire de référencement peuvent imposer de rendre disponible l'ensemble du contenu utile si c'était le cas. Nous avons donc opté pour montrer un exemple de la pratique d'amélioration progressive, c'est-à-dire que si JavaScript est désactivé, la barre d'onglets (qui n'est pas du contenu utile) sera masquée et tout le contenu sera affiché. C'est jQuery qui se chargera de rétablir l'affichage « normal ». Pour être réellement complet, il serait bon aussi de gérer la position en relative et le décalage vers le haut (top : -6px ;) de la balise #contenu au chargement de la page pour éviter le risque de masquer le bas de la partie située au-dessus.
Enfin, nous avons affecté des styles CSS (en se fixant comme contrainte de ne pas utiliser d'image) pour rendre l'ensemble un peu moderne. Il n'est pas pour autant question de parler ici de design (les webdesigners ne s'en remettraient pas !), mais juste d'un début de mise en page minimaliste. Ici aussi, la volonté a été d'utiliser les styles CSS selon une optique d'amélioration progressive : tout est disponible pour qu'un utilisateur ayant un navigateur à jour puisse profiter de l'ergonomie et du visuel optimum, si tel n'est pas le cas, alors certains styles ne pourront pas être rendus, mais la page continuera d'être fonctionnelle.
Notez que le modèle de boîte d'Internet Explorer jusqu'à la version 7 génère un décalage entre les onglets et le contenu. Le but ici n'étant pas de détailler le code CSS, nous ne règlerons pas ce bogue (il faudrait, pour ces navigateurs, augmenter le décalage vers le haut de 15 pixels supplémentaires), d'autant qu'Internet Explorer 6 et 7 sont en voie de disparition.
Le code de la page est le suivant (le contenu en lui-même de chaque onglet a été supprimé, il s'agit de fragments de Lorem Ipsum, du texte généré automatiquement pour remplir des maquettes) :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8"/>
<title>Onglets</title>
<style type="text/css">
#onglets{
display: none;
}
#onglets li{
position: relative;
float: left;
list-style: none;
padding: 2px 5px 7px;
margin-right: 5px;
border: 1px solid #1175AE;
cursor: pointer;
background-color: #EEEEEE;
color: #0D5995;
z-index: 1;
}
#onglets .actif{
border-bottom: none;
font-weight: bold;
z-index: 10;
}
#contenu{
clear: both;
position: relative;
margin: 0 20px;
padding: 10px;
border: 5px solid #0D5995;
z-index: 5;
top: -6px;
background-color: #EEEEEE;
color: #0F67A1;
width: 500px;
overflow: hidden;
border-radius: 15px;
}
</style>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
$(function() {
$('#onglets').css('display', 'block');
$('#onglets').click(function(event) {
var actuel = event.target;
if (!/li/i.test(actuel.nodeName) ||
actuel.className.indexOf('actif') > -1) {
return;
}
$(actuel).addClass('actif').siblings().removeClass('actif');
setDisplay();
});
function setDisplay() {
var modeAffichage;
$('#onglets li').each(function(rang) {
modeAffichage = $(this).hasClass('actif') ? '' :'none';
$('.item').eq(rang).css('display', modeAffichage);
});
}
setDisplay();
});
</script>
</head>
<body>
<h1>Navigation par onglets</h1>
<ul id="onglets">
<li class="actif">Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<div id="contenu">
<div class="item">
<h2>Premier onglet</h2>
...
</div>
<div class="item">
<h2>Deuxième onglet</h2>
...
</div>
<div class="item">
<h2>Troisième onglet</h2>
...
</div>
</div>
</body>
</html>
6-1. Le HTML▲
Le code HTML de la page est relativement simple. En dehors du titre de la page, il est composé des deux éléments principaux : les onglets, sous forme de liste non ordonnée <ul> et le contenu des onglets, inséré dans une <div> globale, chaque sous-ensemble étant lui aussi intégré dans une balise <div> comprenant un titre <h2> et du texte. Pour l'affectation des styles ainsi que pour faciliter l'utilisation de jQuery, les deux parties principales possèdent des identifiants, les éléments de contenu possèdent eux une classe commune.
6-2. Le CSS▲
Le style de la liste #onglet définit la propriété display à none pour rendre invisible les onglets si JavaScript est désactivé.
Pour les éléments de la liste, la position fixée à relative va permettre d'utiliser la propriété z-index pour gérer l'empilement des éléments, en particulier l'effet de suppression de la bordure du contenu. La propriété float annule le mode d'affichage vertical par défaut et aligne les onglets horizontalement, de même, list-style fixé à none permet de ne pas afficher de puce. Le padding permet d'agrandir l'espace autour du texte et le margin-right de décaler les onglets les uns par rapport aux autres. S'agissant d'éléments à cliquer, la propriété cursor permet de le signaler en modifiant l'image du pointeur de la souris lors du survol.
Une classe particulière pour les éléments de liste concernera l'onglet en cours d'affichage. Dans ce cas, on supprime la bordure inférieure (il aurait été possible de la supprimer pour tous les onglets, mais cela aurait nécessité de multiplier les déclarations pour chaque côté des bordures, de cette façon, le rendu est identique et simplifié). On applique un style spécifique (texte en gras) et surtout, on indique que cet onglet doit se trouver au-dessus de la pile des calques avec un z-index plus élevé que les autres..
La <div> #contenu possède elle aussi une position en relative, cela va permettre de la décaler vers le haut et de la superposer aux onglets sur quelques pixels (propriété top). Si aucune bordure n'est présente, les éléments seront collés, il faut donc la décaler de l'épaisseur de la bordure de la <div> #content plus de celle de la liste #onglets, soit 6 pixels. En fonction de la valeur du z-index de ceux-ci, ils seront positionnés en dessous ou au-dessus dans la pile d'affichage, ce qui permet de masquer ou non la bordure supérieure et de donner l'impression d'association entre l'onglet et le contenu affiché. La propriété margin permet de décaler la balise par rapport au bord de la page, padding décale le texte par rapport aux bords de la balise. Les angles sont arrondis avec la propriété border-radius (si elle est prise en charge par le navigateur). La propriété overflow, permet de masquer le contenu qui dépasse de l'espace alloué à la balise. Elle sera utile ultérieurement lorsque nous ajouterons des effets. Enfin, la propriété clear permet d'annuler l'effet du float de l'élément précédent et de rétablir un flux normal.
6-3. Le script▲
Le script jQuery pour gérer l'affichage du contenu des onglets est particulièrement court. Si nous n'avions pas utilisé jQuery, le code aurait été beaucoup plus long, mais aussi beaucoup plus complexe à comprendre et donc à reprendre ultérieurement. Grâce à jQuery, seule la partie gérant réellement le comportement est codée, le reste (il s'agit souvent de boucles et de conditions, que ce soit sur les éléments à traiter ou les capacités du navigateur) est géré par la bibliothèque. En résumé, vous n'avez qu'à vous préoccuper de l'aspect « métier ».
La première ligne : $(function(){…} comme ce sera le plus souvent le cas, est destinée à faire patienter jQuery jusqu'à ce que le DOM soit chargé (le DOM, pas la page, c'est-à-dire qu'ici, s'il y avait des images à afficher, jQuery n'attendrait pas leur chargement intégral avant de commencer à travailler).
Une fois le DOM prêt à être utilisé, la première action va être d'afficher la barre d'onglets :
$('#onglets').css('display', 'block');
pour pouvoir commencer à naviguer. L'intérêt de procéder de la sorte, en dehors de rendre l'ensemble de la page accessible sans JavaScript, est que même si le chargement de la page était long, il ne serait pas possible de naviguer avant que la fonctionnalité ne soit en place et utilisable.
On met ensuite en place le gestionnaire d'événements pour le clic sur chaque onglet :
$('#onglets').click(function(event) {...}
Vous noterez que l'on ne va pas placer un gestionnaire sur chacun des onglets, mais sur la barre d'onglets (balise <ul>) elle-même. On peut considérer que cela reviendrait au même de le faire sur les balises <li>, car l'instruction serait quasiment la même. En fait, le gain ne se fait pas au niveau de la taille du code, mais de la façon dont il va fonctionner. Même s'il suffit d'une seule instruction pour affecter le gestionnaire sur chaque item de la liste, cela nécessite malgré tout de mettre en réalité autant de gestionnaires que d'onglets. En plaçant le gestionnaire sur la balise <ul>, il n'y aura qu'un seul événement géré par JavaScript, on peut donc considérer que cette façon de faire est plus optimisée.
Une fois l'événement clic détecté, on commence par vérifier quelle balise a réellement reçu l'événement (notez que la propriété target retourne l'élément du DOM, qui possède donc des propriétés standards nodeName et className).
var actuel = event.target;
Si ce n'est pas une balise <li> ou si c'est celle déjà active (repérable par son nom de classe CSS), on ne fait rien et on stoppe l'exécution de la fonction avec l'instruction return.
if (!/li/i.test(actuel.nodeName) || actuel.className.indexOf('actif') > -1)
{
return;
}
Notez l'utilisation de l'expression régulière /li/i pour vérifier le nom de la balise. Le i signifie que l'on fait une recherche insensible à la casse, car la propriété nodeName est supposée retourner le nom en majuscules. Notez aussi que dès que l'instruction return est rencontrée dans le code, cela stoppe l'exécution de la fonction : si l'on entre dans cette condition, alors le reste du code de la fonction ne sera pas exécuté.
On actualise ensuite les noms de classe pour mettre à jour l'affichage de l'onglet actif (on est maintenant obligés d'utiliser la fonction $() pour transformer actuel en objet jQuery) :
$(actuel).addClass('actif').siblings().removeClass('actif');
On souhaite que seul l'onglet sur lequel on vient de cliquer possède la classe actif, comme on connait cet élément (référencé par la variable actuel), on peut lui attribuer directement, il ne reste ensuite qu'à supprimer si besoin le nom de classe pour tous ses éléments frères (.siblings()).
Enfin, on lance la fonction setDisplay() qui va ajuster l'affichage du contenu correspondant à l'onglet actif.
La fonction setDisplay() commence par définir la variable qui sera utilisée :
var modeAffichage;
Puis, elle fait une boucle sur tous les onglets :
$('#onglets li').each(function(rang){...}
Dans cette boucle, on vérifie s'il s'agit de l'onglet actif ou non pour déterminer la valeur de la propriété display à affecter pour la balise <div> correspondant :
modeAffichage = $(this).hasClass('actif') ? '' : 'none';
Notez qu'il est maladroit et déconseillé de déclarer une variable dans une boucle, c'est pourquoi la déclaration a été faite en début de fonction, la boucle ne fait quant à elle que des affectations.
Enfin, on récupère, cette <div> de contenu (les onglets et le contenu étant dans le même ordre dans le code, le paramètre passé à la fonction de boucle peut être utilisé dans la méthode .eq() pour retrouver le bon élément) et on lui affecte le display adapté :
$('.item').eq(rang).css('display', modeAffichage);
Pour le moment, on a fait que prévoir ce qui va se passer lors d'un clic, mais l'ensemble des contenus des onglets est toujours visible sur la page, on appelle donc la fonction setDisplay() pour initialiser l'affichage.
Le rendu dans le navigateur au chargement de la page est illustré à la figure 5-1.
On constate que cette fonctionnalité particulièrement utile se met en place avec une vingtaine de lignes jQuery. En comparaison, la même fonctionnalité en JavaScript pur pourrait donner :
window.onload = function(){
document.getElementById('onglets').style.display = 'block';
document.getElementById('onglets').onclick = function(e){
var actuel = e ? e.target : window.event.srcElement;
if (!/li/i.test(actuel.nodeName) || actuel.className.
indexOf('actif') > -1) {
return;
}
var allOnglets = document.getElementById('onglets').
getElementsByTagName('li'),
i = allOnglets.length;
while(i--){
if(allOnglets[i] == actuel){
allOnglets[i].className += ' actif';
}
else{
allOnglets[i].className = allOnglets[i].className.
replace('actif', '');
if(String.trim){
allOnglets[i].className.trim();
}
}
}
setDisplay();
}
setDisplay();
};
function setDisplay(){
var allOnglets = document.getElementById('onglets').
getElementsByTagName('li'),
allContenus = document.getElementById('contenu').
getElementsByTagName('div'),
i = allOnglets.length;
while(i--){
if(allOnglets[i].className.indexOf('actif') == -1){
allContenus[i].style.display = 'none';
}
else{
allContenus[i].style.display = 'block';
}
}
}
Bien entendu, ce code ne prend pas en compte des méthodes JavaScript telles que forEach() pour un tableau ou getElementsByClassName(), tout simplement parce que ces méthodes ne peuvent pas encore être considérées comme compatibles sur tous les navigateurs, or le code se doit d'être fonctionnel, quel que soit le navigateur utilisé.
D'autre part, la gestion des événements se fait à l'aide de onload et onclick comme propriétés d'éléments HTML. Or, cette façon de procéder n'est pas satisfaisante pour un code réutilisable, car elle risque de poser des problèmes pour ajouter, avec d'autres scripts, des gestions d'événements identiques pour ces éléments (chaque nouvelle déclaration écrasera la précédente). Une gestion similaire à celle que propose jQuery alourdira un peu plus le code.
Pour des exemples plus complexes, le ratio sera beaucoup plus élevé.