Correction auto complétion

Parent Previous Next


Correction



Votre système d'auto-complétion est terminé ? Bien ! Fonctionnel ou pas, l'important est d'essayer et de comprendre d'où proviennent vos erreurs, donc ne vous en faites pas si vous n'avez pas réussi à aller jusqu'au bout.


Le corrigé complet


Vous trouverez ici la correction des différentes parties nécessaires à l'auto-complétion. Commençons tout d'abord par le code PHP du serveur, car nous vous avions conseillé de commencer par celui-ci :

<?php

 

    $data = unserialize(file_get_contents('towns.txt')); // Récupération de la liste complète des villes

    $dataLen = count($data);

 

    sort($data); // On trie les villes dans l'ordre alphabétique

 

    $results = array(); // Le tableau où seront stockés les résultats de la recherche

 

    // La boucle ci-dessous parcourt tout le tableau $data, jusqu'à un maximum de 10 résultats

 

        for ($i = 0 ; $i < $dataLen && count($results) < 10 ; $i++) {

            if (stripos($data[$i], $_GET['s']) === 0) { // Si la valeur commence par les mêmes caractères que la recherche

           

                array_push($results, $data[$i]); // On ajoute alors le résultat à la liste à retourner

             

            }

        }

 

    echo implode('|', $results); // Et on affiche les résultats séparés par une barre verticale |

 

?>


Vient ensuite la structure HTML, qui est on ne peut plus simple :

<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8" />

    <title>TP : Un système d'auto-complétion</title>

  </head>

 

  <body>

 

    <input id="search" type="text" autocomplete="off" />

 

    <div id="results"></div>

 

  </body>

</html>


Et pour finir, voici le code Javascript :

(function() {

 

    var searchElement = document.getElementById('search'),

        results = document.getElementById('results'),

        selectedResult = -1, // Permet de savoir quel résultat est sélectionné : -1 signifie « aucune sélection »

        previousRequest, // On stocke notre précédente requête dans cette variable

        previousValue = searchElement.value; // On fait de même avec la précédente valeur

 

 

 

    function getResults(keywords) { // Effectue une requête et récupère les résultats

       

        var xhr = new XMLHttpRequest();

        xhr.open('GET', './search.php?s='+ encodeURIComponent(keywords));

 

        xhr.onreadystatechange = function() {

            if (xhr.readyState == 4 && xhr.status == 200) {

                 

                displayResults(xhr.responseText);

 

            }

        };

 

        xhr.send(null);

 

        return xhr;

 

    }

 

    function displayResults(response) { // Affiche les résultats d'une requête

       

        results.style.display = response.length ? 'block' : 'none'; // On cache le conteneur si on n'a pas de résultats

 

        if (response.length) { // On ne modifie les résultats que si on en a obtenu

 

            response = response.split('|');

            var responseLen = response.length;

 

            results.innerHTML = ''; // On vide les résultats

 

            for (var i = 0, div ; i < responseLen ; i++) {

 

                div = results.appendChild(document.createElement('div'));

                div.innerHTML = response[i];

                 

                div.onclick = function() {

                    chooseResult(this);

                };

 

            }

 

        }

 

    }

 

    function chooseResult(result) { // Choisit un des résultats d'une requête et gère tout ce qui y est attaché

       

        searchElement.value = previousValue = result.innerHTML; // On change le contenu du champ de recherche et on enregistre en tant que précédente valeur

        results.style.display = 'none'; // On cache les résultats

        result.className = ''; // On supprime l'effet de focus

        selectedResult = -1; // On remet la sélection à zéro

        searchElement.focus(); // Si le résultat a été choisi par le biais d'un clic, alors le focus est perdu, donc on le réattribue

 

    }

 

 

 

    searchElement.onkeyup = function(e) {

       

        e = e || window.event; // On n'oublie pas la compatibilité pour IE

 

        var divs = results.getElementsByTagName('div');

 

        if (e.keyCode == 38 && selectedResult > -1) { // Si la touche pressée est la flèche « haut »

           

            divs[selectedResult--].className = '';

             

            if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu

                divs[selectedResult].className = 'result_focus';

            }

 

        }

 

        else if (e.keyCode == 40 && selectedResult < divs.length - 1) { // Si la touche pressée est la flèche « bas »

           

            results.style.display = 'block'; // On affiche les résultats

 

            if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu

                divs[selectedResult].className = '';

            }

 

            divs[++selectedResult].className = 'result_focus';

 

        }

 

        else if (e.keyCode == 13 && selectedResult > -1) { // Si la touche pressée est « Entrée »

           

            chooseResult(divs[selectedResult]);

 

        }

 

        else if (searchElement.value != previousValue) { // Si le contenu du champ de recherche a changé

 

            previousValue = searchElement.value;

 

            if (previousRequest && previousRequest.readyState < 4) {

                previousRequest.abort(); // Si on a toujours une requête en cours, on l'arrête

            }

 

            previousRequest = getResults(previousValue); // On stocke la nouvelle requête

 

            selectedResult = -1; // On remet la sélection à zéro à chaque caractère écrit

 

        }

 

    };

 

})();



