Laisser un commentaire

Créer un scroll infini en jQuery

L’infinite scroll, pour ceux d’entre vous qui ne savent pas de quoi il s’agit, c’est ce système qui charge automatiquement de nouveaux éléments au bas de votre page lorsque vous “scrollez” – c’est à dire allez en bas de la page avec la molette de votre souris.

Le scroll infini remplace habilement les boutons “suivants”, “précédents” ainsi que les numéros de pages sur de nombreux sites. Facebook et Twitter l’utilisent dans leurs timelines respectives. On va donc voir comment programmer notre propre système de scroll infini sans avoir recours à aucun plug-in !

À quoi sert le scroll infini ?

Avant de vous expliquer comment mettre en place ce système, laissez-moi rapidement digresser sur les avantages et inconvénients qui en découlent. Comme toute techno un peu “hype”, on a tendance à la voir partout parce que ça fait classe, même là où elle n’est pas très utile… Je veux donc rapidement faire le point sur l’utilité réelle de celle-ci.

De la même manière qu’une pagination classique (avec numéros de pages, liens précédents et suivants) permet de le faire, l’infinite scroll est utile pour répartir le contenu lorsque celui-ci est conséquent.

Impensable en effet de charger 500 articles de blogs sur la même page. D’une part votre serveur en prendrait un coup, d’autre part, vos visiteurs risqueraient de s’impatienter…

Cependant, et puisque je prends en exemple des pages de blogs, cette technique n’est pas forcément adaptée à tous types de sites. Elle convient à merveille pour des sites tels que Twitter, flickr, 500px, sur lesquels on a plutôt tendance à parcourir le contenu de manière “nonchalante”, si je puis dire.

En revanche, sur un blog, il peut être lassant de devoir scroller pendant 5 minutes pour retrouver une page d’article, d’autant plus qu’avec le système des pages, chacune d’entre elle possède un lien unique, tandis qu’avec le scroll infini, que néni.

Néanmoins, ce système a plus d’un atout dans son sac. Ayant recourt à l’Ajax, il autorise une navigation fluide et permet de soulager un peu votre serveur :

Maintenant que j’ai fini mon petit topo ergonomie et “user-friendlyness”, parlons de ce qui vous intéresse, j’ai nommé le code !

Préparatifs

Je vais mettre l’accent sur la partie JavaScript/jQuery étant donné que c’est le cœur du sujet. Cependant, en plus de notions évidentes de JavaScript, de jQuery, et d’ajax, il vous faudra maîtriser un minumum le HTML/CSS, le PHP et le SQL.

En effet, je n’expliquerai que brièvement ce qui ne concerne pas le code jQuery et, de toute manière, il faudra que vous soyez en mesure d’adapter tout ceci à votre cas particulier.

Le template HTML de base

J’annonce la couleur… ce sera en noir et blanc ! Quand je dis “de base”, ça veut dire le strict minimum, on ne s’occupe pas de design ici. Voilà donc pour notre fichier html :

<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Mon super site</title>
        <link rel="stylesheet" href="/css/main.css">
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
        <script src="/js/main.js"></script>
    </head>
    <body>
        <div id="content">
        </div>
    </body>
</html>

On trouve dans l’ordre :

Concernant le format de vos données, c’est vous qui voyez, l’important est qu’elles soient contenues dans la div #content, le reste, c’est votre affaire !

Le CSS n’est pas vraiment notre problème ici, la feuille de style contiendra surtout votre design. Les seules choses que nous y ajouterons, c’est une classe hidden avec un display:none et un #loader en display:none aussi.

#content .hidden{
    display: none;
}
#content #loader{
    display: none;
}

Cette classe permettra tout simplement de ne pas faire apparaître les éléments ajoutés par défaut, et ainsi de leur appliquer un effet d’apparition en fondu. Quant au loader, il viendra s’afficher pendant le chargement de nouveaux éléments.

Requête SQL

Je prendrai en exemple MySQL, car c’est celui que je connais, et c’est sans doute la base de données que la plupart d’entre-vous utilisez.

Le concept est simple, vous avez votre première page, admettons index.php, sur laquelle sont chargés les 20 premiers articles du blog (pour rester dans notre exemple blog). Votre requête SQL ressemble plus ou moins à cela :

SELECT id, title, content, date FROM Articles LIMIT 20 OFFSET 0;

