ネイティブ HTML5 ドラッグ&ドロップ

HTML5 Rocks

はじめに

Google は、アニメーション、丸い角、ドラッグ&ドロップなどの複雑な UI 要素を簡素化するために、長年にわたって JQuery や Dojo などのライブラリを使用してきました。高度で没入感をもたらすエクスペリエンスをウェブで実現するには、外観の魅力が重要なことに疑いの余地はありません。しかし、すべてのデベロッパーが使用する一般的なタスクにライブラリが必要なのはなぜでしょうか。

ドラッグ&ドロップ(DnD)は、HTML5 の世界の優等生です。HTML5 の仕様では、ほぼすべての種類の要素をページ上でドラッグ可能にすることを宣言するための、イベントベースのメカニズム、JavaScript API、および追加的なマークアップが定義されています。ブラウザが特定の機能をネイティブ サポートすることに反対だという方はおそらくいないでしょう。DnD がブラウザでネイティブ サポートされていれば、ウェブ アプリケーションはより高速で応答性が高くなります。

機能の検出

DnD を利用する多くのアプリケーションは、DnD を使用しないと操作性がよくありません。たとえば、チェス ゲームの駒が動かない状況を想像してください。これは問題です。ブラウザのサポートはほぼ完全ではあるものの、操作性に影響がある場合に対策を提供できるように、ブラウザが DnD(または操作性に影響する特定の HTML5 機能)を実装しているかどうかを確認することが重要です。DnD を使用できない場合は、アプリケーションの機能を維持するために、ライブラリからそれに対応するフォールバックを起動します。

API を利用する必要がある場合は、ブラウザのユーザーエージェントを探すのではなく、常に機能の検出を使用します。優れた機能検出ライブラリの 1 つに、Modernizr があります。Modernizr では、テスト対象の機能ごとにブール値のプロパティが設定されます。そのため、DnD の確認も容易です。

if (Modernizr.draganddrop) {
  // Browser supports HTML5 DnD.
} else {
  // Fallback to a library solution.
}

ドラッグ可能なコンテンツを作成する

オブジェクトをドラッグ可能にする処理はシンプルです。移動可能にする要素に draggable=true 属性を設定します。画像、リンク、ファイル、他の DOM ノードなど、ほぼすべての要素をドラッグ可能にすることができます。

たとえば、配列を変更できる列を作成してみましょう。基本的なマークアップでは次のようになります。

<div id="columns">
  <div class="column" draggable="true"><header>A</header></div>
  <div class="column" draggable="true"><header>B</header></div>
  <div class="column" draggable="true"><header>C</header></div>
</div>

ほとんどのブラウザでは、選択中のテキスト、img 要素、href 属性を使用するアンカー要素は、デフォルトでドラッグ可能であることに注意してください。たとえば、google.com のロゴをドラッグすると、ゴースト画像が生成されます。

ブラウザでの画像のドラッグ
ほとんどのブラウザは、デフォルトで画像のドラッグをサポートしています。

このゴースト画像はアドレスバー、<input type="file" /> 要素、さらにはデスクトップにもドロップできます。他の種類のコンテンツをドラッグ可能にするには、HTML5 DnD API を利用する必要があります。

CSS3 でちょっとしたテクニックを使用することで、マークアップを整理して列のように見せることができます。cursor: move を追加すると、対象が移動可能であることをユーザーに示す視覚的なインジケータが表示されます。

<style>
/* Prevent the text contents of draggable elements from being selectable. */
[draggable] {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  /* Required to make elements draggable in old WebKit */
  -khtml-user-drag: element;
  -webkit-user-drag: element;
}
.column {
  height: 150px;
  width: 150px;
  float: left;
  border: 2px solid #666666;
  background-color: #ccc;
  margin-right: 5px;
  -webkit-border-radius: 10px;
  -ms-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 3px #000;
  -ms-box-shadow: inset 0 0 3px #000;
  box-shadow: inset 0 0 3px #000;
  text-align: center;
  cursor: move;
}
.column header {
  color: #fff;
  text-shadow: #000 0 1px;
  box-shadow: 5px;
  padding: 5px;
  background: -moz-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -webkit-gradient(linear, left top, right top,
                               color-stop(0, rgb(0,0,0)),
                               color-stop(0.50, rgb(79,79,79)),
                               color-stop(1, rgb(21,21,21)));
  background: -webkit-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -ms-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  border-bottom: 1px solid #ddd;
  -webkit-border-top-left-radius: 10px;
  -moz-border-radius-topleft: 10px;
  -ms-border-radius-topleft: 10px;
  border-top-left-radius: 10px;
  -webkit-border-top-right-radius: 10px;
  -ms-border-top-right-radius: 10px;
  -moz-border-radius-topright: 10px;
  border-top-right-radius: 10px;
}
</style>

