はじめに

私たちを取り巻くWeb技術は、もはや社会的なインフラとしてめまぐるしく進化しています。HTMLやCSSはもちろんのこと、JavaScriptやライブラリ、フレームワークなど、それぞれがニーズにキャッチアップする形で、機能強化を繰り返しています。その中でも、Web技術の中核に位置するにもかかわらず、意外と見過ごされがちなのがHTMLの進化です。本連載は、このHTMLと関連するJavaScript APIにフォーカスして、その新機能を手軽に試していただこうというものです。理解も利用もたやすいHTMLなので、ライトな気持ちで「こんなことができるようになったのか」を感じていただきます。

[NOTE]サンプルについて
本記事の配布サンプルは、以下のURLから入手できます。新機能を試していくので、ブラウザは最新である方がよいでしょう。本記事のサンプルは、執筆時点で最新のChromeで動作することを確認しています。
https://github.com/wateryinhare62/mynavi_html/

連載第12回の目的

この回では、ページ内コンテンツのドラッグ&ドロップや、ブラウザ外からのドラッグ&ドロップも可能にするDrag & Drop APIを試します。ページ内でコンテンツを移動させたり、File System APIとの連携でローカルファイルをブラウザにドラッグ&ドロップで表示させる例を紹介します。

Drag & Drop API

Drag & Drop APIは、ブラウザにおいてドラッグ&ドロップ操作をサポートするJavaScript APIです。Drag & Drop APIを使うことで、ブラウザ内におけるマウス移動やタップ操作によるコンテンツの移動やコピー、ブラウザ外部からのデータの受け入れ、ブラウザ外部へのデータの受け渡しなどが可能になります。ユーザーが直感的に要素を移動/配置できるので、たとえば以下のような活用が考えられるでしょう。

  • 写真管理アプリ、クラウドストレージ、CMSなどでフォルダやファイルをドロップして一括インポートする
  • ECサイトアプリで商品をカートにドラッグして追加する
  • テキストボックス、チェックボックス、日付入力欄などをドラッグしてフォームを作成する
  • アプリのダッシュボード上のウィジェットを自由に並べ替える
  • 画像編集/デザインツールで素材をキャンバスに配置する
  • メール/チャットアプリでメッセージにファイルを添付する

Drag and Drop APIは、ほとんどの機能がBaselineにおいてWidely Availableであり、プラットフォームやブラウザの種類を問わずに広く利用できます。

  • 図1:完成サンプル

    図1:完成サンプル

Drag and Drop APIの構成要素

Drag and Drop APIは、主にdraggable属性、ドラッグ&ドロップ関連イベント、DataTransferオブジェクトから構成されます。また、Drag and Dropというくらいなので、ドラッグとドロップが大まかな作業区分となります。それぞれの区分で、属性、イベント、オブジェクトを組み合わせて操作を実現するというのが基本です。ドラッグ対象とする要素(ドラッグオブジェクト)側と、ドロップ側の要素(ドロップターゲット)側の作業を大まかにまとめると表1のようになります。

表1:ドラッグオブジェクトとドラッグターゲットにおける作業区分

区分 属性/イベント 概要
ドラッグ draggable属性 要素がドラッグ可能であることをマークする
dragstartイベント ドラッグ開始時の処理を行う
dragstopイベント ドラッグ終了時の処理を行う
dragイベント 一定間隔でドラッグ中の処理を行う
ドロップ dragenterイベント ドラッグオブジェクトがドロップターゲットに入ったときの処理を行う
dragleaveイベント ドラッグオブジェクトがドロップターゲットから出たときの処理を行う
dragoverイベント 一定間隔でドラッグオブジェクトがドロップターゲットに入っているときの処理を行う
dropイベント ドラッグオブジェクトがドロップターゲットにドロップされたときの処理を行う

このように、ドラッグオブジェクトにdraggable属性を指定し、dragstart/dragstopなどのイベントでドラッグ時の挙動を制御できます。同様にドラッグターゲットにおいてはdragenter/dragleaveなどのイベントでドロップ時の挙動を制御できます。 また関連して、DataTransferオブジェクトのdropEffectプロパティとeffectAllowedプロパティを設定することで、ドラッグ中の視覚効果(コピー、移動、リンクなどの操作内容が分かるように、マウスポインタの形状を変更するなど)を制御することもできます。

