Cara Membuat Pengunggah File Drag-and-Drop Dengan JavaScript Vanilla

Diterbitkan: 2022-03-10
Ringkasan cepat Dalam artikel ini, kami akan menggunakan JavaScript ES2015+ “vanilla” (tanpa kerangka kerja atau pustaka) untuk menyelesaikan proyek ini, dan diasumsikan Anda memiliki pengetahuan tentang JavaScript di browser. Contoh ini harus kompatibel dengan setiap browser evergreen plus IE 10 dan 11.

Ini 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:

Pengunggah gambar seret-dan-lepas sedang beraksi
Demonstrasi halaman web di mana Anda dapat mengunggah gambar melalui drag and drop, melihat pratinjau gambar yang diunggah segera, dan melihat kemajuan unggahan di bilah kemajuan.

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.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

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:

  1. Mendemonstrasikan cara mendapatkan data untuk file yang dijatuhkan.
  2. Membawa kita ke tempat yang sama dengan input file dengan handler onchange -nya: waiting for handleFiles .

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.