Le but du jeux est que lorsqu’un visiteur atteint le bas de votre page, les 20 articles suivant soient ajoutés à ceux déjà présents. Au niveau de la requête, on obtient donc ceci :

SELECT id, title, content, date FROM Articles LIMIT 20 OFFSET 20;

Vous constatez que le requête est la même à un détail près : l’OFFSET. Nous voulons les 20 articles suivants, nous avions les 20 premiers, donc avec un OFFSET à 20, on récupère 20 nouveaux articles à partir du 21ème. La requête suivante, la troisième donc, aura pour OFFSET 40 et ainsi de suite.

L’Ajax

Nous avons vu les requêtes SQL dont nous avions besoin, expliquons maintenant brièvement comment récupérer le contenu lié. Comme mentionné plus haut, je ne vais pas expliquer l’ajax ici, je pars donc du principe que vous êtes au fait de cette technologie.

Toujours dans l’exemple du blog, la syntaxe des articles présents au chargement de la page – les 20 premiers donc, contenus dans la div #content – pourrait ressembler à ça :

<!-- on utilise les nouvelles balises HTML5, mais on aurait pu mettre une div !-->
<article id="0">
    <h1>Titre de l'article</h1>
    <p>Un paragraphe</p>
    d'autres trucs commes des listes des images, d'autre paragraphes, etc…
</article>

<article id="1">
    …
</article>

…

Dans notre exemple, les éléments chargés par défaut constituent une liste d’articles, lesquels sont contenus dans leurs balises article respectives. Ici, nous ajouterons simplement la classe .hidden à article. Si vous vous demandez pourquoi on ajoute cette classe, je vous invite à relire la partie sur le template de base…

Le rendu de votre page de résultat, c’est à dire une fois mouliné par PHP – je ne vais pas non plus expliquer le traitement de la requête MySQL par PHP – par exemple ajax.php, pourrait donc ressembler à quelque chose de la sorte :

<article id="21" class="hidden">
    <h1>Titre de l'article</h1>
    <p>Un paragraphe</p>
    d'autres trucs commes des listes des images, d'autre paragraphes, etc…
</article>

<article id="22" class="hidden">
    …
</article>

…

J’espère que jusque là tout est clair, car nous allons maintenant commencer le partie vraiment intéressante de cet article, le jQuery !

jQuery !

Je ne vais pas vous balancer le code d’un coup, on va y aller étape par étape afin que vous compreniez bien ce que nous faisons.

Déclaration de la fonction et mise en place des événements

function infiniteScroll() {
    // vérifie si c'est un iPhone, iPod ou iPad
    var deviceAgent = navigator.userAgent.toLowerCase();
    var agentID = deviceAgent.match(/(iphone|ipod|ipad)/);

    // on déclence une fonction lorsque l'utilisateur utilise sa molette
    $(window).scroll(function() {
        // cette condition vaut true lorsque le visiteur atteint le bas de page
        // si c'est un iDevice, l'évènement est déclenché 150px avant le bas de page
        if(($(window).scrollTop() + $(window).height()) == $(document).height()
        || agentID && ($(window).scrollTop() + $(window).height()) + 150 > $(document).height()) {
            // on effectue nos traitements
        }
    });
};

Mine de rien, en 18 lignes, il y a déjà pas mal à dire. Dans l’ensemble, les commentaires parlent d’eux-même, je ne vais donc pas tout re-détailler ligne par ligne. Ce sont les lignes 12 et 13 qui sont intéressantes et qui méritent notre attention.

Elles calculent en effet si l’utilisateur a atteint le bas de page, et si oui, elle vont lancer le traitement pour charger notre contenu. En outre, une condition légèrement différente est appliquée aux iDevices. Attardons-nous un peu sur le calcul effectué pour savoir si on a atteint le bas de page, afin de mieux comprendre les subtilités de cette condition.

Nous avons ici recours à trois fonctions : $(document).height(), $(window).height() et $(window).scrollTop(). La première retourne la hauteur totale de notre page en pixels, la seconde nous renvoit la hauteur du morceau de page qui est affiché dans notre navigateur, enfin, la dernière concerne le nombre de pixels qu’il faut scroller/remonter pour atteindre le haut de la page. Un petit schéma s’avérera peut-être plus parlant :

scrolltop et windowheight expliqués

En toute logique, si nous faisons la somme du bout de page affiché à l’écran et de ce qu’il reste à scroller pour atteindre le haut de la page, lorsque nous sommes tout en bas, elle est égale à la hauteur totale de notre page.

