Vanilla JavaScript ile Sürükle ve Bırak Dosya Yükleyici Nasıl Yapılır

Yayınlanan: 2022-03-10
Kısa özet ↬ 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, her zaman yeşil kalan her tarayıcı artı IE 10 ve 11 ile uyumlu olmalıdır.

Dosya 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 görüntü yükleyicisi iş başında
Sürükle ve bırak yoluyla görüntüleri yükleyebileceğiniz, yüklenmekte olan görüntüleri hemen önizleyebileceğiniz ve bir ilerleme çubuğunda yüklemenin ilerlemesini görebileceğiniz bir web sayfasının gösterimi.

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.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

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:

  1. Bırakılan dosyalar için verilerin nasıl alınacağını gösterir.
  2. Bizi, onchange işleyicisiyle file 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.