Les explications

Ce TP n'est pas compliqué en soi mais aborde de nouveaux concepts, il se peut donc que vous soyez quelque peu perdus à la lecture des codes fournis. Laissez-nous vous expliquer comment tout cela fonctionne.


Le serveur : analyser et retourner les données


Comme indiqué plus tôt, il est préférable de commencer par coder notre script serveur, cela évite bien des désagréments par la suite, car il est possible d'analyser manuellement les données retournées par le serveur, et ce sans avoir déjà codé le script client. On peut donc s'assurer du bon fonctionnement du serveur avant de s'attaquer au client.


Tout d'abord, il nous faut définir comment le serveur va recevoir les mots-clés de la recherche. Nous avons choisi la méthode GET et un nom de champ « s », ce qui nous donne la variable PHP $_GET['s'].


Les codes qui vont suivre utilisent diverses fonctions que vous ne connaissez peut-être pas. Si une fonction vous est inconnue, n'hésitez pas à retourner au début de ce chapitre, vous y trouverez de plus amples informations.


Avant de commencer notre analyse de données, il nous faut précharger le fichier, convertir son contenu en tableau PHP et enfin trier ce dernier. À cela s'ajoutent le calcul de la taille du tableau généré ainsi que la création d'un tableau pour sauvegarder les résultats en cohérence avec la recherche :

<?php

 

    $data = unserialize(file_get_contents('towns.txt')); // Récupération de la liste complète des villes

    $dataLen = count($data);

 

    sort($data); // On trie les villes dans l'ordre alphabétique

 

    $results = array(); // Le tableau où seront stockés les résultats de la recherche

 

?>


Maintenant que toutes les données sont accessibles, il va nous falloir les analyser. Basiquement, il s'agit de la même opération qu'en Javascript : une boucle pour parcourir le tableau et une condition pour déterminer si le contenu est valide.


Voici ce que cela donne en PHP :

<?php

// La boucle ci-dessous parcourt tout le tableau $data, jusqu'à un maximum de 10 résultats

 

    for ($i = 0 ; $i < $dataLen && count($results) < 10 ; $i++) {

        if (stripos($data[$i], $_GET['s']) === 0) { // Si la valeur commence par les mêmes caractères que la recherche

       

            // Du code…

         

        }

    }

 

?>


La boucle for possède une condition un peu particulière qui stipule qu'elle doit continuer à tourner tant qu'elle n'a pas lu tout le tableau $data et qu'elle n'a pas atteint le nombre maximum de résultats à retourner. Cette limite de résultats est nécessaire, car une auto-complétion ne doit pas afficher tous les résultats sous peine de provoquer des ralentissements dus au nombre élevé de données, sans compter qu'un trop grand nombre de résultats serait difficile à parcourir (et à analyser) pour l'utilisateur.


La fonction stripos() retourne la première occurrence de la recherche détectée dans la valeur actuellement analysée. Il est nécessaire de vérifier que la valeur retournée est bien égale à 0, car nous ne souhaitons obtenir que les résultats qui commencent par notre recherche. La triple équivalence (===) s'explique par le fait que la fonction stripos() retourne false en cas d'échec de la recherche, ce que la double équivalence (==) aurait confondu avec un 0.


