Lors de mes premiers pas dans le monde WordPress, j'écrivais tout moi-même. Lorsque j'avais fini de rédiger mon article, je créais un sommaire, avec les liens vers les titres... Ça marchait très bien, mais... Qu'est-ce que c'était long, redondant et ennuyeux !
J'ai alors cherché à automatiser cela. Il y a des plugins, mais je n'ai même pas cherché, je trouvais qu'installer un plugin pour ça, c'est comme porter un scaphandre pour faire du vélo : c'est lourd, et ça sert à rien. En cherchant un peu, j'ai découvert ce super article de Wabeo, qui marche parfaitement... à condition de ne pas utiliser de constructeurs de page. Et oui, avec Brizy par exemple, le sommaire ne s'affichait plus : le constructeur supprimait les IDs...
Alors quand on ne peut pas faire quelque chose côté serveur... Faisons-le côté client !
Avant de commencer...
Sommaire : côté serveur ou client
En effet, réécrire une page, une fois qu'elle s'est chargée n'est jamais optimisé. Sur le plan SEO, cela peut ralentir le chargement global de la page. Sur le plan matériel, un utilisateur qui a un ordi lent pourrait ne pas voir le sommaire s'afficher tout de suite.
Par contre, en terme de rapidité de requête côté serveur, le code proposé par wabeo prend plus de temps, et le poids du contenu envoyé est légèrement augmenté, puisque le menu est créé avant l'envoi des données au client. C'est insignifiant dans cet exemple, mais c'est à garder à l'esprit, si l'on doit réaliser un calcul plus important à chaque chargement, qui créera du code plus lourd, de privilégier JS à PHP pour accélérer le temps de réponse du serveur et sa disponibilité (sur un site très sollicité).
Le mieux serait sans doute de pouvoir créer ce sommaire lors de la création de l'article, puis de l'enregistrer en base de données. Peut-être le sujet d'une prochaine astuce ?
Les règles de l'art
Autant le dire tout de suite : choisir cette solution, parce qu'un constructeur de page nous limite, c'est pas très propre.
Mais c'est le problème de beaucoup de constructeurs. Simples, faciles d'utilisation, rapides à prendre en main et à utiliser, ils sont par contre limitants dès qu'on veut faire des choses "originales".
Le résultat, c'est que le constructeur crée son code, souvent très lourd même s'il est optimisé, auquel on vient ajouter le nôtre. Encore une fois, dans ce cas précis, ce n'est pas le plus dérangeant, parce que le code qu'on réalise est léger, mais vous avez déjà toustes expérimenté les règles js ou css pour réécrire quelque chose que le constructeur tient à faire d'une manière et vous d'une autre... Et ça se finit souvent à grands coups de !important
! 😩
Bon, on est d'accord : ce problème devrait être plus celui des constructeurs de page que le nôtre, mais toujours est-il qu'à la fin, on doit assumer le code face aux clients et utilisateurs...
Passons au code
Pour réaliser notre sommaire, nous avons plusieurs étapes à réaliser :
- Retrouver tous les titres, et leur ajouter un ID, pour créer les ancres,
- Créer le sommaire avec les liens vers ces ancres,
- animer tout cela.
Retrouver tous les titres
Le plus efficace et le moins dangereux, c'est de mettre un ID à la section dans laquelle vous viendrez insérer le contenu de votre article (appelé ici container
). ça évitera par exemple, de récupérer les H6 de votre header ou de votre footer... !
// On commence par une petite fonction qui nous permet de
// récupérer les titres, pour fabriquer les ID :
function sanitize_title(title) {
var sanitizedTitle = title.toLowerCase()
.replace(/[^a-z0-9\s]/g, '') // Suppression des caractères non-alphanumériques
.replace(/\s /g, '-') // Remplacement des espaces par des tirets
.normalize('NFD') // Normalisation des caractères accentués
.replace(/[\u0300-\u036f]/g, ''); // Suppression des accents
return sanitizedTitle;
}
Ensuite on récupère tous les titres, on les assainit, puis on leur ajoute leur ID :
// A vous de choisir si vous voulez faire apparaître tous les titres
// Dans votre sommaire :
var titres = document.getElementById('container').querySelectorAll('h2,h3,h4,h5,h6')
titres.forEach(function(titre){
// On met les ancre dans le html, que brizy a la mauvaise manie de supprimer
titre.id = sanitize_title(titre.textContent);
}
Créer le sommaire
Maintenant que tous nos titres ont leur ID, nous pouvons les appeler. Pareil, je vous conseille de prévoir une conteneur dans lequel vous viendrez ajouter le sommaire. Ici, l'ID de ce conteneur sera sommaire
, tout simplement 😊
// Commençons pas initialiser :
var sommaire = document.getElementById('sommaire_astuce');
sommaire.innerHTML = "";
var menu;
var lvl1 = 0;
var lvl2 = 0;
var lvl3 = 0;
var lvl4 = 0;
var lvl5 = 0;
// On pourrait optimiser ça avec un tableau levels = [0,0,0,0,0],
// Mais je vais privilégier ici la lisibilité.
// Il peut être sympa d'alterner des chiffres et des lettres en fonction des importances des titres :
var lettres = ["le zéro !","a","b","c","d","e"];
// Ensuite, dans la même boucle qu'au-dessus, on crée le sommaire :
titres.forEach(function(titre){
switch (titre.tagName.toLowerCase()) {
case 'h2':
lvl1 ;
lvl2 = 0;
lvl3 = 0;
lvl4 = 0;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lvl1 '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl1' '">' niveau titre.textContent '</a>' ;
break;
case 'h3':
lvl2 ;
lvl3 = 0;
lvl4 = 0;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lettres[lvl2] '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl2' '">' niveau titre.textContent '</a>' ;
break;
case 'h4':
lvl3 ;
lvl4 = 0;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lvl3 '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl3' '">' niveau titre.textContent '</a>' ;
break;
case 'h5':
lvl4 ;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lettres[lvl4] '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl4' '">' niveau titre.textContent '</a>' ;
break;
case 'h6':
lvl5 ;
var niveau = '<span class="title_lvl">' lvl5 '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl5' '">' niveau titre.textContent '</a>' ;
break;
default:
console.log("Autre type d'élément");
}
sommaire.innerHTML = menu;
});
Super ! On a maintenant des ancres et un sommaire ! cela pourrait nous suffire... Non, on aime les choses vivantes, donnons vie à ce sommaire !
Animons notre sommaire
Parce que jquery est mort, c'est pas la peine de continuer à donner des exemples de codes avec. On peut tout faire en javascript, sans importer aucune bibliothèque.
Se déplacer en douceur dans la page
document.addEventListener('DOMContentLoaded', function() {
var sommaireLink = document.querySelector('#sommaire_astuce a');
sommaireLink.addEventListener('click', function(event) {
event.preventDefault();
var h = this.getAttribute('href');
var targetElement = document.querySelector(h);
if (targetElement) {
var offsetTop = targetElement.offsetTop - 50 ;
// Ici, en enlevant 50, on place le titre à 50px du top.
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
}
});
});
Bon, ce code peut ne pas marcher en fonction des constructeurs de page que vous utilisez, certains ont déjà une gestion de la navigation à travers la page, et vous risquez d'avoir du mal à modifier cela... (merci Brizy...)
Surligner dans le sommaire les titres visibles
Et parce que Wabeo nous a fait un super truc sur son site, voici un code pour mettre en surbrillance les titres visibles, pour savoir où on en est dans la page. C'est esthétique et pratique, merci à lui pour l'idée !
window.addEventListener('scroll', function() {
var liens = sommaire.querySelectorAll('a');
titres.forEach(function(titre, index) {
var rect = titre.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
liens[index].classList.add('highlight');
} else {
liens[index].classList.remove('highlight');
}
});
});
Avec évidemment le petit code css de ce fameux highlight
:
.highlight, .highlight * {
color: violet !important;
}
Conclusion
Ça y est, on a un sommaire, qui se crée automatiquement, sans qu'on ait besoin d'y penser ! C'est quand même sacrément pratique.
Si on doit récapituler et regrouper tous ces bouts de code en un seul, Voici ce uqe ça donne :
<script>
function sanitize_title(title) {
var sanitizedTitle = title.toLowerCase()
.replace(/[^a-z0-9\s]/g, '') // Suppression des caractères non-alphanumériques
.replace(/\s /g, '-') // Remplacement des espaces par des tirets
.normalize('NFD') // Normalisation des caractères accentués
.replace(/[\u0300-\u036f]/g, ''); // Suppression des accents
return sanitizedTitle;
}
var titres = document.getElementById('astuce_container').querySelectorAll('h2,h3,h4,h5,h6');
var sommaire = document.getElementById('sommaire');
sommaire.innerHTML = "";
var menu;
var lvl1 = 0; // Cette remise à zéro permet, lorsqu'on passe au deuxième titre h2, que le prochain h3 soit 1 et pas n 1
var lvl2 = 0;
var lvl3 = 0;
var lvl4 = 0;
var lvl5 = 0;
var lettres = ["le zéro !","a","b","c","d","e"];
titres.forEach(function(titre){
// D'abord on remet les ancre dans le html, que brizy a la mauvaise manie de supprimer
titre.id = sanitize_title(titre.textContent);
// Ensuite, en fonction du niveau du titre, on lui donne un style :
switch (titre.tagName.toLowerCase()) {
case 'h2':
lvl1 ;
lvl2 = 0;
lvl3 = 0;
lvl4 = 0;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lvl1 '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl1' '">' niveau titre.textContent '</a>' ;
break;
case 'h3':
lvl2 ;
lvl3 = 0;
lvl4 = 0;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lettres[lvl2] '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl2' '">' niveau titre.textContent '</a>' ;
break;
case 'h4':
lvl3 ;
lvl4 = 0;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lvl3 '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl3' '">' niveau titre.textContent '</a>' ;
break;
case 'h5':
lvl4 ;
lvl5 = 0;
var niveau = '<span class="title_lvl">' lettres[lvl4] '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl4' '">' niveau titre.textContent '</a>' ;
break;
case 'h6':
lvl5 ;
var niveau = '<span class="title_lvl">' lvl5 '.</span>';
menu = '<a href="#' sanitize_title(titre.textContent) '" class="title_lvl5' '">' niveau titre.textContent '</a>' ;
break;
default:
console.log("Autre type d'élément");
}
sommaire.innerHTML = menu;
});
// Permet de surligner les titres du sommaire qui sont visibles :
window.addEventListener('scroll', function() {
var liens = sommaire.querySelectorAll('a');
titres.forEach(function(titre, index) {
var rect = titre.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
liens[index].classList.add('highlight');
} else {
liens[index].classList.remove('highlight');
}
});
});
// Permet de se déplacer avec douceur dans la page :
document.addEventListener('DOMContentLoaded', function() {
var sommaireLink = document.querySelector('#sommaire a');
sommaireLink.addEventListener('click', function(event) {
event.preventDefault();
var h = this.getAttribute('href');
var targetElement = document.querySelector(h);
if (targetElement) {
var offsetTop = targetElement.offsetTop - 50 ;
window.scrollTo({
top: offsetTop,
behavior: 'smooth'
});
}
});
});
</script>
Et un exemple du css qui va avec :
#sommaire {
display: block;
}
#sommaire * {
color: black;
}
#sommaire > a:hover, #sommaire > a:hover * {
color: blue;
}
.title_lvl{
margin-right: 5px;
font-weight: 700;
}
.title_lvl1{
font-weight: 700;
display: block;
}
.title_lvl2{
font-weight: 500;
margin-left: 15px;
display: block;
}
.title_lvl3{
font-weight: 400;
margin-left: 30px;
display: block;
}
.title_lvl4{
font-weight: 300;
margin-left: 45px;
display: block;
}
.title_lvl5{
font-weight: 200;
margin-left: 60px;
display: block;
}
.highlight, .highlight * {
color: blue !important;
}