ドラッグ&ドロップ処理の基本

まずは、ドラッグ&ドロップ処理の基本をまとめたサンプルを通じて、draggable属性、各種イベントの使い方を見ていきましょう。このサンプルでは、ドラッグオブジェクト、ドラッグターゲットを1個ずつ配置し、以下の処理を行っています。

  • ドラッグ中のドラッグオブジェクトのコンテンツとスタイルの変更
  • ドラッグオブジェクトが乗っているときの、ドロップターゲットのコンテンツとスタイルの変更
  • ドロップ時のドロップターゲットのコンテンツの変更

ドラッグを開始してドロップターゲットに乗せドロップすると、動きに応じてそれぞれの背景色やコンテンツが変化します(図2)。

  • 図2:ドラッグ&ドロップ処理の基本

    図2:ドラッグ&ドロップ処理の基本

(1)ドラッグオブジェクトのマーク

リスト1は、ドラッグオブジェクト、ドラッグターゲットとなるマークアップです。

リスト1:dandd_basic.html

(1)ドラッグオブジェクトのマーク
<div id="draggable" draggable="true">ドラッグしてください!</div>
<div class="dropzone">ドロップしてください!</div>

このようにドラッグ可能とする要素には、draggable属性を付与してドラッグ可能であることをマークします。draggable属性はtrueとfalseの2値をとり、既定値はドラッグできないことを表すfalseです。必ず2値のいずれかを指定し、「draggable」のように値なしの論理属性として指定することはできません。なお、多くのブラウザは既定でテキストと画像のドラッグを可能にしているので、この属性を指定するとなるとこの例のようにdiv要素などとなるでしょう。

[NOTE]CSSの内容
サンプルでは見た目を調整するためにCSSを適用していますが、本記事の内容と直接の関係はないので、CSSの内容については掲載を割愛しています。全容は配布サンプルを参照してください。

リスト2は、イベントハンドラを記述したJavaScriptコードです。

リスト2:dandd_basic.html

const draggable = document.getElementById('draggable');
const dropzone = document.querySelector('.dropzone');
const draggableMessage = draggable.textContent;
const dropzoneMessage = dropzone.textContent;
(2)ドラッグ関連イベントの処理(ドラッグオブジェクト)
draggable.addEventListener('dragstart', () => {
  (2)-1 ドラッグの開始でスタイルを変更する
  draggable.classList.add('dragging');
});
draggable.addEventListener('dragend', () => {
  (2)-2 ドラッグの終了でスタイルとコンテンツを復元する
  draggable.classList.remove('dragging');
  draggable.textContent = draggableMessage;
});
draggable.addEventListener('drag', (ev) => {
  (2)-3 ドラッグ中はコンテンツを座標に変更する
  draggable.textContent = `ドラッグ中...(${ev.clientX}, ${ev.clientY})`;
});
(3)ドロップ関連イベントの処理(ドロップターゲット)
dropzone.addEventListener('dragenter', () => {
  (3)-1 ドラッグオブジェクトが入った際にスタイルを変更する
  dropzone.classList.add('dropping');
  dropzone.textContent = 'ドロップ可能です!';
});
dropzone.addEventListener('dragleave', () => {
  (3)-2 ドラッグオブジェクトが出た際にスタイルとコンテンツを復元する
  dropzone.classList.remove('dropping');
  dropzone.textContent = dropzoneMessage;
});
dropzone.addEventListener('dragover', (ev) => {
  (3)-3 ドラッグオブジェクトが入っているときdropイベントを発生可能にする
  ev.preventDefault();
});
(4)ドロップされたときの処理(ドロップターゲット)
dropzone.addEventListener('drop', (ev) => {
  (4)-1 ドロップされたらスタイルを復元しコンテンツを変更する
  ev.preventDefault();
  dropzone.classList.remove('dropping');
  dropzone.textContent = 'ドロップされました!';
});

冒頭部は、ドラッグ終了でコンテンツを復元するための処理で、(2)と(3)はドラッグオブジェクト、ドロップターゲットで発生するイベントの処理です。それぞれ見てみます。

