Cette page est davantage la description pas à pas d’une page qui n’a
jamais été mise en production. Je souhaitais explorer le
téléversement d’une image avec son affichage préalable et la
possibilité de l’ajouter par glisser-déposer. Il y aurait beaucoup
de choses à peaufiner avant d’avoir une version utilisable
(ne serait-ce qu’au niveau de l’accessibilité) mais ça peut vous
inspirer.
Vous pouvez essayer d’ajouter des images, on de les déposer dans la zone dédiée.
Rien n’est envoyé sur le serveur.
On a une prévisualisation des images sélectionnées et la capacité de le
faire au clavier (ce qui est toujours un bon exercice d’accessibilité).
<formaction="/"method="post"enctype="multipart/form-data"><!-- (1)! --><fieldset><inputtype="file"name="files"id="files"accept="image/*"multipledata-max-size-kb="500"/><!-- (2)! --><labelfor="files"class="btn-like"><!-- (3)! --> Click to upload image(s) or drag & drop below
</label><divrole="alert"aria-live="polite"hidden></div><!-- (4)! --><output></output><!-- (5)! --><canvas>Drag & drop image here</canvas><!-- (6)! --></fieldset><inputtype="submit"name="submit"/></form>
On n’oublie pas d’indiquer que l’on souhaite envoyer des fichiers.
On accepte toutes formes d’images et on ajoute un attribut pour spécifier la taille maximale (qui sera appliquée en JS).
On style le label comme un bouton car on va utiliser une astuce.
On prépare un endroit où afficher nos erreurs.
On utilise l’élément output pour la prévisualisation de l’image, qui permet d’afficher le résultat d’une action utilisateur·ice.
On utilise un élément canvas pour délimiter la zone où l’on va pouvoir glisser-déposer les images.
Au sujet du label stylé comme un bouton, il s’agit d’une astuce issue de cet article qui consiste à masquer l’input en lui-même car le label a les mêmes propriétés au clic et que le bouton natif n’est pas si évident à styler. Surtout lorsqu’on veut garder la cohérence avec la zone de dépôt.
Je n’ajoute pas la CSS complète ici car c’est vite verbeux
(elle est dans l’exemple), je veux juste montrer ce qui est nécessaire
pour que le label nous serve à sélectionner des images :
Une autre chose qui est pertinente, c’est de pouvoir avoir un retour
visuel lorsqu’on passe l’image sur la zone où l’on peut la déposer.
Ici on ajoute une classe active à notre élément lorsqu’on passe dessus,
la principale difficulté étant de devoir écouter plusieurs évènements
pour la même action.
Ici c’est un gros morceau. On écoute un change sur l’input
ou un drop sur notre dropzone (élément canvas).
À partir de là, on va vérifier si la taille limite est dépassée
et afficher un message d’erreur si c’est le cas ou afficher l’image
téléversée si elle est inférieure à la taille autorisée.
;(functionhandleImageSelection(inputSelector,outputSelector,dropzoneSelector,errorsSelector){'use strict'constinput=document.querySelector(inputSelector)constoutput=document.querySelector(outputSelector)constdropzone=document.querySelector(dropzoneSelector)consterrors=document.querySelector(errorsSelector)input.addEventListener('change',(event)=>{event.stopPropagation()event.preventDefault()handleImages(event.target.files)},false)dropzone.addEventListener('drop',(event)=>{event.stopPropagation()event.preventDefault()// TODO: filter out files not matching the// `accept="image/*"` pattern.handleImages(event.dataTransfer.files)input.files=event.dataTransfer.files// (1)!},false)functionhandleImages(images){Array.from(images).forEach((image)=>{constisOversized=checkOversize(image.size)if(isOversized)returndisplayImage(image)})}functioncheckOversize(imageSize){constmaxSizeKb=Number(input.dataset.maxSizeKb)constisOversized=Math.round(imageSize/1024)>maxSizeKbif(isOversized){errors.removeAttribute('hidden')errors.innerHTML=`<p>Oversized! ${maxSizeKb}Kb is the limit</p>`input.value=''returntrue}else{errors.innerHTML=''errors.setAttribute('hidden','hidden')dropzone.setAttribute('hidden','hidden')returnfalse}}functiondisplayImage(image){// No need for a FileReader in that case:// https://developer.mozilla.org/en-US/docs/Web/API/↩// File_API/Using_files_from_web_applications↩// #example_using_object_urls_to_display_imagesconstfigure=document.createElement('figure')constimg=document.createElement('img')img.file=image// Required for future upload, fragile?img.src=window.URL.createObjectURL(image)img.onload=(event)=>window.URL.revokeObjectURL(event.target.src)img.alt='Image preview'figure.appendChild(img)constfigcaption=document.createElement('figcaption')figcaption.textContent=image.namefigure.appendChild(figcaption)output.appendChild(figure)}})('input[type="file"]','output','canvas','[role="alert"]')
Requis pour que les fichiers déposés soient ajoutés à l’input !
;(functionuploadImageWithProgress(formSelector,imagesSelector){'use strict'constform=document.querySelector(formSelector)form.addEventListener('submit',(event)=>{event.stopPropagation()event.preventDefault()constimages=document.querySelectorAll(imagesSelector)Array.from(images).forEach((image)=>{constoptions={method:event.target.method,body:image.file,}console.log(`Uploading "${image.file.name}"`)request(event.target.action,options,displayPercentage).then(console.log.bind(console)).catch(console.log.bind(console))})},false)functionrequest(url,options={},onProgress){// See https://github.com/github/fetch/issues/89#issuecomment-256610849returnnewPromise((resolve,reject)=>{constxhr=newXMLHttpRequest()xhr.open(options.method||'get',url)for(letheaderinoptions.headers||{})xhr.setRequestHeader(header,options.headers[header])xhr.onload=(event)=>resolve(event.target.responseText)xhr.onerror=rejectif(xhr.upload&&onProgress)xhr.upload.onprogress=onProgressxhr.send(options.body)})}functiondisplayPercentage(event){// TODO: use a `progress` HTML element.if(event.lengthComputable){constpercentage=Math.round((event.loaded*100)/event.total)console.log(percentage)}}})('form','output img')
Visualiser la progression
Ça serait pas mal d’avoir un endroit où faire le téléversement
pour aller jusqu’au bout de la démo. Peut-être via httpbin ?