Accueil Dev WEB

Jquery: upload en drag and drop

De retour pour un article, après quelque mois d’absence bien occupés au boulot ;)

Ici je vais vous parler de nouvelles possibilités javascript liées aux drag’n'drop ainsi qu’aux traitements de fichiers. Le but est de pouvoir drag’n'drop un fichier depuis votre explorateur de fichier, vers le navigateur, et d’en lancer ainsi un upload automatique avec suivi de progression, le tout en asynchrone ;)
Je vais d’ailleurs essayer de mettre un maximum de jQuery, comme à mon habitude ! J’aurais pu pousser le développement jusqu’à monter un plugin jQuery, mais finalement, à vous de jouer ;) Cet article n’a pas pour objectif de vous fournir une solution, mais plutôt de vous expliquer son fonctionnement. Vous avez des solutions toutes faites comme celui de valums ou celui de Aquantum (il en existe des tas d’autres).

Allez, une fois n’est pas coutume, une petite démo ! :D


    Pour des raisons de sécurité, vos fichiers ne sont bien sur pas réellement conservés sur mon serveur ;)

    Vous pouvez retrouver des démonstrations sur site en production sur Youtube et Gmail.

    Rentrons dans le vif du sujet. Nous allons décomposer cet outil en 3 grandes étapes:

    1. 1. Gérer un drag’n'drop de fichier vers le navigateur
    2. 2. Upload automatique du fichier
    3. 3. Gestion d’une barre de progression

    1. Gérer un drag’n'drop de fichier vers le navigateur

    Commençons par mettre en place une structure xHTML basique.

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <script type="text/javascript" src="http://code.jquery.com/jquery-1.5.1.min.js"></script>
        <script type="text/javascript" src="ini_dnd.js"></script>
        <link rel="stylesheet" type="text/css" href="uploaddnd.css" />
    </head>
    <body>
        <div id="output">
            <ul id="output-listing"></ul>
        </div>
    </body>
    </html>
    

    Notre div#output sera notre élément xHTML prêt à accueillir un drop, et notre div#output-listing consistera en une liste textuelle des fichiers envoyés. N’hésitez pas à les styliser comme vous le souhaitez.
    Un script externe est appelé, ini_dnd.js , qui contient l’intégralité du code dont nous aurons besoin.

    La première étape est de déclarer les évènements liés au drop d’une entité, et de leur assigner des fonctions:

    $(document).ready(function(){
    
    	// On pose les évènements nécessaires au drag'n'drop
    	$('#output').bind({
    		"dragenter dragexit dragover" : do_nothing,
    		drop : drop
    	});
    
    });
    
    // Fonction stoppant toute évènement natif et leur propagation
    function do_nothing(evt){
    	evt.stopPropagation();
    	evt.preventDefault();
    }
    
    function drop(evt){
    	do_nothing(evt);
    	console.log('test');
    }
    

    Il est important de définir les 4 évènements liés au drag’n'drop, de façon à empêcher toute action native du navigateur. Le seul évènement qui nous intéresse pour notre application est « drop ». Les autres exécutent une fonction qui empêche l’action native navigateur.
    Remarquez la structure du bind, nous permettant d’utiliser une string pour assigner plusieurs évènements à la fois à la même fonction (pas grand chose à voir avec notre fonctionnalité, mais ca ravira les lecteurs qui ne connaissaient pas ;) ).

    On peut maintenant tester de drag’n'drop un fichier depuis notre système de fichier vers notre div#ouput. Cela fonctionne !

    2. Upload automatique du fichier

    Passons à l’upload asynchrone de notre fichier. Vous allez voir qu’il est possible de le réaliser entièrement grâce à jQuery, en modifiant un peu le comportement par défaut de notre librairie préférée ;) SEXY !

    Ce qui va nous permettre de récupérer les informations du fichier à partir de notre drag’n'drop, c’est la nouvelle propriété « dataTransfer » d’un objet Event. Bien sûr, cette propriété n’est pas comprise par tous les navigateurs (IE 9 ? :p). Et à ce titre, jQuery ne reconnait pas cette propriété dans ses propres objets Event. Heureusement, ils nous proposent de pouvoir accéder à toutes les propriétés fournies par le navigateur via la propriété « originalEvent » de leurs objets Event. Mais j’ai choisi une autre solution, que je trouve plus sympa: ajouter la propriété « dataTransfer » aux propriétés natives des Event. Et oui, c’est possible ! Via l’Array « props » de $.event , nous allons pouvoir y ajouter notre fameuse propriété. Modifions donc le document.ready :

    $(document).ready(function(){
    
    	// On ajoute la propriété spéciale dataTransfer à nos events jQuery
    	$.event.props.push("dataTransfer");
    
    	// On pose les évènements nécessaires au drag'n'drop
    	$('#output').bind({
    		"dragenter dragexit dragover" : do_nothing,
    		drop : drop
    	});
    
    });
    

    Puis, modifions la fonction « drop » pour nous afficher le contenu de notre propriété:

    function drop(evt){
    	do_nothing(evt);
    	console.log(evt.dataTransfer);
    }
    

    Nous récupérons bien toutes les propriétés souhaitées, en particulier evt.dataTransfer.files , notre Array de fichiers ! Il va maintenant falloir l’envoyer en POST sur un script côté serveur, afin de pouvoir l’enregistrer physiquement. Pour cela, il serait assez simple de passer directement par un objet XMLHttpRequest car jQuery ne le gère pas par défaut. Mais, comme à mon habitude, je veux le faire avec jQuery. Vous allez voir qu’il suffit simplement de modifier 2 options par défaut de la méthode $.ajax afin de transmettre des fichiers.
    Quoiqu’il arrive, nous allons utiliser l’objet FormData des spécifications « XMLHttpRequest Level 2″. Ce n’est pas un objet nouveau, celui-ci est même connu de IE (notre référence des bas-fonds).
    Voici notre fonction drop modifiée pour notre nouvelle fonctionnalité:

    function drop(evt){
    	do_nothing(evt);
    
    	var files = evt.dataTransfer.files;
    
    	// On vérifie que des fichiers ont bien été déposés
    	if(files.length>0){
    		for(var i in files){
    			// Si c'est bien un fichier
    			if(files[i].size!=undefined) {
    
                	var fic=files[i];
    
                    // On construit notre objet FormData
                    var fd=new FormData;
                    fd.append('fic',fic);
    
                    // Requete ajax pour envoyer le fichier
                    $.ajax({
                        url:'/tests/dnd/save_fic.php',
                        type: 'POST',
                        data: fd,
                        processData:false,
                        contentType:false
                    });
    
    				// On ajoute notre fichier à la liste
    				$('#output-listing').append('<li>'+files[i].name+'</li>');
    
    			}
    		}
    	}
    
    }
    

    Nous déclarons un nouveau FormData et nous y insérons notre objet File.

    Ensuite, afin de pouvoir envoyer en POST un fichier, il nous faut modifier 2 options par défaut dans l’appel à la méthode ajax:

    • processData:false
    • contentType:false

    Si l’on regarde le coeur de jQuery, la méthode ajax va traiter nos données avant de les envoyer. Hors, elle ne reconnait pas notre fichier. Nous lui demandons donc de ne pas traiter les données envoyées via notre option « processData ». Puis, pour la même raison, nous devons empêcher le moteur jQuery de déterminer et envoyer un content-type dans les headers.
    Nous n’avons plus qu’à passer notre FormData dans l’option « data », et le tour est joué. Voici ce que pourrait être le contenu de mon fichier /tests/dnd/save_fic.php :

    	$fic=$_FILES['fic'];
    
    	move_uploaded_file($fic['tmp_name'],$_SERVER['DOCUMENT_ROOT'].'/tests/dnd/'.$fic['name']);
    

    Vous pouvez tester, votre upload est fonctionnel !!! Il ne reste plus qu’à afficher une barre de progression dynamique afin d’informer l’utilisateur de l’avancement.

    3. Gestion d’une barre de progression

    Puisque notre script va être amené à gérer des uploads multiples et simultanés, il faut y penser de suite pour produire autant de barres de progression qu’il le faut. Voici l’exemple xHTML d’une barre de progression que nous utiliserons dans notre exemple:

    <div class="progress_bar loading"><div class="percent">0%</div></div>
    

    De la même façon que pour notre div#output , stylisez votre progressbar comme vous le souhaitez. Nous ajouterons cette barre dynamiquement via javascript, et le script gérera la modification de la width de .percent ainsi que son contenu HTML.

    Ensuite, nous allons préparer l’affichage de la progressbar, afin d’indiquer à l’utilisateur le bon fonctionnement de l’upload. Sous notre requête ajax, générons notre progressbar ainsi:

        // On prépare la barre de progression au démarrage
        var id_tmp=fic.size;
        $('#output').after('
    
    0%
    ');

    Ici je base l’id sur la taille, c’est certainement loin d’être la méthode la plus optimisée, mais vous n’aurez aucun mal à trouver un système plus fonctionnel ;)

    La barre de progression est maintenant mise en place. Il va falloir la modifier dynamiquement en fonction de la réponse du serveur au cours de l’upload. Encore une fois, jQuery n’étant pas prévu pour, il va nous falloir mettre en place une feinte afin de pouvoir ajouter ca via notre méthode ajax.
    Il faut savoir que par défaut, l’évènement « progress » est disponible sur un objet « XMLHttpRequest » via sa propriété « upload » : xhr.upload.addEventListener(‘progress’, function(){}); . Hors, jQuery ne nous permet pas d’accéder à cette propriété via les options de la méthode ajax. Par contre, la librairie nous fournie un moyen de récupérer ET utiliser un objet xhr personnalisé !!! Et c’est ce que nous allons faire ;)

        // On ajoute un listener progress sur l'objet xhr de jQuery
        xhr = jQuery.ajaxSettings.xhr();
        if(xhr.upload){
            xhr.upload.addEventListener('progress', function (e) {
                update_progress(e,fic);
            },false);
        }
        provider=function(){ return xhr; };
    
        $.ajax({
            url:'/tests/dnd/save_fic.php',
            type: 'POST',
            data: fd,
            xhr:provider,
            processData:false,
            contentType:false
        });
    

    Nous récupérons l’objet xhr, nous vérifions qu’il contient la propriété « upload » (suivant les navigateurs), et nous lui ajoutons notre évènement personnalisé ! Il suffit ensuite de passer ce nouvel objet xhr via l’option « xhr » de la méthode ajax.

    Lors du trigger de l’évènement « progress », j’exécute donc la fonction « update_progress », en lui passant en argument notre évènement, ainsi que notre fichier (pour pouvoir accéder à la bonne barre de progression! ;) ). Voici la définition de la fonction « update_progress », qui ne nécessite pas de commentaire particulier au vu de sa facilité:

    // Mise à jour de la barre de progression
    function update_progress(evt,fic) {
    
    	var id_tmp=fic.size;
    
    	if (evt.lengthComputable) {
    		var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
    		if (percentLoaded <= 100) {
    			$('#'+id_tmp+' .percent').css('width', percentLoaded + '%');
    			$('#'+id_tmp+' .percent').html(percentLoaded + '%');
    		}
    	}
    }
    

    Enfin, il ne nous reste plus qu'à compléter à 100% la barre de progression lorsque l'exécution du script est terminée. En effet, l'évènement "progress" ne sera pas trigger à la toute fin de l'upload. Ajoutons donc une simple fonction en callback de l'ajax:

        $.ajax({
            url:'/tests/dnd/save_fic.php',
            type: 'POST',
            data: fd,
            xhr:provider,
            processData:false,
            contentType:false,
            complete:function(data){
                $('#'+data.responseText+' .percent').css('width', '100%');
                $('#'+data.responseText+' .percent').html('100%');
            }
        });
    

    Cette fonction nécessite que l'on renvoie le nom du fichier que l'on vient d'uploader. Le script système pourrait donc devenir:

    	$fic=$_FILES['fic'];
    
    	move_uploaded_file($fic['tmp_name'],$_SERVER['DOCUMENT_ROOT'].'/tests/dnd/'.$fic['name']);
    
    	$id_tmp=filesize($fic['tmp_name']);
    	echo $id_tmp;
    

    Et voila, nous en avons terminé! L'outil est pleinement fonctionnel, amusez-vous bien avec !
    Vous pouvez retrouver mon exemple complet avec les sources sur ce lien démo.

    N'hésitez pas à me laisser vos commentaires sur tout ce qui pourrait être amélioré, ainsi que vos éventuels problèmes ou questions sur son fonctionnement et sa mise en place ;)

    A bientôt pour un nouvel article je l'espère!

    19 Commentaires

    1. epeedelorage
      11 décembre 2011 at 4 h 13 min

      Salut,

      Merci, ca m’a bien aidé même si je n’utilise pas jQuerry.

      Cependant j’ai le même bug que ds la démo au niveau des barres de progressions lorsqu’on drop plusieurs fichier à la fois : le % est actualisé sur la même barre !

      Via un console.log(fic); ds la fonction update_progress, on voit que c’est tjrs le même fichier qui est passé en paramètre… Une idée ?

    2. Renaud Feigenbaum
      13 décembre 2011 at 14 h 07 min

      Salut,
      Oui bien vu, j’ai un peu modifié le javascript pour répondre à ta problématique. Tu peux retrouver le nouvel exemple sur http://www.spiblog.fr/tests/dnd/index_nobugmulti.html .

      Grosso modo, on se sert maintenant entièrement de l’évènement progress pour update les barres de progression. Tout se déroule dans la fonction update_progress .
      Ceci pose un nouveau problème: ajoutons un fichier, puis via un nouveau drag’n'drop, ajoutons le même fichier. Une nouvelle barre de chargement apparait et l’ancienne n’est plus update. Il faudrait simplement ajouter un contrôle de fichier, qui vérifie avant de lancer l’upload que ce fichier n’a pas déjà été envoyé (via un Array tab_already_sended par exemple). Je te laisse faire, ce n’est pas très sorcier !

    3. Vianney
      28 février 2012 at 12 h 27 min

      Cet article est très instructeur !

    4. 18 avril 2012 at 17 h 30 min

      Bonjour, cet article est très bien fais. J’ai juste un problème, je n’arrive pas a traité les informations liés a l’upload de l’image. Mon image s’upload tout simplement pas.

    5. 18 avril 2012 at 17 h 36 min

      Je viens de résoudre le problème :)

      Franchement fabuleux

      Merci beaucoup

    6. Renaud Feigenbaum
      19 avril 2012 at 9 h 27 min

      Un plaisir :)

    7. 31 mai 2012 at 20 h 19 min

      Bonjour,
      Merci pour ce script et les explications ! Je tente de le faire fonctionner depuis un moment, l’upload semble fonctionner, la barre de progression se met à jour, montrant bien que l’upload est en cours, mais je ne comprends pas, mon objet $_FILES est vide lors de l’appel de la page qui traite le fichier après l’upload. J’ai vérifié plusieurs fois les noms de variable, j’ai regardé aussi du côté du enctype, mais rien à faire, mon objet reste vide… Une piste pour m’aider ?

    8. Renaud Feigenbaum
      4 juin 2012 at 11 h 14 min

      Bonjour Arnaud, êtes-vous sur d’avoir utilisé un objet « FormData » ? C’est lui qui indique que les données sont envoyées sous l’enctype « multipart/form-data ».
      Auriez-vous un lien online de votre test?

    9. 9 juin 2012 at 17 h 53 min

      Bonjour,
      Merci pour votre réponse. Oui j’utilisais bien un objet FormData, et j’ai fini par trouver d’ou venait l’erreur. J’avais bêtement remplacé l’appel a code.jquery.com par un appel à une version locale de jquery (1.4.2), qui visiblement ne prenait pas en charge les fonctions utilisées. Problème réglé donc, merci encore pour ce script formidable !

    10. Info
      11 juin 2012 at 17 h 01 min

      Bonjour, j’essaye de tester votre module d’upload mais j’ai une erreur : Notice: Undefined index: fic
      Avez-vous une idée ?
      Merci d’avance

    11. Renaud Feigenbaum
      11 juin 2012 at 17 h 26 min

      Bonjour, vous essayer de le tester depuis mon lien demo ou depuis une interface que vous avez vous-même mis en place?
      Si c’est l’option 2: avez-vous un lien à me communiquer?

    12. Info
      11 juin 2012 at 17 h 48 min

      Oui c’est l’option 2.
      Non désole pas de lien je travaille en local actuellement
      Mon erreur est que la variable $fic dans le php semble vide ou même indéfini…

    13. Renaud Feigenbaum
      12 juin 2012 at 10 h 11 min

      J’aurais tendance à penser au même problème que pour Arnaud: utilisez-vous une version de jQuery à jour? Utilisez-vous bien un objet FormData?
      Si vos deux réponses sont affirmatives, vous pouvez nous copier un var_dump de votre variable $_FILES .

    14. kamui.studio
      21 août 2012 at 19 h 54 min

      Merci !!

      Exactement ce dont j’avais besoin, à savoir une explication des fonctionnalités, plutôt qu’un plugin lâché sans commentaires.

      Encore Merci !!

    15. 15 octobre 2012 at 12 h 01 min

      Bonjour, merci beaucoup pour ces explications, cela m’a permis d’intégrer le drag n drop d’image dans le back office de mes site e-commerce.

      J’ai toutefois un petit soucis, lorsque que je drop une image pour la 2eme fois celle ci s’ouvre dans l’explorer, je suis obligé de recharger la page pour que cela fonction a nouveau.

      Une idée ?

      Merci d’avance.

    16. Renaud Feigenbaum
      15 octobre 2012 at 12 h 36 min

      Bonjour, puisqu’une fois le premier drop effectué, les prochains drop ne sont plus catch, il doit y avoir un procédé dans votre source qui soit unbind l’évènement, soit qui le bind pour une seule exécution.
      Pour vous aider sur cette hypothèse, vous pouvez me communiquer un lien de démo de votre application.
      Autre possibilité: une erreur JS dans le callback du drop pourrait provoquer l’instabilité présentée ici. Pour déterminer cela, un simple affichage d’une console (firebug par exemple) pendant tout le process vous fixera sur cette hypothèse!

    17. 5 novembre 2012 at 16 h 56 min

      Bonjour merci pour votre réponse, pour vous donner un lien de démo, je doit vous créer un compte sur ma boutique de dev.

      pourriez-vous me contactez par mail pour faire cela en privé ?

      Merci d’avance.

    18. 29 janvier 2013 at 16 h 59 min

      Just solved the multibar problem and it work like a charm

      just replace the progress function like that:
      if(xhr.upload){
      (function(fic) {
      xhr.upload.onprogress = function(e)
      {
      update_progress(e,fic);
      };
      }(fic));
      }

    19. 27 mars 2013 at 1 h 37 min

      Amazing article it helped me a lot,
      thanks

    Ajouter un commentaire

    Votre Email n'est jamais publiée ou partagée. Les champs requis sont marqués *.

    *
    *