(2)ドラッグオブジェクトのスタイルとコンテンツを変更する

dragstartイベントハンドラでは、ドラッグオブジェクトを取得したり、そのスタイルやコンテンツを変更したりできます((2)-1)。ほとんどのブラウザではドラッグイメージ(マウスポインタや指の動きに追従して表示される半透明のイメージ)が表示されるのでスタイルの変更は必須ではありませんが、ユーザーに有効な視覚的フィードバックを提供する手段とできます。なお、ここでは適切なスタイルを設定したCSSクラスdraggingの追加によって、スタイルを変更しています。
dragendイベントハンドラでは、dragstartイベントで変更したスタイルやコンテンツを復元するときに利用できます((2)-2)。ここではCSSクラスdraggingの削除によって、スタイルを復元しています。
dragイベントハンドラでは、ドラッグ中であることを示したり、マウスポインタの位置などをユーザーにフィードバックできます((2)-3)。

(3)ドロップターゲットのコンテンツとスタイルを変更する

dragenterイベントハンドラでは、ドラッグオブジェクトが入った際にドロップターゲットのスタイルやコンテンツを変更したりすることで、dragstartイベント同様にユーザーに有効な視覚的フィードバックを提供できます((3)-1)。なお、ここでは適切なスタイルを設定したCSSクラスdroppingの追加によって、スタイルを変更しています。
dragleaveイベントハンドラは、dragenterイベントで変更したスタイルやコンテンツを復元するときに利用できます((3)-2)。ここではCSSクラスdroppingの削除によって、スタイルを復元しています。
dragoverイベントハンドラでは、preventDefaultメソッドの呼び出しが最低限必要です((3)-3)。これは、ドロップ時にdropイベントを発生させるために必要な処理なので、漏れなく設定する必要があります。

(4)ドロップ時のスタイルの復元とコンテンツの変更

dropイベントハンドラでは、スタイルを復元し、ドロップされたことを示すコンテンツに変更しています。ここでは単にドロップされたことを示すだけでしたが、実用的には後ほど紹介するようにコンテンツの移動/コピーが主な処理になるでしょう。

視覚効果(マウスポインタ形状)の変更

dragstart/dragenterなどのイベントにより、スタイルを変更するなどの直接的な視覚的フィードバックが可能ですが、dropEffect/effectAllowedプロパティを使ってドラッグ操作そのものについての視覚効果を示すこともできます。たとえば、ドラッグ操作がコピーなのか移動なのか、リンクになるのか禁止されているのかといった情報を、マウスポインタの形状変更などで示すことができます。

  • 図3:マウスポインタ形状の変更

    図3:マウスポインタ形状の変更

ドラッグ&ドロップ絡みのイベントハンドラにおいては、引数にDragEventオブジェクトが渡されます。このdataTransferプロパティ(DataTransferオブジェクト)においてdropEffect/effectAllowedプロパティを設定することで、ドラッグ中の視覚効果(マウスポインタの形状など)を指定できます。それぞれ以下の役割を持ちます。

  • dropEffect:ドラッグ操作の効果を表す文字列。"copy"、"move"、"link"、"none"のいずれかの値
  • effectAllowed:許可されているドラッグ操作を表す文字列。"none"、"copy"、"copyLink"、"copyMove"、"link"、"linkMove"、"move"、"all"、"initialized"のいずれかの値。既定値は"initialized"で"all"と同じ

リスト3は、視覚効果の種類を選択できるドロップダウンメニューを配置し、ドロップターゲットとしたdiv要素にdragenterイベントハンドラにおいてドラッグオブジェクトが入った際に、選択したエフェクトを設定する例です。

リスト3:effect.html

<select id="dropEffect">
  <option value="copy">copy</option>
  <option value="link">link</option>
  <option value="move">move</option>
  <option value="none">none</option>
