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

はじめに

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

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

この新機能の開発中に何度も繰り返した経験を共有したいと思います。

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

まず、ご利用のブラウザが HTML5 のドラッグ&ドロップに完全に対応していることをご確認ください。 これを簡単に行うには、Modernizr というライブラリを使用して特定の機能を確認します。

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

繰り返し 1

セドンが 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>

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

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

2)誤って何かをデスクトップにドラッグするのを防ぐために、ユーザーが明示的に何かを行う必要があります。Box フォルダの編集者が、コンピュータ上で望ましくない行為を実行する実行ファイルをアップロードできる可能性があります。ファイルがデスクトップにダウンロードされるタイミングをユーザーが正確に把握できるようにする必要があります。

繰り返し 2

そこで、Ctrl キーを押しながらドラッグ(Windows の Ctrl キーを押しながらファイルをドラッグする)も試すことにしました。この操作は、Windows デスクトップでファイルを複製するための操作と同じです。 また、ファイルが誤ってダウンロードされるのを防ぐために、追加の作業が必要になります(追加の手順は必要ありません)。

反復処理 1 の jQuery プラグインは、DnD ダウンロードをページ上の DnD と緊密に統合する必要があるため、廃止されています。ご興味のある方は、jQuery UI の Draggable プラグインの修正版を使用しています。ターゲット要素のマウスダウン イベント内に、次のコードを挿入します。

// 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 では、ブラウザのロケーション バーにファイルをダウンロードできますが、DnD では機能しません。

「実際の 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 ウェブワーカーも役に立ちません。イベント リスナーがアタッチされたときに setData を行う必要があるようです。

実際には、パフォーマンスは許容範囲内です。同期 Ajax(Sjax)呼び出しでは、URL 文字列を取得するだけで、非常に高速になります。HTTP ヘッダーに大きなオーバーヘッドが伴い、WebSocket で対処できる可能性があります。ただし、この種のテクノロジーの利用が増えるまで、WebSocket を使用してすべての更新をクライアントに送信することには意味がありません。

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

  • 列 dnd
  • リストの並べ替え
  • 画像ギャラリーの作成
  • キャンバス画像のエクスポート

リファレンス