Une fois qu'un résultat cohérent a été trouvé, il ne reste plus qu'à l'ajouter à notre tableau $results :

<?php

 

    for ($i = 0 ; $i < $dataLen && count($results) < 10 ; $i++) {

        if (stripos($data[$i], $_GET['s']) === 0) { // Si la valeur commence par les mêmes caractères que la recherche

       

            array_push($results, $data[$i]); // On ajoute alors le résultat à la liste à retourner

         

        }

    }

 

?>


Une fois que la boucle a terminé son exécution, il ne reste plus qu'à retourner le contenu de notre tableau de résultats sous forme de chaîne de caractères. Lors de la présentation de ce TP, nous avons évoqué le fait de retourner les résultats séparés par une barre verticale, c'est donc ce que nous appliquons dans le code suivant :

<?php

    echo implode('|', $results); // On affiche les résultats séparés par une barre verticale |

?>


Ainsi, notre script côté client n'aura plus qu'à faire un bon vieux split('|') sur la chaîne de caractères obtenue grâce au serveur pour avoir un tableau listant les résultats obtenus.


Le client : préparer le terrain


Une fois le code du serveur écrit et testé, il ne nous reste « plus que » le code client à écrire. Cela commence par le code HTML, qui se veut extrêmement simple avec un champ de texte sur lequel nous avons désactivé l'auto-complétion ainsi qu'une balise <div> destinée à accueillir la liste des résultats obtenus :

<input id="search" type="text" autocomplete="off" />

 

<div id="results"></div>


Voilà tout pour la partie HTML ! En ce qui concerne le Javascript, il nous faut tout d'abord, avant de créer les événements et autres choses fastidieuses, déclarer les variables dont nous allons avoir besoin. Plutôt que de les laisser traîner dans la nature, nous allons les déclarer dans une IEF (pour les trous de mémoire sur ce terme, c'est par ici) :

(function() {

 

    var searchElement = document.getElementById('search'),

        results = document.getElementById('results'),

        selectedResult = -1, // Permet de savoir quel résultat est sélectionné : -1 signifie « aucune sélection »

        previousRequest, // On stocke notre précédente requête dans cette variable

        previousValue = searchElement.value; // On fait de même avec la précédente valeur

 

})();


Si l'utilité de la variable previousValue vous semble douteuse, ne vous en faites pas, vous allez vite comprendre à quoi elle sert !


Le client : gestion des événements


L'événement utilisé est keyup et va se charger de gérer les interactions entre l'utilisateur et la liste de suggestions. Il doit permettre, par exemple, de naviguer dans la liste de résultats et d'en choisir un avec la touche Entrée, mais il doit aussi détecter quand le contenu du champ de recherche change et alors faire appel au serveur pour obtenir une nouvelle liste de résultats.


Commençons tout d'abord par initialiser l'événement en question ainsi que les variables nécessaires :

searchElement.onkeyup = function(e) {

   

    e = e || window.event; // On n'oublie pas la compatibilité pour IE

 

    var divs = results.getElementsByTagName('div'); // On récupère la liste des résultats

 

};


Commençons tout d'abord par gérer les touches fléchées Haut et Bas. C'est là que notre variable selectedResult entre en action, car elle stocke la position actuelle de la sélection des résultats. Avec -1, il n'y a aucune sélection et le curseur se trouve donc sur le champ de recherche ; avec 0, le curseur est positionné sur le premier résultat, 1 désigne le deuxième résultat, etc.


Pour chaque déplacement de la sélection, il vous faut appliquer un style sur le résultat sélectionné afin que l'on puisse le distinguer des autres. Il existe plusieurs solutions pour cela, cependant nous avons retenu celle qui utilise les classes CSS. Autrement dit, lorsqu'un résultat est sélectionné, vous n'avez qu'à lui attribuer une classe CSS qui va modifier son style. Cette classe doit bien sûr être retirée dès qu'un autre résultat est sélectionné. Concrètement, cette solution donne ceci pour la gestion de la flèche Haut :

if (e.keyCode == 38 && selectedResult > -1) { // Si la touche pressée est la flèche « haut »

 

    divs[selectedResult--].className = ''; // On retire la classe de l'élément inférieur et on décrémente la variable « selectedResult »

     

    if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu

        divs[selectedResult].className = 'result_focus'; // On applique une classe à l'élément actuellement sélectionné

    }

 

}


