Comment créer un téléchargeur de fichiers par glisser-déposer avec Vanilla JavaScript

Publié: 2022-03-10
Résumé rapide ↬ Dans cet article, nous utiliserons JavaScript ES2015+ "vanille" (pas de frameworks ni de bibliothèques) pour mener à bien ce projet, et il est supposé que vous avez une connaissance pratique de JavaScript dans le navigateur. Cet exemple devrait être compatible avec tous les navigateurs evergreen plus IE 10 et 11.

C'est un fait connu que les entrées de sélection de fichiers sont difficiles à styliser comme les développeurs le souhaitent, donc beaucoup les cachent simplement et créent un bouton qui ouvre la boîte de dialogue de sélection de fichiers à la place. De nos jours, cependant, nous avons une façon encore plus sophistiquée de gérer la sélection de fichiers : le glisser-déposer.

Techniquement, cela était déjà possible car la plupart (sinon toutes ) les implémentations de l'entrée de sélection de fichiers vous permettaient de faire glisser des fichiers dessus pour les sélectionner, mais cela nécessite que vous affichiez réellement l'élément file . Utilisons donc les API qui nous sont fournies par le navigateur pour implémenter un sélecteur et un téléchargeur de fichiers par glisser-déposer.

Dans cet article, nous utiliserons JavaScript ES2015+ "vanille" (pas de frameworks ni de bibliothèques) pour mener à bien ce projet, et il est supposé que vous avez une connaissance pratique de JavaScript dans le navigateur. Cet exemple - mis à part la syntaxe ES2015 +, qui peut facilement être changée en syntaxe ES5 ou transpilée par Babel - devrait être compatible avec tous les navigateurs à feuilles persistantes plus IE 10 et 11.

Voici un aperçu rapide de ce que vous allez fabriquer :

Téléchargeur d'images par glisser-déposer en action
Une démonstration d'une page Web dans laquelle vous pouvez télécharger des images par glisser-déposer, prévisualiser les images en cours de téléchargement immédiatement et voir la progression du téléchargement dans une barre de progression.

Glisser-déposer des événements

La première chose dont nous devons discuter est les événements liés au glisser-déposer car ils sont le moteur de cette fonctionnalité. En tout, le navigateur déclenche huit événements liés au glisser-déposer : drag , dragend , dragenter , dragexit , dragleave , dragover , dragstart et drop . Nous ne les passerons pas tous en revue car drag , dragend , dragexit et dragstart sont tous déclenchés sur l'élément qui est glissé, et dans notre cas, nous allons faire glisser des fichiers depuis notre système de fichiers plutôt que des éléments DOM , donc ces événements ne s'afficheront jamais.

Si vous êtes curieux à leur sujet, vous pouvez lire de la documentation sur ces événements sur MDN.

Plus après saut! Continuez à lire ci-dessous ↓

Comme vous pouvez vous y attendre, vous pouvez enregistrer des gestionnaires d'événements pour ces événements de la même manière que vous enregistrez des gestionnaires d'événements pour la plupart des événements de navigateur : via addEventListener .

 let dropArea = document.getElementById('drop-area') dropArea.addEventListener('dragenter', handlerFunction, false) dropArea.addEventListener('dragleave', handlerFunction, false) dropArea.addEventListener('dragover', handlerFunction, false) dropArea.addEventListener('drop', handlerFunction, false)

Voici un petit tableau décrivant ce que font ces événements, en utilisant dropArea de l'exemple de code afin de rendre le langage plus clair :

Événement Quand est-il viré ?
dragenter L'élément déplacé est déplacé sur dropArea, ce qui en fait la cible de l'événement de dépôt si l'utilisateur le dépose à cet endroit.
dragleave L'élément glissé est glissé hors de dropArea et sur un autre élément, ce qui en fait la cible de l'événement drop à la place.
dragover Toutes les quelques centaines de millisecondes, pendant que l'élément déplacé se trouve sur dropArea et se déplace.
drop L'utilisateur relâche le bouton de la souris, déposant l'élément glissé sur dropArea.

