Envoi d'un fichier en HTML 5 avec XMLHttpRequest
Je vais vous montrer comment envoyer un fichier avec une zone de glissé-déposé (drag and drop en anglais) et une barre de progression sans utiliser jQuery.
Nous allons utiliser des Events et leurs propriétés DataTransfer.
Nous allons créer un objet XMLHttpRequest, une fonction pour vérifier le poids et le type du fichier. C'est ici qu'HTML5 entre en jeux avec l'api File, puisqu'il est possible d'obtenir des informations sur des fichiers stockés localement (sur la machine du visiteur).
HTML
<form id="uploadForm" action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" id="MAX_FILE_SIZE" name="MAX_FILE_SIZE" value="10485760" />
<p id="uploadFileSelect"><label for="img">Image : </label><input id="uploadFileButton" name="img" type="file"></p>
<p id="uploadFileDropArea">Glissez votre image ici</p>
<p><label> </label><span class="txtGris">Fichier de type JPEG, PNG, GIF</span></p>
<p><label> </label><span class="txtGris">Poids maximum : 10 Mo</span></p>
<p id="uploadSend"><label> </label><input type="submit" value="Envoyer"></p>
</form><!--uploadForm-->
<div id="uploadContent">
<h1>Image</h1>
<div id="uploadProgress" class="progressBar"><span id="uploadProgressBar" style="width: 0%"></span></div>
<p id="uploadResult"></p>
</div><!--uploadContent-->
Nous avons deux blocs <p> permettant de sélectionner un fichier :
- #uploadFileSelect qui contient un champ <input type="file" />
- #uploadFileDropArea qui ne contient que le texte "Glissez votre image ici"
Si le visiteur utilise un navigateur compatible avec l'envoi de fichier nous affichons la zone de glissé-déposé (#uploadFileDropArea), sinon nous affichons le champ de sélection (#uploadFileSelect).
Comment savoir si le navigateur est compatible avec l'envoi de fichier ?
Il suffit de créer un objet XMLHttpRequest et de vérifier que la méthode upload soit disponible.
<script type="text/javascript">
//<![CDATA[
var objRequest = new XMLHttpRequest();
if(objRequest.upload){
// le navigateur est compatbible
}else{
// le navigateur n'est pas compatbible
}
//]]>
</script>
Javascript
Maintenant que nous savons déterminer si le navigateur peut envoyer des fichiers avec XMLHttpRequest, commençons par initialiser quelques variables de bases.
// zone de glissé déposé
var dropArea = document.getElementById("uploadFileDropArea");
// bloc div dans lequel on va affiche le résultat de l'envoi du fichier
var uploadResult = document.getElementById("uploadResult");
// bloc p qui contient le champ de sélection pour les navigateurs non compatibles
var uploadFileSelect = document.getElementById("uploadFileSelect");
// objet XMLHttpRequest
var objRequest = new XMLHttpRequest();
Si le navigateur est compatible, on initialise et affiche la zone de glissé déposé, puis on masque le bouton <input type="file" />
if(objRequest.upload){
// un fichier entre dans la zone
dropArea.addEventListener("dragover", FileDrag, false);
// un fichier sort dans la zone
dropArea.addEventListener("dragleave", FileDrag, false);
// un fichier est déposé dans la zone
dropArea.addEventListener("drop", FileSelectHandler, false);
// affiche la zone sur laquelle l'utilisateur peut glisser un fichier
dropArea.style.display = "block";
// masque le bouton de sélection de fichier
uploadFileSelect.style.display = "none";
}
Nous venons d'attacher deux fonctions à la zone de glissé déposé : FileDrag() et FileSelectHandler(). Les deux fonctions reçoivent un paramètre de type event.
function FileDrag(evt){
// stop la diffusion de l'évenements dans le DOM
evt.stopPropagation();
// empêche le navigateur d'exécuter son comportement par défaut (ouvrir ou afficher le fichier glissé)
evt.preventDefault();
// change la classe du bloc
dropArea.className = (evt.type=="dragover" ? "hover":"");
}
function FileSelectHandler(evt){
// appele la fonction qui gère le drag and drop pour stopper l'évênement
FileDrag(evt);
// charge le fichier
UploadFile(evt.dataTransfer.files[0]);
}
Par défaut, lorsque vous glissez un fichier sur une fenêtre de navigateur, il va chercher à l'afficher ou à l'ouvrir. Pour empêcher ce comportement on utilise la fonction preventDefault().
Le navigateur va également propager l'événement à travers la chaine DOM. Même si dans cet exemple ça ne pose pas de problème, il est important de prendre l'habitude de stopper cette propagation avec la fonction... stopPropagation().
FileSelectHandler() va appeler FileDrag() pour stopper la diffusion de l'événement et chargera ensuite le fichier glissé. UploadFile() est appelé comme unique paramètre le fichier à transférer.
Pour accéder au fichier, nous allons utiliser la propriété dataTransfer de l'objet event. Datatransfer n'est disponible que lors des opérations de glissé déposé :
- ondrag
- ondragend
- ondragenter
- ondragleave
- ondragover
- ondragstart
- ondrop
Datatransfer possède l'attribut files qui est une liste de fichier. Dans cet exemple on ne cherche pas à envoyer plusieurs fichiers, il suffit donc d'accéder au premier fichier de la liste. Comme c'est une liste Javascript, le premier élément est indexé à 0 : evt.dataTransfer.files[0]
UploadFile(evt.dataTransfer.files[0]);
Gérer l'envoi du fichier en Javascript
Nous avons accès au fichier grâce à un objet event, il ne reste plus qu'à l'envoyer !
function UploadFile(file){
if(!objRequest.upload){
uploadResult.innerHTML = "Votre naviguateur ne supporte pas l'envoi de fichier";
Si la méthode upload n'est pas disponible, on ne peut pas envoyer de fichier avec XMLHttpRequest.
}else if(file.size > document.getElementById("MAX_FILE_SIZE").value){
// le fichier est trop gros
uploadResult.innerHTML = "Le fichier est trop gros. Limite : "+FileConvertSize(document.getElementById("MAX_FILE_SIZE").value);
}else if(!(file.type=="image/jpeg" || file.type=="image/png" || file.type=="image/gif")){
// le fichier n'est pas du bon type
uploadResult.innerHTML = "Le type du fichier est incorrect : PNG, JPG, GIF";
Avant d'envoyer le fichier on vérifie son poids et son type mime.
Pour le poids c'est très simple. Nous avions défini MAX_FILE_SIZE dans le formulaire. Il suffit de comparer ce champ avec le poids du fichier, que l'on trouve grâce à file.size. Les deux étant exprimé en octets nous n'avons aucune conversion à faire !
Pour le type MIME, il suffit de comparer file.type avec les différents types de fichier que nous souhaitons traiter. Ici du JPEG, du PNG et du GIF. J'ai écrit un article sur les types MIME courant, n'hésitez pas à le consulter.
}else{
// tout est ok, on peut envoyer le fichier
// crée une fonction pour afficher la progresion de la requete
objRequest.upload.onprogress = function(evt){
if(evt.lengthComputable){
var percent = Math.ceil((evt.loaded / evt.total)*100);
document.getElementById("uploadProgressBar").style.width = percent+'%';
uploadResult.innerHTML = percent+' %';
}
}
// onreadystatechange est appelé à chaque changement d'état de la requete
objRequest.onreadystatechange=function(){
if(objRequest.readyState==4 && objRequest.status==200){
uploadResult.innerHTML = objRequest.responseText;
}
}
Le fichier n'est pas trop gros et il est au bon format, on peut enfin l'envoyer! Nous allons préparer deux fonctions que nous allons attacher à notre objet XMLHttpRequest.
La première nous servira à afficher la progression du chargement avec onprogress. La function onprogress passe un paramètre avec 3 attributs en lecture seule :
- lengthComputable (boolean) indique si la dimension peut-être calculé. Si cette lengthComputable==0, total n'aura aucune valeur
- loaded (unsigned long) est la part déjà chargée
- total (unsigned long) est le poids total du fichier
Si lengthComputable est égal à 0, le problème vient sans doute de votre serveur qui n'envoie pas une valeur Content-Length correcte. Si la valeur est correcte, il suffit de faire une division pour connaître le pourcentage chargé.
La seconde fonction est appelé à chaque changement d'état de notre objet XMLHttpRequest. On l'attache à onreadystatechange et on vérifie la valeur de l'attribut readyState :
- 0 = UNSENT
- 1 = OPENED
- 2 = HEADERS_RECEIVED
- 3 = LOADING
- 4 = DONE
Pendant l'envoi du fichier onreadystatechange sera donc appelé 5 fois.
La valeur qui nous intéresse est readyState==4, ce qui veut dire que la requête est terminée, mais cela ne suffit pas. Il faut également vérifier que le serveur a renvoyé un statut 200 OK. Si c'est le cas, tout va bien sinon il faudra afficher un message d'erreur à l'utilisateur !
Ne reste plus qu'à afficher la réponse du serveur avec objRequest.responseText. Si le serveur a renvoyé des données XML vous pouvez les traiter avec objRequest.responseXML.
XMLHttpRequest.open() et XMLHttpRequest.send()
La fin approche !
Le fichier est prêt à être envoyé et nous avons les fonctions pour traiter les réponses du serveur. Mais il manque l'essentiel, c'est-à-dire envoyer le fichier. Heureusement c'est très simple, quelques lignes suffisent.
// créer l'objet FormData
var formData = new FormData();
// ajoute l'image
formData.append("img", file);
// ouvre une requete post, avec l'adresse du formulaire
objRequest.open("POST", document.getElementById("uploadForm").action);
// on envois le fichier
objRequest.send(formData);
// indique le début du chargement
uploadResult.innerHTML = "Chargement en cours";
// affiche la barre de progression
document.getElementById("uploadProgress").style.display = "block";
// masque le bouton d'envoi
document.getElementById("uploadSend").style.display = "none";
}
}
1- Nous allons créer un objet de type FormData. Un objet FormData c'est une suite d'entrée avec pour chacune un nom et une valeur. Comme une liste indexée en PHP.
2- On ajoute l'image. On accédera à l'image avec la superglobale $_FILES['img']
3- Initialise la requête avec open. Open peut recevoir 5 paramètres
- method "GET", "POST", ...
- url d'envoi du formulaire (on récupère la valeur de l'attribut action du formulaire)
- async (optionnel) envoi du formulaire de façon asynchronne ou non (par défaut vaut 1)
- user (optionnel) nom d'utilisateur
- password (optionnel) mot de passe
4- Envoi la requête avec send. Send ne reçoit aucun paramètre.
5- On affiche la barre de progression
PHP
Le fichier PHP est le même que dans l'article sur la méthode d'envoi de fichier avec jQuery.
Exemple concret
Voir cet exemple en fonctionnement.
Pour une utilisation réelle il faut prévoir deux fichiers PHP différents :
- Un pour les navigateurs compatibles qui se contente de traiter l'image et d'envoyer une réponse simple (c'est le cas de cet exemple)
- Un autre pour les navigateurs qui ne sont pas compatible et pour qui il faut affiché un page complète.
Dans cette démo j'ai été un peu flemmard.