Vous constaterez que la première condition doit vérifier deux règles. La première est la touche frappée, jusque là tout va bien. Quant à la seconde règle, elle consiste à vérifier que notre sélection n'est pas déjà positionnée sur le champ de texte, afin d'éviter de sortir de notre « champ d'action », qui s'étend du champ de texte jusqu'au dernier résultat suggéré par notre auto-complétion.


Curieusement, nous retrouvons une seconde condition (ligne 5) effectuant la même vérification que la première : selectedResult > -1. Cela est en fait logique, car si l'on regarde bien la troisième ligne, la valeur de selectedResult est décrémentée, il faut alors effectuer une nouvelle vérification.


Concernant la flèche Bas, les changements sont assez peu flagrants, ajoutons donc la gestion de cette touche à notre code :

else if (e.keyCode == 40 && selectedResult < divs.length - 1) { // Si la touche pressée est la flèche « bas »

   

    results.style.display = 'block'; // On affiche les résultats « au cas où »

 

    if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu

        divs[selectedResult].className = '';

    }

 

    divs[++selectedResult].className = 'result_focus';

 

}


Ici, les changements portent surtout sur les valeurs à analyser ou à modifier. On ne décrémente plus selectedResult mais on l'incrémente. La première condition est modifiée afin de vérifier que l'on ne se trouve pas à la fin des résultats au lieu du début, etc.


Et, surtout, l'ajout d'une nouvelle ligne (la troisième) qui permet d'afficher les résultats dans tous les cas. Pourquoi cet ajout ? Eh bien, pour simplifier l'utilisation de notre script. Vous le constaterez plus tard, mais lorsque vous choisirez un résultat (donc un clic ou un appui sur Entrée) cela entraînera la disparition de la liste des résultats. Grâce à l'ajout de notre ligne de code, vous pourrez les réafficher très simplement en appuyant sur la flèche Bas !


Venons-en maintenant à la gestion de cette fameuse touche Entrée :

else if (e.keyCode == 13 && selectedResult > -1) { // Si la touche pressée est « Entrée »

   

    chooseResult(divs[selectedResult]);

 

}


Alors oui, vous êtes en droit de vous demander quelle est cette fonction chooseResult(). Il s'agit en fait d'une des trois fonctions que nous allons créer, mais plus tard ! Pour le moment, retenez seulement qu'elle permet de choisir un résultat (et donc de gérer tout ce qui s'ensuit) et qu'elle prend en paramètre l'élément à choisir. Nous nous intéresserons à son code un peu plus tard.


Maintenant, il ne nous reste plus qu'à détecter quand le champ de texte a été modifié.


C’est simple, à chaque fois que l'événement keyup se déclenche, cela veut dire que le champ a été modifié, non ?


Pas tout à fait, non ! Cet événement se déclenche quelle que soit la touche relâchée, cela inclut donc les touches fléchées, les touches de fonction, etc. Tout cela nous pose problème au final, car nous souhaitons savoir quand la valeur du champ de recherche est modifiée et non pas quand une touche quelconque est relâchée. Il y aurait une solution à cela : vérifier que la touche enfoncée fournit bien un caractère, cependant il s'agit d'une vérification assez fastidieuse et pas forcément simple à mettre en place si l'on souhaite être compatible avec Internet Explorer.


C’est donc là que notre variable previousValue entre en piste ! Le principe est d'y enregistrer la dernière valeur du champ de recherche. Ainsi, dès que notre événement se déclenche, il suffit de comparer la variablepreviousValue à la valeur actuelle du champ de recherche ; si c'est différent, alors on enregistre la nouvelle valeur du champ dans la variable, on effectue ce qu'on a à faire et c'est reparti pour un tour. Simple, mais efficace !


Une fois que l'on sait que la valeur de notre champ de texte a été modifiée, il ne nous reste plus qu'à lancer une nouvelle requête effectuant la recherche auprès du serveur :

