ドラッグ操作
以下は、ドラッグ & ドロップ操作が行われる時の各段階についての解説です。
メモ:
この文書で記述されているドラッグ操作は DataTransfer インターフェイスを使用します。この文書では DataTransferItem インターフェイスや DataTransferItemList インターフェイスは説明しません。
draggable 属性
ウェブページにおいては、既定のドラッグ & ドロップの挙動が使われる場合がいくつかあります。文字列の選択範囲、画像、リンクなどのドラッグなどがこれにあたります。画像かリンクがドラッグされた時は、画像もしくはリンク先の URL がドラッグデータとして設定され、ドラッグ操作が始まります。その他の要素は、既定のドラッグ操作が行われるためには選択範囲に含まれていなければなりません。実際の様子を確認するには、ウェブページの一部を選択して、その上でマウスのボタンを押下し、そのまま選択範囲をドラッグしてください。ドラッグ中、選択範囲の内容を半透明で描画した物がマウスポインターに伴って表示されるでしょう。ただしこの挙動は、ドラッグされたデータを加工するイベントリスナーが存在しない場合の、既定のドラッグの挙動によるものです。
HTML では、画像、リンク、選択範囲の上での既定の動作を除くと、既定でドラッグ可能な他の要素はありません。
上記以外の他の HTML 要素をドラッグできるようにするには、以下の 3 つのことをしなくてはなりません。
- ドラッグできるようにしたい要素の
draggable属性の値を"true"に設定する。 dragstartイベントにリスナーを設定し、そのリスナーの中でドラッグデータを設定する。- 上記で定義されたリスナーの中でドラッグデータを設定する。
以下は、コンテンツの一部がドラッグできるようにする例です。
<p draggable="true">このテキストはドラッグが<strong>できます</strong>。</p>
const draggableElement = document.querySelector('p[draggable="true"]');
draggableElement.addEventListener("dragstart", (event) =>
event.dataTransfer.setData("text/plain", "このテキストはドラッグができます"),
);
draggable 属性を "true" に設定すると、その要素はドラッグできるようになります。この属性が設定されていない、あるいは "false" に設定されている場合、その要素をドラッグする事はできず、代わりにテキストが選択されるでしょう。
draggable 属性は画像やリンクを含めてあらゆる要素に設定できます。ただし、画像とリンクについてだけは既定値が trueとなっていますので、実際にこれらの要素で使う場合は、要素をドラッグできないようにするために draggable 属性に false を設定するという場合がほとんどでしょう。
メモ: 要素がドラッグ可能になった場合、文字列やその要素に含まれている他の要素が、マウスによるクリックやドラッグなどの通常の操作では選択する事ができなくなることに注意してください。ユーザーが文字列を選択するには、通常の操作の代わりに、 Alt キーを押しながらマウスで選択するか、キーボードで操作を行う必要があります。
ドラッグ操作の開始
この例では、 dragstart イベントのリスナーを addEventListener() メソッドで追加します。
<p draggable="true">このテキストはドラッグが<strong>できます</strong>。</p>
const draggableElement = document.querySelector('p[draggable="true"]');
draggableElement.addEventListener("dragstart", (event) =>
event.dataTransfer.setData("text/plain", "このテキストはドラッグができます"),
);
ユーザーがドラッグを開始しようとした時、 dragstart イベントが発行されます。
この例では dragstart のリスナーは、ドラッグされる要素自身に追加されていますが、他の多くのイベントがそうであるようにドラッグイベントもバブリングしますので、より上位の祖先要素でイベントを監視することもできます。
dragstart イベントでは、以下で解説しているドラッグデータ、フィードバック画像、ドラッグの効果を設定することができます。ドラッグデータの指定は必須ですが、多くの状況では、フィードバック画像とドラッグの種類は既定のもので問題ありません。
ドラッグデータ
すべてのドラッグイベントは、ドラッグデータを保持するための dataTransfer と呼ばれるプロパティを持っています (dataTransfer は DataTransfer オブジェクトの一つです)。
ドラッグが行われた際には、何をドラッグするのかを識別するためのデータをドラッグに関連付ける必要があります。例えば、テキストボックス内で選択されたテキストがドラッグされた場合、ドラッグデータアイテムに関連付けられたデータはテキストそのものです。同様に、ウェブページ上のリンクがドラッグされた場合、ドラッグデータにはリンクの URL が含まれます。
ドラッグデータには、データの型 (または形式) とデータの値の 2 つの情報が含まれています。形式は型の文字列 (テキストデータの場合は text/plain など) で、値はテキストの文字列です。ドラッグの開始時に、型とデータを指定してデータを追加します。ドラッグ中、 dragenter および dragover イベントのイベントリスナーでは、ドラッグされるデータのデータ型を使って、ドロップが許可されているかどうかをチェックします。たとえば、リンクを受け付けるドロップターゲットでは、 text/uri-list というデータ型がチェックされます。ドロップイベントが発生すると、リスナーはドラッグされたデータを取得し、ドロップ位置に挿入します。
ドラッグデータの types プロパティは、 text/plain や image/jpeg のような MIME タイプの文字列のリストを返します。独自の型を作成することもできます。よく使用される型は、推奨されるドラッグ型の記事に記載されています。
一つのドラッグ操作で、複数の異なる形式のデータを提供できます。この仕組みにより、独自の形式や、その形式のデータを受け取れない要素向けのフォールバック用の形式など、データをより適切な形式で引き渡すことができます。通常、最後のフォールバック先として使われる形式は、 text/plain 型として表される普通のテキストデータです。このデータは元のテキストの単純な文字列となるでしょう。
データを dataTransfer に設定するには、 setData() メソッドを使います。このメソッドは、次の例のようにデータの型とデータの値の 2 つの引数を取ります。
event.dataTransfer.setData("text/plain", "ドラッグされたテキスト");
この例では、データの値は「ドラッグされたテキスト」で、形式は text/plain です。
データは複数の形式で提供できます。これを実現するには、異なる形式を指定して setData() メソッドを複数回呼び出します。最も具体的な形式から、具体的でない形式に向けて呼び出します。
const dt = event.dataTransfer;
dt.setData("application/x.bookmark", bookmarkString);
dt.setData("text/uri-list", "https://www.mozilla.org");
dt.setData("text/plain", "https://www.mozilla.org");
これは、 3 つの異なる型のデータを追加する例です。最初の型の application/x.bookmark は独自の型です。他のアプリケーションはこの型に対応していないでしょうが、同じウェブサイトやアプリケーションの中の領域同士でのドラッグでは、このような独自の形式を利用できます。
また、他の型でもデータを提供することで、このような独自形式に対応していない他のアプリケーション向けにも、代替の形式でドラッグできるようになります。 application/x.bookmark 型はそのアプリケーションの中ではより使いやすく詳細な情報を提供できますが、他の型で渡されるデータは、単純な 1 つの URL もしくは文字列となります。
なお、この例では text/uri-list と text/plain も同じデータを含んでいます。このようにすることが多いのですが、こうしなければならない訳ではありません。
同じ形式で 2 回データを登録すると、古いデータは新しいデータによって置き換えられますが、データの形式の登録の順番自体は古いデータを登録した時のままになります。
登録したデータは clearData() メソッドによって削除できます。このメソッドは、削除するデータの形式を引数として求めます。
event.dataTransfer.clearData("text/uri-list");
clearData() メソッドの引数によるデータ形式の指定は省略可能です。データの形式が指定されなかった時は、すべての型のデータが削除されます。ドラッグ開始時にデータが 1 つも登録されなかった場合、もしくは後の処理で全てのデータが削除された場合、ドラッグ操作は発生しません。
ドラッグのフィードバック画像の設定
ドラッグが行われた時、ドラッグ元 (dragstart イベントが発行された要素) を元にして OS によって画像が生成され (例えば Windows では半透明の画像になります)、ドラッグしている間マウスポインターと一緒に表示されます。この画像は自動的に生成されるため、あなたが用意する必要はありません。しかし、 setDragImage() によって、独自のドラッグ中のフィードバック画像を指定することができます。
event.dataTransfer.setDragImage(image, xOffset, yOffset);
3 つの引数が必要です。一つ目は、画像への参照です。この参照は、通常は <img> 要素ですが、 <canvas> やその他の要素でもよいでしょう。フィードバック画像は、画像が画面上でどのように見えるかを考慮して生成されますが、画像の場合は、元のサイズで描画されます。 setDragImage() メソッドの第 2、第 3 引数には、マウスポインターに対する相対的な画像の表示位置を指定します。
文書中に存在しないものをフィードバック画像として使うために、以下の例のようにして、画像や canvas を利用することもできます。
function dragWithCustomImage(event) {
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 50;
const ctx = canvas.getContext("2d");
ctx.lineWidth = 4;
ctx.moveTo(0, 0);
ctx.lineTo(50, 50);
ctx.moveTo(0, 50);
ctx.lineTo(50, 0);
ctx.stroke();
const dt = event.dataTransfer;
dt.setData("text/plain", "ドラッグされるデータ");
dt.setDragImage(canvas, 25, 25);
}
この例では、 canvas の大きさは 50×50 ピクセルで、オフセット値はそれぞれの半分の値 (25) となっており、画像はマウスポインターの中央に表示されます (マウスポインターが画像の中央に表示されます)。
ドラッグの効果
ドラッグを行う時の操作には、いくつかの種類があります。 copy (コピー) はドラッグされているデータが現在の場所からドロップ先の場所にコピーされることを示します。 move (移動) はドラッグされているデータがドロップ先に移動されることを示し、 link (リンク) はドラッグ元とドロップ先の場所との間に何らかの形での関連付けや繋がりが作られることを示します。
dragstart イベントのリスナーにおいて、 effectAllowed プロパティに値を設定することで、 ドラッグ元について上記の 3 つの操作のうちどれが許可されているのかを示すことができます。
event.dataTransfer.effectAllowed = "copy";
この例では、コピー (copy) のみが許可されています。
複数の種類の操作を組み合わせることもできます。
none-
どの操作も許可されていない(ドロップを禁止)。
copy-
コピーのみが許可されている。
move-
移動のみが許可されている。
link-
リンクのみが許可されている。
copyMove-
コピーまたは移動のみが許可されている。
copyLink-
コピーまたはリンクのみが許可されている。
linkMove-
リンクまたは移動のみが許可されている。
all-
コピー、移行、リンクの全ての操作が許可されている。
- 初期化されていない場合
-
既定値は
allです。
上に列挙されている値のいずれかと全く等しい値だけが利用可能であることに注意してください。 effectAllowed プロパティを copyMove に設定すると、コピーや移動の操作を許可しますが、ユーザーがリンク操作を行うことを防ぐことができます。 effectAllowed プロパティを変更しない場合、 'all' が指定された時と同様に、すべての操作が許可されます。ですので、特定の種類の操作を除外したい場合を除いて、プロパティの値を手動で設定する必要はありません。
ドラッグ操作の間、 dragenter または dragover イベントのリスナーは、操作が許可されているかどうかを確かめるために effectAllowed プロパティを参照できます。これらのイベントにおいて、関連するプロパティである dropEffect プロパティへ、実際に行われる操作の種類 1 つだけが指定されるべきです。 dropEffect プロパティの値として妥当なものは、none、copy、move、または link のみです。このプロパティへは、複数の操作を組み合わせた値は指定できません。
dragenter および dragover イベントにおいて、 dropEffect プロパティはユーザーが要求している操作に初期化されます。ユーザーは操作の種類を修飾キーを押すことにより変更することができます。実際に使用されるキーはプラットフォームごとに異なりますが、大抵の場合は Shift キーと Control キーが、コピー・移動・リンクの各操作の切り替えに使われるでしょう。マウスポインターはどの操作が望まれているのかを示すために、例えば copy ならカーソルの横に「+」記号が表示される、といった風に変化するでしょう。
dragenter または dragover イベントの間に dropEffect プロパティの値を変更すると、ユーザーが選択した操作の種類を上書きし、特定のドロップ操作を強制することができます。この時に指定できる操作の種類は、 effectAllowed プロパティの値として列挙されている操作に含まれていなくてはならないことに注意してください。それ以外の値を設定した場合は、許可されている操作の中から代わりの値が設定されます。
event.dataTransfer.dropEffect = "copy";
この例では、「コピー」が行なわれる効果です。
その場所へのドロップが禁止されていることを示すために、値として none を設定することもできます。
drop および dragend イベントの中では、 dropEffect プロパティをチェックすることで最終的に選択されている効果を特定できます。選択された効果が "move" であれば、 dragend イベントの中でドラッグ元から元のデータを削除するべきです。
ドロップ先の指定
dragenter および dragover イベントのリスナーは、ドラッグされている項目がどの場所にドロップされようとしているのかを正確に示す働きをすることが多いです。ウェブページやアプリケーションのほとんどの領域は、ドロップデータを受け取る場所としては不適切です。従って、これらのイベントに対する既定の動作はドロップを禁止する働きをします。
ドロップを許可したい場合は、 dragenter および dragover イベントの両方をキャンセルして、既定の処理を防ぐ必要があります。これを行うには、イベントの preventDefault() メソッドを呼び出してください。
const draggableElement = document.querySelector('p[draggable="true"]');
draggableElement.addEventListener("dragenter", (event) => {
event.preventDefault();
});
draggableElement.addEventListener("dragover", (event) => {
event.preventDefault();
});
preventDefault() メソッドを呼び出すと、 dragenter および dragover イベントのどちらにおいても、その場所がドロップ可能な場所であるということを示します。多くの場合は、例えばリンクがドラッグされている時だけなど、特定の状況でのみ preventDefault() メソッドを呼び出したいと思うでしょう。
これを実現するには、条件を確かめて、条件が満たされている時だけイベントをキャンセルするような関数を使って下さい。条件が満たされていない時はイベントをキャンセルしないでおけば、ユーザーがマウスのボタンを放してもその場所へのドロップは行われません。
ドロップを受け付けるか拒絶するかを決める最も一般的な方法は、データ転送の仕組みに含まれているドラッグデータの型を判別するものです。例えば、画像やリンク、もしくはその両方のみを受け付けるといった事ができます。これを実現するには、イベントの dataTransfer (プロパティ) の types プロパティを確認します。 types プロパティはドラッグが開始された時に登録されたタイプ文字列のリストで、最も適切なものから最も適切でないものの順で並んでいます。
function doDragOver(event) {
const isLink = event.dataTransfer.types.includes("text/uri-list");
if (isLink) {
event.preventDefault();
}
}
この例では、型のリストの中に text/uri-list 型があるかどうかを確認するために contains メソッドを使用しています。もし条件が真であれば、イベントはキャンセルされて、ドロップが許可されるでしょう。もしドラッグデータがリンクを含んでいなければ、イベントはキャンセルされず、その場所でのドロップも行われません。
実際に行われる処理の種類をより適切に示すために、 effectAllowed や dropEffect プロパティのいずれか、あるいはその両方に値を指定したいと思う事もあるでしょう。当然ですが、イベントをキャンセルするのを忘れると、これらのプロパティの値を変えても何も起こりません。
ドロップのフィードバック
その場所へのドロップが許可されていることをユーザーに示す方法はいくつかあります。マウスポインターは dropEffect プロパティの値に応じて適切なものに変化します。
実際の正確な表示のされ方はユーザーのプラットフォームに依存しますが、通常は例えば「コピー」に対しては「+」記号が表示され、また、ドロップが許可されていない時は「ここにはドロップできません」という意味のアイコンが表示されるでしょう。多くの場合において、このポインターによるフィードバックは十分に役立ちます。
より凝った視覚効果のために、例えばドロップが行われる位置に要素を挿入するなど、 dragenter イベントの間に他の操作をすることもできます。この例なら、挿入される要素は、挿入箇所を示すマーカーあるいはドラッグされている要素が新しい位置に挿入された時の状態のプレビューなどとして利用できるでしょう。このような効果は、例えば <img> 要素を生成して、 dragenter イベントの処理中にドキュメント中に単に挿入するだけで実現できます。
dragover イベントは、マウスポインターが現在指している要素において発行されます。挿入点のマーカーを dragover イベントの発行に応じて移動させたいと思うのは自然な欲求でしょう。そのような場合には、他のマウスイベントでマウスポインターの位置を取得するために使われるのと同じ要領で、イベントの clientX と clientY プロパティを利用できます。
最後に、ドラッグ中にマウスポインターが要素の上を離れる時、 dragleave イベントが発行されます。これは挿入点のマーカーやハイライト表示を消すのにちょうどいいタイミングです。このイベントをキャンセルする必要はありません。 dragleave イベントは、ドラッグがキャンセルされた時でも常に発行されますので、このイベントによって、挿入点の消去などを確実に行うことができます。
ドロップの実行
ユーザーがマウスのボタンを離した時、ドラッグ & ドロップの操作は終了します。
有効なドロップ対象となっている要素の上でマウスのボタンが離された場合、最後の dragenter と dragover イベントはキャンセルされて、ドロップが成功し、 drop イベントがそのドロップ対象において発行されます。それ以外の場所でボタンが放された場合は、ドラッグ操作はキャンセルされ、 drop イベントは発行されません。
drop イベントの間、あなたはドロップされたデータをイベントから取得して、ドロップ位置に挿入することになります。どのドラッグ & ドロップ操作が望まれていたのかは、 dropEffect プロパティで判別することができます。
すべてのドラッグ & ドロップ関連のイベントにおいて、イベントの dataTransfer プロパティはドラッグされた対象に関するデータを保持しています。データの取得には getData() メソッドを利用することになるでしょう。
function onDrop(event) {
const data = event.dataTransfer.getData("text/plain");
event.target.textContent = data;
event.preventDefault();
}
getData() メソッドは、取得したいデータの型を引数として取ります。実行すると、ドラッグ操作の開始時に setData() メソッドによって登録された値が文字列として返されます。その型に対するデータが存在しない場合は、空文字が返されます。当然ながら、直前の dragover イベントでの処理においてチェックした時と同様に、あなたはデータの正しい形式が利用可能かどうかを知りたいと思うでしょう。
上記の例では、まずデータを取得し、ドロップ対象の内容テキストとしてそれを挿入しています。これは p 要素や div 要素がドロップ対象の領域として使われる事を想定しており、ドラッグされたテキストをドロップ位置に挿入するという効果をもたらします。
ウェブページにおいては、ドロップを受け付けた場合、イベントの preventDefault() メソッドを呼び出すべきです。これによって、ブラウザー内でのドロップ時の既定の挙動がキャンセルされます。例えば、リンクがウェブページにドロップされた場合、 Firefox はそのリンク先を読み込もうとします。イベントをキャンセルすることで、この動作は抑止されます。
他の形式でデータを取得することもできます。データがリンクであった場合、そのデータは text/uri-list 型でも提供されているでしょう。その場合、リンクを内容に挿入することができます。
function doDrop(event) {
const lines = event.dataTransfer.getData("text/uri-list").split("\n");
lines
.filter((line) => !line.startsWith("#"))
.forEach((line) => {
const link = document.createElement("a");
link.href = line;
link.textContent = line;
event.target.appendChild(link);
});
event.preventDefault();
}
この例は、ドラッグされたデータからリンクを挿入します。名前から想像できる通り、 text/uri-list 型は実際に複数の URL の改行区切りのリストを含んでいる場合があります。このコードでは、 split を使って文字列を行ごとに分割し、各行に繰り返し処理を行って、それぞれをリンクとして文書中に挿入しています。ナンバー記号 (#) で始まるものはコメントとして除外していることに注意してください。
単純な使い方として、リストの中の最初の有効な URL を取得するために、特別な型 URL も利用できます。
const link = event.dataTransfer.getData("URL");
これによって、コメントの除外などの処理は一切不要になります。しかし、これはリストの中の最初の URL だけしか取得できないという制限があります。
URL 型は特別な省略表記用の型で、 types プロパティで取得できる型のリストには列挙されません。
時には、複数の形式をサポートして、そのうち最も適切な形式で提供されたデータを取得したいと思う事もあるでしょう。以下の例では、3 つの形式がドロップ対象によってサポートされています。
以下の例は、提供されたデータの中で最も適切なデータを返す例です。
function doDrop(event) {
const supportedTypes = [
"application/x-moz-file",
"text/uri-list",
"text/plain",
];
const types = event.dataTransfer.types.filter((type) =>
supportedTypes.includes(type),
);
if (types.length) {
const data = event.dataTransfer.getData(types[0]);
// Use this type of data…
}
event.preventDefault();
}
ドラッグの終了
ドラッグ操作が終了すると、 dragend イベントがドラッグ元 (dragstart イベントが発行されるのと同じ要素) において発行されます。このイベントは、ドラッグ操作が成功したかキャンセルされたかに関わらず発行されます。どの操作が行われたのかは、 dropEffect プロパティを参照して知ることができます。
dragend イベントにおいて dropEffect プロパティの値がnoneである場合、ドラッグ操作がキャンセルされたことを意味します。それ以外の場合は、プロパティの値は実際に行われた操作の種類を示します。ドラッグ元はこの情報に基づいて、ドラッグされた項目を「移動」の操作の後に元の場所から削除することができます。 mozUserCancelled プロパティの値は、ユーザーが(Escape キーを押すなどして)ドラッグ操作をキャンセルした場合は true となり、不正なドロップ先だった場合などの他の理由でドラッグ操作がキャンセルされた場合や、ドロップに成功した場合は false となります。
ドロップ操作は同じウィンドウの中または他のアプリケーションの上で行われ得ます。いずれの場合も常に dragend イベントは発行されます。このイベントの screenX および screenY プロパティの値には、ドロップが行われたときの画面上での座標が設定されます。
dragend イベントの伝搬が終了した後、ドラッグ & ドロップの操作は完了します。