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

findElementメソッド

【抜粋】
  // find the first node with the given tagName, starting from the
  // node the event was triggered on; traverses the DOM upwards
  findElement: function(event, tagName) {
    var element = Event.element(event);
    while (element.parentNode && (!element.tagName ||
        (element.tagName.toUpperCase() != tagName.toUpperCase())))
      element = element.parentNode;
    return element;
  },

コメントのおおよその意味は以下。

「イベントを発生させた要素から、DOMツリーを上方へ調べていき、指定されたタグ名を持つ最初の要素が見つける。」

付け加えると、イベント発生要素自身も検討対象です。また、該当要素が見つからなかった場合は、ルートであるdocumentオブジェクトが返却されます。

【例】
<html>
<head>
<title></title>
<style>
<!--
div{
  margin:20px;
  padding:10px;
}
#base{
  background-color:gray;
  width:500px;
  height:500px;
  color:white;
}
#parent{
  background-color:blue;
  width:400px;
  height:400px;
  color:white;
}
#sub{
  background-color:green;
  width:200px;
  height:200px;
  color:white;
}
#child1{
  background-color:yellow;
  width:150px;
  height:100px;
}
#child2{
  background-color:yellow;
  width:150px;
  height:100px;
}
-->
</style>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
<!--
function test(event){
  event = event ? event : window.event;
  elem = Event.findElement(event, 'div');
  if(!elem.parentNode){
    alert("document root");
    return;
  }
  var bgcolor = Element.getStyle(elem, 'background-color');
  Element.setStyle(elem, {'background-color':'red'});
  setTimeout(function(){
    Element.setStyle(elem, {'background-color': bgcolor});
  }, 1000);
}
function init(){
  $('base').onclick = test;
}
//-->
</script>
</head>
<body onload="init();">
<form id="base" onsubmit="return false;">id:base<br/>
<div id="parent">id:parent<br/>
  <button id="child1">id:child1</button>
  <div id="sub">id:sub<br/>
    <button id="child2">id:child2</button>
  </div>
</div>

</form>
</body>
</html>

form要素baseのonclickに関数を登録しています。baseの中の、div要素parent内の任意の場所をクリックすると、DOMツリー上方で直近のdiv要素の背景色が赤になります(1秒後に元に戻しています。その間にクリックしないでやってください^^; 色が元に戻らなくなる場合があるので・・・)。

button要素child1ではdiv要素parent、button要素child2ではdiv要素subが対象になります。div要素parentとdiv要素subでは、それぞれ自身が対象です。

form要素baseでは、「DOMツリー上方で直近のdiv要素」に該当がなく、対象はdocumentオブジェクトになります。この場合(parentNodeの有無で判定)はalertを出しています。

使えそうなメソッドだとは思うのですが、具体的な使用場面は今のところ思いつかないです^^;

observersプロパティ

【抜粋】
  observers: false,

今まで登録したイベントに関する情報を保持するためのプロパティです。実際には後述_observeAndCacheメソッドで作成・登録されますが、明示のために記述されているようです。

observers自体は配列となり、その配列要素は、以下の形式の配列です。

[(要素オブジェクト), (イベント名), (実行関数), (キャプチャ動作可否)]

_observeAndCacheメソッドで使われる、要素のaddEventListenerメソッドの引数と同じです。

