Vanilla JavaScript ile Sürükle ve Bırak Dosya Yükleyici Nasıl Yapılır
Yayınlanan: 2022-03-10Dosya seçimi girdilerini geliştiricilerin istediği şekilde biçimlendirmenin zor olduğu bilinen bir gerçektir, o kadar çok kişi basitçe onu gizler ve bunun yerine dosya seçimi iletişim kutusunu açan bir düğme oluşturur. Ancak günümüzde, dosya seçimini işlemek için daha da meraklı bir yolumuz var: sürükle ve bırak.
Teknik olarak, bu zaten mümkündü, çünkü dosya seçimi girdisinin çoğu ( tümü değilse) uygulamaları, onları seçmek için dosyaları üzerine sürüklemenize izin verdi, ancak bu, file
öğesini gerçekten göstermenizi gerektiriyor. Öyleyse, bir sürükle ve bırak dosya seçici ve yükleyiciyi uygulamak için tarayıcı tarafından bize verilen API'leri kullanalım.
Bu makalede, bu projeyi tamamlamak için "vanilla" ES2015+ JavaScript (çerçeve veya kitaplık yok) kullanacağız ve tarayıcıda çalışan bir JavaScript bilgisine sahip olduğunuz varsayılmaktadır. Bu örnek - kolayca ES5 sözdizimine değiştirilebilen veya Babel tarafından aktarılabilen ES2015+ sözdiziminin yanı sıra - her zaman yeşil kalan tarayıcı artı IE 10 ve 11 ile uyumlu olmalıdır.
Yapacağınız şeye hızlı bir bakış:
Sürükle ve Bırak Etkinlikleri
Tartışmamız gereken ilk şey, sürükle ve bırak ile ilgili olaylar çünkü bu özelliğin arkasındaki itici güç onlar. Toplamda, tarayıcının sürükle ve bırak ile ilgili tetiklediği sekiz olay vardır: drag
, dragend
, dragenter
, dragexit
, dragleave
, dragover
, dragstart
ve drop
. Hepsinin üzerinden geçmeyeceğiz çünkü drag
, dragend
, dragexit
ve dragstart
tümü sürüklenen öğede tetiklenir ve bizim durumumuzda dosyaları DOM öğeleri yerine dosya sistemimizden sürükleyeceğiz , bu yüzden bu olaylar asla ortaya çıkmayacak.
Bunları merak ediyorsanız, bu olaylarla ilgili bazı belgeleri MDN'de okuyabilirsiniz.
Tahmin edebileceğiniz gibi, bu olaylar için olay işleyicilerini, çoğu tarayıcı olayı için olay işleyicilerini kaydettiğiniz şekilde kaydedebilirsiniz: addEventListener
aracılığıyla.
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)
Dili daha net hale getirmek için kod örneğinden dropArea
kullanarak bu olayların ne yaptığını açıklayan küçük bir tablo:
Etkinlik | Ne Zaman Ateşlenir? |
---|---|
dragenter | Sürüklenen öğe, dropArea üzerine sürüklenerek, kullanıcı onu oraya bırakırsa, onu drop olayının hedefi haline getirir. |
dragleave | Sürüklenen öğe, dropArea'dan başka bir öğeye sürüklenir ve bunun yerine onu drop olayının hedefi haline getirir. |
dragover | Her birkaç yüz milisaniyede bir, sürüklenen öğe dropArea'nın üzerindeyken ve hareket ediyor. |
drop | Kullanıcı, sürüklenen öğeyi dropArea'ya bırakarak fare düğmesini serbest bırakır. |
Sürüklenen öğenin dropArea öğesinin bir alt öğesi üzerine sürüklendiğini, dropArea
dragleave
üzerinde dropArea
ve dragenter
öğesinin yeni target
olduğu için o alt öğe üzerinde etkinleşeceğini unutmayın. drop
olayı dropArea
kadar yayılacaktır (yayılma oraya varmadan önce farklı bir olay dinleyicisi tarafından durdurulmadıkça), bu nedenle olayın target
olmamasına rağmen dropArea
devam edecektir.
Ayrıca, özel sürükle ve bırak etkileşimleri oluşturmak için, bu olaylar için her bir dinleyicide event.preventDefault()
çağırmanız gerekeceğini unutmayın. Bunu yapmazsanız, tarayıcı, bıraktığınız dosyayı, drop
olay işleyicisine göndermek yerine açarak sona erer.
Formumuzu Ayarlama
Sürükle ve bırak işlevi eklemeye başlamadan önce, standart bir file
girişi olan temel bir forma ihtiyacımız olacak. Teknik olarak bu gerekli değildir, ancak kullanıcının sürükle ve bırak API'sini desteklemeyen bir tarayıcısı olması durumunda bunu bir alternatif olarak sağlamak iyi bir fikirdir.
<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>
Oldukça basit bir yapı. input
bir onchange
işleyicisi fark edebilirsiniz. Buna daha sonra bir göz atacağız. Ayrıca, JavaScript'i etkinleştirmemiş kişilere yardımcı olmak için form
bir action
ve bir submit
düğmesi eklemek de iyi bir fikir olacaktır. Ardından, daha temiz bir form için onlardan kurtulmak için JavaScript'i kullanabilirsiniz. Her durumda, ister kurum içinde geliştirilmiş bir şey olsun, isterse bunu sizin için yapması için Cloudinary gibi bir hizmet kullanıyor olun, yüklemeyi kabul etmek için bir sunucu tarafı komut dosyasına ihtiyacınız olacak . Bu notlar dışında burada özel bir şey yok, o yüzden bazı stilleri atalım:
#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; }
Bu tarzların çoğu henüz devreye girmiyor, ama sorun değil. Şimdilik öne çıkanlar, file
girişinin gizli olması, ancak label
bir düğme gibi görünecek şekilde şekillendirilmesidir, böylece insanlar dosya seçimi iletişim kutusunu açmak için tıklayabileceklerini fark edeceklerdir. Ayrıca, bırakma alanını kesik çizgilerle çizerek bir kuralı takip ediyoruz.
Sürükle ve Bırak İşlevini Ekleme
Şimdi durumun özüne geliyoruz: sürükle ve bırak. Sayfanın en altına veya ayrı bir dosyaya bir komut dosyası atalım, nasıl isterseniz öyle yapın. Komut dosyasında ihtiyacımız olan ilk şey, bırakma alanına bir referanstır, böylece ona bazı olaylar ekleyebiliriz:
let dropArea = document.getElementById('drop-area')
Şimdi bazı olaylar ekleyelim. Varsayılan davranışları önlemek ve olayların gereğinden fazla köpürmesini durdurmak için tüm olaylara işleyiciler ekleyerek başlayacağız:
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }
Şimdi, bırakma alanının kenarlık renginin rengini değiştirmek için CSS kullanarak öğeyi gerçekten doğru alana sürüklediklerini kullanıcıya bildirmek için bir gösterge ekleyelim. Stiller zaten #drop-area.highlight
seçicisinin altında olmalıdır, bu yüzden gerektiğinde bu highlight
sınıfını eklemek ve kaldırmak için JS kullanalım.
;['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') }
Daha önce bahsettiğim şeyden dolayı vurgulama için hem dragenter
hem de dragover
kullanmak zorunda kaldık. Fareyi doğrudan dropArea
üzerine getirmeye başlarsanız ve ardından alt öğelerinden birinin üzerine gelirseniz, dragleave
tetiklenir ve vurgu kaldırılır. dragover
olayı, dragenter
ve dragleave
olaylarından sonra tetiklenir, bu nedenle vurgu, kaldırıldığını görmeden önce dropArea
geri eklenecektir.
Ayrıca, sürüklenen öğe belirlenen alandan ayrıldığında veya öğeyi bıraktığınızda vurgulamayı kaldırırız.
Şimdi tek yapmamız gereken, bazı dosyalar düştüğünde ne yapacağımızı bulmak:
dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }
Bu bizi tamamlamaya yakın bir yere getirmez, ancak iki önemli şey yapar:
- Bırakılan dosyalar için verilerin nasıl alınacağını gösterir.
- Bizi,
onchange
işleyicisiylefile
input
bulunduğu yere götürür:handleFiles
.
files
bir dizi değil, bir FileList
olduğunu unutmayın. Bu nedenle, handleFiles
, üzerinde daha kolay yineleme yapmak için onu bir diziye dönüştürmemiz gerekecek:
function handleFiles(files) { ([...files]).forEach(uploadFile) }
Bu antiklimaktikti. Gerçek etli şeyler için uploadFile
.
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 */ }) }
Burada, sunucuya gönderilecek form verilerini oluşturmak için yerleşik bir tarayıcı API'si olan FormData
kullanıyoruz. Daha sonra görüntüyü sunucuya göndermek için fetch
API'sini kullanırız. URL'yi arka ucunuz veya hizmetinizle çalışacak şekilde değiştirdiğinizden ve sunucuya ihtiyaç duyduğu tüm bilgileri vermek için ihtiyaç duyabileceğiniz tüm ek form verilerini formData.append
olduğundan emin olun. Alternatif olarak, Internet Explorer'ı desteklemek istiyorsanız XMLHttpRequest
kullanmak isteyebilirsiniz; bu, bunun yerine uploadFile
şöyle görüneceği anlamına gelir:
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) }
Sunucunuzun nasıl kurulduğuna bağlı olarak, 200
yerine farklı status
numaraları aralıklarını kontrol etmek isteyebilirsiniz, ancak bizim amacımız için bu işe yarayacaktır.
Ek özellikler
Temel işlevlerin tümü budur, ancak çoğu zaman daha fazla işlevsellik isteriz. Spesifik olarak, bu eğitimde, seçilen tüm görüntüleri kullanıcıya gösteren bir önizleme bölmesi ekleyeceğiz, ardından kullanıcının yüklemelerin ilerlemesini görmesini sağlayan bir ilerleme çubuğu ekleyeceğiz. Öyleyse, görüntüleri önizlemeye başlayalım.
Resim Önizleme
Bunu yapmanın birkaç yolu vardır: Resim yüklenene kadar bekleyebilir ve sunucudan resmin URL'sini göndermesini isteyebilirsiniz, ancak bu, beklemeniz gerektiği ve resimler bazen oldukça büyük olabileceği anlamına gelir. Bugün inceleyeceğimiz alternatif, drop
olayından aldığımız dosya verilerinde FileReader API'sini kullanmaktır. Bu eşzamansızdır ve alternatif olarak FileReaderSync'i kullanabilirsiniz, ancak arka arkaya birkaç büyük dosyayı okumaya çalışıyor olabiliriz, bu nedenle bu, iş parçacığını bir süre bloke edebilir ve deneyimi gerçekten mahvedebilir. Öyleyse bir previewFile
işlevi oluşturalım ve nasıl çalıştığını görelim:
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) } }
Burada new FileReader
oluşturuyoruz ve File
nesnesi ile onun üzerinde readAsDataURL
. Belirtildiği gibi, bu eşzamansızdır, bu nedenle okumanın sonucunu almak için bir onloadend
olay işleyicisi eklememiz gerekir. Ardından, yeni bir resim öğesi için src
olarak 64 temel veri URL'sini kullanırız ve onu gallery
öğesine ekleriz. Bunun şimdi çalışması için yapılması gereken sadece iki şey var: gallery
öğesini ekleyin ve previewFile
gerçekten çağrıldığından emin olun.
İlk olarak, form
etiketinin sonundan hemen sonra aşağıdaki HTML'yi ekleyin:
<div></div>
Özel birşey yok; bu sadece bir div. Stiller ve içindeki resimler zaten belirlenmiş, yani orada yapacak bir şey kalmadı. Şimdi handleFiles
işlevini aşağıdaki şekilde değiştirelim:
function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }
Bunu yapmanın birkaç yolu vardır, örneğin kompozisyon veya içinde uploadFile
ve previewFile
çalıştıran forEach
için tek bir geri arama gibi, ancak bu da işe yarar. Bununla birlikte, bazı görselleri bıraktığınızda veya seçtiğinizde, neredeyse anında formun altında görünmelidirler. Bununla ilgili ilginç olan şey, - belirli uygulamalarda - aslında görüntüleri yüklemek istemeyebilirsiniz, bunun yerine bunların veri URL'lerini localStorage
veya daha sonra uygulama tarafından erişilecek başka bir istemci tarafı önbelleğinde depolamanızdır. Şahsen bunun için herhangi bir iyi kullanım örneği düşünemiyorum, ancak bazılarının olduğuna bahse girerim.
İlerlemeyi İzleme
Bir şey biraz zaman alabilirse, bir ilerleme çubuğu, bir kullanıcının ilerlemenin gerçekten kaydedildiğini fark etmesine yardımcı olabilir ve tamamlanmasının ne kadar süreceğine dair bir gösterge verebilir. HTML5 progress
etiketi sayesinde bir ilerleme göstergesi eklemek oldukça kolaydır. Bu sefer bunu HTML koduna ekleyerek başlayalım.
<progress max=100 value=0></progress>
Bunu label
hemen arkasına veya form
ile div
galerisi arasına yerleştirebilirsiniz, hangisi daha çok hoşunuza giderse. Bu nedenle, body
etiketleri içinde istediğiniz yere yerleştirebilirsiniz. Bu örnek için hiçbir stil eklenmedi, bu nedenle tarayıcının servis verilebilir varsayılan uygulamasını gösterecektir. Şimdi JavaScript'i eklemeye çalışalım. İlk önce fetch
kullanarak uygulamaya bakacağız ve ardından XMLHttpRequest
için bir sürüm göstereceğiz. Başlamak için, betiğin en üstünde birkaç yeni değişkene ihtiyacımız olacak:
let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')
fetch
kullanırken yalnızca bir yüklemenin ne zaman bittiğini belirleyebiliriz, bu nedenle izlediğimiz tek bilgi, yüklemek için kaç dosyanın seçildiği ( filesToDo
olarak) ve yüklemeyi bitiren dosya sayısıdır ( filesDone
olarak). Ayrıca, hızlı bir şekilde güncelleyebilmemiz için #progress-bar
öğesine bir referans tutuyoruz. Şimdi ilerlemeyi yönetmek için birkaç fonksiyon oluşturalım:
function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }
Yüklemeye başladığımızda, ilerleme çubuğunu sıfırlamak için initializeProgress
çağrılacak. Ardından, tamamlanan her yüklemede, tamamlanan yüklemelerin sayısını artırmak ve ilerleme çubuğunu mevcut ilerlemeyi gösterecek şekilde güncellemek için progressDone
çağırırız. Öyleyse, birkaç eski işlevi güncelleyerek bu işlevleri çağıralım:
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 */ }) }
Ve bu kadar. Şimdi XMLHttpRequest
uygulamasına bir göz atalım. uploadFile
için hızlı bir güncelleme yapabiliriz, ancak XMLHttpRequest
aslında bize fetch
işlevinden daha fazla işlevsellik sağlar, yani her istekte yükleme ilerlemesi için bir olay dinleyicisi ekleyebiliriz, bu da bize periyodik olarak isteğin ne kadarının olduğu hakkında bilgi verir. bitti. Bu nedenle, ne kadarının yapıldığını değil, her bir isteğin tamamlanma yüzdesini izlememiz gerekiyor. O halde, filesDone
ve filesToDo
için bildirimleri aşağıdakilerle değiştirerek başlayalım:
let uploadProgress = []
O zaman fonksiyonlarımızı da güncellememiz gerekiyor. progressDone
updateProgress
olarak yeniden adlandıracağız ve bunları aşağıdaki gibi değiştireceğiz:
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 }
Şimdi initializeProgress
, her dosyanın %0 tamamlandığını belirten, sıfırlarla doldurulmuş, numFiles
eşit uzunlukta bir dizi başlatır. updateProgress
, hangi görüntünün ilerlemesini güncelleştirdiğini buluyoruz ve bu dizindeki değeri sağlanan percent
olarak değiştiriyoruz. Daha sonra tüm yüzdelerin ortalamasını alarak toplam ilerleme yüzdesini hesaplıyoruz ve ilerleme çubuğunu hesaplanan toplamı yansıtacak şekilde güncelliyoruz. fetch
örneğinde yaptığımızla aynı şekilde handleFiles
içinde initializeProgress
öğesini çağırıyoruz, bu nedenle şimdi güncellememiz gereken tek şey uploadFile
çağırmak için 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) }
Unutulmaması gereken ilk şey, bir i
parametresi eklediğimizdir. Bu, dosya listesindeki dosyanın dizinidir. Bu parametreyi iletmek için handleFiles
güncellememiz gerekmez, çünkü bu, öğenin dizinini zaten geri aramalara ikinci parametre olarak veren forEach
kullanıyor. Ayrıca progress
olay dinleyicisini xhr.upload
ekledik, böylece ilerleme ile updateProgress
çağırabiliriz. Olay nesnesi (kodda e
olarak anılır) üzerinde iki uygun bilgi parçası vardır: o ana kadar loaded
bayt sayısını içeren yüklendi ve dosyanın total
bayt sayısını içeren toplam.
|| 100
|| 100
adet var çünkü bazen bir hata varsa e.loaded
ve e.total
sıfır oluyor bu da hesaplamanın NaN
olarak çıkacağı anlamına geliyor, bu yüzden dosyanın yapıldığını bildirmek yerine 100
kullanılıyor. Ayrıca 0
kullanabilirsiniz. Her iki durumda da hata, kullanıcıyı bunlar hakkında bilgilendirebilmeniz için readystatechange
işleyicisinde görünecektir. Bu yalnızca NaN
ile matematik yapmaya çalışırken istisnaların oluşmasını önlemek içindir.
Çözüm
Bu son parça. Artık görüntüleri sürükle ve bırak yoluyla yükleyebileceğiniz, yüklenen görüntüleri hemen önizleyebileceğiniz ve bir ilerleme çubuğunda yüklemenin ilerlemesini görebileceğiniz bir web sayfanız var. CodePen'de son sürümü ( XMLHttpRequest
ile) çalışırken görebilirsiniz, ancak dosyaları yüklediğim hizmetin sınırları olduğunu unutmayın, bu nedenle birçok kişi test ederse, bir süreliğine bozulabilir.