</select>
<div id="draggable" draggable="true">ドラッグしてください!</div>
<div id="dropzone">ここにドロップしてください!</div>
<script>
  const dropEffect = document.getElementById('dropEffect');
  const draggable = document.getElementById('draggable');
  const dropzone = document.getElementById('dropzone');
  (1)ドラッグスタート時にeffectAllowedプロパティを設定する
  draggable.addEventListener('dragstart', (ev) => {
    ev.dataTransfer.effectAllowed = 'all';
  });
  (2)ドロップターゲットに入ったらdropEffectプロパティを設定する
  dropzone.addEventListener('dragover', (ev) => {
    ev.preventDefault();
    ev.dataTransfer.dropEffect = dropEffect.value;
  });
</script>

ポイントは、以下の2つです。

  • effectAllowedプロパティは、dragstartイベントハンドラ内部で設定します((1))。他のイベントハンドラ内で設定しても無効です。ここでは、すべての効果を意味する"all"としていますが、これは既定であるので設定を省いても問題ありません。
  • dropEffectプロパティは、dragoverイベントハンドラ内部で設定します((2))。設定しない場合には、effectAllowedプロパティで許可された視覚効果の一つが設定されます。ここでは、ドロップダウンメニューで選択したものを、明示的に設定しています。

なお、dropEffectプロパティをdragoverイベントハンドラ内部で設定するのは、キー操作(Ctrl、Shiftキーを併用するなど)によってドラッグ操作の変更に対応できるようにするためです。実際に操作してみると、選択したエフェクトに応じて、ドロップターゲットに入ったときのマウスポインタ形状が変化するのを確認できるでしょう(図3)。

ドラッグ&ドロップによる画像の移動

ここまで、Drag and Drop APIの基本機能を見てきましたが、そのまとめとしてページ上のコンテンツをドラッグ&ドロップで移動する例を紹介します。基本的にはリスト1をベースに、ドラッグオブジェクトとしての画像を追加、それをドロップターゲットに移動するコードを追加していきます。

  • 図4:ドラッグ&ドロップによる画像の移動
  • 図4:ドラッグ&ドロップによる画像の移動

    図4:ドラッグ&ドロップによる画像の移動

マークアップとコードはリスト4のとおりです。リスト1と共通する部分は割愛しています。

リスト4:drag_move.html

<div id="draggable">ドラッグしてください!<p>
  <img id="img01" src="img/cat_01.png" alt="猫の画像" height="50px">
  …途中の画像は省略…
  <img id="img09" src="img/cat_09.png" alt="猫の画像" height="50px">