Notez que l'élément glissé est glissé sur un enfant de dropArea , dragleave se déclenchera sur dropArea et dragenter se déclenchera sur cet élément enfant car il s'agit de la nouvelle target . L'événement drop se propagera jusqu'à dropArea (à moins que la propagation ne soit arrêtée par un autre écouteur d'événement avant qu'il n'y parvienne), il se déclenchera donc toujours sur dropArea bien qu'il ne soit pas la target de l'événement.

Notez également que pour créer des interactions personnalisées par glisser-déposer, vous devrez appeler event.preventDefault() dans chacun des écouteurs de ces événements. Si vous ne le faites pas, le navigateur finira par ouvrir le fichier que vous avez déposé au lieu de l'envoyer au gestionnaire d'événements de drop .

Configurer notre formulaire

Avant de commencer à ajouter des fonctionnalités de glisser-déposer, nous aurons besoin d'un formulaire de base avec une entrée de file standard. Techniquement, ce n'est pas nécessaire, mais c'est une bonne idée de le proposer comme alternative au cas où l'utilisateur dispose d'un navigateur sans prise en charge de l'API glisser-déposer.

 <div> <form class="my-form"> <p>Upload multiple files with the file dialog or by dragging and dropping images onto the dashed region</p> <input type="file" multiple accept="image/*" onchange="handleFiles(this.files)"> <label class="button" for="fileElem">Select some files</label> </form> </div>

Structure assez simple. Vous remarquerez peut-être un gestionnaire onchange sur l' input . Nous verrons cela plus tard. Ce serait également une bonne idée d'ajouter une action au form et un bouton d' submit pour aider les personnes qui n'ont pas activé JavaScript. Ensuite, vous pouvez utiliser JavaScript pour vous en débarrasser pour un formulaire plus propre. Dans tous les cas, vous aurez besoin d'un script côté serveur pour accepter le téléchargement, qu'il s'agisse de quelque chose développé en interne ou que vous utilisiez un service comme Cloudinary pour le faire pour vous. À part ces notes, il n'y a rien de spécial ici, alors ajoutons quelques styles :

 #drop-area { border: 2px dashed #ccc; border-radius: 20px; width: 480px; font-family: sans-serif; margin: 100px auto; padding: 20px; } #drop-area.highlight { border-color: purple; } p { margin-top: 0; } .my-form { margin-bottom: 10px; } #gallery { margin-top: 10px; } #gallery img { width: 150px; margin-bottom: 10px; margin-right: 10px; vertical-align: middle; } .button { display: inline-block; padding: 10px; background: #ccc; cursor: pointer; border-radius: 5px; border: 1px solid #ccc; } .button:hover { background: #ddd; } #fileElem { display: none; }

Beaucoup de ces styles n'entrent pas encore en jeu, mais ce n'est pas grave. Les faits saillants, pour l'instant, sont que l'entrée file est masquée, mais son label est conçue pour ressembler à un bouton, afin que les utilisateurs réalisent qu'ils peuvent cliquer dessus pour afficher la boîte de dialogue de sélection de fichier. Nous suivons également une convention en décrivant la zone de dépôt avec des lignes pointillées.

Ajout de la fonctionnalité glisser-déposer

Passons maintenant au cœur de la situation : glisser-déposer. Jetons un script en bas de la page, ou dans un fichier séparé, comme bon vous semble. La première chose dont nous avons besoin dans le script est une référence à la zone de dépôt afin que nous puissions y attacher des événements :

 let dropArea = document.getElementById('drop-area')

Ajoutons maintenant quelques événements. Nous commencerons par ajouter des gestionnaires à tous les événements pour empêcher les comportements par défaut et empêcher les événements de remonter plus haut que nécessaire :

 ;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }

Ajoutons maintenant un indicateur pour faire savoir à l'utilisateur qu'il a effectivement fait glisser l'élément sur la bonne zone en utilisant CSS pour changer la couleur de la bordure de la zone de dépôt. Les styles devraient déjà être là sous le sélecteur #drop-area.highlight , alors utilisons JS pour ajouter et supprimer cette classe de highlight si nécessaire.

 ;['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, highlight, false) }) ;['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, unhighlight, false) }) function highlight(e) { dropArea.classList.add('highlight') } function unhighlight(e) { dropArea.classList.remove('highlight') }

Nous avons dû utiliser à la fois dragenter et dragover pour la mise en évidence à cause de ce que j'ai mentionné plus tôt. Si vous commencez à survoler directement dropArea , puis sur l'un de ses enfants, le dragleave sera déclenché et la surbrillance sera supprimée. L'événement dragover est déclenché après les événements dragenter et dragleave , de sorte que la surbrillance sera rajoutée sur dropArea avant de la voir supprimée.

Nous supprimons également la surbrillance lorsque l'élément déplacé quitte la zone désignée ou lorsque vous déposez l'élément.

Il ne nous reste plus qu'à déterminer ce qu'il faut faire lorsque certains fichiers sont supprimés :

 dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }

Cela ne nous amène nulle part près de l'achèvement, mais cela fait deux choses importantes :

  1. Montre comment obtenir les données des fichiers qui ont été supprimés.
  2. Nous amène au même endroit où se trouvait l' input du file avec son gestionnaire onchange : en attente de handleFiles .

Gardez à l'esprit que files n'est pas un tableau, mais un FileList . Ainsi, lorsque nous implémenterons handleFiles , nous devrons le convertir en un tableau afin de le parcourir plus facilement :

 function handleFiles(files) { ([...files]).forEach(uploadFile) }

