事例紹介 - Chrome でのドラッグ&ドロップ ダウンロード

はじめに

ドラッグ&ドロップ(DnD)は HTML 5 の多くの優れた機能の 1 つで、Firefox 3.5、Safari、Chrome、IE でサポートされています。Google は最近、Google Chrome ユーザーがブラウザからデスクトップにファイルをドラッグ&ドロップできる新機能をリリースしました。これは非常に便利な機能ですが、Ryan Seddon がこの新機能のリバース エンジニアリングの発見に関する記事を投稿するまで、あまり知られていませんでした。

Box.net は、これらの新機能によりクラウド コンテンツ管理ソリューションを改善し、デベロッパー コミュニティに貢献できることを大変嬉しく思っています。このたび、DnD Download がサービスに統合されましたのでお知らせいたします。 Box ユーザーは、Chrome ブラウザからパソコンにファイルを直接ドラッグして、ファイルをダウンロードして保存できるようになりました。

この新機能の開発中に、何度も反復処理を行った方法をご紹介します。

Drag and Drop API のサポートを確認する

まず、お使いのブラウザが HTML5 のドラッグ&ドロップを完全にサポートしていることを確認します。これを簡単に行うには、Modernizr というライブラリを使用して特定の機能をチェックします。

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

繰り返し 1

まず、Seddon が Gmail で見つけ出した方法を試しました。ファイルのアンカーリンクに「data-downloadurl」という新しい属性を追加しました。このプロセスでは、HTML5 のカスタム データ属性を使用します。data-downloadurl には、ファイルの MIME タイプ、宛先ファイル名(ダウンロードするファイルの目的のファイル名)、ファイルのダウンロード URL を含める必要があります。そのため、HTML テンプレートに次のように追加します。

<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>

これにより、次のような出力が生成されます。

<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>

Seddon の記事に基づいて von Schorsch が作成した jQuery pluginに基づいて、ブラウザの機能検出を行う jQuery プラグインを追加しました。ハイライト表示されている行は、von Schorsch のバージョンに追加した行です。

(function($) {

$.fn.extend({
dragout: function() {
var files = this;
if (files.length > 0) {
    $(files).each(function() {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    if (this.addEventListener) {
        this.addEventListener("dragstart", function(e) {
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
            e.dataTransfer.setData("DownloadURL", url);
        }
        },false);
    }
    });
}
}
});

})(jQuery);

このようにしたのは、事前にブラウザを検出しないと、IE で HTML 要素に addEventListener() を実行すると JavaScript エラーが発生するためです。IE は独自の attachEvent() メソッドを使用します。e.dataTransfer は IE では未定義です(現時点では)。Firefox(Mozilla)では e.dataTransfer.constructor が DataTransfer を返しますが、WebKit ブラウザ(Chrome と Safari)では Clipboard コンストラクタを実装しています。Safari では e.dataTransfer.setData('DownloadURL','http://www.box.net') が false を返しますが、Chrome ではこのステートメントに対して true を返します。上記のすべてのテストを行うと、この機能は Chrome でのみ使用できるようになります。次のようにすればよいと主張されるかもしれません。

/chrome/.test( navigator.userAgent.toLowerCase() )

ただし、ブラウザ検出よりも機能検出をおすすめします。ただし、技術的には、この方法では DnD ダウンロードが機能するかどうかは検出されません。

反復 1 の問題

1)現在、フォルダ間でファイルを移動またはコピーするためにオンページのドラッグ&ドロップが有効になっているため、ドラッグ&ドロップによるダウンロードとオンページのドラッグ&ドロップを区別する方法が必要です。技術的には、これらの 2 つのアクションを組み合わせることはできません。ユーザーがファイルを Box.net アカウント内の別のフォルダに移動するか、デスクトップにドラッグするかを予測することはできません。この 2 つのアクションはまったく異なります。また、カーソルがブラウザ ウィンドウの外側にあるかどうかを簡単に検出する方法はありません。window.onmouseout(IE)と document.onmouseout(他のブラウザ)を使用して、mouseout イベントをドキュメントに関連付け、e.relatedTarget.nodeName == "HTML" かどうかを確認できます(e は、利用可能な mouseout イベントまたは window.event のいずれかです)。ただし、イベントのバブルリングにより、これは非常に困難です。特に Box.net などの複雑なウェブアプリでは、画像やレイヤの上にカーソルを合わせたときに、イベントがランダムにトリガーされることがあります。

2)ユーザーが何かを誤ってデスクトップにドラッグしないように、ユーザーが明示的に操作を行うようにします。Box フォルダの編集者が、ダウンロードしたユーザーのパソコンで望ましくない動作を行う実行可能ファイルをアップロードする可能性があります。ファイルがパソコンにダウンロードされる正確なタイミングをユーザーに知らせたい。

繰り返し 2

そのため、Ctrl+ドラッグ(Windows Ctrl キーを押しながらファイルをドラッグする)をテストすることにしました。この操作は、Windows デスクトップでファイルのコピーを行う場合と同じです。また、ファイルが誤ってダウンロードされないようにするには、ユーザーによる追加作業(追加の手順ではない)も必要です。

ドラッグ&ドロップによるダウンロードをページ上のドラッグ&ドロップと緊密に統合する必要があるため、イテレーション 1 の jQuery プラグインは廃止されました。興味をお持ちの方は、jQuery UI の Draggable プラグインの修正版を使用しています。ターゲット要素の mousedown イベント内に、次のコードを配置します。

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
    that[0].addEventListener("dragstart",function(e) {
        // e.dataTransfer in Firefox uses the DataTransfer constructor
        // instead of Clipboard
        // make sure it's Chrome and not Safari (both webkit-based).
        // setData on DownloadURL returns true on Chrome, and false on Safari
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL','http://www.box.net')) {
        var url = (this.dataset && this.dataset.downloadurl) ||
                    this.getAttribute("data-downloadurl");
        e.dataTransfer.setData("DownloadURL", url);
        }
    }, false);
    return;
}
}

