前回は、<input type="file">で選択した画像をプレビューしました。
今回は、
- プレビューした画像をドラッグ&ドロップできるようにする
- 送信する時、画像ファイルと共に配列
photoOrder(画像の位置情報)を送信できるようにする
目次
前回の続き
-
(5)ドラッグ&ドロップ
①各画像がドロップを受け付けるようにする
②ドラッグデータを手動で登録し、ドラッグ中の要素を取得する
③「画像の入れ替え」と「位置情報の入れ替え」 - (6)JavaScriptで作った値を<form>で送信できるようにする
ファイル完成(前回の内容を含めた完全版)
(5)ドラッグ&ドロップ
プレビューした画像をドラッグ&ドロップして、互いの位置を入れ替え、並び替えられるようにしたいです。
(例)画像3をドラッグし、画像1のところでドロップするとします。
こちらを中心に、関連リンクも読みながら実装しました。
最終的に、各画像にイベントリスナーを4つ追加することになりました。イベントによって、imgの示す画像が違うので注意が必要です。
promise.then((selectedPhotos) => {
for(let i = 0; i < selectedPhotos.length; i++){
const img = selectedPhotos[i];
const div = document.createElement('div');
div.id = 'div-' + i;
div.appendChild(img);
allPhotos.appendChild(div);
// ここから続き
img.addEventListener('dragstart', function(event){
// imgはドラッグ中の要素。画像3
});
img.addEventListener('dragenter', function(event){
// imgはドロップされる要素。画像1
});
img.addEventListener('dragover', function(event){
// imgはドロップされる要素。画像1
});
img.addEventListener('drop', function(event){
// imgはドロップされる要素。画像1
});
}
});
- dragstart : (画像3)ドラッグ開始時に1回発生
- dragenter : (画像1)ドラッグ中の要素が自分の上に入った時、1回発生
- dragover :(画像1)ドラッグ中の要素が自分の上にある間、50ミリ秒~5ミリ秒ごとに発生
- drop : (画像1)ドラッグ中の要素がドロップした時に1回発生
実は、現段階ではdropイベントが発生しません。各画像がドロップを受け付けていないからです。
①各画像がドロップを受け付けるようにする
各画像は、「ドラッグができる要素」 でもあり、 「ドロップを受け付ける要素」 でもある必要があります。画像は、既定でドラッグができるようになっていますが、ドロップは受け付けません。ドロップを受け付けるようにします。
ウェブページにおいては、既定のドラッグ & ドロップの挙動が使われる場合がいくつかあります。文字列の選択範囲、画像、リンクなどのドラッグなどがこれにあたります。
(出典 : ドラッグ操作 - draggable属性)
- dragenterイベントと、dragoverイベントで
event.preventDefault();を呼び出すだけ
img.addEventListener('dragenter', function(event){
event.preventDefault();
});
img.addEventListener('dragover', function(event){
event.preventDefault();
});
これらのイベントに対する既定の動作はドロップを禁止する働きをします。ドロップを許可したい場合は、 dragenter および dragover イベントの両方をキャンセルして、既定の処理を防ぐ必要があります。これを行うには、イベントの preventDefault() メソッドを呼び出してください。
(出典 : ドラッグ操作 - ドロップ先の指定)
これで、各画像はドロップを受け付けるようになりました。dropイベントが発生するようになりました。
現在、4つのイベントが発生する状態です。
これらのイベントを含む全てのドラッグイベントは、dataTransferプロパティを持ちます。
img.addEventListener('任意のドラッグイベント', function(event){
console.log(event.dataTransfer); // DataTransferオブジェクトが確認できる
});
これは、DataTransferオブジェクトです。DataTransferオブジェクトは、ドラッグ&ドロップ操作にまつわる様々な情報を持っており、その中の1つに、ドラッグデータがあります。ドラッグデータとは、「ドラッグ中の要素に関する情報(データの型と値)」 です。実装ではこのドラッグデータを使っていきます。
すべてのドラッグイベントは、ドラッグデータを保持するための dataTransfer と呼ばれるプロパティを持っています (dataTransfer は DataTransfer オブジェクトの一つです)。
ドラッグが行われた際には、何をドラッグするのかを識別するためのデータをドラッグに関連付ける必要があります。(省略)ウェブページ上のリンクがドラッグされた場合、ドラッグデータにはリンクの URL が含まれます。
ドラッグデータには、データの型 (または形式) とデータの値の 2 つの情報が含まれています。形式は型の文字列 (テキストデータの場合は text/plain など) で、値はテキストの文字列です。
(出典 : ドラッグ操作 - ドラッグデータ)
出典のドラッグデータの部分に、DataTransferオブジェクトへのリンクが張ってあります。
「event.dataTransferプロパティ = DataTransferオブジェクト = ドラッグデータを持つ」
という認識で、進めていきます。
実は、ドラッグデータの操作はDataTransferオブジェクトを使う方法と、DataTransferItem / DataTransferItemListオブジェクトを使う方法があります。今回は、参考記事の冒頭にある通り、DataTransferオブジェクトを使います。
メモ: この文書で記述されているドラッグ操作は DataTransfer インターフェイスを使用します。この文書では DataTransferItem インターフェイスや DataTransferItemList インターフェイスは説明しません。
両者について少し補足を書きました。良ければ読んでみて下さい。
→ 【コラム】DataTransfer / DataTransferItemList / DataTransferItemの関係
ドラッグデータ
- ドラッグ中の要素に関する情報(データの型と値) のこと
- ドラッグデータへの書き込みはdragstartイベントのみ可能。読み取り(取得)はdragstartイベントとdropイベントのみ可能(大体dropイベント)。それ以外のイベントでは「保護モード」となり、アクセスできない
それぞれのドラッグ&ドロップ操作には、関連付けられた「ドラッグデータストア」が存在します。これは、DragEvent の dataTransfer プロパティを通じてアクセス可能な DataTransfer オブジェクトです。(省略)データストアへの変更を加えられるのは、 dragstart ハンドラー内のみです。その他のドラッグイベントにおける dataTransfer では、データストアは変更できません。(省略)さらに、データストアから「読み取り」が可能なのは、dragstart イベントを除き、drop イベント中のみとなります(これによりドロップ先がデータを取得できます)。その他のイベントでは、データストアにアクセスすることはできません。
(出典 : HTML ドラッグ&ドロップ API - ドラッグデータストア)
dragstart および drop イベント以外では、データストアは「保護モード」となり、コードがデータ本体にアクセスすることを許可しません。
(出典 : ドラッグデータストアでの作業 - 保護モード)
これからドラッグデータを使って、実装していきます。
ドラッグデータをどのように使うのか
改めて。画像3をドラッグし、画像1のところでドロップするとします。ドロップした瞬間、位置を入れ替えたいです。
dropイベントを使います。ここのimgは、画像1です。画像3と入れ替える処理を書くのですが、画像3はどこから手に入れたらいいのか…と。
img.addEventListener('drop', function(event){
// imgはドロップされる要素。画像1
});
ここで、ドラッグデータです。ドラッグデータから、「ドラッグ中の要素(画像3)に関する情報」を取得します。画像は既定でドラッグができる要素で、既にドラッグデータがブラウザによって登録されています。
画像、リンク、選択範囲などのデフォルトでドラッグ可能な項目については、ドラッグデータはブラウザーによって既に定義されています。
(出典 : HTML ドラッグ&ドロップ API - ドラッグデータストア)
何が登録されているのかドラッグデータを確認すると、 「複数のデータ型」 で 「画像3のsrc属性の値(データURL)」 が登録されていました。
しかし今回の実装では、画像3を取得するにあたり、src属性の値ではなくid属性の値を使いたいです。そのため、まずは手動でドラッグデータを登録していきます。
→実装へ
ドラッグデータの確認方法
ドラッグデータは、「ドラッグ中の要素に関する情報(データの型と値)」。
データの型
-
DataTransfer.typesプロパティ
img.addEventListener('drop', function(event){
console.log(event.dataTransfer.types);
});
(★)
['text/plain', 'text/uri-list', 'text/html', 'Files']
結果はブラウザによって異なるかもしれません。
- 1つのデータ型につき、1つの値を持つ
同じ形式で 2 回データを登録すると、古いデータは新しいデータによって置き換えられます
(出典 : ドラッグ操作 - ドラッグデータ)
- (★)
Filesは、データ型ではない
ドラッグ操作にファイルが含まれている場合は、その型の 1 つが文字列 Files になります。
(出典 : DataTransfer: types プロパティ)
ファイル項目の型が直接公開されていないことに気づくかもしれません。これらはアクセスは可能ですが、files リスト内の各 File オブジェクトの type プロパティ経由でのみアクセスできます。したがって、ファイルを読み取れない場合、その型も知ることができません
(出典 : ドラッグデータストアでの作業 - ドラッグデータストアの構造)
今回の実装では不要ですが、Filesのデータ型を確認したい場合は以下のようにします。
img.addEventListener('drop', function(event){
console.log(event.dataTransfer.files[0].type);
});
image/jpeg
ドラッグデータには、text/plain型の文字列、text/uri-list型の文字列、text/html型の文字列、image/jpeg型のファイルが登録されていると分かります。
値
-
文字列 :
DataTransfer.getData()メソッドで取得
先ほど出力したデータ型を1つ指定し、対応する値を得る -
ファイル :
DataTransfer.filesプロパティで取得
img.addEventListener('drop', function(event){
event.dataTransfer.getData("text/plain");
event.dataTransfer.getData("text/uri-list");
event.dataTransfer.getData("text/html");
event.dataTransfer.files; // FileListオブジェクト
});
getData()で取得される値は、すべて「ドラッグ中の画像(画像3)」の「src属性の値(データURL)」です(出力は非常に長いので、ここでは確認しません)。text/html型の値のみ、HTMLタグを含む<img src="データURL" id="photo-3">という文字列でした。
ドラッグデータストアからテキストデータを読み取るには、(省略)DataTransfer.getData() メソッドを使用します。
ドラッグデータストアからファイルデータを読み取る場合、(省略)DataTransfer.files プロパティを使用します。
(出典 : ドラッグデータストアでの作業 - ドラッグデータストアの読み取り)
②ドラッグデータを手動で登録し、ドラッグ中の要素を取得する
id属性が必要な理由は、配列photoOrderを更新するためです。
先ほどドラッグデータを確認したところ、ドラッグ中の要素である 「画像3のsrc属性」 を取得できると分かりました。単にその場でドラッグ&ドロップをするだけなら、これを取得し、互いの<img>のsrc属性の値を入れ替えればよいです。
しかし、これは<img>を操作するだけで、画像の並び順は送信先のファイルに反映されません。今回は、送信先のファイルでも画像の並び順を保ちたいです。
そのため、前回の記事で、「画像の位置情報を管理する」配列photoOrderを作成しました。ドラッグ&ドロップで画像の位置を入れ替えるたびに、配列photoOrderを更新します。
配列photoOrderには、画像のid属性の一部(画像番号)を格納しています。「画像3のid属性」も欲しいです。
ドラッグデータに登録されていたtext/html型の値は、<img src="データURL" id="photo-3">という文字列です。id属性が含まれています。しかし、この一部分を抜き出すよりも、ドラッグデータに「画像3のid属性」を追加して、取得しようと思います。
前置きが長くなりましたが、手動でドラッグデータを登録します。
- setData() で、ドラッグデータに「データ型と値」を登録
ドラッグデータへの書き込みはdragstartイベントのみ可能
DataTransfer.setData() メソッドは、ドラッグ操作のドラッグデータに指定したデータと型を設定します。
(出典 : DataTransfer: setData() メソッド)
img.addEventListener('dragstart', function(event){// imgは画像3
event.dataTransfer.setData("application/x-img-id", img.id);// 値'photo-3'を登録
});
application/x-img-idは、自分で考えたデータ型です(後述)。
- getData()で取得。setData()で登録したデータ型を指定し、対応する値(文字列)を得る
dropイベントで取得
getData() メソッドは、取得したいデータの型を引数として取ります。実行すると、ドラッグ操作の開始時に setData() メソッドによって登録された値が文字列として返されます。
(出典 : ドラッグ操作 - ドロップの実行)
img.addEventListener('drop', function(event){// imgは画像1
const dragImgId = event.dataTransfer.getData("application/x-img-id"); // 'photo-3'
const dragImg = document.getElementById(dragImgId);//(★)
});
(★)ドラッグデータからid属性('photo-3')を取得し、それを使って画像3を取得しました。
- データ型
application/x-img-idは独自の型
setData() で独自の型を登録した例です。
最初の型の application/x.bookmark は独自の型です。他のアプリケーションはこの型に対応していないでしょうが、同じウェブサイトやアプリケーションの中の領域同士でのドラッグでは、このような独自の形式を利用できます。
(出典 : ドラッグ操作 - ドラッグデータ)
独自の型を使う理由は、1つのデータ型につき1つの値しか登録できないからです。上書きしてしまいます。
同じ形式で 2 回データを登録すると、古いデータは新しいデータによって置き換えられます
(出典 : ドラッグ操作 - ドラッグデータ)
画像は既定でドラッグができる要素で、ブラウザがドラッグデータを登録しています。私は、ドラッグ中の画像のid属性の値をtext/plain型で登録しようと思いました。しかし、既にブラウザがドラッグ中の画像のsrc属性の値を、text/plain型で登録しています。既定で設定されているものなので上書きせず、独自の型を使って、新しく登録しました。
③「画像の入れ替え」と「位置情報の入れ替え」
現在、画像1と画像3が手元にあります。これから、両者の親要素<div>を取得し、互いの<div>に入れ替えます。
ここで配列photoOrderです。これは、画像の位置情報を管理しています。
- 配列
photoOrderには、画像番号が入っている(例 :<img id="photo-3">の「3」) - 配列photoOrderのインデックス = 属する<div>
※前回の記事から例としてこの並び順を使っています。これは、プレビュー完成時点の画像の並び順です。プレビューは非同期処理なので、実際の並び順は常に変化します。
図の通り、「3が入っているインデックスは0」です。これで、「画像3が入っている<div>はdiv-0」ということが分かります。同様に、「1が入っているインデックスは1」、つまり「画像1が入っている<div>はdiv-1」です。
画像番号のインデックスを求め、そのインデックスを使って、両者の<div>を取得することができます。
img.addEventListener('drop', function(event){
const dragImgId = event.dataTransfer.getData("application/x-img-id");//'photo-3'
const dragImg = document.getElementById(dragImgId);// 画像3取得
// ↓ ここから
// <img>のid属性から、画像番号(Number型)を取得
const dragImgNumber = parseInt(dragImg.id.slice(6), 10);//3
const dropImgNumber = parseInt(img.id.slice(6), 10);//1
// 画像番号のインデックスを求める
const dragIndex = photoOrder.indexOf(dragImgNumber);//0
const dropIndex = photoOrder.indexOf(dropImgNumber);//1
// インデックスを使って<div>取得
const dragImgDiv = document.getElementById('div-' + dragIndex);
const dropImgDiv = document.getElementById('div-' + dropIndex);
// (★)画像の位置を入れ替え
dragImgDiv.appendChild(img);//div-0に画像1
dropImgDiv.appendChild(dragImg);//div-1に画像3
// (★)配列photoOrderを入れ替え
photoOrder[dragIndex] = dropImgNumber;
photoOrder[dropIndex] = dragImgNumber;
});
- parseInt関数とslice関数
id属性の値'photo-3'や'photo-1'の数字部分をslice関数で抜き出します。これはstring型の'3'と'1'。parseInt関数で、10進数のNumber型3と1に変換します。
(★)画像の位置を入れ替え
- ドラッグ要素(画像3)が入っていた<div>に、ドロップ要素(画像1)を入れる
- ドロップ要素(画像1)が入っていた<div>に、ドラッグ要素(画像3)を入れる
(★)配列photoOrderを入れ替え
photoOrder = [3, 1, 2, 0];を、photoOrder = [1, 3, 2, 0];に更新
- photoOrder[0] = 1;
- photoOrder[1] = 3;
これで完成です。最後に、JavaScriptで作成した、配列photoOrderを送信できるようにします。
(6)JavaScriptで作った値を<form>で送信できるようにする
-
<input type="hidden" name="photo-order" value="[1, 3, 2, 0]"/>を作り、<form>に追加 - value属性にJavaScriptで生成した値を埋め込むには、JSON形式に変換する。変換しないで試したら、送信先(PHP側)で上手く配列として値が取り出せなかった
'use strict';
const photo = document.getElementById('photo');
const photoOrder = [];
const upload = document.getElementById('upload');// 新たに追加。<form>を取得
//........省略
photoOrder[dragIndex] = dropImgNumber;
photoOrder[dropIndex] = dragImgNumber;
});
}
});
});
// ↓ ここから
upload.addEventListener("submit", function(){
const json = JSON.stringify(photoOrder);// 配列をJSON形式に変換
const input = document.createElement('input');
input.type = "hidden";
input.name = "photo-order";
input.value = json;
upload.appendChild(input);
});
preview.js【完成】
'use strict';
const photo = document.getElementById('photo');
const photoOrder = [];
const upload = document.getElementById('upload');
photo.addEventListener("change", function(){
const numberOfPhoto = this.files.length;
const allPhotos = document.getElementById('all-photos');
allPhotos.classList.add('layout-' + numberOfPhoto);
const selectedPhotos = [];
const promise = new Promise((resolve) => {
for(let i = 0; i < numberOfPhoto; i++){
const fileReader = new FileReader();
fileReader.readAsDataURL(this.files[i]);
fileReader.addEventListener("load", function(){
const img = document.createElement('img');
img.src = this.result;
img.id = 'photo-' + i;
selectedPhotos.push(img);
photoOrder.push(i);
if(selectedPhotos.length == numberOfPhoto){
resolve(selectedPhotos);
}
});
}
});
promise.then((selectedPhotos) => {
for(let i = 0; i < selectedPhotos.length; i++){
const img = selectedPhotos[i];
const div = document.createElement('div');
div.id = 'div-' + i;
div.appendChild(img);
allPhotos.appendChild(div);
img.addEventListener('dragstart', function(event){
event.dataTransfer.setData("application/x-img-id", img.id);
});
img.addEventListener('dragenter', function(event){
event.preventDefault();
});
img.addEventListener('dragover', function(event){
event.preventDefault();
});
img.addEventListener('drop', function(event){
const dragImgId = event.dataTransfer.getData("application/x-img-id");
const dragImg = document.getElementById(dragImgId);
const dragImgNumber = parseInt(dragImg.id.slice(6), 10);
const dropImgNumber = parseInt(img.id.slice(6), 10);
const dragIndex = photoOrder.indexOf(dragImgNumber);
const dropIndex = photoOrder.indexOf(dropImgNumber);
const dragImgDiv = document.getElementById('div-' + dragIndex);
const dropImgDiv = document.getElementById('div-' + dropIndex);
dragImgDiv.appendChild(img);
dropImgDiv.appendChild(dragImg);
photoOrder[dragIndex] = dropImgNumber;
photoOrder[dropIndex] = dragImgNumber;
});
}
});
});
upload.addEventListener("submit", function(){
const json = JSON.stringify(photoOrder);
const input = document.createElement('input');
input.type = "hidden";
input.name = "photo-order";
input.value = json;
upload.appendChild(input);
});
これで、画像ファイルと位置情報photoOrderを送信することができます。
次回は、送信先のファイルで配列photoOrderを使い、プレビューと同じ並びで画像を表示します。
続きはこちらに書きました。
【コラム】DataTransfer / DataTransferItemList / DataTransferItemの関係
2つの方法が共存している
- 「新しい方法」: DataTransferItemList / DataTransferItem
- 「古い方法」 : DataTransfer
HTML ドラッグ&ドロップ API は複数の改訂を経て、データストアを管理する 2 つの方法が共存する結果となりました。DataTransferItemList および DataTransferItem インターフェイスが導入される前の「旧方式」では、DataTransfer 上で以下のプロパティが使用されていました。(省略)
(出典 : ドラッグデータストアでの作業 - ドラッグデータストアの構造)
それぞれどこにある?
DataTransferオブジェクトが、DataTransferItemListオブジェクトを持っています(DataTransfer.itemsプロパティ)。
dropイベントで確認していきます。
DataTransfer
全てのドラッグイベントはdataTransferプロパティを持ちます。これはDataTransferオブジェクトです。
img.addEventListener('drop', function(event){
console.log(event.dataTransfer); // DataTransferオブジェクト
});
DataTransfer {... items: DataTransferItemList, types: Array, files: FileList}
itemsプロパティは、DataTransferItemListオブジェクトです。
全てのドラッグデータのリストである DataTransferItemList オブジェクトを返します。
DataTransferItemList / DataTransferItem
DataTransferオブジェクトが、DataTransferItemListオブジェクトを持っていることが分かりました(DataTransfer.itemsプロパティ、つまりはevent.dataTransfer.itemsプロパティ)。
DataTransferItemListオブジェクトは、「DataTransferItemオブジェクトのリスト」。
「全てのドラッグデータのリスト」。
img.addEventListener('drop', function(event){
for (const item of event.dataTransfer.items) { // DataTransferItemListオブジェクト
console.log(item); // DataTransferItemオブジェクト
}
});
DataTransferItem {kind: 'string', type: 'text/plain'}
DataTransferItem {kind: 'string', type: 'text/uri-list'}
DataTransferItem {kind: 'string', type: 'text/html'}
DataTransferItem {kind: 'file', type: 'image/jpeg'}
ドラッグデータは、DataTransferItemオブジェクトと言えます。
このような引用もあります。
JavaScript オブジェクトを任意のウェブページへ直接転送することはできませんし、外部アプリケーションへの転送はなおさら不可能です。そのため、ウェブページとデータのやり取りを行うには、データを文字列に(または File として)シリアライズする必要があります。ドラッグ&ドロップでは、この文字列は DataTransferItem オブジェクトにカプセル化されます。 このオブジェクトは特定の type(通常は text/html などの MIME タイプ)も定義しており、文字列の解釈方法を規定します。
(出典 : HTML ドラッグ&ドロップ API - ドラッグデータストア)
データの型と値を持つ。まさにドラッグデータです。この引用から、ドラッグデータの値は、文字列かファイルであることも分かります。kindプロパティの説明にもこうあります。
DataTransferItem.kind 読取専用
ドラッグデータの項目の種類で、string(文字列)か file(ファイル)のどちらかです。
実装では 「DataTransferオブジェクト = ドラッグデータを持つ」 という認識で進めます。具体的には、ドラッグデータのリスト(DataTransferItemListオブジェクト)を持っているのです(DataTransfer.itemsプロパティ)。