C'était décevant. Entrons dans uploadFile pour les vrais trucs charnus.

 function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(() => { /* Done. Inform the user */ }) .catch(() => { /* Error. Inform the user */ }) }

Ici, nous utilisons FormData , une API de navigateur intégrée pour créer des données de formulaire à envoyer au serveur. Nous utilisons ensuite l'API de fetch pour envoyer l'image au serveur. Assurez-vous de modifier l'URL pour qu'elle fonctionne avec votre back-end ou votre service, et formData.append toutes les données de formulaire supplémentaires dont vous pourriez avoir besoin pour donner au serveur toutes les informations dont il a besoin. Alternativement, si vous souhaitez prendre en charge Internet Explorer, vous pouvez utiliser XMLHttpRequest , ce qui signifie que uploadFile ressemblera à ceci :

 function uploadFile(file) { var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

Selon la configuration de votre serveur, vous voudrez peut-être vérifier différentes plages de numéros de status plutôt que seulement 200 , mais pour nos besoins, cela fonctionnera.

Caractéristiques supplémentaires

C'est toute la fonctionnalité de base, mais souvent nous voulons plus de fonctionnalités. Plus précisément, dans ce didacticiel, nous ajouterons un volet de prévisualisation qui affiche toutes les images choisies à l'utilisateur, puis nous ajouterons une barre de progression qui permettra à l'utilisateur de voir la progression des téléchargements. Alors, commençons par prévisualiser les images.

Aperçu de l'image

Vous pouvez procéder de plusieurs manières : vous pouvez attendre que l'image ait été téléchargée et demander au serveur d'envoyer l'URL de l'image, mais cela signifie que vous devez attendre et que les images peuvent parfois être assez volumineuses. L'alternative - que nous allons explorer aujourd'hui - consiste à utiliser l'API FileReader sur les données de fichier que nous avons reçues de l'événement drop . Ceci est asynchrone et vous pouvez également utiliser FileReaderSync, mais nous pourrions essayer de lire plusieurs fichiers volumineux à la suite, ce qui pourrait bloquer le fil pendant un certain temps et vraiment gâcher l'expérience. Créons donc une fonction previewFile et voyons comment cela fonctionne :

 function previewFile(file) { let reader = new FileReader() reader.readAsDataURL(file) reader.onloadend = function() { let img = document.createElement('img') img.src = reader.result document.getElementById('gallery').appendChild(img) } }

Ici, nous créons un new FileReader et appelons readAsDataURL avec l'objet File . Comme mentionné, ceci est asynchrone, nous devons donc ajouter un gestionnaire d'événements onloadend afin d'obtenir le résultat de la lecture. Nous utilisons ensuite l'URL de données de base 64 comme src pour un nouvel élément d'image et l'ajoutons à l'élément de gallery . Il n'y a que deux choses à faire pour que cela fonctionne maintenant : ajoutez l'élément de la gallery et assurez-vous que previewFile est bien appelé.

Tout d'abord, ajoutez le code HTML suivant juste après la fin de la balise form :

 <div></div>

Rien de spécial; c'est juste une div. Les styles sont déjà spécifiés pour cela et les images qu'il contient, il n'y a donc plus rien à faire là-bas. Remplaçons maintenant la fonction handleFiles par la suivante :

 function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }

Vous auriez pu le faire de plusieurs manières, telles que la composition ou un seul rappel à forEach qui a exécuté uploadFile et previewFile , mais cela fonctionne aussi. Et avec cela, lorsque vous déposez ou sélectionnez des images, elles devraient apparaître presque instantanément sous le formulaire. La chose intéressante à ce sujet est que, dans certaines applications, vous ne souhaitez peut-être pas réellement télécharger des images, mais plutôt stocker leurs URL de données dans localStorage ou dans un autre cache côté client auquel l'application pourra accéder ultérieurement. Personnellement, je ne peux pas penser à de bons cas d'utilisation pour cela, mais je suis prêt à parier qu'il y en a.

Suivi des progrès

Si quelque chose peut prendre un certain temps, une barre de progression peut aider un utilisateur à se rendre compte que des progrès sont réellement en cours et donner une indication du temps qu'il faudra pour terminer. L'ajout d'un indicateur de progression est assez simple grâce à la balise progress HTML5. Commençons par ajouter cela au code HTML cette fois.

 <progress max=100 value=0></progress>

Vous pouvez placer cela juste après l' label ou entre le form et la galerie div , selon ce que vous préférez. D'ailleurs, vous pouvez le placer où vous voulez dans les balises body . Aucun style n'a été ajouté pour cet exemple, il affichera donc l'implémentation par défaut du navigateur, qui est réparable. Travaillons maintenant sur l'ajout du JavaScript. Nous allons d'abord examiner l'implémentation à l'aide de fetch , puis nous montrerons une version pour XMLHttpRequest . Pour commencer, nous aurons besoin de quelques nouvelles variables en haut du script :

 let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')