結果(ドラッグ可能ですが何も実行されません):

A
B
C

上の例では、ほとんどのブラウザでドラッグ対象のコンテンツのゴースト画像が作成されます。ブラウザによっては(特に FF)、ドラッグ操作中にある種のデータを送信する必要があります。次のセクションでは、ドラッグ/ドロップ イベント モデルを処理するリスナーを追加して、列の例をさらに高度なものにします。

ドラッグ イベントをリッスンする

ドラッグ&ドロップ プロセス全体を監視する、以下のようなさまざまなイベントをアタッチできます。

  • dragstart
  • drag
  • dragenter
  • dragleave
  • dragover
  • drop
  • dragend

DnD フローを処理するには、ソース要素(ドラッグが始まる場所)、データ ペイロード(ドロップするもの)、ターゲット(ドロップを受け取る領域)の概念が必要です。ソース要素は、画像、リスト、リンク、ファイル オブジェクト、HTML のブロックなどさまざまです。ターゲットは、ユーザーがドロップするデータを受け取る 1 つまたは複数のドロップ ゾーンです。ターゲットにすることができない要素(画像など)がある点に注意してください。

1. ドラッグを開始する

コンテンツに draggable="true" 属性を定義してから、dragstart イベント ハンドラをアタッチして、各列の DnD シーケンスを開始します。

ユーザーがドラッグを開始したら、次のコードで列の不透明度を 40% に設定します。

function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
});

結果:

A
B
C

dragstart イベントのターゲットはソース要素なので、this.style.opacity を 40% に設定することにより、この要素が現在移動している選択内容であるという視覚的なフィードバックをユーザーに与えます。必要な処理のうち残っているものは、ドラッグの完了時に列の不透明度を 100% に戻すことだけです。この処理を行う場所は、dragend イベントです。この処理については、後で詳しく説明します。

2. dragenter、dragover、dragleave

dragenterdragoverdragleave イベント ハンドラを使用すると、ドラッグ プロセス中に視覚による追加的な合図を提供することができます。たとえば、ドラッグ中にカーソルが列に移動されるとボーダーを点線にするということが可能になります。これによって、列もドロップ ターゲットであることをユーザーに示すことができます。

<style>
.column.over {
  border: 2px dashed #000;
}
</style>
function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

function handleDragOver(e) {
  if (e.preventDefault) {
    e.preventDefault(); // Necessary. Allows us to drop.
  }

  e.dataTransfer.dropEffect = 'move';  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnter(e) {
  // this / e.target is the current hover target.
  this.classList.add('over');
}

function handleDragLeave(e) {
  this.classList.remove('over');  // this / e.target is previous target element.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false);
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
});

このコードには注目する点がいくつかあります。

  • DnD イベント モデル内での位置に応じて、各タイプのイベントの this/e.target は変わります。
  • リンクのようなものをドラッグする場合は、そのリンク先に移動するというブラウザのデフォルト動作を回避する必要があります。そのためには、dragover イベントで e.preventDefault() を呼び出します。もう 1 つの推奨方法として、同じハンドラで false を返すこともできます。このような処理が必要かどうかはブラウザによって異なりますが、追加しておいても問題は起きません。
  • dragenter は、「over」クラスを切り替えるために、dragover の代わりに使用されます。dragover を使用すると、列の上にカーソルが置かれている間、イベント dragover が継続的に発生するため、CSS クラスが何度も切り替えられます。その結果、最終的にブラウザのレンダラが大量の不要な作業を実行することになります。再描画を最小限に抑えるのは常によい考えです。

3. ドラッグを完了する

実際のドロップを処理するには、drop および dragend イベントのイベント リスナーを追加します。このハンドラでは、ドロップに関するブラウザのデフォルト動作を回避する必要があります。デフォルト動作では、一般的に何らかの不要なリダイレクトが行われます。drop イベントによって DOM を起動しないようにするには、e.stopPropagation() を呼び出します。

この列の例は、drop イベントを配置しなければほとんど機能がありません。ただし、drop イベントを配置する前の最初の改善として、dragend を使用することで各列から「over」クラスを削除します。

...

function handleDrop(e) {
  // this / e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // stops the browser from redirecting.
  }

  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnd(e) {
  // this/e.target is the source node.

  [].forEach.call(cols, function (col) {
    col.classList.remove('over');
  });
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false)
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
  col.addEventListener('drop', handleDrop, false);
  col.addEventListener('dragend', handleDragEnd, false);
});

結果:

A
B
C

これまでの手順をよく見ると、この例では、まだ希望どおりに列をドロップできないことに気付くかもしれません。DataTransfer オブジェクトを入力してみましょう。