(キャプチャ動作可否)は、trueならイベント実行順序がキャプチャ(イベント登録要素から子要素へ)、falseならバブル(イベント発生要素から親要素へ)という意味です。ただし、IEでは常にバブルになります。(参考サイト:「http://www.hawk.34sp.com/smpview.php?src=dom2event」)

登録された情報は後述unloadCacheメソッドで使用されます。このメソッドはいままで登録されたイベントをすべて削除します。これは後にコメントされていますが*1IEでの不具合に対処するためのようです。

http://www.imgsrc.co.jp/~kuriyama/prototype/prototype.js.html#Reference.Extensions.Event」では内部プロパティとされていますが、後述のstopObservingメソッドを使用する場合に外部使用もあり得るかもしれません。stopObservingメソッドで述べます。

_observeAndCacheメソッド

【抜粋】
  _observeAndCache: function(element, name, observer, useCapture) {
    if (!this.observers) this.observers = [];
    if (element.addEventListener) {
      this.observers.push([element, name, observer, useCapture]);
      element.addEventListener(name, observer, useCapture);
    } else if (element.attachEvent) {
      this.observers.push([element, name, observer, useCapture]);
      element.attachEvent('on' + name, observer);
    }
  },

後述observeメソッドで使用されるメソッド。observeメソッドの主要部分はこちらに記述されています。外部から使用されることは想定していません。

引数は、内部で使用している要素のaddEventListenerメソッドと同じです。後述observeメソッドも同じ引数をとります。

observersプロパティがまだ存在しなければ(=false)、空配列を設定しています。

イベントへの関数登録には、Netscape等では要素のaddEventListenerメソッド、IEではattachEventメソッドを使用します。どちらも、observersプロパティに現在の引数を追加しています。

IEのattachEventメソッドには引数useCapture(キャプチャ動作可否)は無関係で、常にバブル動作になります。

例は後述observeメソッドを参照してください。

unloadCache

【抜粋】
  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;
  },

observersプロパティに登録されている情報を引数にして、後述stopObservingメソッドを呼び出し、今までイベント登録した関数をすべて削除するメソッド。後にwindwのunloadイベントに登録されます。IEでの不具合対応です。外部から使用されることは想定していません。

一見、何か変に見えるかもしれません。普通、「this」であるところが「Event」になっています。ただ、一箇所だけ「this」があります。

unloadCacheメソッドは、windwのunloadイベントで実行されており、この「this」は「Event」ではありません。Netscape等はイベント登録要素である「window」を指すことになります(IEは何を指すのかはっきり分かりません・・・分かったら追記します)。このため、通常「this」を使うところを「Event」で記述しているわけです。

apply関数は、この場合、第二引数で、Event.stopObservingの引数を配列で指定したいがために使用されており、第一引数はとりあえずthisにしているだけです。

ただ、イベントへの登録時にbindメソッド*2を使用して、thisを使用できるようにしてもよかった気がします。内部メソッドであることを強調したかったのかな?

observeメソッド

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

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

    this._observeAndCache(element, name, observer, useCapture);
  },

引数elementの要素の、引数nameのイベントに、引数observerの関数を登録するメソッド。引数useCaptureはイベント時のキャプチャ動作可否を指定するもの。IEでは無効です。

第二引数のイベント名は頭に「on」を付けないので注意してください。

処理の主要部分は前述_observeAndCacheメソッドで行います。ここではブラウザがKonquerorSafariKHTMLの場合に、イベント'keypress'指定を'keydown'指定に変換するくらいです。

【例】
<html>
<head>
<title></title>
<style>
<!--
div{
  margin:20px;
  padding:10px;
}
#parent1, #parent2{
  background-color:blue;
  width:300px;
  height:150px;
  color:white;
}
#child1{
  background-color:yellow;
  width:200px;
  height:100px;
}
#child2{
  background-color:red;
  width:200px;
  height:100px;
}
-->
</style>
<script language="javascript" src="prototype.js" charset="utf-8"></script>
<script>
<!--
var parent = function(event){
  alert("parent\nelement=" + Event.element(event).id);
  if($('stop').checked){
    Event.stop(event);
  }
}.bindAsEventListener(this);

var child = function(event){
  alert("child\nelement=" + Event.element(event).id);
  if($('stop').checked){
    Event.stop(event);
  }
}.bindAsEventListener(this);

var parent_sub = function(event){
  alert("parent_sub\nelement=" + Event.element(event).id);
  if($('stop').checked){
    Event.stop(event);
  }
}.bindAsEventListener(this);

var child_sub = function(event){
  alert("child_sub\nelement=" + Event.element(event).id);
  if($('stop').checked){
    Event.stop(event);
  }
}.bindAsEventListener(this);

