Cara Membuat Pengunggah File Drag-and-Drop Dengan JavaScript Vanilla
Diterbitkan: 2022-03-10Ini adalah fakta yang diketahui bahwa input pemilihan file sulit untuk ditata seperti yang diinginkan pengembang, sehingga banyak yang menyembunyikannya dan membuat tombol yang membuka dialog pemilihan file. Namun, saat ini, kami memiliki cara yang lebih menarik dalam menangani pemilihan file: seret dan lepas.
Secara teknis, ini sudah dimungkinkan karena sebagian besar (jika tidak semua ) implementasi input pemilihan file memungkinkan Anda untuk menyeret file ke atasnya untuk memilihnya, tetapi ini mengharuskan Anda untuk benar-benar menampilkan elemen file
. Jadi, mari kita benar-benar menggunakan API yang diberikan kepada kita oleh browser untuk mengimplementasikan pemilih dan pengunggah file drag-and-drop.
Dalam artikel ini, kami akan menggunakan JavaScript ES2015+ “vanilla” (tanpa kerangka kerja atau pustaka) untuk menyelesaikan proyek ini, dan diasumsikan bahwa Anda memiliki pengetahuan tentang JavaScript di browser. Contoh ini — selain sintaks ES2015+, yang dapat dengan mudah diubah ke sintaks ES5 atau ditranspilasikan oleh Babel — harus kompatibel dengan setiap browser evergreen plus IE 10 dan 11.
Berikut ini sekilas tentang apa yang akan Anda buat:
Acara Seret dan Lepas
Hal pertama yang perlu kita bahas adalah peristiwa yang terkait dengan drag-and-drop karena mereka adalah kekuatan pendorong di balik fitur ini. Secara keseluruhan, ada delapan peristiwa yang dijalankan browser terkait dengan drag and drop: drag
, dragend
, dragenter
, dragexit
, dragleave
, dragover
, dragstart
, dan drop
. Kami tidak akan membahas semuanya karena drag
, dragend
, dragexit
, dan dragstart
semuanya ditembakkan pada elemen yang sedang diseret, dan dalam kasus kami, kami akan menyeret file dari sistem file kami daripada elemen DOM , jadi acara ini tidak akan pernah muncul.
Jika Anda penasaran dengan mereka, Anda dapat membaca beberapa dokumentasi tentang peristiwa ini di MDN.
Seperti yang Anda harapkan, Anda dapat mendaftarkan event handler untuk event ini dengan cara yang sama seperti Anda mendaftarkan event handler untuk sebagian besar event browser: melalui 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)
Berikut adalah tabel kecil yang menjelaskan apa yang dilakukan peristiwa ini, menggunakan dropArea
dari contoh kode untuk membuat bahasa lebih jelas:
Peristiwa | Kapan Dipecat? |
---|---|
dragenter | Item yang diseret diseret ke dropArea, menjadikannya target untuk peristiwa drop jika pengguna menjatuhkannya di sana. |
dragleave | Item yang diseret diseret dari dropArea dan ke elemen lain, menjadikannya target untuk acara drop. |
dragover | Setiap beberapa ratus milidetik, saat item yang diseret melewati area drop dan bergerak. |
drop | Pengguna melepaskan tombol mouse mereka, menjatuhkan item yang diseret ke dropArea. |
Perhatikan bahwa item yang diseret diseret ke atas anak dari dropArea
, dragleave
akan menembak pada dropArea
dan dragenter
akan menembakkan elemen anak itu karena itu adalah target
baru . Acara drop
akan menyebar hingga dropArea
(kecuali jika propagasi dihentikan oleh pendengar acara yang berbeda sebelum sampai di sana), jadi itu akan tetap menyala di dropArea
meskipun itu bukan target
acara.
Perhatikan juga bahwa untuk membuat interaksi seret dan lepas khusus, Anda harus memanggil event.preventDefault()
di setiap listener untuk peristiwa ini. Jika tidak, browser akan membuka file yang Anda jatuhkan alih-alih mengirimkannya ke pengendali peristiwa drop
.
Menyiapkan Formulir Kami
Sebelum kita mulai menambahkan fungsionalitas drag-and-drop, kita memerlukan formulir dasar dengan input file
standar. Secara teknis ini tidak diperlukan, tetapi merupakan ide yang baik untuk menyediakannya sebagai alternatif jika pengguna memiliki browser tanpa dukungan untuk API seret dan lepas.
<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>
Struktur yang cukup sederhana. Anda mungkin melihat handler onchange
pada input
. Kami akan melihat itu nanti. Sebaiknya tambahkan juga action
ke form
dan tombol submit
untuk membantu orang-orang yang tidak mengaktifkan JavaScript. Kemudian Anda dapat menggunakan JavaScript untuk menyingkirkannya untuk bentuk yang lebih bersih. Bagaimanapun, Anda akan memerlukan skrip sisi server untuk menerima unggahan, apakah itu sesuatu yang dikembangkan sendiri, atau Anda menggunakan layanan seperti Cloudinary untuk melakukannya untuk Anda. Selain catatan itu, tidak ada yang istimewa di sini, jadi mari kita masukkan beberapa gaya:
#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; }
Banyak dari gaya ini yang belum dimainkan, tapi tidak apa-apa. Sorotan, untuk saat ini, adalah bahwa input file
disembunyikan, tetapi label
ditata agar terlihat seperti tombol, sehingga orang akan menyadari bahwa mereka dapat mengkliknya untuk membuka dialog pemilihan file. Kami juga mengikuti konvensi dengan menguraikan area drop dengan garis putus-putus.
Menambahkan Fungsionalitas Seret-dan-Lepas
Sekarang kita sampai pada inti situasinya: seret dan lepas. Mari letakkan skrip di bagian bawah halaman, atau di file terpisah, bagaimanapun Anda ingin melakukannya. Hal pertama yang kita butuhkan dalam skrip adalah referensi ke area drop sehingga kita dapat melampirkan beberapa acara ke dalamnya:
let dropArea = document.getElementById('drop-area')
Sekarang mari kita tambahkan beberapa acara. Kami akan mulai dengan menambahkan penangan ke semua acara untuk mencegah perilaku default dan menghentikan acara agar tidak meledak lebih tinggi dari yang diperlukan:
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }
Sekarang mari tambahkan indikator untuk memberi tahu pengguna bahwa mereka memang telah menyeret item ke area yang benar dengan menggunakan CSS untuk mengubah warna warna batas area drop. Gaya seharusnya sudah ada di bawah pemilih #drop-area.highlight
, jadi mari gunakan JS untuk menambah dan menghapus kelas highlight
itu bila perlu.
;['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') }
Kami harus menggunakan dragenter
dan dragover
untuk penyorotan karena apa yang saya sebutkan sebelumnya. Jika Anda mulai mengarahkan kursor langsung ke dropArea
lalu mengarahkan kursor ke salah satu turunannya, dragleave
akan diaktifkan dan sorotan akan dihapus. Acara dragover
dipecat setelah acara dragenter
dan dragleave
, jadi sorotan akan ditambahkan kembali ke dropArea
sebelum kita melihatnya dihapus.
Kami juga menghapus sorotan saat item yang diseret meninggalkan area yang ditentukan atau saat Anda menjatuhkan item.
Sekarang yang perlu kita lakukan adalah mencari tahu apa yang harus dilakukan ketika beberapa file dijatuhkan:
dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }
Ini tidak membawa kita mendekati penyelesaian, tetapi melakukan dua hal penting:
- Mendemonstrasikan cara mendapatkan data untuk file yang dijatuhkan.
- Membawa kita ke tempat yang sama dengan
input
file
dengan handleronchange
-nya: waiting forhandleFiles
.
Ingatlah bahwa files
bukan array, tetapi FileList
. Jadi, ketika kita mengimplementasikan handleFiles
, kita perlu mengonversinya menjadi sebuah array untuk mengulanginya dengan lebih mudah:
function handleFiles(files) { ([...files]).forEach(uploadFile) }
Itu antiklimaks. Mari masuk ke uploadFile
untuk hal-hal yang nyata .
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 */ }) }
Di sini kami menggunakan FormData
, API browser bawaan untuk membuat data formulir untuk dikirim ke server. Kami kemudian menggunakan API fetch
untuk benar-benar mengirim gambar ke server. Pastikan Anda mengubah URL agar berfungsi dengan back-end atau layanan Anda, dan formData.append
data formulir tambahan yang mungkin Anda perlukan untuk memberikan semua informasi yang dibutuhkan server. Atau, jika Anda ingin mendukung Internet Explorer, Anda mungkin ingin menggunakan XMLHttpRequest
, yang berarti uploadFile
akan terlihat seperti ini:
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) }
Bergantung pada bagaimana server Anda diatur, Anda mungkin ingin memeriksa rentang nomor status
yang berbeda daripada hanya 200
, tetapi untuk tujuan kami, ini akan berhasil.
Fitur tambahan
Itu semua fungsionalitas dasar, tetapi seringkali kita menginginkan lebih banyak fungsionalitas. Secara khusus, dalam tutorial ini, kami akan menambahkan panel pratinjau yang menampilkan semua gambar yang dipilih kepada pengguna, lalu kami akan menambahkan bilah kemajuan yang memungkinkan pengguna melihat kemajuan unggahan. Jadi, mari kita mulai dengan melihat pratinjau gambar.
Pratinjau Gambar
Ada beberapa cara yang dapat Anda lakukan: Anda dapat menunggu hingga gambar diunggah dan meminta server untuk mengirim URL gambar, tetapi itu berarti Anda harus menunggu dan gambar terkadang berukuran cukup besar. Alternatifnya — yang akan kita jelajahi hari ini — adalah dengan menggunakan FileReader API pada data file yang kami terima dari drop
event. Ini tidak sinkron, dan Anda dapat menggunakan FileReaderSync sebagai alternatif, tetapi kami dapat mencoba membaca beberapa file besar secara berurutan, jadi ini dapat memblokir utas cukup lama dan benar-benar merusak pengalaman. Jadi mari kita buat fungsi previewFile
dan lihat cara kerjanya:
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) } }
Di sini kita membuat new FileReader
dan memanggil readAsDataURL
di atasnya dengan objek File
. Seperti yang disebutkan, ini tidak sinkron, jadi kita perlu menambahkan event handler onloadend
untuk mendapatkan hasil read. Kami kemudian menggunakan URL data 64 dasar sebagai src
untuk elemen gambar baru dan menambahkannya ke elemen gallery
. Hanya ada dua hal yang perlu dilakukan untuk membuat ini berfungsi sekarang: tambahkan elemen gallery
, dan pastikan previewFile
benar-benar dipanggil.
Pertama, tambahkan HTML berikut tepat setelah akhir tag form
:
<div></div>
Tidak ada yang spesial; itu hanya sebuah div. Gaya sudah ditentukan untuk itu dan gambar di dalamnya, jadi tidak ada lagi yang bisa dilakukan di sana. Sekarang mari kita ubah fungsi handleFiles
menjadi berikut ini:
function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }
Ada beberapa cara Anda bisa melakukan ini, seperti komposisi, atau satu panggilan balik ke forEach
yang menjalankan uploadFile
dan previewFile
di dalamnya, tetapi ini juga berfungsi. Dan dengan itu, ketika Anda menjatuhkan atau memilih beberapa gambar, mereka akan muncul hampir seketika di bawah formulir. Hal yang menarik tentang ini adalah — dalam aplikasi tertentu — Anda mungkin sebenarnya tidak ingin mengunggah gambar, tetapi menyimpan URL datanya di localStorage
atau cache sisi klien lainnya untuk diakses oleh aplikasi nanti. Saya pribadi tidak dapat memikirkan kasus penggunaan yang baik untuk ini, tetapi saya berani bertaruh ada beberapa.
Pelacakan Kemajuan
Jika sesuatu mungkin memakan waktu cukup lama, bilah kemajuan dapat membantu pengguna menyadari bahwa kemajuan sebenarnya sedang dibuat dan memberikan indikasi berapa lama waktu yang dibutuhkan untuk diselesaikan. Menambahkan indikator kemajuan cukup mudah berkat tag progress
HTML5. Mari kita mulai dengan menambahkannya ke kode HTML kali ini.
<progress max=100 value=0></progress>
Anda dapat meletakkannya tepat setelah label
atau di antara form
dan galeri div
, mana saja yang lebih Anda sukai. Dalam hal ini, Anda dapat menempatkannya di mana pun Anda inginkan di dalam tag body
. Tidak ada gaya yang ditambahkan untuk contoh ini, sehingga akan menampilkan implementasi default browser, yang dapat diservis. Sekarang mari kita bekerja menambahkan JavaScript. Pertama-tama kita akan melihat implementasinya menggunakan fetch
dan kemudian kita akan menampilkan versi untuk XMLHttpRequest
. Untuk memulai, kita memerlukan beberapa variabel baru di bagian atas skrip:
let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')
Saat menggunakan fetch
, kami hanya dapat menentukan kapan unggahan selesai, jadi satu-satunya informasi yang kami lacak adalah berapa banyak file yang dipilih untuk diunggah (sebagai filesToDo
) dan jumlah file yang telah selesai diunggah (sebagai filesDone
). Kami juga menyimpan referensi ke elemen #progress-bar
sehingga kami dapat memperbaruinya dengan cepat. Sekarang mari kita buat beberapa fungsi untuk mengelola kemajuan:
function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }
Saat kami mulai mengunggah, initializeProgress
akan dipanggil untuk mengatur ulang bilah kemajuan. Kemudian, dengan setiap unggahan yang selesai, kami akan memanggil progressDone
untuk menambah jumlah unggahan yang selesai dan memperbarui bilah kemajuan untuk menunjukkan kemajuan saat ini. Jadi mari kita panggil fungsi-fungsi ini dengan memperbarui beberapa fungsi lama:
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 */ }) }
Dan itu saja. Sekarang mari kita lihat implementasi XMLHttpRequest
. Kami hanya dapat membuat pembaruan cepat untuk uploadFile
, tetapi XMLHttpRequest
sebenarnya memberi kami lebih banyak fungsionalitas daripada fetch
, yaitu kami dapat menambahkan event listener untuk kemajuan unggahan pada setiap permintaan, yang secara berkala akan memberi kami informasi tentang seberapa banyak permintaan tersebut selesai. Karena itu, kami perlu melacak persentase penyelesaian setiap permintaan, bukan hanya berapa banyak yang selesai. Jadi, mari kita mulai dengan mengganti deklarasi untuk filesDone
dan filesToDo
dengan yang berikut:
let uploadProgress = []
Kemudian kita perlu memperbarui fungsi kita juga. Kami akan mengganti nama progressDone
menjadi updateProgress
dan mengubahnya menjadi berikut:
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 }
Sekarang initializeProgress
menginisialisasi array dengan panjang yang sama dengan numFiles
yang diisi dengan nol, yang menunjukkan bahwa setiap file selesai 0%. Di updateProgress
kami menemukan gambar mana yang mengalami pembaruan kemajuan dan mengubah nilai pada indeks itu ke percent
yang disediakan. Kami kemudian menghitung persentase kemajuan total dengan mengambil rata-rata semua persentase dan memperbarui bilah kemajuan untuk mencerminkan total yang dihitung. Kami masih memanggil initializeProgress
di handleFiles
sama seperti yang kami lakukan pada contoh fetch
, jadi sekarang yang perlu kami perbarui adalah uploadFile
untuk memanggil 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) }
Hal pertama yang perlu diperhatikan adalah kita menambahkan parameter i
. Ini adalah indeks file dalam daftar file. Kami tidak perlu memperbarui handleFiles
untuk meneruskan parameter ini karena menggunakan forEach
, yang sudah memberikan indeks elemen sebagai parameter kedua untuk panggilan balik. Kami juga menambahkan pendengar acara progress
ke xhr.upload
sehingga kami dapat memanggil updateProgress
dengan kemajuan tersebut. Objek acara (disebut sebagai e
dalam kode) memiliki dua informasi terkait di dalamnya: loaded
yang berisi jumlah byte yang telah diunggah sejauh ini dan total
yang berisi jumlah byte file secara total.
|| 100
|| 100
buah ada di sana karena terkadang jika ada kesalahan, e.loaded
dan e.total
akan menjadi nol, yang berarti perhitungan akan keluar sebagai NaN
, jadi 100
digunakan sebagai gantinya untuk melaporkan bahwa file tersebut selesai. Anda juga bisa menggunakan 0
. Dalam kedua kasus tersebut, kesalahan akan muncul di handler readystatechange
sehingga Anda dapat memberi tahu pengguna tentangnya. Ini hanya untuk mencegah pengecualian dilemparkan karena mencoba melakukan matematika dengan NaN
.
Kesimpulan
Itu bagian terakhir. Anda sekarang memiliki halaman web tempat Anda dapat mengunggah gambar melalui drag and drop, melihat pratinjau gambar yang diunggah segera, dan melihat kemajuan unggahan di bilah kemajuan. Anda dapat melihat versi final (dengan XMLHttpRequest
) beraksi di CodePen, tetapi perlu diketahui bahwa layanan tempat saya mengunggah file memiliki batasan, jadi jika banyak orang mengujinya, mungkin akan rusak untuk sementara waktu.