Eventオブジェクト(に対する拡張)(3)

stopObservingメソッド

【抜粋】
  stopObserving: function(element, name, observer, useCapture) {
    var element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    if (element.removeEventListener) {
      element.removeEventListener(name, observer, useCapture);
    } else if (element.detachEvent) {
      element.detachEvent('on' + name, observer);
    }
  }

要素のイベントに登録された関数を削除するメソッド。observeメソッドでの登録時と同じ引数を指定する必要があります。この場合、登録時の引数を保持しておく以外に、observersプロパティを利用する手もあると思います。特定要素や関数を処理する場合は有効だと思います。

ただ、コードを見ると、外部使用を前提としていないメソッドな気がします。というのも、observersプロパティがほったらかしだからです。これだとイベントから関数を削除しても、observersプロパティに情報が残ってしまいます。どうも、windowのonUnload時に、前述unloadCacheメソッドから呼び出されることが前提のメソッドな気がします・・・。

一応、例を提示します。

【例】
<html>
<head>
<title></title>
<style>
<!--
#test1{
  background-color:yellow;
  width:200px;
  height:100px;
}
#test2{
  background-color:red;
  width:200px;
  height:100px;
}
-->
</style>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
<!--
function func1(event){
  event = event || window.event;
  alert("func1\nelement=" + Event.element(event).id + "\ntype:" + event.type);
}

function func2(event){
  event = event || window.event;
  alert("func2\nelement=" + Event.element(event).id + "\ntype:" + event.type);
}

function init(){
  Event.observe('test1', 'click', func1);
  Event.observe('test1', 'click', func2);
  Event.observe('test2', 'click', func1);
  Event.observe('test2', 'click', func2);
  Event.observe('test2', 'click', func2, true);
  show();
}

function show(){
  Element.update('test', Object.inspect(Event.observers).escapeHTML());
}

Event.observe(window, 'load', init);
//-->
</script>
</head>
<body>
<button id="test1">id:test1</button>
<button id="test2">id:test2</button><br/>
<br/>
<button id="stop" onclick="Event.stopObserving('test1', 'click', func1);show();">
Event.stopObserving('test1', 'click', func1);</button><br/>
<button id="stop" onclick="Event.stopObserving('test2', 'click', func2, true);show();">
Event.stopObserving('test2', 'click', func2, true);</button>
<div id="test"></div>
</body>
</html>

button要素test1、test2をクリックすると、登録した関数によりalertが表示されます。下の二つのボタンを押すと、指定のイベントを削除します。上のボタンが「test1要素のonclickイベントのfunc1関数」、下のボタンが「test2要素のonclickイベントのfunc2関数(イベントキャプチャ)」です。再度test1、test2ボタンを押すと、その分alertの表示が減ります。

ただし、IEではイベントキャプチャは無効なので、下の二つ目のボタンを2回以上押すと、test2でfunc2が実行されなくなります。NetscapeFirefoxOperaは2回以上押してもfunc2は1回実行されます。このfunc2はイベントバブル指定です(分かりづらいですが^^;)。

windowのonload時と、各stopObservingメソッド使用後に、Event.observersをObject.inspectで表示していますが、まったく変わりません。

改修検討

stopObservingでEvent.observersを更新してしないのは、実用上あまり問題にならないかもしれませんが、ちょっと気持ち悪いです(windowのonunloadですでに削除済みのイベント関数の削除が試みられる。エラーにはならないようですが・・・)。

また、stopObservingの引数は、observeメソッドで使用した引数をそのまま使用しなければなりません。このため、プログラマは使用した引数をそのままコピーしてくるか、どこかに保持しておく必要があります。が、それならEvent.observersの情報を使いたくなります。

さらに進めて、要素、イベント、関数、キャプチャ指定のうち、任意のものを指定するだけで、該当するイベント関数をすべて削除することができると便利な気がします。

この辺を考慮して、改修案を作成してみました。

【改修案-1】stopObservingメソッドを「_stopObserving」に改名

  _stopObserving: function(element, name, observer, useCapture) {
(省略)
  }
【改修案-2】unloadCacheメソッドのstopObserving使用箇所を
           「_stopObserving」に変更

  unloadCache: function() {
(省略)
      Event._stopObserving.apply(this, Event.observers[i]);
(省略)
  },

2006/09/24 追記。改修案-3は当初から若干修正しています。

【改修案-3】stopObservingメソッドを以下で再設定
  stopObserving: function(element, name, observer, useCapture) {
    if (!Event.observers) return;
    element = $(element);
    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    //Event.observersから該当する情報を抽出
    var observers = this.observers.partition(function(value){
      return (    (!element    || value[0] == element    )
               && (!name       || value[1] == name       )
               && (!observer   || value[2] == observer   )
               && (useCapture == null || value[3] == useCapture )
             )
    });
  
    if(observers[0].length == 0) return;
  
    //該当するものを_stopObservingメソッドによりイベントから削除
    observers[0].each(function(value){
      this._stopObserving.apply(this, value);
    }.bind(this));
  
    //イベントから削除したものをEvent.observersから削除
    this.observers = observers[1];
  }

以下、改修案を用いた場合の例です。ここではprototype.jsをそのまま読み込み、その後スクリプトでメソッドの追加・上書きをしています。

