Cum să faci un instrument de încărcare de fișiere prin glisare și plasare cu Vanilla JavaScript
Publicat: 2022-03-10Este un fapt cunoscut că intrările de selecție a fișierelor sunt dificil de stilat așa cum doresc dezvoltatorii, așa că mulți pur și simplu îl ascund și creează un buton care deschide dialogul de selecție a fișierelor. În zilele noastre, totuși, avem o modalitate și mai sofisticată de a gestiona selecția fișierelor: trageți și plasați.
Din punct de vedere tehnic, acest lucru a fost deja posibil deoarece majoritatea (dacă nu toate ) implementările de intrare de selecție a fișierelor v-au permis să trageți fișierele peste el pentru a le selecta, dar acest lucru necesită să afișați de fapt elementul file
. Deci, să folosim de fapt API-urile oferite de browser pentru a implementa un selector de fișiere și un dispozitiv de încărcare cu drag-and-drop.
În acest articol, vom folosi „vanilla” ES2015+ JavaScript (fără cadre sau biblioteci) pentru a finaliza acest proiect și se presupune că aveți cunoștințe de lucru despre JavaScript în browser. Acest exemplu – în afară de sintaxa ES2015+, care poate fi schimbată cu ușurință în sintaxa ES5 sau transpilată de Babel – ar trebui să fie compatibil cu orice browser evergreen plus IE 10 și 11.
Iată o privire rapidă asupra a ceea ce vei face:
Evenimente drag-and-drop
Primul lucru pe care trebuie să-l discutăm sunt evenimentele legate de drag-and-drop, deoarece ele sunt forța motrice din spatele acestei funcții. În total, există opt evenimente declanșate de browser legate de drag and drop: drag
, dragend
, dragenter
, dragexit
, dragleave
, dragstart
, dragover
și drop
. Nu le vom analiza pe toate, deoarece drag
, dragend
, dragexit
și dragstart
sunt toate declanșate pe elementul care este tras și, în cazul nostru, vom trage fișiere din sistemul nostru de fișiere, mai degrabă decât elementele DOM , astfel încât aceste evenimente nu vor apărea niciodată.
Dacă sunteți curios despre ele, puteți citi câteva documentații despre aceste evenimente pe MDN.
După cum v-ați putea aștepta, puteți înregistra handlere de evenimente pentru aceste evenimente în același mod în care înregistrați handlere de evenimente pentru majoritatea evenimentelor din browser: prin 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)
Iată un mic tabel care descrie ce fac aceste evenimente, folosind dropArea
din exemplul de cod pentru a face limbajul mai clar:
Eveniment | Când este concediat? |
---|---|
dragenter | Elementul tras este tras peste dropArea, făcându-l ținta pentru evenimentul drop dacă utilizatorul îl aruncă acolo. |
dragleave | Elementul tras este tras din dropArea și pe alt element, făcându-l ținta pentru evenimentul drop. |
dragover | La fiecare câteva sute de milisecunde, în timp ce elementul tras este peste dropArea și se mișcă. |
drop | Utilizatorul eliberează butonul mouse-ului, plasând elementul tras pe dropArea. |
Rețineți că elementul tras este tras peste un element copil al dropArea
, dragleave
se va declanșa pe dropArea
și dragenter
se va declanșa pe acel element copil deoarece este noua target
. Evenimentul drop
se va propaga până la dropArea
(cu excepția cazului în care propagarea este oprită de un alt ascultător de eveniment înainte de a ajunge acolo), așa că se va declanșa în continuare pe dropArea
, deși nu este target
evenimentului.
De asemenea, rețineți că, pentru a crea interacțiuni personalizate de tip drag-and-drop, va trebui să apelați event.preventDefault()
în fiecare dintre ascultătorii acestor evenimente. Dacă nu o faceți, browserul va ajunge să deschidă fișierul pe care l-ați aruncat în loc să-l trimită la handlerul de evenimente de drop
.
Configurarea formularului nostru
Înainte de a începe să adăugăm funcționalitatea drag-and-drop, vom avea nevoie de un formular de bază cu o intrare file
standard. Din punct de vedere tehnic, acest lucru nu este necesar, dar este o idee bună să-l oferiți ca alternativă în cazul în care utilizatorul are un browser fără suport pentru API-ul drag-and-drop.
<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>
Structură destul de simplă. Este posibil să observați un handler onchange
pe input
. Ne vom uita la asta mai târziu. De asemenea, ar fi o idee bună să adăugați o action
în form
și un buton de submit
pentru a ajuta persoanele care nu au JavaScript activat. Apoi puteți folosi JavaScript pentru a scăpa de ele pentru o formă mai curată. În orice caz, veți avea nevoie de un script pe partea de server pentru a accepta încărcarea, fie că este ceva dezvoltat intern, fie că utilizați un serviciu precum Cloudinary pentru a face acest lucru pentru dvs. În afară de acele note, nu este nimic special aici, așa că hai să aruncăm câteva stiluri în:
#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; }
Multe dintre aceste stiluri nu intră încă în joc, dar este în regulă. Cele mai importante momente sunt că intrarea file
este ascunsă, dar label
acestuia este stilată astfel încât să arate ca un buton, astfel încât oamenii își vor da seama că pot face clic pe el pentru a afișa dialogul de selecție a fișierului. De asemenea, urmărim o convenție prin conturarea zonei de picătură cu linii întrerupte.
Adăugarea funcției de glisare și plasare
Acum ajungem la miezul situației: drag and drop. Să aruncăm un script în partea de jos a paginii sau într-un fișier separat, oricum ai vrea să o faci. Primul lucru de care avem nevoie în script este o referință la zona de drop, astfel încât să putem atașa câteva evenimente la aceasta:
let dropArea = document.getElementById('drop-area')
Acum să adăugăm câteva evenimente. Vom începe cu adăugarea de handlere la toate evenimentele pentru a preveni comportamentele implicite și pentru a opri evenimentele să apară mai mult decât este necesar:
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }
Acum să adăugăm un indicator pentru a-i anunța utilizatorului că într-adevăr a tras elementul peste zona corectă, folosind CSS pentru a schimba culoarea culorii marginii zonei de fixare. Stilurile ar trebui să fie deja acolo sub selectorul #drop-area.highlight
, așa că haideți să folosim JS pentru a adăuga și elimina acea clasă de highlight
atunci când este necesar.
;['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') }
A trebuit să folosim atât dragenter
, cât și dragover
pentru evidențiere din cauza a ceea ce am menționat mai devreme. Dacă începeți să treceți cu mouse-ul direct peste dropArea
și apoi să treceți peste unul dintre copiii săi, atunci dragleave
va fi declanșat și evidențierea va fi eliminată. Evenimentul dragover
este declanșat după evenimentele dragenter
și dragleave
, astfel încât evidențierea va fi adăugată înapoi în dropArea
înainte de a vedea că este eliminat.
De asemenea, eliminăm evidențierea atunci când elementul tras părăsește zona desemnată sau când plasați elementul.
Acum tot ce trebuie să facem este să ne dăm seama ce să facem când unele fișiere sunt aruncate:
dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }
Acest lucru nu ne aduce aproape de finalizare, dar face două lucruri importante:
- Demonstrează cum să obțineți datele pentru fișierele care au fost aruncate.
- Ne duce în același loc în care a fost
input
file
cu handlerul săuonchange
: așteptândhandleFiles
.
Rețineți că files
nu sunt o matrice, ci o listă de FileList
. Deci, când implementăm handleFiles
, va trebui să-l convertim într-o matrice pentru a itera mai ușor peste el:
function handleFiles(files) { ([...files]).forEach(uploadFile) }
A fost anticlimatic. Să intrăm în uploadFile
pentru lucruri adevărate .
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 */ }) }
Aici folosim FormData
, un browser API încorporat pentru crearea datelor de formular pentru a le trimite către server. Apoi folosim API-ul fetch
pentru a trimite efectiv imaginea către server. Asigurați-vă că modificați adresa URL pentru a funcționa cu back-end-ul sau serviciul dvs. și formData.append
orice date suplimentare de formular de care aveți nevoie pentru a oferi serverului toate informațiile de care are nevoie. Ca alternativă, dacă doriți să acceptați Internet Explorer, poate doriți să utilizați XMLHttpRequest
, ceea ce înseamnă că uploadFile
ar arăta astfel:
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) }
În funcție de modul în care este configurat serverul dvs., este posibil să doriți să verificați diferite intervale de numere de status
, mai degrabă decât doar 200
, dar pentru scopurile noastre, acest lucru va funcționa.
Caracteristici suplimentare
Aceasta este toată funcționalitatea de bază, dar adesea dorim mai multe funcționalități. Mai exact, în acest tutorial, vom adăuga un panou de previzualizare care afișează utilizatorului toate imaginile alese, apoi vom adăuga o bară de progres care îi permite utilizatorului să vadă progresul încărcărilor. Deci, să începem cu previzualizarea imaginilor.
Previzualizare imagine
Există câteva moduri în care puteți face acest lucru: puteți aștepta până după ce imaginea a fost încărcată și să cereți serverului să trimită adresa URL a imaginii, dar asta înseamnă că trebuie să așteptați, iar imaginile pot fi destul de mari uneori. Alternativa – pe care o vom explora astăzi – este să folosim API-ul FileReader pe datele fișierului pe care le-am primit de la evenimentul de drop
. Acesta este asincron și, în mod alternativ, puteți utiliza FileReaderSync, dar am putea încerca să citim mai multe fișiere mari la rând, astfel încât acest lucru ar putea bloca firul pentru o perioadă de timp și ar putea strica experiența. Deci, să creăm o funcție de previewFile
și să vedem cum funcționează:
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) } }
Aici creăm un new FileReader
și apelăm readAsDataURL
pe el cu obiectul File
. După cum am menționat, acesta este asincron, așa că trebuie să adăugăm un handler de evenimente onloadend
pentru a obține rezultatul citirii. Apoi folosim URL-ul de date de bază 64 ca src
pentru un nou element de imagine și îl adăugăm la elementul gallery
. Există doar două lucruri care trebuie făcute pentru ca acest lucru să funcționeze acum: adăugați elementul gallery
și asigurați-vă că previewFile
este de fapt apelat.
Mai întâi, adăugați următorul cod HTML imediat după sfârșitul etichetei form
:
<div></div>
Nimic special; este doar un div. Stilurile sunt deja specificate pentru el și imaginile din el, așa că nu mai e nimic de făcut acolo. Acum să schimbăm funcția handleFiles
în următoarea:
function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }
Există câteva moduri în care ați fi putut face acest lucru, cum ar fi compoziția sau un singur apel înapoi la forEach
care a rulat uploadFile
și previewFile
în el, dar funcționează și acest lucru. Și cu asta, atunci când aruncați sau selectați unele imagini, acestea ar trebui să apară aproape instantaneu sub formular. Lucrul interesant este că, în anumite aplicații, s-ar putea să nu doriți să încărcați imagini, ci să stocați adresele URL de date ale acestora în localStorage
sau într-un alt cache la nivelul clientului pentru a fi accesat de aplicație mai târziu. Nu mă pot gândi personal la cazuri bune de utilizare pentru asta, dar sunt dispus să pariez că există unele.
Urmărirea progresului
Dacă ceva ar putea dura ceva timp, o bară de progres poate ajuta utilizatorul să realizeze că progresul este de fapt realizat și să ofere o indicație despre cât timp va dura să fie finalizat. Adăugarea unui indicator de progres este destul de ușoară datorită etichetei de progress
HTML5. Să începem prin a adăuga asta la codul HTML de data aceasta.
<progress max=100 value=0></progress>
Puteți plasa asta imediat după label
sau între form
și galerie div
, oricare doriți mai mult. De altfel, îl puteți plasa oriunde doriți în etichetele body
. Nu au fost adăugate stiluri pentru acest exemplu, așa că va afișa implementarea implicită a browserului, care este funcțională. Acum să lucrăm la adăugarea JavaScript. Mai întâi ne vom uita la implementare folosind fetch
și apoi vom afișa o versiune pentru XMLHttpRequest
. Pentru a începe, vom avea nevoie de câteva variabile noi în partea de sus a scriptului:
let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')
Când folosim fetch
, putem stabili doar când se termină încărcarea, astfel încât singura informație pe care o urmărim este câte fișiere sunt selectate pentru a încărca (ca filesToDo
) și numărul de fișiere care s-au terminat încărcat (ca filesDone
). De asemenea, păstrăm o referință la elementul #progress-bar
, astfel încât să îl putem actualiza rapid. Acum să creăm câteva funcții pentru gestionarea progresului:
function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }
Când începem încărcarea, va fi apelat initializeProgress
pentru a reseta bara de progres. Apoi, cu fiecare încărcare finalizată, vom apela progressDone
pentru a crește numărul de încărcări finalizate și vom actualiza bara de progres pentru a afișa progresul curent. Deci, să numim aceste funcții actualizând câteva funcții vechi:
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 */ }) }
Si asta e. Acum să aruncăm o privire la implementarea XMLHttpRequest
. Am putea doar să facem o actualizare rapidă pentru uploadFile
, dar XMLHttpRequest
ne oferă de fapt mai multe funcționalități decât fetch
, și anume putem adăuga un ascultător de evenimente pentru progresul încărcării la fiecare solicitare, care ne va oferi periodic informații despre cât de mult este solicitată. terminat. Din acest motiv, trebuie să urmărim procentul de finalizare a fiecărei solicitări în loc de doar câte sunt efectuate. Deci, să începem cu înlocuirea declarațiilor pentru filesDone
și filesToDo
cu următoarele:
let uploadProgress = []
Apoi, trebuie să ne actualizăm și funcțiile. Vom redenumi progressDone
la updateProgress
și le vom modifica astfel:
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 }
Acum initializeProgress
inițializează o matrice cu o lungime egală cu numFiles
care este umplută cu zerouri, indicând faptul că fiecare fișier este 0% complet. În updateProgress
aflăm ce imagine are progresul actualizat și modificăm valoarea la acel index la percent
furnizat. Apoi calculăm procentul total de progres luând o medie a tuturor procentelor și actualizăm bara de progres pentru a reflecta totalul calculat. În continuare apelăm initializeProgress
în handleFiles
la fel ca și în exemplul de fetch
, așa că acum tot ce trebuie să actualizăm este uploadFile
pentru a apela 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) }
Primul lucru de remarcat este că am adăugat un parametru i
. Acesta este indexul fișierului din lista de fișiere. Nu trebuie să actualizăm handleFiles
pentru a transmite acest parametru deoarece folosește forEach
, care oferă deja indexul elementului ca al doilea parametru pentru apeluri inverse. De asemenea, am adăugat ascultătorul de evenimente de progress
la xhr.upload
, astfel încât să putem apela updateProgress
cu progresul. Obiectul eveniment (denumit e
în cod) are două informații relevante pe el: loaded
care conține numărul de octeți care au fost încărcați până acum și total
care conține numărul de octeți în care este fișierul în total.
Cel || 100
|| 100
piese sunt acolo, deoarece uneori, dacă există o eroare, e.loaded
și e.total
vor fi zero, ceea ce înseamnă că calculul va ieși ca NaN
, așa că 100
este folosit în schimb pentru a raporta că fișierul este gata. Puteți folosi și 0
. În ambele cazuri, eroarea va apărea în handlerul readystatechange
, astfel încât să puteți informa utilizatorul despre acestea. Acest lucru este doar pentru a preveni aruncarea de excepții pentru că încercați să faceți matematică cu NaN
.
Concluzie
Asta e piesa finală. Acum aveți o pagină web în care puteți încărca imagini prin glisare și plasare, previzualizați imaginile care sunt încărcate imediat și puteți vedea progresul încărcării într-o bară de progres. Puteți vedea versiunea finală (cu XMLHttpRequest
) în acțiune pe CodePen, dar fiți conștienți de faptul că serviciul în care încarc fișierele are limite, așa că dacă mulți oameni îl testează, se poate întrerupe pentru o perioadă.