Lors de l'utilisation de fetch , nous ne pouvons déterminer que le moment où un téléchargement est terminé. Ainsi, les seules informations que nous suivons sont le nombre de fichiers sélectionnés à télécharger (en tant que filesToDo ) et le nombre de fichiers dont le téléchargement est terminé (en tant que filesDone ). Nous gardons également une référence à l'élément #progress-bar afin de pouvoir le mettre à jour rapidement. Créons maintenant quelques fonctions pour gérer la progression :

 function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }

Lorsque nous commençons le téléchargement, initializeProgress sera appelé pour réinitialiser la barre de progression. Ensuite, à chaque téléchargement terminé, nous appellerons progressDone pour incrémenter le nombre de téléchargements terminés et mettre à jour la barre de progression pour afficher la progression actuelle. Appelons donc ces fonctions en mettant à jour quelques anciennes fonctions :

 function handleFiles(files) { files = [...files] initializeProgress(files.length) // <- Add this line files.forEach(uploadFile) files.forEach(previewFile) } function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(progressDone) // <- Add `progressDone` call here .catch(() => { /* Error. Inform the user */ }) }

Et c'est tout. Examinons maintenant l'implémentation de XMLHttpRequest . Nous pourrions simplement faire une mise à jour rapide de uploadFile , mais XMLHttpRequest nous donne en fait plus de fonctionnalités que fetch , à savoir que nous sommes en mesure d'ajouter un écouteur d'événement pour la progression du téléchargement sur chaque demande, ce qui nous donnera périodiquement des informations sur la quantité de la demande est achevé. Pour cette raison, nous devons suivre le pourcentage d'achèvement de chaque demande au lieu du nombre de demandes effectuées. Commençons donc par remplacer les déclarations de filesDone et filesToDo par ce qui suit :

 let uploadProgress = []

Ensuite, nous devons également mettre à jour nos fonctions. Nous allons renommer progressDone en updateProgress et les modifier pour qu'ils soient les suivants :

 function initializeProgress(numFiles) { progressBar.value = 0 uploadProgress = [] for(let i = numFiles; i > 0; i--) { uploadProgress.push(0) } } function updateProgress(fileNumber, percent) { uploadProgress[fileNumber] = percent let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length progressBar.value = total }

Maintenant initializeProgress initialise un tableau avec une longueur égale à numFiles qui est rempli de zéros, indiquant que chaque fichier est complet à 0 %. Dans updateProgress nous découvrons quelle image voit sa progression mise à jour et changeons la valeur à cet index en percent fourni. Nous calculons ensuite le pourcentage de progression total en prenant une moyenne de tous les pourcentages et mettons à jour la barre de progression pour refléter le total calculé. Nous appelons toujours initializeProgress dans handleFiles de la même manière que dans l'exemple de fetch , donc maintenant tout ce que nous devons mettre à jour est uploadFile pour appeler updateProgress .

 function uploadFile(file, i) { // <- Add `i` parameter var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) // Add following event listener xhr.upload.addEventListener("progress", function(e) { updateProgress(i, (e.loaded * 100.0 / e.total) || 100) }) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

La première chose à noter est que nous avons ajouté un paramètre i . Il s'agit de l'index du fichier dans la liste des fichiers. Nous n'avons pas besoin de mettre à jour handleFiles pour transmettre ce paramètre car il utilise forEach , qui donne déjà l'index de l'élément comme deuxième paramètre aux rappels. Nous avons également ajouté l'écouteur d'événement de progress à xhr.upload afin que nous puissions appeler updateProgress avec la progression. L'objet événement (appelé e dans le code) contient deux informations pertinentes : loaded qui contient le nombre d'octets qui ont été téléchargés jusqu'à présent et total qui contient le nombre d'octets total du fichier.

Le || 100 || 100 pièce est là parce que parfois s'il y a une erreur, e.loaded et e.total seront nuls, ce qui signifie que le calcul sortira comme NaN , donc le 100 est utilisé à la place pour signaler que le fichier est terminé. Vous pouvez également utiliser 0 . Dans les deux cas, l'erreur apparaîtra dans le gestionnaire readystatechange afin que vous puissiez en informer l'utilisateur. C'est simplement pour éviter que des exceptions ne soient levées pour essayer de faire des calculs avec NaN .

Conclusion

C'est la dernière pièce. Vous avez maintenant une page Web sur laquelle vous pouvez télécharger des images par glisser-déposer, prévisualiser les images en cours de téléchargement immédiatement et voir la progression du téléchargement dans une barre de progression. Vous pouvez voir la version finale (avec XMLHttpRequest ) en action sur CodePen, mais sachez que le service sur lequel je télécharge les fichiers a des limites, donc si beaucoup de gens le testent, il peut se casser pendant un certain temps.