DataTransfer オブジェクト

dataTransfer プロパティは、DnD のすべての動作の中核です。このプロパティには、ドラッグ操作で送信されたデータが保存されます。dataTransferdragstart イベントで設定され、drop イベントで読み取りと処理が行われます。e.dataTransfer.setData(format, data) を呼び出すと、オブジェクトのコンテンツが MIME タイプに設定され、データ ペイロードが引数として渡されます。

この例では、データ ペイロードはソース列の実際の HTML に設定されます。

var dragSrcEl = null;

function handleDragStart(e) {
  // Target (this) element is the source node.
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

dataTransfer にも、MIME タイプごとにドラッグ データをフェッチするための getData(format) があります。列のドロップを処理するために変更した結果は次のとおりです。

function handleDrop(e) {
  // this/e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // Stops some browsers from redirecting.
  }

  // Don't do anything if dropping the same column we're dragging.
  if (dragSrcEl != this) {
    // Set the source column's HTML to the HTML of the columnwe dropped on.
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

列の入れ替えを容易にするために、便宜的に dragSrcEl というグローバル変数を追加しました。handleDragStart() では、ソース列の innerHTML はこの変数に格納され、後から handleDrop() で読み取られて、ソース列とターゲット列の HTML の入れ替えが実行されます。

結果:

A
B
C

ドラッグのプロパティ

dataTransfer には、ドラッグ プロセス中に視覚的なフィードバックをユーザーに提供するためのプロパティがあります。これらのプロパティを使用して、各ドロップ ターゲットが特定のデータ型にどのように応答するかを制御することもできます。

dataTransfer.effectAllowed
要素に対してユーザーが実行できる「ドラッグの種類」を制限します。これは、ドラッグ&ドロップ プロセス モデルにおいて dragenter および dragover イベント中に dropEffect を初期化するために使用されます。このプロパティの値は、nonecopycopyLinkcopyMovelinklinkMovemovealluninitialized に設定できます。
dataTransfer.dropEffect
dragenter および dragover イベント中にユーザーに与えるフィードバックを制御します。ユーザーがターゲット要素にカーソルを移動すると、ブラウザのカーソルは実行する操作(コピー、移動など)の種類を示す形に変わります。この効果では、nonecopylinkmove のいずれかの値を処理できます。
e.dataTransfer.setDragImage(img element, x, y)
ブラウザのデフォルトの「ゴースト画像」フィードバックを使用する代わりに、ドラッグ アイコンを設定することもできます。
var dragIcon = document.createElement('img');
dragIcon.src = 'logo.png';
dragIcon.width = 100;
e.dataTransfer.setDragImage(dragIcon, -10, -10);

結果(これらの列をドラッグすると、Google ロゴが表示されます):

A
B
C

ファイルをドラッグする

DnD API を使用すると、デスクトップからブラウザ ウィンドウのウェブ アプリケーションにファイルをドラッグできます。このアイデアの延長線上にあるものとして、Google Chrome はブラウザのファイル オブジェクトをデスクトップにドラッグする機能をサポートしています。

ドラッグイン: デスクトップからブラウザにドラッグする

デスクトップからファイルをドラッグする処理は、他の種類のコンテンツとして DnD イベントを使用することで実現します。主な違いは drop ハンドラ内にあります。dataTransfer.getData() を使用してファイルにアクセスする代わりに、dataTransfer.files プロパティにそのデータを含めます。

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; f = files[i]; i++) {
    // Read the File objects in this FileList.
  }
}

デスクトップからブラウザにファイルをドラッグする詳細な手順については、Reading local files in JavaScriptUsing drag and drop for selecting を参照してください。

ドラッグアウト: ブラウザからデスクトップにドラッグする

ブラウザからデスクトップにファイルをドラッグする詳細な手順については、CSS Ninja の Drag out files like Gmail を参照してください。

機能を微調整して移動回数のカウンタを追加した最終的な成果を次に示します。

A
B
C
D

この列の例で興味深い点は、列がドラッグ ソースであり、ドロップ ターゲットでもあることです。より一般的なシナリオは、ソース要素とターゲット要素が異なる場合です。デモについては、html5demos.com/drag を参照してください。

まとめ

HTML5 の DnD モデルが JQuery UI のような他のソリューションと比べて複雑であることは確かです。それでも、ブラウザのネイティブ API を活用できるときは、ぜひ活用してください。結局のところ、それが HTML5 の本質です。HTML5 により、ブラウザのネイティブの高機能な API セットを標準化して使用できるようになります。DnD 機能を実装する一般的なライブラリにデフォルトで HTML5 のネイティブ サポートが含まれ、必要に応じてカスタム JS ソリューションにフォールバックするようになることが望まれます。

参考資料

Comments

0