function init(){
  Event.observe('parent1', 'click', parent);
  Event.observe('parent1', 'click', parent_sub);
  Event.observe('child1', 'click', child);
  Event.observe('child1', 'click', child_sub);
  Event.observe('parent2', 'click', parent, true);
  Event.observe('parent2', 'click', parent_sub, true);
  Event.observe('child2', 'click', child, true);
  Event.observe('child2', 'click', child_sub, true);
}
Event.observe(window, 'load', init);
//-->
</script>
</head>
<body>

<input type="checkbox" id="stop"/>STOP
<div id="parent1">id:parent1<br/>
<button id="child1">id:child1</button>
</div>
<div id="parent2">id:parent2<br/>
<button id="child2">id:child2</button>
</div>
<div id="test"></div>
</body>
</html>
>
</html>

結果表示についてはalertに逃げました^^;

bodyタグにonload属性を書かずに、Event.observeメソッドで登録しています(Event.observe(window, 'load', init);)。

Event.observeの第三引数の関数指定に、bindAsEventListenerメソッド*3を使用してみました。2006/09/22 修正。後述stopObservingの例にあわせて、関数宣言時にbindAsEventListenerを使用し、これを変数にセットしました。関数内ではIEでも引数でイベントオブジェクトが取れます。ここでの効果はそれだけですが^^;→2006/09/25修正。attachEventで登録すると、IEでも引数にイベントオブジェクトが設定されるんですね。知らなかったです・・・。なので、ここでのbindAsEventListenerメソッドに意味はありません><;→更に追記。「hawklab.jp」に関連する事柄が書かれていました。あながち間違いではないかも・・・。

↑すみません、このあたりは後で整理します。。。

div要素parent1の中にbutton要素child1、div要素parent2の中にbutton要素child2を作成しました。それぞれ、onclickイベントに、div要素には関数parentとparent_sub、button要素にはchildとchild_subを登録しています。addEventListenerやattachEventを使うと、同じイベントにも複数の関数を登録できます。

parent2およびchild2では、Event.observeの第四引数をtrueにしました。これでNetscape等ではイベント実行順序にキャプチャを指定することができます。IEでは無効です。Operaは意味不明です(後述します^^;)。

実行結果を以下に示します。

  • IE6
クリックした要素 alertの表示順序(略記)
parent1 parent_sub > parent
child1 child_sub > child > parent_sub > parent
parent2 parent_sub > parent
child2 child_sub > child > parent_sub > parent

1、2とも同じ結果。attachEventって、後から追加された関数から先に実行されるんですね・・・。。→2006/09/29修正。というわけではないようです。

【参考サイト】Pujolsさんの日記
IEのattachEventについて、「イベント時呼び出し順」は、「設定順」になっていない
  • Netscape7.1, Firefox1.5.0.4
クリックした要素 alertの表示順序(略記)
parent1 parent > parent_sub
child1 child > child_sub > parent > parent_sub
parent2 parent > parent_sub
child2 parent > parent_sub > child > child_sub

一番素直な動きです。

  • Opera9.0
クリックした要素 alertの表示順序(略記)
parent1 parent > parent_sub
child1 child > child_sub > parent > parent_sub
parent2 (実行されず)
child2 parent > parent_sub

・・・Operaは訳が分かりません^^;

ここで、STOPにチェックを入れると、先に実行されたイベントの関数でEvent.stopメソッドが呼ばれ、後のイベントが抑制されます。ただし、同一イベントの他の関数の実行までは抑制しません。試してみてください。

2006/09/22追記。Netscape等で、要素parent2へのparent_subの関数の登録時のuseCapture引数をfalseにして、要素child2をクリックすると、「parent > child > child_sub > parent_sub」になります。イベントキャプチャが、イベントバブルより優先して実行されてます。この状態でSTOPにチェックを入れると、「parent」のみになります。

んー。useCapture引数はまだ使いづらいですかね・・・。

中途半端ですが、今日はここまで。

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

*1:1551行目

*2:Functionクラス追加メソッド。bindAsEventListenerでも可

*3:Functionクラス拡張メソッド