Hatena::Groupjavascript

JavaScriptで遊ぶよ

 | 

2009-05-10

クロスブラウザでイベントを簡単に扱う

09:59

コメントいただいた部分を修正して追記した。


何度再発明された車輪か知らないけど、こんなのが便利かも?

var EventGroup = function(target) {
    this.target = target || window;
    this.handlers = {};
    this.doPreventDefault = true;
    this.doStopPropagation = true;
    var self = this;
    this.delegate = function(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        if (!e.stopPropatation) e.stopPropagation = function() {this.cancelBubble = true};
        if (!e.preventDefault) e.preventDefault = function() {this.returnValue = false};
        if (this.doPreventDefault) e.preventDefault();
        if (this.doStopPropagation) e.stopPropagation();
        var handlers = self.handlers[e.type];
        if (handlers) {
            for (var i = 0; i < handlers.length; i++) handlers[i].call(self, e);
        }
    }
    return this;
}
EventGroup.prototype.listen = function(type, func) {
    var target = this.target;
    var handlers = this.handlers[type];
    if (handlers) {
        for (var i = 0; i < handlers.length; i++) if (handlers[i] === func) break;
        if (i == handlers.length) handlers.push(func);
    } else {
        this.handlers[type] = [func];
        if (target.addEventListener) {
            target.addEventListener(type, this.delegate, false);
        } else if (target.attachEvent) {
            target.attachEvent('on' + type, this.delegate);
        } else {
            target['on' + type] = this.delegate;
        }
    }
    return this;
};
EventGroup.prototype.unlisten = function(type, func) {
    var target = this.target;
    var handlers = this.handlers[type];
    if (handlers && func) {
        for (var i = 0; i < handlers.length; i++) {
            if (handlers[i] === func) {
                handlers.splice(i, 1);
                break;
            }
        }
        if (handlers.length) return this;
    }
    handlers = null;
    if (target.removeEventListener) {
        target.removeEventListener(type, this.delegate, false);
    } else if (target.detachEvent) {
        target.detachEvent('on' + type, this.delegate);
    } else {
        target['on' + type] = null;
    }
    return this;
};
EventGroup.prototype.destroy = function(type) {
    for (var type in this.handlers) if (this.handlers.hasOwnProperty(type)) {
        this.unlisten(type);
    }
};

こう使う。

var mouse = new EventGroup()
mouse.listen('mousemove',function(e){
	// 引数にはイベントが入る。
	// this は mouse インスタンス自身を差し、イベント間のコミュニケーションに使える。
	this.x = e.clientX;
	this.y = e.clientY;
}).listen('mousedown',function(e){
	alert(e.target); // ターゲットはこれで取得できる。
	alert([this.x,this.y]);  // イベント間コミュニケーションの例。
	mouse.destroy();
});

「デフォルトイベントを通す」とか「バブリングを止めない」とか細かいことをアレコレしたい場合はこうなる。

var keyboard = new EventGroup( document.getElementsById('myinput') );
keyboard.doPreventDefault = false;
keyboard.doStopPropagation = false;
keyboard.listen('keydown',function(e){
// 処理
}).listen('keyup',function(e){
// 処理
})

ただし、自分は IE を持ってないので本当にクロスブラウザかは知らない。


ちょっと言い訳

EventGroup ではなくて ListenerGroup なんだけど、EventGruop のほうがピンと来るかなーと思ってそうした。

最初に書いてたバージョンはこれ。

修正した部分は、コメントにもあったように、まず preventDefault と stopPropagation をメソッドの有無で判定するようにした。

それから、一つの要素に同じイベントタイプのリスナーを何個でも付けられるようになった。

function a(){};
function b(){};
var mouse = new EventGroup().listen('mousemove',a).listen('mousemove',b);

同じハンドラーを2度追加したときの順番は Firefox に合わせるようにした。

こういう場合、unlisten はこう書ける。

mouse.unlisten('mousemove',a); // 特定の関数だけリスナーから外す。
mouse.unlisten('mousemove');  // 全部の関数をリスナーから外す。

まあこういうのは結局「どこまで妥協できるか」になってくるわけで、例えば上のコードでは、addEventListener も attachEvent も実装されていないブラウザでは

var click1 = new EventGroup().listen('click',function(){ })
var click2 = new EventGroup().listen('click',function(){ })

のように書くと、1つ目の関数は上書きされてしまう。(その代わり上に挙げたように、同じイベントグループにまとめれば両方実行される)

同様に、relatedTarget なんかはクロスブラウザにしてないし、currentTarget に至ってはお手上げ状態。jQuery のソースも見てみたけど、大変なことしてるね。

ちゃんとブラウザ間で挙動を揃えようと思ったらあんなことになるわけで。

上に書いたやつは、あそこまでのライブラリを入れるのは勘弁という、ちょっとした所で使えるかなーというもの。


「何でもできる大きなライブラリ」より「出来ることと出来ないこともあるけど、やってることが完全に把握できる小さなソース」を使いたいです、僕は。

os0xos0x2009/05/11 12:18addEventListener, attachEventはひとつの対象について同じイベントタイプを複数設定できるので、
new EventGroup().listen('mousemove',function(e){}).listen('mousemove',function(e){});
とできると期待(?)しますが、今の実装だとhandlerを上書きしてしまいます。その点を考慮したほうが良さそうです。
この辺りは、http://nanto.asablo.jp/blog/2007/03/23/1339498 が物凄く参考になります(すでにチェック済みかもしれませんが)。

あと、attachEventもハンドラに対して引数としてイベントオブジェクトを渡します。なので、stopPropagation, preventDefaultを使うかどうかはそのメソッドを持っているかどうかで判定する必要があります。

その他ではクロスブラウザで問題になりそうなところは特になさそうです。

edvakfedvakf2009/05/11 13:26同じイベントタイプを複数というのは考えてなかったです…

今の実装でいうなら、同じ要素に同じイベントタイプのリスナーを2つ付ける場合は、別の「イベント郡」に属するものとして

var click1 = new EventGroup().listen('click',function(){ })
var click2 = new EventGroup().listen('click',function(){ })

と書くしかないですね。そこらへんどういう実装にするか考えてみます。

「attachEventもハンドラに対して引数としてイベントオブジェクトを渡します。」これは知りませんでした。貴重な意見をありがとうございます。

トラックバック - http://javascript.g.hatena.ne.jp/edvakf/20090510
 |