【参考】上記改修案を用いた場合の例
<html>
<head>
<title></title>
<style>
<!--
#test1{
  background-color:yellow;
  width:200px;
  height:100px;
}
#test2{
  background-color:red;
  width:200px;
  height:100px;
}
-->
</style>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
<!--
//stopObservingメソッドを「_stopObserving」に改名
Event._stopObserving = function(element, name, observer, useCapture) {
  var element = $(element);
  useCapture = useCapture || false;

  if (name == 'keypress' &&
      (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
      || element.detachEvent))
    name = 'keydown';

  if (element.removeEventListener) {
    element.removeEventListener(name, observer, useCapture);
  } else if (element.detachEvent) {
    element.detachEvent('on' + name, observer);
  }
}

//unloadCacheメソッドのstopObserving使用箇所を「_stopObserving」に変更
Event.unloadCache = function() {
  if (!Event.observers) return;
  for (var i = 0; i < Event.observers.length; i++) {
    Event._stopObserving.apply(this, Event.observers[i]);
    Event.observers[i][0] = null;
  }
  Event.observers = false;
}

//stopObservingメソッドを再設定
Event.stopObserving = function(element, name, observer, useCapture){
  if (!Event.observers) return;
  element = $(element);
  if (name == 'keypress' &&
      (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
      || element.detachEvent))
    name = 'keydown';

  //Event.observersから該当する情報を抽出
  var observers = this.observers.partition(function(value){
    return (    (!element    || value[0] == element    )
             && (!name       || value[1] == name       )
             && (!observer   || value[2] == observer   )
             && (useCapture == null || value[3] == useCapture )
           )
  });
  
  if(observers[0].length == 0) return;
  
  //該当するものを_stopObservingメソッドによりイベントから削除
  observers[0].each(function(value){
    this._stopObserving.apply(this, value);
  }.bind(this));
  
  //イベントから削除したものをEvent.observersから削除
  this.observers = observers[1];
}

//window onunloadへの再登録
Event.stopObserving(window);
Event.observe(window, 'unload', Event.unloadCache, false);

function func1(event){
  alert("func1\nelement=" + Event.element(event).id + "\ntype:" + event.type);
}

function func2(event){
  alert("func2\nelement=" + Event.element(event).id + "\ntype:" + event.type);
}

function init(){
  Event.observe('test1', 'click', func1);
  Event.observe('test1', 'click', func2);
  Event.observe('test1', 'dblclick', func1);
  Event.observe('test2', 'click', func1);
  Event.observe('test2', 'click', func2);
  Event.observe('test2', 'click', func2, true);
  Event.observe('test2', 'dblclick', func2);
  show();
}

function show(){
  Element.update('test', Object.inspect(Event.observers).escapeHTML());
}

Event.observe(window, 'load', init);
//-->
</script>
</head>
<body>

<button id="test1">id:test1</button>
<button id="test2">id:test2</button><br/>
<br/>
<button id="stop" onclick="Event.stopObserving('test1');show();">
Event.stopObserving('test1');</button><br/>
<button id="stop" onclick="Event.stopObserving(null, 'click');show();">
Event.stopObserving(null, 'click');</button><br/>
<button id="stop" onclick="Event.stopObserving(null ,null, func1);show();">
Event.stopObserving(null,null,func1);</button><br/>
<button id="stop" onclick="Event.stopObserving(null, null, null, true);show();">
Event.stopObserving(null, null, null, true);</button><br/>

<div id="test"></div>
</body>
</html>

下のボタンは、

  • 最初が要素test1からイベントをすべて削除
  • 2番目はすべてのonclickイベントを削除(この場合、ondblclickが実行可能になる。ダブルクリックしてみてください)
  • 3番目が関数func1をすべて削除
  • 4番目がイベントキャプチャ指定のものを削除

になります。

これだと、Event.observersの内容が変わっていくのが分かると思います。

ただし、以下を注意する必要があります。

  • 第4引数のイベントキャプチャの無指定時は、true、falseどちらも対象とする。

falseとみなさないので注意してください。

  • observeメソッドで登録したイベント関数しか扱えない。

別の手段で登録されたイベント関数は削除できません。その場合は_stopObservingメソッドを使います・・・。

・・・あ゛。これでも特定イベント関数を削除したい場合は、observeの引数をコピーしなきゃいけないことに変わりないや><; すみません、その点は勘弁してください・・・。

ものすごく中途半端で申し訳ないですが、今日はここまで。

>>Eventオブジェクト(に対する拡張)(4)に続く<<

(以下は個人的な資料です)

最初のstopObservingメソッド改修案。上の例ではpartitionメソッドを使用してコードをシンプルにした。

【個人資料】最初のstopObservingメソッド改修案
  stopObserving: function(element, name, observer, useCapture) {
    element = $(element);
    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    //Event.observersから該当する情報を抽出
    var observers = this.observers.select(function(value){
      return (    (!element    || value[0] == element    )
               && (!name       || value[1] == name       )
               && (!observer   || value[2] == observer   )
               && (useCapture == null || value[3] == useCapture )
             )
    });
    
    if(observers.length == 0) return;
    
    //該当するものをstopObservingメソッドによりイベントから削除
    observers.each(function(value){
      this._stopObserving.apply(Event, value);
    }.bind(this));
    
    //イベントから削除したものをEvent.observersから削除
    this.observers = this.observers.reject(function(eoElm){
      return observers.any(function(oElem){
        return oElem.all(function(oElemElem, i){
          return (oElemElem == eoElm[i]);
        });
      });
    });
  }