Lizzie Crowdagger
Afficher des notes de bas de page en «popup» avec JavaScript
Dans le roman sur lequel je travaille en ce moment, je suis revenue à une vieille déviance que j’avais mis de côté dans mes fictions ces derniers temps : les notes de page. Et donc, même si le texte est très loin d’être achevé, il y a déjà beaucoup de notes de bas de pages, parce que j’aime les notes de bas de page[1].
Le logiciel que j’utilise pour la mise en page de mes romans en PDF, HTML et EPUB, Crowbook, gère bien les notes de bas de page, mais pour le HTML dans le navigateur[2], il manquait la fonctionnalité pour afficher cette note de bas de page de façon un peu plus « moderne » que juste un lien vers la fin de l’article.
Il était donc temps d’ajouter cela à Crowbook. Cette opération ne s’est pas révélée particulièrement difficile, mais en cherchant des ressources sur ce sujet je me suis rendue compte qu’il y avait beaucoup d’options proposant de télécharger une bibliothèque, ou des solutions simples qui utilisaient une bibliothèque comme JQuery, mais finalement assez peu qui expliquaient de manière simple comment faire soi-même sans recourir à une bibliothèque externe[3], surtout en partant du principe que vous ne vous y connaissez pas forcément des masses à Javascript, comme c’est mon cas.
Je me suis donc dit je pouvais peut-être donc partager cette expérience pour combler un peu ce manque.
Le code HTML
Avant de nous attaquer au Javascript, commençons d’abord par montrer le code HTML duquel on va partir. Il y a deux parties distinctes aux notes de bas de page :
la référence, ou l’appel, où il va typiquement y avoir un numéro au-dessus d’un mot dans le texte, éventuellement entre crochets, même si ça peut aussi être une astérisque ou une petite croix ;
la définition, c’est-à dire le contenu réel de la note.
La référence
Pour la référence, il n’y a pas vraiment de questions à se poser, il suffit de mettre le numéro en hauteur :
<sup>1</sup>
Bon, en vrai ça, ça suffit pour un texte en PDF ou en papier, où il y a une notion de pages et où la lectrice peut juste se référer au bas de la page[4]. Le problème c’est qu’en HTML il n’y a pas vraiment de notions de « page », donc en pratique pour les notes de « bas de page » il va généralement falloir scroller, ce qui est quand même un peu pénible. Donc, pour simplifier les choses, on va quand même rajouter un lien. Et pareil, on aimerait bien avoir un lien après pour remonter une fois qu’on a lu la définition, donc pour ça il faut qu’on mette une « ancre » ou une id.
<a id = "fn_reference_1" href = "#fn_definition_1" class = "footnote_reference"> <sup>[1]</sup> </a>.
Au passage, j’ai aussi rajouté une class à l’élément <a>
, ça sera pratique plus tard pour le CSS et le javascript, mais pour l’instant on n’en a pas vraiment besoin.
La définition
Pour la définition, rien de bien compliqué, il suffit juste, d’une part, de définir un identifiant correspondant au lien utilisé dans la référence, et, d’autre part de faire un renvoi vers cette référence pour pouvoir remonter facilement :
<p> <a href = "#fn_reference_1">[1]</a> </p> <aside id = "fn_definition_1"> <p> Bla bla bla </p> </aside>
Ici j’utilise l’élément <aside>
pour le contenu (ou la définition) de la note, ce qui me parait pas déconnant :
L’élément
<aside>
(en français, « aparté ») représente une partie d’un document dont le contenu n’a qu’un rapport indirect avec le contenu principal du document. Les apartés sont fréquemment présents sous la forme d’encadrés ou de boîtes de légende.
Mais vous faites évidemment comme vous voulez. Pareil, là j’ai mis le numéro et le contenu sur deux paragraphes différents ce qui est peut-être un peu moche, selon ce que vous voulez vous pourriez mettre ça dans une liste[5], peu importe.
Ici ce qui est important c’est surtout de définir l’identifiant fn_definition_1
utilisé par le lien de la référence, et surtout pour après on aura besoin que cet id
correspondant à un élément englobant tout le contenu de la note, mais ça peut aussi être un <div>
par exemple.
Évidemment, pour des questions de mise en page avec CSS vous aurez sans doute envie aussi de mettre quelques classes etc. mais là fondamentalement on n’en a pas besoin, donc ne compliquons pas.
Le javascript
Maintenant, ce qu’on aimerait c’est pouvoir afficher le contenu de cette note quand l’utilisatrice survole (ou clique, peu importe) la référence.
Pour ça, il va falloir faire trois choses :
une fonction
show_popup
pour afficher le contenu de la note au bon endroit quand la souris passe au-dessus de la référence ;une fonction
remove_popups
pour faire disparaitre ce pop-up quand le curseur de la souris ne survole plus la référencepour chaque référence à une note de bas de page (donc chaque élément de la classe
footnote_reference
), connecter les bons évènements aux deux fonctions précédentes.
Trouver les références et connecter les évènements
Bon, là c’est pas très compliqué, il s’agit essentiellement de faire une boucle. Le seul truc auquel il faut faire attention, c’est qu’on veut que cette boucle ne soit exécutée qu’après que toutes notes de bas de page aient bien été définies. On pourrait juste le mettre à la fin du document HTML, mais pour ne pas s’embêter et s’assurer que ça marche aussi juste en chargeant le script au début du document, on va utiliser :
document.addEventListener('DOMContentLoaded', function() { // ... });
Ça va faire en sorte qu’on va en fait définir une fonction anonyme, qui sera exécutée lorsque le contenu du document sera entièrement chargé.
Dans cette fonction, on va devoir récupérer toutes nos références à des notes de bas de page, c’est-à-dire tous les éléments à qui on a attribué la classe footnote_reference
. Pour cela il suffit d’utiliser document.querySelectorAll('.footnote_reference')
.
(querySelectorAll
utilise la même syntaxe que les selectors CSS, c’est-à-dire que si c’est préfixé par #
ça va sélectionner un identifiant, et par .
toute une classe.)
Ensuite, il faut faire en sorte que les fonctions show_popup
et remove_popups
(qu’on n’a pas encore écrites) soient appelées lorsque la souris survole ou arrête de survoler la référence. Pour ça, on va utiliser addEventListener
avec les évènements “mouseenter” et “mouseleave”[6].
Ce qui nous donne, au final, le code suivant :
document.addEventListener('DOMContentLoaded', function() { var anchors = document.querySelectorAll('.footnote_reference'); for (var i = 0; i < anchors.length; i++) { var anchor = anchors[i]; anchor.addEventListener( "mouseenter", (event) => { show_popup(event); } ); anchor.addEventListener( "mouseleave", (event) => { remove_popups(); } ); } });
Petite remarque si, comme vous, vous êtes plutôt nulle en javascript : dans le
addEventListener
, faites attention ce que vous utilisez comme variable et n’utilisez à priori que la variableevent
.Dans ma première tentative je passais également la variable
anchor
à ma fonctionshow_popup
, et non seulement ça ne faisait pas ce que je voulais mais ça ne me mettait pas non plus un message d’erreur pour me dire « c’est une mauvaise idée ce que tu fais », juste ça restait bloqué au dernière élément de la liste, et forcément ça marchait moins bien.Ne faites pas comme moi.
La fonction remove_popups
Le but de cette fonction va être de faire disparaitre tout popup qui serait éventuellement affiché.
Pour cela, on va commencer par récupérer tous les popups avec, à nouveau, querySelectorAll
en utilisant la classe “popup_footnote”, qu’il faudra qu’on pense donc (dans la fonction suivante) à mettre à nos popups.
Ensuite, on va faire une boucle, et tout simplement les supprimer de leur parent, apparemment c’est comme ça qu’on supprime des éléments en Javascript, si vous avez une solution plus élégante n’hésitez pas à me le dire en commentaire.
Cela donne donc le code suivant :
function remove_popup() { var footnotes = document.querySelectorAll('.popup_footnote'); for (var i = 0; i < footnotes.length; i++) { var f = footnotes[i]; if (f.parentNode) { f.parentNode.removeChild(f); } } }
La fonction show_popup
C’est peut-être la fonction la plus cruciale, mais elle ne reste pas très compliqué.
La difficulté principale c’est que cette fonction, on a vu qu’on l’appelait avec juste event
en paramètre. La question c’est comment, à partir de ça, récupérer le contenu de de la note de bas de page associée ? En fait, ce n’est pas si compliqué :
var id = event.target.getAttribute("id"); var target_id = id.replace("reference", "definition"); var content = document.getElementById(target_id).innerHTML;
D’abord, avec event.target.getAttribute("id")
on récupère l’identifiant de la référence qui est survolée, par exemple fn_reference_1
. Ce qu’on voudrait, c’est avoir l’identifiant de notre définition qu’on a mise dans un <aside>
, donc fn_definition_1
. Pour ça, il suffit donc de remplacer “reference” par “definition”.
Maintenant, on n’a plus qu’à récupérer cet élément avec getElementById
, puis à récupérer le contenu à l’intérieur avec innerHTML
.
Ensuite, ce contenu, qui est déjà présent en bas de la page, on va vouloir l’afficher à côté de la référence. Pour ça, on peut faire quelque chose comme :
event.target.insertAdjacentHTML('afterend', content);
event.target
correspond à notre référence qui est en train d’être survolée, et insertAdjacentHTML
va permettre de rajouter simplement du HTML derrière (parce qu’on a choisi ‘afterend’, sinon ça pourrait être devant).
Le problème, c’est que pour l’instant ça n’apparait pas du tout comme une popup, mais ça vient juste casser la mise en page du document. Et comme on n’a pas mis de classe “popup_footnote”, notre fonction remove_popups
ne va pas marcher non plus.
Pour l’afficher en mode popup, le plus simple va être d’utiliser position:fixed
ou position:absolute
comme style, pour que l’élément soit retiré du flux normal.
Honnêtement je n’ai jamais trop compris la différence entre les deux, là j’ai testé avec
fixed
et ça marchait, donc je n’ai pas cherché plus loin.
Donc, au lieu de juste donner content
à insertAdjacentHTML
, on va lui passer :
'<aside class = "popup_footnote" style = "position:fixed; top: 0px;">' + content + '</aside>'
C’est moche, mais ça marche. J’avoue que je connais très mal javascript, j’ai vu cette syntaxe pour construire des chaines de caractères mais je ne sais pas s’il y a un équivalent qui ressemblerait à
format!("<aside class = 'popup_footnote' style='position:fixed; top={top}px;'>{content}</aside>")en Rust ?
Là, normalement, ça devrait marcher à peu près, sauf que la note apparait tout en haut de l’écran, et pas au niveau de là où on se trouve dans la lecture. (Et sur fond transparent, mais on règlera ça avec du CSS).
Pour modifier ça, et par exemple l’afficher en dessous de notre référence, on va utiliser getBoundingClientRect()
pour obtenir les coordonnées de notre élément survolé.
var top = Math.round(event.target.getBoundingClientRect().top + event.target.getBoundingClientRect().height); event.target.insertAdjacentHTML('afterend', '<aside class = "popup_footnote" style = "position: fixed; top: '+ top + 'px; ">' + content + '</aside>')
Donc on récupère la position du haut de notre élément avec .top
, on ajoute la hauteur avec .height
pour que ça corresponde à la position du bas, et on règle tout ça comme le haut de notre popup_footnote
.
Ce qui donne pour l’ensemble de notre fonction le code suivant :
function show_footnote(event) { remove_popups(); var id = event.target.getAttribute("id"); var target_id = id.replace("reference", "definition"); var content = document.getElementById(target_id).innerHTML; var top = Math.round(event.target.getBoundingClientRect().top + event.target.getBoundingClientRect().height); event.target.insertAdjacentHTML('afterend', '<aside class = "popup_footnote" style = "position: fixed; top: '+ top + 'px; ">' + content + '</aside>') }
L’élément HTML popover
En recherchant comment faire ce que je voulais, j’ai découvert l’attribut HTML popover
. À l’heure actuelle, il est encore expérimental et pas forcément supporté par les navigateurs un peu anciens, mais à l’avenir ce sera sans doute une solution alternative.
Code javascript complet
function remove_popups() { var footnotes = document.querySelectorAll('.popup_footnote'); for (var i = 0; i < footnotes.length; i++) { var f = footnotes[i]; if (f.parentNode) { f.parentNode.removeChild(f); } } } function show_popup(event) { remove_popups(); var id = event.target.getAttribute("id"); var target_id = id.replace("reference", "definition"); var content = document.getElementById(target_id).innerHTML; var top = Math.round(event.target.getBoundingClientRect().top + event.target.getBoundingClientRect().height); event.target.insertAdjacentHTML('afterend', '<aside class = "popup_footnote" style = "position: fixed; top: '+ top + 'px; ">' + content + '</aside>') } document.addEventListener('DOMContentLoaded', function() { var anchors = document.querySelectorAll('.footnote_reference'); for (var i = 0; i < anchors.length; i++) { var anchor = anchors[i]; anchor.addEventListener( "mouseenter", (event) => { show_popup(event); } ); anchor.addEventListener( "mouseleave", (event) => { remove_popup(); } ); } });
Un peu de CSS pour compléter
Là, normalement, le code devrait fonctionner mais, pour l’instant, le texte de la note apparait en transparence sur le reste du texte, ce qui n’est pas franchement lisible.
Pour corriger cela, il sera donc nécessaire, à minimum, de modifier un tout petit peu le style de notre popup_footnote
avec, par exemple :
.popup_footnote { background-color: #e8e897; }
Et puis vous pouvez évidemment rentre tout ça un peu plus joli moins moche avec, par exemple :
.popup_footnote { margin: 2em; padding : 1em; right: 2em; max-width: 20em; }
pour que ça s’affiche à droite de la page, avec un peu de marge et « pas trop large ».
Limites
Voilà, j’espère que ce genre de tutorial intéresse certaines personnes. L’exemple que je donne ici est assez limité en fonctionnalités : il n’y a pas d’animations[7], il y a sans doute plein de choses à améliorer, et sans doute des cas où c’est pertinent d’utiliser une bibliothèque voire un framework
.
Mais parfois, c’est pas la peine, et même sans y connaitre grand-chose en Javascript ou CSS on peut bricoler un truc un peu basique.