Ctrl キーを有効にする以外にも、小さなトースター ツールチップも追加しました。これは、ユーザーが通常のページ内ドラッグを実行したときに表示されます。Ctrl キーを押しながらファイル アイコンをデスクトップにドラッグすると、ファイルをダウンロードできることをユーザーに伝えます。

反復 2 の問題

セキュリティ上の懸念から、Box.net では、静的ファイルに直接アクセスするための永続的な URL は公開されていません。これは Box.net に固有の問題ではありません。どのオンライン ストレージ サービスでも、ファイルが公開されているかどうか、目的のダウンロードが適切な権限を持つユーザーによってリクエストされているかどうかを確認するための追加のセキュリティ レイヤなしで、永続的な URL を公開することはできません。

アイテムの「ダウンロード URL」(例: https://www.box.net/box_download_file?file_id=f_60466690)にアクセスすると、「302 Found」ステータス コードが返され、ファイルの一時的な「実際の URL」であるランダムな URL(例: https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b)にリダイレクトされます。ただし、数分ごとに期限切れになるため、HTML 出力に配置することは現実的ではありません。数分前に生成された HTML 出力のリンクからファイルをダウンロードしようとすると、「404」が返される可能性があります。

DnD ダウンロードは、リソースを直接参照する実際の URL でのみ機能します。リダイレクトが関係する場合、現在のところチェーンに沿って追跡できるほどスマートではありません(セキュリティ上の理由から、チェーンに沿って追跡することはできません)。そのため、上記のリンク https://www.box.net/box_download_file?file_id=f_60466690 をブラウザの URL バーに入力するとファイルをダウンロードできますが、ドラッグ&ドロップでは機能しません。

「実際の URL」と「リダイレクト URL」の違いをより明確に示すために、以下のスクリーンショットをご覧ください。

302 リダイレクト URL
302 リダイレクト URL
実際の URL
実際の URL

繰り返し 3

Ajax を試してみましょう。

前回の反復処理でコードを少し変更して、次のようにしました。

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart", function(e) {
    // e.dataTransfer in Firefox uses the DataTransfer constructor
    // instead of Clipboard
    // make sure it's Chrome and not Safari (both webkit-based).
    // setData on DownloadURL returns true on Chrome, and false on Safari
    if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
        e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    $.ajax({
        complete: function(data) {
        e.dataTransfer.setData("DownloadURL", data.responseText);
        },
        type:'GET',
        url: url
    });
    }
}, false);
return;
}
}

なるほど。ドラッグ開始時に、サーバーに Ajax 呼び出しを即座に実行して、ファイルの最新のダウンロード URL を取得します。ただし、機能しません。

同期呼び出し(Sjax)にする必要があります。イベント リスナーが接続されたときに setData を行う必要があるようです。jQuery の API に基づいて、ハイライト表示された行は次のようになります。

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});

ネットワーク接続を外すまでは正常に動作していました。同期呼び出しを行うため、呼び出しが成功するまでブラウザがフリーズします。Ajax 呼び出しが失敗した場合(404 エラー、またはまったく応答しない場合)、ブラウザはクラッシュしたかのように、まったく解凍されません。

次のようにすると、安全性が大幅に高まります。

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
error: function(xhr) {
if (xhr.status == 404) {
    xhr.abort();
}
},
type: 'GET',
timeout: 3000,
url: url
});

この機能のデモとして、Box.net アカウントに静的ファイルをアップロードしてみてください。Ctrl キーを押しながら、ファイル アイコンをデスクトップにドラッグします。アカウントをお持ちでない場合は、30 秒もかからずに作成できます。

この機能を使用すると、創造力を発揮してさまざまなことを実現できます。画像を Windows プリンタ ダイアログにドラッグすると、画像がすぐに印刷されます。Box からスマートフォンのドライブに曲をコピーしたり、Box から IM クライアントにファイルをドラッグして友人に直接転送したりできます。生産性を高めるための無限の可能性を秘めています。

プリンタにファイルを送信する
ファイルをプリンタにドラッグする。
ファイルの IM クライアントへのドラッグ
ファイルを IM クライアントにドラッグしている。

考慮事項と今後の改善

同期呼び出しによってブラウザが一時的にロックされる可能性があるため、これはまだ理想的ではありません。HTML 5 の Web Worker も役に立ちません。Web Worker は非同期である必要があります。イベント リスナーが接続されたときに setData を行う必要があるようです。

実際には、パフォーマンスは十分に許容範囲内です。同期 Ajax(Sjax)呼び出しは、URL 文字列を取得するだけなので、非常に高速です。HTTP ヘッダーに大きなオーバーヘッドが発生しますが、WebSocket で対処できます。ただし、この種のテクノロジーがより広く使用されるようになるまで、WebSocket を使用して細かい更新をクライアントに送信する価値はありません。

また、今後 API に複数ファイルのダウンロード機能が追加されることを願っています。カスタム チェックボックスと組み合わせて、ユーザー インターフェースで複数のファイルを選択できると、非常に便利です。さらに、送信されたフォームの結果から生成されたテキスト ファイルなど、クライアントが生成したファイルをこの方法でダウンロードできると便利です。

  • 列のドラッグ&ドロップ
  • リストを並べ替える
  • 画像ギャラリーの作成
  • キャンバス画像のエクスポート

参照