m2

これからドラッグ&ドロップを書く人のために


2012/07/22 追記
久しぶりに確認したところ、IE9 では以下で指摘している IE (このときは IE6 でした) の問題(3, 4, 6)がすべて解消されていました。
また、Firefox での問題(5)も Firefox14 で試したらは解消されていました(ただ、All-in-One Sidebar のパネルの上では mousemove イベントが発生しないようでした)。

Chrome20 や Safari5 でも問題無く動作していますから、現在においてドラッグ&ドロップを実装するには mousedown 時の preventDefault() だけでOKと言えそうです。すばらしいですね!

safari で動かないらしいのでどなたか情報ください!(くやしい!) というかこういうのに勝ち負けはないので実装できてる方は是非トラックバックお願いします。当方既に ipod 中毒ですし mac mini は core2 になったら買いますから心配無用です!!(?) 後述の DomEventSupport を外すとうまくいくかどうかも報告していただけると助かります。
  • -

JavaScript によるドラッグ&ドロップといえば何らかのライブラリを利用するのが今の主流だと思います。が、今回は bookmarklet で使いたかったのでそういう訳にもいかず、勉強を兼ねて他のライブラリを参考にしながら書いてみることにしました。
簡単に実装できると高を括っていたのですが、意外にもブラウザ間の違いが大きく影響し、大変苦労しました。
その際に問題になった点と解決方法の一つをメモしておきます。


目次

  1. 基本的な実装
  2. ドラッグ時の文字列選択を抑止する
  3. IEでドラッグ時の文字列選択を抑止する
  4. IEでウィンドウの外にドラッグした時のマウスイベントを取得する
  5. Firefoxでウィンドウの外にドラッグした時のマウスイベントを取得する
  6. IEで文字列選択中はドラッグ&ドロップさせない(2007/03/07 追記)

手元のブラウザ(Opera 9.10, Firefox 2.0.0.2, IE6)で動作確認しています。IE7 は未確認です。

<参考>

ドラッグ&ドロップが簡単に出来るJavaScriptライブラリまとめ:phpspot開発日誌
http://phpspot.org/blog/archives/2006/09/javascript_27.html
の全部。

基本的な実装

ドラッグ&ドロップを実装する際、イベント登録は以下の3つで事足ります。

イベント登録コード*1 処理内容
element.addEventListener('mousedown', dragStart, false) ドラッグを開始する。
document.addEventListener('mousemove', dragging, false) ドラッグ対象を動かす。ドラッグが開始されていないなら無視。
document.addEventListener('mouseup', dragEnd, false) ドラッグを終了(ドロップ)する。

element はドラッグ&ドロップする対象です。この要素の上でマウスボタンを押すとドラッグを開始します。
mousemove イベントと mouseup イベントは document オブジェクトに登録します。これは、例えば element そのものにイベントを登録すると、マウスを素早く動かして element からはみ出した場合、イベントを拾いきれなくなってしまうためです。
細かい話は省略して、とにかく動くように実装したのが以下になります。

確かにドラッグできますが、ドラッグ中に文字列選択になってしまってうまくありません。

ドラッグ時の文字列選択を抑止する

ドラッグ時の文字列選択は、文字列選択の開始、つまり mousedown イベントでのブラウザのデフォルトの挙動を抑止することで起きなくなります。

    var super_dragStart = DnDSupport2.prototype.dragStart;
    DnDSupport2.prototype.dragStart = function(e) {
        super_dragStart.apply(this,arguments); // 元々の処理
        if( this.isDragging ) {
            e.preventDefault(); // デフォルトの挙動を抑止
        }
    }

(拙作 DomEventSupport を利用しています。上記の e.preventDefault();IE では e.returnValue = false; を実行します。)
これを実装したのが以下になります。

2. mousedown イベントで preventDefault する
http://miya2000.github.com/dnd01/ddtest.html#downprevent

OperaFirefox は問題なさそうですが、IE では依然として文字列選択になってしまいます。

IEでドラッグ時の文字列選択を抑止する

IE では mousedown イベントに加えて mousemove イベントでもデフォルトの挙動を抑止することで、文字列選択を抑止することができました。

    var super_dragging = DnDSupport3.prototype.dragging;
    DnDSupport3.prototype.dragging = function(e) {
        super_dragging.apply(this,arguments);
        if( this.isDragging ) {
            e.preventDefault();
        }
    }

これを実装したのが以下になります。