Maintenant, pourquoi appliquons-nous une condition différente concernant les iBidules ? Si vous avez effectué quelques recherches concernant le scroll infini et l’iPhone, il est fort possible que vous ayez compris que bien des plugins ne fonctionnent pas avec les devices d’Apple (ipad et iPod touch compris).

La raison en est simple, mais en toute franchise, j’ai dû chercher un bon moment avant de trouver la source du problème. C’est finalement une doc d’apple qui me l’a faite comprendre. Voici d’abord une image (tirée du site d’Apple) qui explique brièvement les calculs du viewport pour Safari mobile.

calculs des window height et document height iphone

On comprend grâce à cette image que la hauteur du document sur safari iOS inclut aussi la barre de status, la barre d’url ainsi que la barre des boutons, soit un document 124px plus grand que sa valeur normale. Voilà la raison pour laquelle $(window).scrollTop() + $(window).height() est toujours inférieur à $(document).height() sur safari iOS.

Pour remédier à cela, il faut donc soustraire 124px à $(document).height() (ou ajouter cette même valeur à la somme des deux autres) pour retrouver une équation équilibrée.

De mon côté, j’ai pris la liberté d’ajouter 150px à $(window).scrollTop() + $(window).height() et de déclencher l’évènement dès que la somme de ces deux valeurs est supérieure (non plus égale) à la hauteur du document.

De cette manière, le chargement se déclenche un peu avant que l’internaute n’atteigne le bas de page. Étant donné que ces périphériques n’ont pas toujours un débit extraordinaire, cela peut s’avérer salutaire. À vous de faire vos réglages aux petits oignons par la suite.

Requête, chargement et affichage des nouveaux éléments

Dit comme ça, on à l’impression que ça fait beaucoup à la fois, mais il y a des choses simples qui, je pense, ne demandent pas de trop amples explications. Le seul point qui nécessite un peu réflexion concerne l’élaboration de la requête. Le reste, c’est finger in the nose si vous avez un peu l’habitude du jQuery.

function infiniteScroll() {
    // cette variable contient notre offset
    // par défaut à 20 puisqu'on a d'office les 20 premiers éléments au chargement de la page
    var offset = 20;

    // ici on ajoute un petit loader gif qui fera patienter pendant le chargement
    $('#content').append('<div id="loader"><img src="/img/ajax-loader.gif" alt="loader ajax"></div>');

    var deviceAgent = navigator.userAgent.toLowerCase();
    var agentID = deviceAgent.match(/(iphone|ipod|ipad)/);

    $(window).scroll(function() {
        if(($(window).scrollTop() + $(window).height()) == $(document).height()
        || agentID && ($(window).scrollTop() + $(window).height()) + 150 > $(document).height()){

            // on affiche donc loader
            $('#content #loader').fadeIn(400);

            // puis on fait la requête pour demander les nouveaux éléments
            $.get('/more/' + offset + '/', function(data){
                // s'il y a des données
                if (data != '') {
                    // on les insère juste avant le loader.gif
                    $('#content #loader').before(data);

                    / on les affiche avec un fadeIn
                    $('#content .hidden').fadeIn(400);

                    /* enfin on incrémente notre offset de 20
                     * afin que la fois d'après il corresponde toujours
                    */
                    offset+= 20;
                }

                // le chargement est terminé, on fait disparaitre notre loader
                $('#content #loader').fadeOut(400);
            });
        }
    });
};

Il n’y a ici rien de sorcier, mais on va revenir un peu sur nos différentes étapes. À la ligne 5, on déclare une variable, offset. Elle indique l’offset que nous allons insérer dans notre requête SQL.

Nous avons dit un peu plus haut qu’au premier chargement de la page, nous avions les vingt premiers articles, à la première requête ajax, nous voulons donc les vingt suivants.

De ce fait, notre offset vaut 20 par défaut. Nous nous en servons à la ligne 24 pour appeler les données. Nous utilisons ici la méthode $.get() de jQuery, nous passons donc la variable offset via $_GET.

Je vous laisse le soin de construire votre script pour récupérer cette variable et faire appel à la BDD. Notez au passage que cette variable n’est pas sécurisée du tout, traitez-là donc en conséquence, avec des requêtes SQL préparées ou un transtypage en int. En outre, nous incrementons notre variable de 20 à la ligne 38.