else if (searchElement.value != previousValue) { // Si le contenu du champ de recherche a changé

 

    previousValue = searchElement.value; // On change la valeur précédente par la valeur actuelle

 

    getResults(previousValue); // On effectue une nouvelle requête

 

    selectedResult = -1; // On remet la sélection à zéro à chaque caractère écrit

 

}


La fonction getResults() sera étudiée plus tard, elle est chargée d'effectuer une requête auprès du serveur, puis d'en afficher ses résultats. Elle prend en paramètre le contenu du champ de recherche.


Il est nécessaire de remettre la sélection des résultats à -1 (ligne 7) car la liste des résultats va être actualisée. Sans cette modification, nous pourrions être positionnés sur un résultat inexistant. La valeur -1 étant celle désignant le champ de recherche, nous sommes sûrs que cette valeur ne posera jamais de problème.


Alors, en théorie, notre code fonctionne plutôt bien, mais il manque cependant une chose : nous ne nous sommes pas encore servis de la variable previousRequest. Rappelez-vous, elle est supposée contenir une référence vers le dernier objet XHR créé, cela afin que sa requête puisse être annulée dans le cas où nous aurions besoin de lancer une nouvelle requête alors que la précédente n'est pas encore terminée. Mettons donc son utilisation en pratique :

else if (searchElement.value != previousValue) { // Si le contenu du champ de recherche a changé

 

    previousValue = searchElement.value;

 

    if (previousRequest && previousRequest.readyState < 4) {

        previousRequest.abort(); // Si on a toujours une requête en cours, on l'arrête

    }

 

    previousRequest = getResults(previousValue); // On stocke la nouvelle requête

 

    selectedResult = -1; // On remet la sélection à zéro à chaque caractère écrit

 

}


Alors, qu'avons-nous de nouveau ? Tout d'abord, il faut savoir que la fonction getResults() est censée retourner l'objet XHR initialisé, nous profitons donc de cela pour stocker ce dernier dans la variable previousRequest (ligne 9).


Ligne 5, vous pouvez voir une condition qui vérifie si la variable previousRequest est bien initalisée et surtout si l'objet XHR qu'elle référence a bien terminé son travail. Si l'objet existe mais que son travail n'est pas terminé, alors on utilise la méthode abort() sur cet objet avant de faire une nouvelle requête.


Le client : déclaration des fonctions


Une fois la mise en place des événements effectuée, il faut passer aux fonctions, car nous faisons appel à elles sans les avoir déclarées. Ces dernières sont au nombre de trois :


Effectuons les étapes dans l'ordre et commençons par la première, getResults(). Cette fonction doit s'occuper de contacter le serveur, de lui communiquer les lettres de la recherche, puis de récupérer la réponse. C'est donc elle qui va se charger de gérer les requêtes XHR. Voici la fonction complète :

function getResults(keywords) { // Effectue une requête et récupère les résultats

       

    var xhr = new XMLHttpRequest();

    xhr.open('GET', './search.php?s='+ encodeURIComponent(keywords));

 

    xhr.onreadystatechange = function() {

        if (xhr.readyState == 4 && xhr.status == 200) {

             

            // Le code une fois la requête terminée et réussie…

 

        }

    };

 

    xhr.send(null);

 

    return xhr;

 

}


Nous avons donc une requête XHR banale qui envoie les termes de la recherche à la page search.php, le tout dans une variable GET nommée « s ». Comme vous pouvez le constater, pensez bien à utiliser la fonction encodeURIComponent() afin d'éviter tout caractère indésirable dans l'URL de la requête.


Le mot-clé return en fin de fonction retourne l'objet XHR initialisé afin qu'il puisse être stocké dans la variable previousRequest pour effectuer une éventuelle annulation de la requête grâce à la méthode abort().


Une fois la requête terminée et réussie, il ne reste plus qu'à afficher les résultats, nous allons donc passer ces derniers en paramètres à la fonction displayResults() :

xhr.onreadystatechange = function() {

    if (xhr.readyState == 4 && xhr.status == 200) {

             

        displayResults(xhr.responseText);

 

    }

};


Passons maintenant à la fonction displayResults(). Cette dernière a pour but d'afficher à l'utilisateur les résultats de la recherche. Son but est donc de parser la réponse de la requête, puis de créer les éléments HTML nécessaires à l'affichage, et enfin de leur attribuer à chacun un des résultats de la recherche. Ce qui nous donne donc ceci :

