VanillaJavaScriptを使用してドラッグアンドドロップファイルアップローダーを作成する方法

公開: 2022-03-10
簡単な要約↬この記事では、「バニラ」ES2015 + JavaScript(フレームワークやライブラリなし)を使用してこのプロジェクトを完了します。ブラウザーでJavaScriptの実用的な知識があることを前提としています。 この例は、すべての常緑ブラウザに加えてIE10および11と互換性があるはずです。

ファイル選択入力を開発者が望むようにスタイル設定するのは難しいことは既知の事実です。そのため、多くの場合、ファイル選択入力を非表示にして、代わりにファイル選択ダイアログを開くボタンを作成します。 しかし、最近では、ファイル選択を処理するためのさらに優れた方法があります。ドラッグアンドドロップです。

技術的には、ファイル選択入力のほとんど(すべてではない)の実装でファイルをドラッグして選択できるため、これはすでに可能でしたが、実際にfile要素を表示する必要があります。 それでは、実際にブラウザーから提供されたAPIを使用して、ドラッグアンドドロップのファイルセレクターとアップローダーを実装しましょう。

この記事では、「バニラ」ES2015 + JavaScript(フレームワークやライブラリなし)を使用してこのプロジェクトを完了します。ブラウザーでJavaScriptの実用的な知識があることを前提としています。 この例は、ES5構文に簡単に変更したり、Babelによってトランスパイルしたりできる、ES2015 +構文を除いて、すべての常緑ブラウザに加えてIE10および11と互換性があるはずです。

作成するものを簡単に見てみましょう。

動作中のドラッグアンドドロップ画像アップローダー
ドラッグアンドドロップで画像をアップロードしたり、アップロードされている画像をすぐにプレビューしたり、進行状況バーでアップロードの進行状況を確認したりできるWebページのデモンストレーション。

ドラッグアンドドロップイベント

最初に説明する必要があるのは、ドラッグアンドドロップに関連するイベントです。これらはこの機能の背後にある原動力であるためです。 ドラッグアンドドロップに関連してブラウザが起動するイベントは全部で8つあります。 drag 、ドラッグエンド、ドラッグdragenter 、ドラッグdragenddragstart dragexit 、ドラッグdragover 、ドラッグdragleavedropです。 dragdragenddragexitdragstartはすべて、ドラッグされている要素に対して実行されるため、これらすべてについて説明することはしません。この場合、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の子の上にドラッグされ、 dragleavedropAreaで起動し、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>

かなりシンプルな構造。 inputonchangeハンドラーがあることに気付くかもしれません。 これについては後で見ていきます。 また、JavaScriptを有効にしていない人を支援するために、 formsubmitボタンに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') }

先に述べたように、ハイライトにはdragenterdragoverの両方を使用する必要がありました。 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つの重要なことを行います。

  1. ドロップされたファイルのデータを取得する方法を示します。
  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) }

コンポジションや、 uploadFilepreviewFileを実行した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よりも多くの機能を提供します。つまり、リクエストごとにアップロードの進行状況を示すイベントリスナーを追加できます。これにより、リクエストの量に関する情報が定期的に提供されます。終了した。 このため、完了したリクエストの数ではなく、各リクエストの完了率を追跡する必要があります。 それでは、 filesDonefilesToDoの宣言を次のように置き換えることから始めましょう。

 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の例で行ったのと同じように、 handleFilesinitializeProgressを呼び出します。したがって、更新する必要があるのは、 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.loadede.totalがゼロになることがあるため、 || 100個がそこにあります。これは、計算がNaNとして出力されることを意味し、代わりに100がファイルの完了を報告するために使用されます。 0を使用することもできます。 いずれの場合も、エラーはreadystatechangeハンドラーに表示されるため、ユーザーに通知することができます。 これは、 NaNを使用して数学を実行しようとしたときに例外がスローされるのを防ぐためだけのものです。

結論

それが最後のピースです。 これで、ドラッグアンドドロップで画像をアップロードしたり、アップロードされている画像をすぐにプレビューしたり、進行状況バーでアップロードの進行状況を確認したりできるWebページができました。 CodePenで( XMLHttpRequestを使用した)最終バージョンの動作を確認できますが、ファイルをアップロードするサービスには制限があるため、多くの人がテストすると、しばらくの間機能しなくなる可能性があることに注意してください。