VanillaJavaScriptを使用してドラッグアンドドロップファイルアップローダーを作成する方法
公開: 2022-03-10ファイル選択入力を開発者が望むようにスタイル設定するのは難しいことは既知の事実です。そのため、多くの場合、ファイル選択入力を非表示にして、代わりにファイル選択ダイアログを開くボタンを作成します。 しかし、最近では、ファイル選択を処理するためのさらに優れた方法があります。ドラッグアンドドロップです。
技術的には、ファイル選択入力のほとんど(すべてではない)の実装でファイルをドラッグして選択できるため、これはすでに可能でしたが、実際にfile
要素を表示する必要があります。 それでは、実際にブラウザーから提供されたAPIを使用して、ドラッグアンドドロップのファイルセレクターとアップローダーを実装しましょう。
この記事では、「バニラ」ES2015 + JavaScript(フレームワークやライブラリなし)を使用してこのプロジェクトを完了します。ブラウザーでJavaScriptの実用的な知識があることを前提としています。 この例は、ES5構文に簡単に変更したり、Babelによってトランスパイルしたりできる、ES2015 +構文を除いて、すべての常緑ブラウザに加えてIE10および11と互換性があるはずです。
作成するものを簡単に見てみましょう。
ドラッグアンドドロップイベント
最初に説明する必要があるのは、ドラッグアンドドロップに関連するイベントです。これらはこの機能の背後にある原動力であるためです。 ドラッグアンドドロップに関連してブラウザが起動するイベントは全部で8つあります。 drag
、ドラッグエンド、ドラッグdragenter
、ドラッグdragend
、 dragstart
dragexit
、ドラッグdragover
、ドラッグdragleave
、 drop
です。 drag
、 dragend
、 dragexit
、 dragstart
はすべて、ドラッグされている要素に対して実行されるため、これらすべてについて説明することはしません。この場合、DOM要素ではなくファイルシステムからファイルをドラッグします。 、したがって、これらのイベントはポップアップしません。
それらに興味がある場合は、MDNでこれらのイベントに関するいくつかのドキュメントを読むことができます。
ご想像のとおり、これらのイベントのイベントハンドラーは、ほとんどのブラウザーイベントのイベントハンドラーを登録するのと同じ方法で、 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)
言語をより明確にするためにコードサンプルのdropArea
を使用して、これらのイベントが何をするかを説明する小さな表を次に示します。
イベント | いつ解雇されますか? |
---|---|
dragenter | ドラッグされたアイテムはdropArea上にドラッグされ、ユーザーがドロップした場合のドロップイベントのターゲットになります。 |
dragleave | ドラッグされたアイテムはdropAreaから別の要素にドラッグされ、代わりにdropイベントのターゲットになります。 |
dragover | ドラッグされたアイテムがdropAreaを超えて移動している間、数百ミリ秒ごと。 |
drop | ユーザーはマウスボタンを離し、ドラッグしたアイテムをdropAreaにドロップします。 |
ドラッグされたアイテムはdropArea
の子の上にドラッグされ、 dragleave
はdropArea
で起動し、dragenterは新しいtarget
であるためその子要素でdragenter
することに注意してください。 drop
イベントはdropArea
まで伝播するため(伝播がそこに到達する前に別のイベントリスナーによって停止されない限り)、イベントのtarget
ではないにもかかわらず、 dropArea
で引き続き発生します。
また、カスタムのドラッグアンドドロップ操作を作成するには、これらのイベントの各リスナーでevent.preventDefault()
を呼び出す必要があることに注意してください。 そうしないと、ブラウザはドロップしたファイルをdrop
イベントハンドラに送信するのではなく、開いてしまいます。
フォームの設定
ドラッグアンドドロップ機能の追加を開始する前に、標準のfile
入力を備えた基本的なフォームが必要です。 技術的にはこれは必要ありませんが、ユーザーがドラッグアンドドロップAPIをサポートしていないブラウザーを使用している場合に備えて、代替手段として提供することをお勧めします。
<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>
かなりシンプルな構造。 input
にonchange
ハンドラーがあることに気付くかもしれません。 これについては後で見ていきます。 また、JavaScriptを有効にしていない人を支援するために、 form
とsubmit
ボタンにaction
を追加することもお勧めします。 次に、JavaScriptを使用してそれらを取り除き、よりクリーンなフォームにすることができます。 いずれの場合も、アップロードを受け入れるにはサーバー側のスクリプトが必要になります。これは、社内で開発されたものであろうと、Cloudinaryなどのサービスを使用して行っているものであろうと関係ありません。 これらのメモ以外に特別なことは何もないので、いくつかのスタイルを入れましょう:
#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; }
これらのスタイルの多くはまだ機能していませんが、それは問題ありません。 今のところ、ハイライトはfile
入力が非表示になっていることですが、そのlabel
はボタンのように見えるようにスタイル設定されているため、クリックしてファイル選択ダイアログを表示できることに気付くでしょう。 また、ドロップ領域の輪郭を破線で示すことにより、規則に従っています。
ドラッグアンドドロップ機能の追加
ここで、状況の要点に到達します。ドラッグアンドドロップです。 ページの下部または別のファイルにスクリプトを入れましょう。ただし、やりたいと思います。 スクリプトで最初に必要なのは、ドロップ領域への参照です。これにより、いくつかのイベントをそれに添付できます。
let dropArea = document.getElementById('drop-area')
それでは、いくつかのイベントを追加しましょう。 まず、すべてのイベントにハンドラーを追加して、デフォルトの動作を防ぎ、イベントが必要以上にバブリングするのを防ぎます。
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }
次に、CSSを使用してドロップ領域の境界線の色を変更することにより、ユーザーが実際にアイテムを正しい領域にドラッグしたことをユーザーに知らせるインジケーターを追加しましょう。 スタイルはすでに#drop-area.highlight
セレクターの下にあるはずなので、必要に応じてJSを使用してそのhighlight
クラスを追加および削除しましょう。
;['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') }
先に述べたように、ハイライトにはdragenter
とdragover
の両方を使用する必要がありました。 dropArea
に直接カーソルを合わせてから、その子の1つにカーソルを合わせると、 dragleave
が実行され、ハイライトが削除されます。 ドラッグオーバーイベントはdragover
イベントとdragenter
イベントの後に発生するため、ハイライトが削除さdropArea
dragleave
追加されます。
また、ドラッグしたアイテムが指定された領域を離れるとき、またはアイテムをドロップするときに、ハイライトを削除します。
今、私たちがする必要があるのは、いくつかのファイルがドロップされたときに何をすべきかを理解することです。
dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }
これは私たちを完成に近づけるものではありませんが、2つの重要なことを行います。
- ドロップされたファイルのデータを取得する方法を示します。
-
onchange
ハンドラーでfile
input
があったのと同じ場所に移動します:handleFiles
を待機します。
files
は配列ではなく、 FileList
であることに注意してください。 したがって、 handleFiles
を実装するときは、より簡単に反復処理するために、それを配列に変換する必要があります。
function handleFiles(files) { ([...files]).forEach(uploadFile) }
それは反気候的でした。 本物の肉付きの良いもののために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 */ }) }
ここでは、サーバーに送信するフォームデータを作成するための組み込みのブラウザAPIであるFormData
を使用します。 次に、 fetch
APIを使用して、実際に画像をサーバーに送信します。 バックエンドまたはサービスで機能するようにURLを変更し、 formData.append
を使用して、サーバーに必要なすべての情報を提供するために必要な追加のフォームデータを追加してください。 または、Internet Explorerをサポートする場合は、 XMLHttpRequest
を使用することをお勧めします。これは、 uploadFile
が代わりに次のようになることを意味します。
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) }
サーバーの設定方法によっては、 200
だけでなく、さまざまな範囲のstatus
番号を確認することもできますが、私たちの目的では、これで問題ありません。
追加機能
これがすべての基本機能ですが、多くの場合、より多くの機能が必要です。 具体的には、このチュートリアルでは、選択したすべての画像をユーザーに表示するプレビューペインを追加してから、ユーザーがアップロードの進行状況を確認できる進行状況バーを追加します。 それでは、画像のプレビューを始めましょう。
画像プレビュー
これを行うには、いくつかの方法があります。画像がアップロードされるまで待って、サーバーに画像のURLを送信するように依頼できますが、それは待つ必要があり、画像がかなり大きくなる場合があることを意味します。 別の方法(今日調査します)は、 drop
イベントから受け取ったファイルデータでFileReaderAPIを使用することです。 これは非同期であり、代わりにFileReaderSyncを使用することもできますが、複数の大きなファイルを連続して読み取ろうとしている可能性があるため、スレッドがかなりの時間ブロックされ、エクスペリエンスが実際に台無しになる可能性があります。 それでは、 previewFile
関数を作成して、それがどのように機能するかを見てみましょう。
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) } }
ここでは、 new FileReader
を作成し、 File
オブジェクトを使用してその上でreadAsDataURL
を呼び出します。 前述のように、これは非同期であるため、読み取りの結果を取得するには、 onloadend
イベントハンドラーを追加する必要があります。 次に、base 64データURLを新しい画像要素のsrc
として使用し、それをgallery
要素に追加します。 これを機能させるために実行する必要があるのは、 gallery
要素を追加することと、 previewFile
が実際に呼び出されることを確認することの2つだけです。
まず、 form
タグの最後の直後に次のHTMLを追加します。
<div></div>
特にない; それはただのdivです。 スタイルとその中の画像はすでに指定されているので、そこで行うことは何も残っていません。 次に、 handleFiles
関数を次のように変更しましょう。
function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }
コンポジションや、 uploadFile
とpreviewFile
を実行したforEach
への単一のコールバックなど、これを行う方法はいくつかありますが、これも機能します。 これにより、いくつかの画像をドロップまたは選択すると、フォームの下にほぼ瞬時に表示されます。 これの興味深い点は、特定のアプリケーションでは、実際には画像をアップロードしたくない場合がありますが、代わりに、後でアプリがアクセスできるように、画像のデータURLをlocalStorage
またはその他のクライアント側キャッシュに保存します。 私は個人的にこれの良いユースケースを考えることはできませんが、いくつかあると確信しています。
進捗状況の追跡
何か時間がかかる場合は、進行状況バーを使用すると、進行状況が実際に行われていることをユーザーが認識し、完了するまでにかかる時間を示すことができます。 HTML5のprogress
タグのおかげで、進行状況インジケーターを追加するのは非常に簡単です。 今回はそれをHTMLコードに追加することから始めましょう。
<progress max=100 value=0></progress>
label
の直後、またはform
とギャラリーdiv
のどちらか好きな方に配置できます。 さらに言えば、 body
タグ内の好きな場所に配置できます。 この例ではスタイルが追加されていないため、ブラウザのデフォルトの実装が表示されます。これはサービス可能です。 それでは、JavaScriptの追加に取り掛かりましょう。 最初にfetch
を使用した実装を確認してから、 XMLHttpRequest
のバージョンを示します。 まず、スクリプトの先頭にいくつかの新しい変数が必要です。
let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')
fetch
を使用する場合、アップロードがいつ終了したかを判断することしかできないため、追跡する情報は、アップロードするために選択されたファイルの数( filesToDo
として)とアップロードが終了したファイルの数( filesDone
として)のみです。 また、 #progress-bar
要素への参照を保持しているため、すばやく更新できます。 次に、進行状況を管理するためのいくつかの関数を作成しましょう。
function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }
アップロードを開始すると、 initializeProgress
が呼び出され、プログレスバーがリセットされます。 次に、アップロードが完了するたびに、 progressDone
を呼び出して、完了したアップロードの数を増やし、進行状況バーを更新して現在の進行状況を表示します。 それでは、いくつかの古い関数を更新して、これらの関数を呼び出しましょう。
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 */ }) }
以上です。 それでは、 XMLHttpRequest
の実装を見てみましょう。 uploadFile
をすばやく更新することもできますが、 XMLHttpRequest
は実際にはfetch
よりも多くの機能を提供します。つまり、リクエストごとにアップロードの進行状況を示すイベントリスナーを追加できます。これにより、リクエストの量に関する情報が定期的に提供されます。終了した。 このため、完了したリクエストの数ではなく、各リクエストの完了率を追跡する必要があります。 それでは、 filesDone
とfilesToDo
の宣言を次のように置き換えることから始めましょう。
let uploadProgress = []
次に、関数も更新する必要があります。 progressDone
の名前をupdateProgress
に変更し、次のように変更します。
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 }
ここで、 initializeProgress
は、ゼロで埋められたnumFiles
に等しい長さの配列を初期化し、各ファイルが0%完了していることを示します。 updateProgress
、進行状況が更新されている画像を見つけ、そのインデックスの値を指定されたpercent
に変更します。 次に、すべてのパーセンテージの平均をとることによって合計進捗パーセンテージを計算し、計算された合計を反映するように進行状況バーを更新します。 fetch
の例で行ったのと同じように、 handleFiles
でinitializeProgress
を呼び出します。したがって、更新する必要があるのは、 uploadFile
を呼び出す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) }
最初に注意することは、 i
パラメーターを追加したことです。 これは、ファイルのリスト内のファイルのインデックスです。 forEach
を使用しているため、このパラメーターを渡すためにhandleFiles
を更新する必要はありません。これは、コールバックの2番目のパラメーターとして要素のインデックスを既に提供しているためです。 また、 progress
イベントリスナーをxhr.upload
に追加して、progressを使用してupdateProgress
を呼び出すことができるようにしました。 イベントオブジェクト(コードではe
と呼ばれます)には、2つの関連情報があります。これまでにアップロードされたバイト数を含むloaded
と、ファイルのtotal
バイト数を含むtotalです。
|| 100
エラーが発生した場合、 e.loaded
とe.total
がゼロになることがあるため、 || 100
個がそこにあります。これは、計算がNaN
として出力されることを意味し、代わりに100
がファイルの完了を報告するために使用されます。 0
を使用することもできます。 いずれの場合も、エラーはreadystatechange
ハンドラーに表示されるため、ユーザーに通知することができます。 これは、 NaN
を使用して数学を実行しようとしたときに例外がスローされるのを防ぐためだけのものです。
結論
それが最後のピースです。 これで、ドラッグアンドドロップで画像をアップロードしたり、アップロードされている画像をすぐにプレビューしたり、進行状況バーでアップロードの進行状況を確認したりできるWebページができました。 CodePenで( XMLHttpRequest
を使用した)最終バージョンの動作を確認できますが、ファイルをアップロードするサービスには制限があるため、多くの人がテストすると、しばらくの間機能しなくなる可能性があることに注意してください。