function displayResults(response) { // Affiche les résultats d'une requête

   

    results.style.display = response.length ? 'block' : 'none'; // On cache le conteneur si on n'a pas de résultats

 

    if(response.length) { // On ne modifie les résultats que si on en a obtenu

 

        response = response.split('|'); // On parse la réponse de la requête afin d'obtenir les résultats dans un tableau

        var responseLen = response.length;

 

        results.innerHTML = ''; // On vide les anciens résultats

 

        for (var i = 0, div ; i < responseLen ; i++) { // On parcourt les nouveaux résultats

 

            div = results.appendChild(document.createElement('div')); // Ajout d'un nouvel élément <div>

            div.innerHTML = response[i];

             

            div.onclick = function() {

                chooseResult(this); // Le résultat sera choisi s'il est cliqué

            };

 

        }

 

    }

 

}


Rien de bien terrible, n'est-ce pas ? Il suffit juste de comprendre que cette fonction crée un nouvel élément pour chaque résultat trouvé et lui attribue un contenu et un événement, rien de plus. 


Maintenant, il ne nous reste plus qu'à étudier la fonction chooseResult(). Basiquement, son but est évident : choisir un résultat, ce qui veut dire qu'un résultat a été sélectionné et doit venir remplacer le contenu de notre champ de recherche.


D'un point de vue utilisateur, l'opération semble simple, mais d'un point de vue développeur il faut penser à gérer pas mal de choses, comme la réinitialisation des styles des résultats par exemple. Voici la fonction :

function chooseResult(result) { // Choisit un des résultats d'une requête et gère tout ce qui y est attaché

   

    searchElement.value = previousValue = result.innerHTML; // On change le contenu du champ de recherche et on enregistre en tant que précédente valeur

    results.style.display = 'none'; // On cache les résultats

    result.className = ''; // On supprime l'effet de focus

    selectedResult = -1; // On remet la sélection à zéro

    searchElement.focus(); // Si le résultat a été choisi par le biais d'un clic, alors le focus est perdu, donc on le réattribue

 

}


Vous voyez, il n'y a rien de bien compliqué pour cette fonction, mais il fallait penser à tous ces petits détails pour éviter d'éventuels bugs minimes.


La correction de ce TP est maintenant terminée, n'hésitez pas à l'améliorer selon vos envies, les possibilités sont multiples.


Idées d'améliorations


Afin de ne pas vous laisser vous reposer sur vos lauriers jusqu'au prochain chapitre, nous vous proposons deux idées d'améliorations.


La première consiste à faciliter la saisie de caractères dans le champ de texte. Le principe consiste à écrire, dans le champ, le premier résultat et à surligner la partie qui n'a pas été mentionnée par l'utilisateur. Exemple :



Le premier résultat est écrit dans le champ et une partie est grisée



Comme vous pouvez le constater, nous avons commencé à écrire les lettres « to », les résultats se sont affichés et surtout le script nous a rajouté les derniers caractères du premier résultat tout en les surlignant afin que l'on puisse réécrire par-dessus sans être gêné dans notre saisie. Ce n'est pas très compliqué à mettre en place, mais cela vous demandera un petit approfondissement du Javascript, notamment grâce au cours « Insertion de balises dans une zone de texte » écrit par Thunderseb sur le Site du Zéro, cela afin de savoir comment surligner seulement une partie d'un texte contenu dans un champ.


La deuxième amélioration consiste à vous faire utiliser un format de structuration afin d'afficher bien plus de données. Par exemple, vous pouvez très bien ajouter des données pour quelques villes (pas toutes quand même), tout transférer de manière structurée grâce au JSON et afficher le tout dans la liste des résultats, comme ceci par exemple :



Il est possible d'afficher des données sur les villes grâce au JSON



Cela fait un bon petit défi, n'est-ce pas ? Sur ce, c'est à vous de choisir si vous souhaitez vous lancer dans cette aventure, nous nous retrouvons à la prochaine partie, qui traitera du HTML5.


Créé avec HelpNDoc Personal Edition: Environnement de création d'aide complet

Site à deux balles