À la ligne 8, nous insérons notre loader à la suite du contenu, celui-ci reste invisible puisque nous avons précisé dans notre CSS un display:none pour #loader. Les lignes qui suivent la requête (après la ligne 24 donc) ne sont pas complexes à comprendre et les commentaires se suffisent à eux-même.

À ce stade, notre fonction est presque complète, il ne reste plus qu’un petit détail à régler.

Le problème de l’événement multiple

Le chargement de nouvelles données repose sur le fait que la condition $(window).scrollTop() + $(window).height() == $(document).height() est remplie.

Cette condition est valable pendant quelques secondes puisque tant que le nouveau contenu n’a pas été ajouté à la page, elle reste vraie. De ce fait, les navigateurs lancent la fonction plusieurs fois en parallèle, ce qui, comme vous pouvez vous en douter, provoque des comportements inattendus.

On va donc contourner ce problème en empêchant la fonction de se relancer si une instance d’elle-même est déjà en cours d’exécution. Pour ce faire, nous allons créer une nouvelle variable, ajaxready, qui sera initalisée à true lors du chargement de la fonction.

Par la suite, dès qu’on lance une requête Ajax, on met cette variable à false, et ce jusqu’à ce que tous les traitements soient terminés. Par la suite, on vérifie avant de lancer une nouvelle requête que cette variable vaut bien true, sinon, on ne fait rien.

Voici le code complet avec l’ajout de la variable ajaxready :

# Buzut – http://buzut.fr – 10/2012
# GPL – http://www.gnu.org/licenses/gpl.html

function infiniteScroll(){
    var offset = 20;

    // on initialise ajaxready à true au premier chargement de la fonction
    $(window).data('ajaxready', true);

    $('#content').append('<div id="loader"><img src="/img/ajax-loader.gif" alt="loader ajax"></div>');

    var deviceAgent = navigator.userAgent.toLowerCase();
    var agentID = deviceAgent.match(/(iphone|ipod|ipad)/);

    $(window).scroll(function() {
        // On teste si ajaxready vaut false, auquel cas on stoppe la fonction
        if ($(window).data('ajaxready') == false) return;

        if(($(window).scrollTop() + $(window).height()) == $(document).height()
        || agentID && ($(window).scrollTop() + $(window).height()) + 150 > $(document).height()) {
            // lorsqu'on commence un traitement, on met ajaxready à false
            $(window).data('ajaxready', false);

            $('#content #loader').fadeIn(400);
            $.get('/more/' + offset + '/', function(data){
                if (data != '') {
                    $('#content #loader').before(data);
                    $('#content .hidden').fadeIn(400);
                    offset+= 20;
                    // une fois tous les traitements effectués,
                    // on remet ajaxready à false
                    // afin de pouvoir rappeler la fonction
                    $(window).data('ajaxready', true);
                }

                $('#content #loader').fadeOut(400);
            });
        }
    });
};

Pourquoi est-ce que l’on place ajaxready à true à la ligne 36, dans notre if, plutôt que juste après le fadeOut du loader à la ligne 39 ? Une idée ? personne ?

En fait, cette condition sur le ajaxready à la ligne 18 remplit une double fonction. D’une part elle empêche la fonction d’être rappelée si elle est déjà en cours d’exécution, d’autre part, si aucune donnée n’est retournée (ligne 28), elle ne sera jamais remise à true (ligne 36).

Cela évite que notre fonction soit rappelée alors que la base SQL n’a plus d’éléments à retourner, et dispense ainsi notre application de requêtes inutiles. À la première requête qui ne retourne rien, la fonction ne peux plus être appelée. Attention cependant, veillez bien à ce que votre page ajax retourne une chaine de caractères vide lorsqu’il n’y a plus rien en base de données !

Il ne vous reste plus qu’à adapter votre code HTML et vos traitement PHP (ou autre d’ailleurs) afin d’intégrer ceci sur votre site. J’espère avoir pu vous éclairer sur la technique du scroll infini. Et si je vous ai évité (ou même ai mis fin) à un terrible mal de tête, un merci fait toujours plaisir.

Commentaires

Rejoignez la discussion !

Vous pouvez utiliser Markdown pour les liens [ancre de lien](url), la mise en *italique* et en **gras**. Enfin pour le code, vous pouvez utiliser la syntaxe `inline` et la syntaxe bloc

```
ceci est un bloc
de code
```