3. IEの場合は mousemove イベントでも preventDefault する
http://miya2000.github.com/dnd01/ddtest.html#moveprevent

こんどはウィンドウの外にドラッグするとイベントが止まるようになってしまいました。

IEでウィンドウの外にドラッグした時のマウスイベントを取得する

もともとウィンドウの外のマウスイベントは取得できないのですが、ウィンドウ内でマウスボタンを押したままウィンドウの外にマウスを移動したときだけ特別にマウスイベントを取得できるようです。が、上のように mousemove イベントで preventDefault するとウィンドウの外ではイベントを取得できなくなります。これではウィンドウの外でマウスボタンを離した時に変なことになっちゃいます。
これは上のまとめのひとつの Toolman DHTML で、

FIXME: IE & Firefox, while dragging, if you drag outside the browser window and then release the mouse button, the mouseup event is lost. Chaos ensues.

と書かれていたので出来ないのかと思っていたのですが、Walterzorn Drag&Drop では普通にウィンドウの外でもドラッグできていたので、どうも画像のドラッグならいけそうだと推測して、実際にうまく動作させることができました。
具体的には img 要素をドラッグする対象に被せることで対応しています。

    DnDSupport4.prototype.createCover = function(){
        var t = document.createElement('img');
        with(t.style){
            zIndex = '10000';
            position = 'absolute';
            cursor = 'move';
            /* 実際は opacity=0 にする */
            backgroundColor = 'red';
            filter = 'alpha(opacity=30)';
        }
        return t;
    };
    DnDSupport4.prototype.initEvent = function() {
        // 被せる要素を作成
        var cover = this.createCover();
        document.body.appendChild(cover);
        
        var self = this;
        var element = this.element;
        // ドラッグ対象要素の上にマウスが乗った時に被せる
        element.addEventListener('mouseover',function(e){
            var loc = DnDSupport.getLocation(element);
            cover.style.left   = loc.x + 'px';
            cover.style.top    = loc.y + 'px';
            cover.style.width  = element.offsetWidth + 'px';
            cover.style.height = element.offsetHeight + 'px';
        },false);
        // ドラッグ対象の代わりにドラッグ開始イベントを登録
        cover.addEventListener ('mousedown', function(e){ self.dragStart(e) }, false);
        document.addEventListener('mousemove', function(e){ self.dragging (e) }, false);
        document.addEventListener('mouseup',   function(e){ self.dragEnd  (e) }, false);
    };

これを実装したのが以下になります。

これで IE は問題なさそうです。

Firefoxでウィンドウの外にドラッグした時のマウスイベントを取得する

今回作成したデモでは気付き難いのですが(自分はかなり後になって気付いた)、Firefox ではテキストの上で mousedown してドラッグを開始すると、上の IE のようにウィンドウの外にドラッグした時点でイベントが止まってしまいます(今回作成したシンプル実装は除く)。
画像や空のブロック要素でも同様のようです。
この場合は直下がテキストでなければよいのですから、上の IE での対策と同じように要素(今回はdiv)を被せることにしました。

    DnDSupport5.prototype.createCover = function(){
        var t = document.createElement('div');
        with(t.style){
            zIndex = '10000';
            position = 'absolute';
            cursor = 'move';
            display = 'block';
            overflow = 'hidden';
            /* 実際は backgroundColor='transparent' にする */
            backgroundColor = 'red';
            opacity = '0.3';
        }
        // 空 div だといかんらしい。
        t.innerHTML = '<div><\/div>';
        return t;
    };

これを実装したのが以下になります。

これでようやく満足のいく動作となりました。
不具合や変なところがあれば教えていただけると助かります。

IEで文字列選択中はドラッグ&ドロップさせない(2007/03/07 追記)

再現させにくいのであまり問題にはならないと思いますが、IEでドラッグ対象の外から内に選択状態にして、その状態でドラッグを始めると、上と同様にウィンドウの外にドラッグした時点でイベントが止まってしまう現象が起きました。
細かい制御は諦めて、文字列選択中はドラッグ&ドロップをさせないようにしました。

    var super_dragStart = DnDSupport6.prototype.dragStart;
    DnDSupport6.prototype.dragStart = function(){
        if ( document.selection.createRange().text.length > 0 ) {
            this.isDragging = false;
            return;
        }
        super_dragStart.apply(this,arguments);
    };

これを実装したのが以下になります。

6. IEで文字列選択中はドラッグ&ドロップさせない
http://miya2000.github.com/dnd01/ddtest.html#disableselecting

*1:実際のコードとは異なります