</p></div>
<div class="dropzone">ここにドロップしてください!<br></div>
<script>
  let dragged = null;   (1)ドラッグオブジェクトを保持
  const draggable = document.getElementById('draggable');
  const dropzone = document.querySelector('.dropzone');
  draggable.addEventListener('dragstart', (ev) => {
    (2)ドラッグオブジェクトを待避し、エフェクトを移動のみ許可する
    dragged = ev.target;
    ev.dataTransfer.effectAllowed = 'move';
  });
  …略…
  (3(ドラッグターゲットに入ったら視覚効果を移動に変更する
  dropzone.addEventListener('dragover', (ev) => {
    ev.preventDefault();
    ev.dataTransfer.dropEffect = 'move';
  });
  dropzone.addEventListener('drop', (ev) => {
    ev.preventDefault();
    dropzone.classList.remove('dropping');
    (4)ドロップターゲットでドロップされたらドロップオブジェクトを移動する
    if (ev.target.className === "dropzone") {
      dragged.parentNode.removeChild(dragged);  // 元の場所から削除
      ev.target.appendChild(dragged);       // ドロップターゲットに追加
    }
  });
</script>

見ての通り、追加される内容は非常にシンプルです。

  • 画像(img要素)は既定でドラッグ可能なのでdraggable属性は不要
  • 9個ある画像のどれがドラッグされるか分からないので、ドラッグオブジェクトを保持する変数を設ける((1))
  • ドラッグが始まったら、ドラッグオブジェクトを変数に待避し、移動である旨のエフェクトに変更する((2))
  • ドロップターゲットに入ったら、視覚効果を移動に変更する(allowEffectが"move"であるので省略できる)
  • ドロップされたら、イベントがドロップターゲットで発生したことを確認し、待避しておいたドラッグオブジェクトを親ノード(parentNode)から削除(removeChild)し、ドロップターゲットに追加(appendChild)する

(4)において、dropイベントがドロップターゲットで発生したか確認する処理は、移動済みの画像でドロップされた際にそれを無視するためです。この処理がないと、親子関係を想定したコードが正しく動作しません。
好きな画像を選んでドロップターゲットに移動させることができることを確認できるでしょう(図4)。

ドラッグ&ドロップによるファイルの読み込み

もう一つ、ドラッグ&ドロップの応用として、第10回で紹介したファイルの読み込み操作をドラッグ&ドロップに対応させてみましょう。ドラッグ&ドロップからファイルの操作につなげるというのは定番でもあるからです。ファイル読み込みの手順そのものは第10回で紹介したshowOpenFilePickerメソッドを使ったときと大きく変わるものではないので、ここではドラッグ&ドロップに関連した部分に集中して手順を紹介します。

  • 図5:ドラッグ&ドロップによるファイルの読み込み

    図5:ドラッグ&ドロップによるファイルの読み込み

リスト5は、第10回のリスト2をベースに、ドロップターゲットとなるdiv要素を追加したHTMLです。

リスト5:drop_read.js

<div id="dropzone">
  ファイルをここにドラッグ&ドロップしてください
</div>

リスト6:drop_read.html

リスト6は、このdiv要素にファイルをドラッグ&ドロップした際のイベントハンドラを含むJavaScriptコードです。

const dropzone = document.getElementById('dropzone');
…略…
dropzone.addEventListener('drop', async (ev) => {
  …略…
  ev.preventDefault();
  dropzone.classList.remove('dropping');
  (1)ドロップされたアイテムを取得する
  const items = ev.dataTransfer.items;
  (2)すべてのアイテムを見ていく(ファイルを読み込んだら終了)
  for (const item of items) {
    (3)ファイルについてのみ処理する
    if (item.kind === 'file') {
      (4)ファイルオブジェクトとして取得し読み込む
      const file = item.getAsFile();
      document.getElementById('filename').textContent = file.name;
      const textContent = document.getElementById('textContent');
      const imageContent = document.getElementById('imageContent');
      (5)テキストか画像かで処理を分ける
      if (file.type.startsWith('text/')) {
        const contents = await file.text();
        textContent.value = contents;
        textContent.style.display = 'block';
        imageContent.style.display = 'none';
      } else if (file.type.startsWith('image/')) {
        const img = new Image();
        img.onload = () => {
          const targetCanvas = imageContent;
          const targetContext = targetCanvas.getContext('2d');
          targetCanvas.width = img.width;
          targetCanvas.height = img.height;
          targetContext.drawImage(img, 0, 0);
          textContent.style.display = 'none';
          imageContent.style.display = 'block';
        };
        img.src = URL.createObjectURL(file);
      }
      break;
    }
  }
});

dragenter/dragleave/dragoverイベントハンドラの内容は、前節のリストと同様で、中心は、dropイベントハンドラです。dropイベント用のイベントハンドラでは、ドロップ操作に対応する、以下の処理を記述します。

  • イベントオブジェクトのdataTransferプロパティから、ドロップされてきた項目itemsを取得する((1))
  • itemsはイテレータであるので、項目を順番に取り出す((3))
  • 項目の種類(kindプロパティ)がfile(ファイル)であるときのみ、読み出し操作を行う((4))
  • ファイルの形式に応じて読み込み方法を切り替え、textarea要素あるいはcanvas要素に設定する((5))。処理の詳細は第10回を参照

エクスプローラーから適当なテキストファイルか画像ファイルをドラッグ&ドロップすると、ファイルが読み込まれてtextarea要素あるいはcanvas要素に反映されることを確認できるでしょう(図5)。

まとめ

Drag and Drop APIはいかがでしたでしょうか。デスクトップ環境に限らず、ブラウザにおいても柔軟性のあるオブジェクト操作が可能になり、アプリケーションの幅を大きく広げる可能性があることをお伝えできたのではないかと思います。 次回も、HTMLとJavaScript APIの注目機能を紹介していきます。ご期待ください。

WINGSプロジェクト 山内直(著) 山田 祥寛(監修)
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
RSS
X:@WingsPro_info(公式)@WingsPro_info/wings(メンバーリスト)
Facebook

<著者について>
WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。