Utiliser des fichiers à partir d'applications web
=================================================

Grâce à l'API *File*, une application ou un site web peut demander à la personne de choisir des fichiers locaux et en lire les contenus. La sélection peut être effectuée à l'aide d'un élément `<input type="file">` ou à l'aide d'un glisser-déposer.

Accéder aux fichiers sélectionnés
---------------------------------

Prenons ce fragment de HTML :


```html
<input type="file" id="input" multiple />
```


L'API *File* permet d'accéder à un objet `FileList` qui contient des objets `File` représentant les fichiers sélectionnés par l'utilisatrice ou l'utilisateur.

L'attribut `multiple` placé sur l'élément `input` permet de sélectionner plusieurs fichiers.

On peut accéder au premier fichier sélectionné grâce à un sélecteur DOM classique :


```js
const fichierSelectionne = document.getElementById("input").files[0];
```

### Accéder aux fichiers sélectionnés lors d'un évènement `change`

Il est aussi possible (même si ça n'est pas obligatoire) d'accéder à la liste `FileList` lors de l'évènement `change`. Pour cela, on utilisera `EventTarget.addEventListener()` pour ajouter un gestionnaire d'évènement sur `change`, comme ceci :


```js
const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handleFiles, false);
function handleFiles() {
  const fileList =
    this.files; /\* on peut désormais manipuler la liste de fichiers \*/
}
```

Obtenir des informations sur le(s) fichier(s) sélectionné(s)
------------------------------------------------------------

L'objet `FileList` fourni par le DOM liste l'ensemble des fichiers sélectionnés par la personne, chacun étant représenté par un objet `File`. On peut connaître le nombre de fichiers sélectionnés en consultant la valeur de la propriété `length` de la liste :


```js
const nbFichiers = fileList.length;
```


Les objets `File` individuels peuvent être récupérés depuis la liste en utilisant les indices, comme pour un tableau.

Trois propriétés des objets `File` fournissent des informations utiles sur les fichiers.

`name`

Le nom du fichier sous la forme d'une chaîne de caractères en lecture seule. Il s'agit uniquement du nom du fichier, aucune information n'est fournie sur l'emplacement.

`size`

La taille du fichier, exprimée en octets, sous la forme d'un entier en lecture seule sur 64 bits.

`type`

Le type MIME du fichier, sous la forme d'une chaîne de caractères en lecture seule ou `""` si le type du fichier n'a pu être déterminé.

### Exemple : indiquer la taille des fichiers

Dans l'exemple qui suit, on illustre comment utiliser la propriété `size`.

#### HTML


```html
<form name="uploadForm">
  <div>
    <input id="uploadInput" type="file" multiple />
    <label for="fileNum">Fichiers sélectionnés&nbsp;:</label>
    <output id="fileNum">0</output>;
    <label for="fileSize">Taille totale&nbsp;:</label>
    <output id="fileSize">0</output>
  </div>
  <div><input type="submit" value="Envoyer des fichiers" /></div>
</form>
```


#### JavaScript


```js
const uploadInput = document.getElementById("uploadInput");
uploadInput.addEventListener(
  "change",
  () => {
    // Calcul de la taille totale
    let numberOfBytes = 0;
    for (const file of uploadInput.files) {
      numberOfBytes += file.size;
    }

    // Approximation à l'unité humaine la plus proche
    const units = ["o", "Ko", "Mo", "Go", "To", "Po", "Eo", "Zo", "Yo"];
    const exponent = Math.min(
      Math.floor(Math.log(numberOfBytes) / Math.log(1024)),
      units.length - 1,
    );
    const approx = numberOfBytes / 1024 \*\* exponent;
    const output =
      exponent === 0
        ? `${numberOfBytes} octets`
        : `${approx.toFixed(3)} ${units[exponent]} (${numberOfBytes} octets)`;

    document.getElementById("fileNum").textContent = uploadInput.files.length;
    document.getElementById("fileSize").textContent = output;
  },
  false,
);
```

Masquer l'élément `<input>` et utiliser la méthode `click()`
------------------------------------------------------------

Il est possible de masquer l'élément `<input>` et de présenter sa propre interface pour l'ouverture du sélecteur de fichiers et d'afficher par ailleurs la sélection de fichier. Pour obtenir ce résultat, on pourra mettre en forme le champ `<input>` avec `display:none` et appeler la méthode `click()` sur l'élément `<input>`.

Prenons ce fragment de code HTML :


```html
<input
 type="file"
 id="fileElem"
 multiple
 accept="image/\*"
 style="display:none" />
<button id="fileSelect" type="button">Choisissez des fichiers</button>
```


Le code qui gère l'évènement `click` pourra ressembler à :


```js
const fileSelect = document.getElementById("fileSelect");
const fileElem = document.getElementById("fileElem");

fileSelect.addEventListener(
  "click",
  (e) => {
    if (fileElem) {
      fileElem.click();
    }
  },
  false,
);
```


Vous pouvez mettre en forme `<button>` comme bon vous semble.

Utiliser un élément `<label>` pour déclencher un champ `<input type="file">` caché
----------------------------------------------------------------------------------

Afin d'ouvrir le sélecteur de fichier sans utiliser de JavaScript (c'est-à-dire sans la méthode `click()`), on pourra utiliser un élément `<label>` à la place. Cependant, il faut ici que l'élément `<input>` ne soit pas masqué à l'aide de `display: none` (ni à l'aide de `visibility: hidden`), car le libellé ne serait alors pas accessible au clavier. Il faut utiliser la technique `visually-hidden` à la place.

Prenons ce fragment de code HTML :


```html
<input
 type="file"
 id="fileElem"
 multiple
 accept="image/\*"
 class="visually-hidden" />
<label for="fileElem">Choisissez des fichiers</label>
```


Et ce fragment de code CSS :


```css
.visually-hidden {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px, 1px, 1px, 1px);
}

input.visually-hidden:is(:focus, :focus-within) + label {
  outline: thin dotted;
}
```


Il n'est ici pas nécessaire d'ajouter de code JavaScript pour invoquer `fileElem.click()`. On peut ici mettre en forme l'élément `<label>` comme bon nous semble. Il faudra fournir une indication visuelle pour l'état du focus sur le champ masqué sur le libellé, que ce soit un contour comme ci-avant (avec `outline` ou une couleur d'arrière-plan (`background-color`), ou une ombre portée (`box-shadow`).

Sélectionner des fichiers avec un glisser-déposer
-------------------------------------------------

Il est aussi possible de permettre la sélection de fichier grâce au glisser-déposer.

La première étape consiste à former une zone de dépôt. La zone de votre contenu qui acceptera le dépôt pourra varier selon le design de votre application, mais on pourra facilement construire un élément prêt à recevoir les évènements de dépôt :


```js
let dropbox;

dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter, false);
dropbox.addEventListener("dragover", dragover, false);
dropbox.addEventListener("drop", drop, false);
```


Dans cet exemple, on utilise un élément avec l'identifiant `dropbox` comme zone de dépôt. Pour ce faire, on ajoute des gestionnaires d'évènements pour `dragenter`, `dragover`, et `drop`.

Nous n'avons pas à nous préoccuper de `dragenter` et `dragover` dans notre exemple, nous allons donc aller à l'essentiel et empêcher l'action par défaut du navigateur :


```js
function dragenter(e) {
  e.stopPropagation();
  e.preventDefault();
}

function dragover(e) {
  e.stopPropagation();
  e.preventDefault();
}
```


La partie vraiment intéressante se situe dans la fonction `drop()` :


```js
function drop(e) {
  e.stopPropagation();
  e.preventDefault();

  const dt = e.dataTransfer;
  const files = dt.files;

  handleFiles(files);
}
```


Ici, on récupère la propriété `dataTransfer` de l'évènement, puis on récupère la liste de fichiers à partir de celle-ci. On passe alors cette liste à la fonction `handleFiles()`. Désormais, on peut utiliser la même logique, que les fichiers aient été sélectionnés via un élément `<input>` ou avec un glisser-déposer.

Exemple : afficher des vignettes pour les images sélectionnées
--------------------------------------------------------------

Prenons comme exemple un site web de partage de photos dans lequel on veut des vignettes de prévisualisation des images sélectionnées avant qu'elles soient effectivement enregistrées sur le site. On peut construire un élément `<input>` et/ou une zone de dépôt comme indiqué avant et appeler la fonction `handleFiles()` qui suit.


```js
function handleFiles(files) {
  for (let i = 0; i < files.length; i++) {
    const file = files[i];

    if (!file.type.startsWith("image/")) {
      continue;
    }

    const img = document.createElement("img");
    img.classList.add("obj");
    img.file = file;
    preview.appendChild(img); // Où "preview" correspond à l'élément div où on affiche le contenu.

    const reader = new FileReader();
    reader.onload = (e) => {
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  }
}
```


Dans la boucle qui parcourt les fichiers, on regarde la propriété `type` de chaque fichier afin de déterminer si son type MIME commence avec la chaîne de caractères "`image/`". Pour chaque fichier qui est une image, on crée un nouvel élément `img`. On pourra utiliser CSS pour mettre en forme les vignettes (y compris leur taille).

Chaque image se voit dotée de la classe `obj`, pour permettre de les identifier dans le DOM. On ajoute un attribut `file` à chaque image en y associant l'objet `File` correspondant. Cela permettra de récupérer les images pour les enregistrer plus tard. On utilise `Node.appendChild()` afin d'ajouter la vignette à la zone de prévisualisation dans le document.

Ensuite, on construit un objet `FileReader` afin de gérer le chargement asynchrone de l'image et son rattachement à l'élément `<img>` correspondant. Après avoir créé ce nouvel objet, on paramètre sa fonction `onload` puis on appelle `readAsDataURL()` afin de commencer l'opération de lecture en arrière-plan. Lorsque le contenu du fichier image aura été chargé, il sera converti en une URL de données qui sera passée à la fonction de rappel `onload()`. Dans notre exemple, on change l'attribut `src` de l'élément `<img>` pour utiliser l'image chargée et la faire apparaître comme vignette à l'écran.

Utiliser des URL d'objets
-------------------------

Les méthodes `URL.createObjectURL()` et `URL.revokeObjectURL()` permet de créer des chaînes de caractères étant des URL référençant toute donnée à laquelle on accède via un objet `File` du DOM, y compris les fichiers locaux sur l'ordinateur de l'utilisatrice ou de l'utilisateur.

Lorsqu'on dispose d'un objet `File` qu'on veut manipuler sous la forme d'une URL, on pourra créer une URL d'objet ainsi :


```js
const objectURL = window.URL.createObjectURL(fileObj);
```


Une URL d'objet est une chaîne de caractères identifiant l'objet `File`. À chaque appel de `URL.createObjectURL()`, c'est une URL d'objet unique qui est créée, mais si une URL d'objet avait déjà été créée pour un même objet. Chacune de ces URL doit être libérée. Cette libération a lieu automatiquement lorsque le document est déchargé. Si votre page utilise les URL d'objet dynamiquement, les URL doivent être libérées explicitement en appelant `URL.revokeObjectURL()` :


```js
URL.revokeObjectURL(objectURL);
```

### Utiliser des URL d'objets pour afficher des images

Dans cet exemple, on utilise des URL d'objets pour afficher des vignettes d'image et on affiche également d'autres informations sur les fichiers comme leur nom ou leur taille.

Voici le fragment de HTML pour l'interface utilisateur :


```html
<input
 type="file"
 id="fileElem"
 multiple
 accept="image/\*"
 style="display:none" />
<a href="#" id="fileSelect">Choisissez des fichiers</a>
<div id="fileList">
  <p>Aucun fichier sélectionné !</p>
</div>
```


Cela nous donne notre élément `<input>` ainsi qu'un lien pour ouvrir le sélecteur de fichier (en effet, le champ de saisie est masqué, voir la section Masquer l'élément `<input>` et utiliser la méthode `click()` pour plus de détails).

La méthode `handleFiles()` utilisée ici est la suivante :


```js
const fileSelect = document.getElementById("fileSelect"),
  fileElem = document.getElementById("fileElem"),
  fileList = document.getElementById("fileList");

fileSelect.addEventListener(
  "click",
  (e) => {
    if (fileElem) {
      fileElem.click();
    }
    e.preventDefault(); // Empêche la navigation jusqu'à "#"
  },
  false,
);

fileElem.addEventListener("change", handleFiles, false);

function handleFiles() {
  if (!this.files.length) {
    fileList.innerHTML = "<p>Aucun fichier sélectionné !</p>";
  } else {
    fileList.innerHTML = "";
    const list = document.createElement("ul");
    fileList.appendChild(list);
    for (let i = 0; i < this.files.length; i++) {
      const li = document.createElement("li");
      list.appendChild(li);

      const img = document.createElement("img");
      img.src = URL.createObjectURL(this.files[i]);
      img.height = 60;
      img.onload = () => {
        URL.revokeObjectURL(img.src);
      };
      li.appendChild(img);
      const info = document.createElement("span");
      info.innerHTML = `${this.files[i].name} : ${this.files[i].size} octets`;
      li.appendChild(info);
    }
  }
}
```


Pour commencer, on récupère l'élément `<div>` qui a l'identifiant `fileList`. Il s'agit du bloc dans lequel on placera notre liste de fichier ainsi que nos vignettes.

Si l'objet `FileList` passé à `handleFiles()` vaut `null`, on met à jour le code HTML du bloc pour afficher « Aucun fichier sélectionné ! ». Sinon, on construit la liste de fichier comme suit :

1. On crée un nouvel élément de liste non-ordonnée `<ul>`.
2. Cette nouvelle liste est insérée dans le bloc `<div>` à l'aide de sa méthode `Node.appendChild()`.
3. Pour chaque fichier (`File`) dans la liste (`FileList`) représentée par `files` :
    1. On crée un nouvel élément `<li>` qu'on insère dans la liste.
    2. On crée un nouvel élément `<img>`.
    3. On définit la source de l'image afin d'utiliser la nouvelle URL d'objet représentant le fichier grâce à `URL.createObjectURL()`.
    4. On définit la hauteur de l'image à 60.
    5. On fait le nécessaire pour que le gestionnaire d'évènement pour `load` libère l'URL d'objet qui ne sera plus nécessaire puisque l'image aura été chargée. Pour cela, on appelle la méthode `URL.revokeObjectURL()` à laquelle on passe la chaîne de caractères qu'est l'URL d'objet que contient `img.src`.
    6. On ajoute le nouvel élément de la liste dans la liste.

Voici le résultat de cet exemple :

Exemple : enregistrer un fichier sélectionné côté serveur
---------------------------------------------------------

On peut également vouloir autoriser le téléversement (*upload*) des fichiers sélectionnés sur le serveur (comme les images sélectionnées dans l'exemple précédent). On peut faire ceci de façon asynchrone.

### Créer les tâches de téléversement

Si on poursuit avec le code de construction des vignettes de l'exemple précédent, on peut exploiter la classe `obj` de chaque vignette et manipuler la propriété `file` de chaque élément correspondant qui contient l'objet `File` correspondant. Cela nous permet de collecter toutes les images sélectionnées à l'aide de `Document.querySelectorAll()` :


```js
function sendFiles() {
  const imgs = document.querySelectorAll(".obj");

  for (let i = 0; i < imgs.length; i++) {
    new FileUpload(imgs[i], imgs[i].file);
  }
}
```


À la deuxième ligne de ce fragment, on récupère un objet `NodeList` appelé `imgs` qui contient tous les éléments du document ayant la classe `obj`. Ici, cela correspondra aux vignettes. Avec cette liste, on peut la parcourir et créer une nouvelle instance de `FileUpload` pour chaque, dont le rôle sera de gérer le téléversement du fichier correspondant vers le serveur.

### Gérer le processus de téléversement d'un fichier

La fonction `FileUpload` prend deux arguments, un élément `<img>` et un fichier à partir duquel lire les données de l'image.


```js
function FileUpload(img, file) {
  const reader = new FileReader();
  this.ctrl = createThrobber(img);
  const xhr = new XMLHttpRequest();
  this.xhr = xhr;

  const self = this;
  this.xhr.upload.addEventListener(
    "progress",
    (e) => {
      if (e.lengthComputable) {
        const percentage = Math.round((e.loaded \* 100) / e.total);
        self.ctrl.update(percentage);
      }
    },
    false,
  );

  xhr.upload.addEventListener(
    "load",
    (e) => {
      self.ctrl.update(100);
      const canvas = self.ctrl.ctx.canvas;
      canvas.parentNode.removeChild(canvas);
    },
    false,
  );
  xhr.open(
    "POST",
    "http://demos.hacks.mozilla.org/paul/demos/resources/webservices/devnull.php",
  );
  xhr.overrideMimeType("text/plain; charset=x-user-defined-binary");
  reader.onload = (evt) => {
    xhr.send(evt.target.result);
  };
  reader.readAsBinaryString(file);
}

function createThrobber(img) {
  const throbberWidth = 64;
  const throbberHeight = 6;
  const throbber = document.createElement("canvas");
  throbber.classList.add("upload-progress");
  throbber.setAttribute("width", throbberWidth);
  throbber.setAttribute("height", throbberHeight);
  img.parentNode.appendChild(throbber);
  throbber.ctx = throbber.getContext("2d");
  throbber.ctx.fillStyle = "orange";
  throbber.update = (percent) => {
    throbber.ctx.fillRect(
      0,
      0,
      (throbberWidth \* percent) / 100,
      throbberHeight,
    );
    if (percent === 100) {
      throbber.ctx.fillStyle = "green";
    }
  };
  throbber.update(0);
  return throbber;
}
```


La fonction `FileUpload()` écrite ici crée un indicateur de progression puis crée une requête `XMLHttpRequest` pour gérer l'envoi des données.

Avant d'envoyer effectivement les données, plusieurs étapes préparatoires ont lieu :

1. Le gestionnaire d'évènement pour l'évènement `progress` de la requête `XMLHttpRequest` est utilisé pour mettre à jour l'indicateur de progression avec le pourcentage d'avancement, afin que l'indicateur soit à jour selon la progression.
2. Le gestionnaire d'évènement pour l'évènement `load` de la requête `XMLHttpRequest` est utilisé pour mettre à jour l'indicateur de progression à 100% afin de s'assurer que l'indicateur atteint bien les 100%. Il retire l'indicateur visuel qui n'est plus utile ,qui disparaît alors à la fin du téléversement.
3. La requête pour envoyer l'image au serveur est ouverte avec la méthode `open()` de `XMLHttpRequest`, ce qui génère une requête POST.
4. Le type MIME pour l'envoi est défini en appelant la fonction `overrideMimeType()` de `XMLHttpRequest`. On utilise ici un type MIME générique. Selon votre cas d'usage, vous pourrez ou non avoir besoin de définir le type MIME.
5. L'objet `FileReader` est utilisé pour convertir le fichier en une chaîne binaire.
6. Enfin, lorsque le contenu est chargé, la méthode `send()` de la requête `XMLHttpRequest` est appelée afin d'envoyer le contenu du fichier vers le serveur.
### Gérer le téléversement du fichier de façon asynchrone

Dans l'exemple qui suit, on utilise du code PHP pour le serveur et du code JavaScript pour le client afin d'illustrer le téléversement asynchrone d'un fichier.


```php
<?php
if (isset($\_FILES['myFile'])) {
 // Exemple :
 move\_uploaded\_file($\_FILES['myFile']['tmp\_name'], "uploads/" . $\_FILES['myFile']['name']);
 exit;
}
?><!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <title>dnd binary upload</title>
    <script type="application/javascript">
 function sendFile(file) {
 const uri = "/index.php";
 const xhr = new XMLHttpRequest();
 const fd = new FormData();

 xhr.open("POST", uri, true);
 xhr.onreadystatechange = () => {
 if (xhr.readyState === 4 && xhr.status === 200) {
 alert(xhr.responseText); // Gérer la réponse
 }
 };
 fd.append('myFile', file);
 // Initier un téléversement multipart/form-data
 xhr.send(fd);
 }

 window.onload = () => {
 const dropzone = document.getElementById("dropzone");
 dropzone.ondragover = dropzone.ondragenter = (event) => {
 event.stopPropagation();
 event.preventDefault();
 }

 dropzone.ondrop = (event) => {
 event.stopPropagation();
 event.preventDefault();

 const filesArray = event.dataTransfer.files;
 for (let i=0; i<filesArray.length; i++) {
 sendFile(filesArray[i]);
 }
 }
 }
 </script>
</head>
<body>
    <div>
        <div id="dropzone" style="margin:30px; width:500px; height:300px; border:1px dotted grey;">Glissez-déposez votre fichier ici.</div>
    </div>
</body>
</html>
```

Exemple : utiliser des URL d'objet avec les autres types de fichier
-------------------------------------------------------------------

On peut manipuler les fichiers d'autres formats de la même façon. Voici comment prévisualiser des vidéos sélectionnées :


```js
const video = document.getElementById("video");
const obj_url = URL.createObjectURL(blob);
video.src = obj_url;
video.play();
URL.revokeObjectURL(obj_url);
```

Voir aussi
----------

* `File`
* `FileList`
* `FileReader`
* `URL`
* Utiliser l'API *Fetch*
* `XMLHttpRequest`
* Utiliser `XMLHttpRequest`