作って納得! DOM 2 Events ― 2007年03月23日 22時14分
ブラウザ上でのプログラミングで避けては通れないのがイベント処理。その仕組みは DOM Level 2 Events にて規定されています。しかし、とりあえず addEventListener メソッドを使ってはいるものの、それがどのような意味を持つか詳しくは知らないといったことはありませんか。そこでここでは、DOM 2 Events のイベントモデルを理解し、ブラウザが裏で何をしているのかを把握するために、実際にそのイベントモデルを実装してみることにします。具体的には、仕様書に定められたインターフェースを JavaScript で実装し、それらを組み合わせてイベントの発生をシミュレートしてみます。
Event
インターフェースEventListener
インターフェースEventTarget
インターフェースDocumentEvent
インターフェースDOMException
インターフェースEventException
インターフェース- 実際に使ってみる (その 1)
- 実際に使ってみる (その 2)
- デモ
- まとめ
- この実装の注意点
Event
インターフェース
まずは Event
インターフェースを実装してみましょう。これはイベントリスナに引数として渡されるイベントオブジェクトに相当します。なお、以下インターフェースを実装するオブジェクトには、便宜的に "JS" の接頭辞をつけることとします。
function JSEvent() {
this.type = "";
this.target = null;
this.currentTarget = null;
this.eventPhase = 0;
this.bubbles = false;
this.cancelable = false;
this.timeStamp = new Date();
this._propagationStopped = false;
this._defaultPrevented = false;
}
コンストラクタでプロパティを設定します。各プロパティについてはそのつど説明していきますが、ここで設定する値は一時的なものであって特に意味はありません。ただし timeStamp
プロパティには現在時刻を表す Date
オブジェクトを設定します。このプロパティはイベントオブジェクトが作成された時間を表すものです。また、仕様にはないけれど内部的に使用するプロパティには、アンダースコアから始まる名前をつけています。
JSEvent.prototype.stopPropagation =
function () {
this._propagationStopped = true;
};
stopPropagation
メソッドはその名のとおりイベントの伝播を停止します。イベントの伝播については後で説明します。このメソッドが呼び出されたからといって即何かが起こるわけではありませんが、呼び出されたことを記録しておくために _propagationStopped
プロパティを true
に設定しておきます。
JSEvent.prototype.preventDefault =
function () {
if (this.cancelable)
this._defaultPrevented = true;
};
イベントの中にはそれに関連付けられたデフォルトアクションを持つものがあります。たとえば、リンク上での click イベントは、リンク先のページに移動するというデフォルトアクションを持ちます。preventDefault
メソッドはそうしたデフォルトアクションをキャンセルするためのメソッドです。ただし、すべてのイベントでデフォルトアクションをキャンセルできるわけではないので、キャンセル可能なイベントでのみキャンセルすることを記録しておきます。
JSEvent.prototype.initEvent =
function (eventTypeArg, canBubbleArg, cancelableArg) {
this.type = eventTypeArg;
this.bubbles = canBubbleArg;
this.cancelable = cancelableArg;
};
initEvent
メソッドはその名のとおりイベントオブジェクトを初期化します。といってもイベントリスナに渡されるイベントオブジェクトはすでに初期化されているので、普段使用することはあまりなかったかと思います。引数に従ってイベントタイプ ("click"
、"load"
など)、バブルするかどうか (後述)、キャンセル可能かどうかを設定します。
JSEvent.CAPTURING_PHASE = 1;
JSEvent.AT_TARGET = 2;
JSEvent.BUBBLING_PHASE = 3;
イベントの発生にはキャプチャリングフェーズ、ターゲットフェーズ、バブリングフェーズの 3 つの段階があります。各段階の詳細については後述しますが、Event
インターフェースにはこれらイベントフェーズを表す定数があり、eventPhase
プロパティの値として使われます。
EventListener
インターフェース
EventListener
インターフェースを実装するオブジェクトは実際にイベントを受け取り、それに対する処理を行います。JavaScript では関数オブジェクトがこれに相当します。イベントは引数として受け取り、返り値は必要ありません。
EventTarget
インターフェース
EventTarget
インターフェースはイベントが発生しうる対象を表します。DOM 2 Events では Node
インターフェースを実装するオブジェクト、すなわち要素ノードオブジェクトや文書ノードオブジェクト (document
) が EventTarget
インターフェースも実装することになっています。
function JSEventTarget() {
this._listeners = {};
}
コンストラクタで _listeners
プロパティを初期化します。このプロパティは、このターゲットに登録されたイベントリスナを格納するハッシュテーブルを表します。
JSEventTarget.prototype.addEventListener =
function (type, listener, useCapture) {
var listeners = this._listeners[type + !!useCapture];
if (!listeners)
listeners = this._listeners[type + !!useCapture] = [];
// Don't register duplicate listeners
for (var i = 0; i < listeners.length; i++)
if (listeners[i] == listener)
return;
listeners.push(listener);
};
イベントリスナを登録する、おなじみ addEventListener
メソッドです。引数 useCapture
は、登録するイベントリスナが呼び出されるイベントフェーズを示しています。実装としては、同一のイベントタイプおよびイベントフェーズで呼び出されるリスナを、配列に格納しておきます。これにより、同じイベントタイプに複数のリスナを関連付けることが可能になります。ただし、あるターゲット上で、ひとつのリスナを同じイベントタイプ、イベントフェーズで二重に登録することはできません。
JSEventTarget.prototype.removeEventListener =
function (type, listener, useCapture) {
var listeners = this._listeners[type + !!useCapture];
if (!listeners) return;
for (var i = 0; i < listeners.length; i++) {
if (listeners[i] == listener) {
listeners.splice(i, 1);
return;
}
}
};
removeEventListener
メソッドはその名のとおりターゲットからリスナを削除します。ターゲット上に、指定されたイベントタイプ、イベントフェーズと関連付けられたリスナが存在しない場合は何もしません。
JSEventTarget.prototype.dispatchEvent =
function (evt) {
if (!evt.type)
throw new JSEventException(JSEventException.UNSPECIFIED_EVENT_TYPE_ERR);
var targets = [this];
var ancestor = this;
while ((ancestor = ancestor.parentNode))
targets.unshift(ancestor);
if (evt.bubbles)
targets.push.apply(targets, targets.slice(0, -1).reverse());
evt.target = this;
evt.eventPhase = JSEvent.CAPTURING_PHASE;
for (var i = 0; i < targets.length; i++) {
var currentTarget = evt.currentTarget = targets[i];
var isTargetPhase = (currentTarget == evt.target);
if (isTargetPhase)
evt.eventPhase = JSEvent.AT_TARGET;
var isCapturingPhase = (evt.eventPhase == JSEvent.CAPTURING_PHASE);
var listeners = currentTarget._listeners[evt.type + isCapturingPhase];
if (listeners) {
for (var j = 0; j < listeners.length; j++)
listeners[j].call(currentTarget, evt);
if (evt._propagationStopped) break;
}
if (isTargetPhase)
evt.eventPhase = JSEvent.BUBBLING_PHASE;
}
var defaultPrevented = evt._defaultPrevented;
evt.target = null;
evt.currentTarget = null;
evt.eventPhase = 0;
evt._propagationStopped = false;
evt._defaultPrevented = false;
return !defaultPrevented;
};
dispatchEvent
メソッドはイベントを発生させるメソッドです。あまりなじみがないかもしれませんが、これこそ DOM 2 Events の中枢といってもいいでしょう。実装もボリュームが多めになっていますが、順に見ていくことにします。
まずはイベントタイプをチェックします。イベントタイプが設定されていない、すなわちイベントが初期化されていない場合は例外 (後述) を投げます。
次にリスナが呼び出されるターゲットを取得します。先に言ったようにイベントの発生には 3 つの段階があり、最初のキャプチャリングフェーズでは dispatchEvent
メソッドが呼び出されたターゲットの祖先ノード、次のターゲットフェーズは dispatchEvent
メソッドが呼び出されたターゲット自身、最後のバブリングフェーズでは再び祖先ノードのリスナが呼び出されます。ただし、祖先ノードは複数存在するので、キャプチャリングフェーズではルートノード (ルート要素ノードではなく文書ノード) から直近の親ノードの順、バブリングフェーズではその逆順でリスナを呼び出していきます。このことを指してイベントが伝播していくともいいます。引数として渡されたイベントがバブルしないものだった場合は、バブリングフェーズは存在しません。
イベントオブジェクトの target
プロパティには dispatchEvent
メソッドが呼び出されたターゲットを指定します。eventPhase
プロパティには最初の段階であるキャプチャリングフェーズを表す定数を指定します。
さて、いよいよ実際にリスナを呼び出す場面です。配列に収めたターゲットを順にたどっていき、それぞれのリスナを取得してさらにそれらを順に呼び出していきます。イベントオブジェクトの currentTarget
プロパティには現在呼び出すリスナを持つターゲットを、eventPhase
プロパティには適切なイベントフェーズを表す定数を指定します。コードを見てもらえばわかるとおり、addEventListener
メソッドで引数 useCapture
に true
を指定して登録したリスナは、キャプチャリングフェーズでしか呼び出されません。逆に false
を指定して登録したリスナは、ターゲットフェーズまたはバブリングフェーズでしか呼び出されなくなります。また、あるターゲットのリスナをすべて呼び出した後に、それらリスナ中でイベントオブジェクトの stopPropagation
メソッドが呼び出されたかどうかをチェックします。もし呼び出されていたのならば、イベントの伝播をそこで中止して後処理に移行します。
後処理ではイベントオブジェクトのプロパティを初期値に戻していきます。また、dispatchEvent
メソッドは、イベントにデフォルトアクションが関連付けられており、かつそれがキャンセルされた場合は false
を、そうでない場合は true
を返します。具体的には、preventDefault
メソッドによりデフォルトアクションがキャンセルされていれば _defaultPrevented
プロパティが true
になっているので、その否定値を返してやります。
DocumentEvent
インターフェース
DocumentEvent
インターフェースはイベントオブジェクトを作成するためのインターフェースです。通常は文書ノード (document
) がこのインターフェースを実装しています。
function JSDocumentEvent() {
this._constructors = {};
}
イベントオブジェクトのコンストラクタを格納するハッシュテーブルを指定します。
JSDocumentEvent.prototype.createEvent =
function (eventType) {
if (eventType in this._constructors)
return new this._constructors[eventType]();
throw new JSDOMException(JSDOMException.NOT_SUPPORTED_ERR);
};
createEvent
メソッドでは引数に従ってイベントオブジェクトを作成します。引数 eventType
は "click"
、"load"
といった個々のイベントタイプではなく、"Event"
、"MouseEvent"
といったイベントオブジェクトのインターフェースを表す文字列です。指定されたインターフェースのイベントオブジェクトを作成できない場合は例外 (後述) を投げます。
JSDocumentEvent.prototype._registerEvent =
function (eventType, constructor) {
this._constructors[eventType] = constructor;
};
ここでは、インターフェースを表す文字列と、そのインターフェースを実装するイベントオブジェクトのコンストラクタを登録するために、_registerEvent
メソッドを使用することにします。
DOMException
インターフェース
function JSDOMException(code) {
// DOM
this.code = code;
// ECMAScript
this.message = this._messages[code] || "";
}
JSDOMException.NOT_SUPPORTED_ERR = 9;
JSDOMException.prototype = new Error();
JSDOMException.prototype.constructor = JSDOMException;
JSDOMException.prototype.name = "DOMException";
JSDOMException.prototype._messages = {
9: "Implementation does not support requested operation"
};
DOMException
インターフェースは DOM 2 Core で規定されています。ここでは Error
オブジェクトを継承し、例外の種類を表す番号とエラーメッセージをプロパティとして持つことにします。本当はもっと多くの例外が規定されているのですが、DOM 2 Events で使われるのは NOT_SUPPORTED_ERR
だけなので、それだけを指定しておきます。
EventException
インターフェース
function JSEventException(code) {
JSDOMException.apply(this, arguments);
}
JSEventException.UNSPECIFIED_EVENT_TYPE_ERR = 0;
JSEventException.prototype = new JSDOMException();
JSEventException.prototype.constructor = JSEventException;
JSEventException.prototype.name = "EventException";
JSEventException.prototype._messages = {
0: "Event's type is not specified"
};
EventException
インターフェースは DOMException
インターフェースを継承し、イベントにまつわる例外を表します。DOM 2 Events では UNSPECIFIED_EVENT_TYPE_ERR
しか規定されていないので、それだけを実装しておきます。
実際に使ってみる (その 1)
基本的なインターフェースの実装が終わったので実際にイベントモデルをシミュレートしてみることにしましょう。まずは JSEventTarget
オブジェクトを継承する JSNode
オブジェクトを作ります。
function JSNode(name, parent) {
JSEventTarget.apply(this);
this.name = name;
this.parentNode = parent;
}
JSNode.prototype = new JSEventTarget();
JSNode.prototype.constructor = JSNode;
コンストラクタ内で JSEventTarget
コンストラクタを適用するのを忘れないようにしてください。
function logEvent(evt) {
print("type: " + evt.type + ", " +
"phase: " + evt.eventPhase + ", " +
"current target: " + evt.currentTarget.name);
}
var parent = new JSNode("parent", null);
var self = new JSNode("self", parent);
parent.addEventListener("foo", logEvent, true);
parent.addEventListener("foo", logEvent, false);
self.addEventListener("foo", logEvent, true);
self.addEventListener("foo", logEvent, false);
とりあえずは親と子を作り、それぞれにイベントリスナを追加します。
var doc = new JSDocumentEvent();
doc._registerEvent("Event", JSEvent);
イベントオブジェクトを作成するための下準備です。イベントタイプ名 "Event"
と JSEvent
オブジェクトを結び付けます。
var evt = doc.createEvent("Event");
evt.initEvent("foo", true, false);
self.dispatchEvent(evt);
そして実際にイベントオブジェクトを作成し初期化、self
上でそのイベントを発生させます。イベントは parent
(キャプチャリングフェーズ)、self
(ターゲットフェーズ)、parent
(バブリングフェーズ) と伝播し、以下のような出力が得られます。
type: foo, phase: 1, current target: parent type: foo, phase: 2, current target: self type: foo, phase: 3, current target: parent
実際に使ってみる (その 2)
別の例としてタイマーを作ってみましょう。このタイマーは一定の時間ごとに timer イベントを発生させます。timer イベントはキャンセル可能であり、キャンセルするとそれ以降の timer イベントは発生しません。また、何度イベントが発生したかを示す count
プロパティを持ちます。
function TimerEvent() {
JSEvent.apply(this);
this.count = 0;
}
TimerEvent.prototype = new JSEvent();
TimerEvent.prototype.constructor = TimerEvent;
TimerEvent.prototype.initTimerEvent =
function (eventTypeArg, canBubbleArg, cancelableArg, countArg) {
this.initEvent(eventTypeArg, canBubbleArg, cancelableArg);
this.count = countArg;
};
TimerEvent
オブジェクトは JSEvent
オブジェクトを継承し、さらに initTimerEvent
メソッドを持っています。DOM 2 Events でも、MouseEvent
インターフェースには initMouseEvent
メソッドといったように、Event インターフェースから派生するインターフェースは、初期化のための専用のメソッドを備えています。
function Timer(interval) {
JSEventTarget.apply(this);
var count = 0;
var self = this;
setTimeout(function () {
var evt = self._doc.createEvent("TimerEvent");
evt.initTimerEvent("timer", true, true, ++count);
var doesContinue = self.dispatchEvent(evt);
if (doesContinue)
setTimeout(arguments.callee, interval);
}, interval);
}
Timer.prototype = new JSEventTarget();
Timer.prototype.constructor = Timer;
Timer.prototype._doc = new JSDocumentEvent();
Timer.prototype._doc._registerEvent("TimerEvent", TimerEvent);
Timer
オブジェクトは指定時間後に timer イベントを発生させます。また、このイベントにはタイマーを続けるというデフォルトアクションがあります。このデフォルトアクションはキャンセル可能なので、dispatchEvent
メソッドの返り値が true
のときのみ実行することになります。
var timer = new Timer(1000);
timer.addEventListener("timer", function (evt) {
print(evt.count);
if (evt.count == 5) evt.preventDefault();
}, false);
この例だと、1 から 5 までの数値が 1 秒ごとに出力されます。
デモ
「実際に使ってみる」その 1 とその 2 をあわせたデモを作りました。実際にイベントが伝播するさまを確かめてください。
まとめ
このように、イベントを発生させるためには、イベントオブジェクトを作成し、それを初期化し、発生源となるターゲットの dispatchEvent
メソッドを呼び出してやる必要があります。これは組み込みのイベントでも同じであり、私たちが普段 load イベントや click イベントを受け取っている裏では、ブラウザがこれら一連の操作を行っている (あるいは行ったかのように振舞っている) のです。また、この仕組みがわかっていれば、Firefox のように EventTarget
インターフェースを実装した XMLHttpRequest
オブジェクトを、ほかのブラウザでも実装するといったこともできるようになります。
この実装の注意点
DOM 2 Events では、現在処理中のターゲットに追加されたリスナは、そのフェーズでは呼び出されないことになっています。また、現在処理中のターゲットから削除されたリスナが呼び出されることもありません。しかし、コードを簡単にするため、この実装ではこれらの動作が起こることを想定していません。
DOM 2 Events ECMAScript Language Binding では、イベントオブジェクトの timeStamp
プロパティの型は Date
オブジェクトであるとされています。しかし、多くのブラウザではミリ秒数を表す数値型として実装されており、DOM 3 Events 草案でも数値型に変更されました。この実装では DOM 2 Events に従い、Date
オブジェクトをその値に使用しています。
Event
インターフェースを実装するイベントオブジェクトを作成するために createEvent
メソッドに渡す文字列は、DOM 2 Events では "HTMLEvents"
ですが、DOM 3 Events 草案では "Event"
になっています。ここでは DOM 3 Events 草案に従い、文字列 "Event"
を渡すと JSEvent
オブジェクトが作成されるようにしました。
DOM 2 Events では、dispatchEvent
メソッドの返り値は、preventDefault
メソッドが呼び出されていたら false
、呼び出されていなかったら true
となっています。Opera 9 はこれに従っていますが、Firefox 2 および Safari 2 は、イベントがキャンセル可能でない場合 (そもそもその場合 preventDefault
メソッドの呼び出しは意味を持ちませんが)、リスナが preventDefault
メソッドを呼び出していても true
を返します。ここでは Firefox 2 および Safari 2 に従い、preventDefault
メソッドが呼び出されており、かつその呼び出しが意味を持つ場合のみ、dispatchEvent
メソッドが false
を返すようにしています。
また、各ブラウザの実装との差異については、「DOM Events とブラウザの実装」も参考になるかと思います。
DOM Events とブラウザの実装 ― 2007年03月23日 22時15分
ブラウザ上でのイベント処理の仕組みは DOM 2 Events および DOM 3 Events 草案にて規定されています。しかし、DOM 2 Events で言及されていない部分など、細かい動作はブラウザごとに異なっていることもあります。そうした仕様と実装の差異を、「作って納得! DOM 2 Events」で触れなかったものも含めて、いくつかまとめてみました。
ターゲットフェーズで呼び出されるリスナ
DOM 2 Events のイベントモデルにおいて、あるノードでイベントが発生すると、そのノードの祖先ノードのイベントリスナが呼び出されるキャプチャリングフェーズ、そのノード自身のイベントリスナが呼び出されるターゲットフェーズ、再び祖先ノードのイベントリスナが呼び出されるバブリングフェーズと、3 段階にわたってイベントが伝播していきます。このうちターゲットフェーズでは、addEventListener
メソッドで第 3 引数 useCapture
を false
に指定して登録したイベントリスナのみが呼び出されることになっています。
Opera 9 はこれに従っていますが、Firefox 2 および Safari 2 ではターゲットフェーズにおいて、useCapture
を true
に指定して登録したイベントリスナまでもが呼び出されてしまいます (参考: Mozilla Bug 235441)。なお、Safari はナイトリービルドにおいて一旦は仕様どおりの動作になったものの、Gecko との互換性を保つためにあえて以前の動作に戻したようです。
EventListener インターフェースを実装するオブジェクト
DOM 2 Events において、EventListener
インターフェースを実装するオブジェクトは handleEvent
メソッドを実装しなくてはいけません。しかし、これは Java のような、関数をファーストオブジェクトとして扱えない言語を考慮に入れてのことであり、付録の ECMAScript Language Binding によれば、ECMAScript では関数オブジェクトが EventListener
インターフェースを実装することになっています。すなわち、関数を addEventListener
メソッドの第 2 引数 listener
などに直接渡せるということです。
ですが、Firefox 2 や Opera 9 では、関数オブジェクトでなくとも handleEvent
メソッドを持つオブジェクトなら、EventListener インターフェースを実装するものとして扱ってくれます。Safari 2 では関数オブジェクトしか EventListener
として使えませんが、Safari のナイトリービルドでは handleEvent
メソッドを持つオブジェクトも EventListener
として使えるようです。
var listener = {
handleEvent: function (evt) { alert(this); },
toString: function () { return "[EventListener implementation]"; }
};
// Click, and alerts "[EventListener implementation]".
// OK: Firefox 2, Opera 9, and maybe Safari 3
element.addEventListener("click", listener, false);
イベントの伝播におけるルートノード
あるノードでイベントが発生すると、キャプチャリングフェーズではルートノードから直近の親ノードまで、バブリングフェーズでは親ノードからルートノードまでのイベントリスナが呼び出されます。ここで、ルートノードというのは、通常は文書ノード (document
) のことを指します。つまり、ある文書中の要素ノードでイベントが発生した場合、キャプチャリングフェーズは document
から始まり、バブリングフェーズは document
で終わるといった具合です。
Opera 9 はこのとおりの実装になっていますが、Firefox 2 および Safari 2 では window
オブジェクトがルートノードの役割を果たしています。すなわち、キャプチャリングフェーズは window
から始まり document
、ルート要素ノードと続いていき、逆にバブリングフェーズは window
で終わるのです。
複数のイベントリスナの実行順
DOM 2 Events では、addEventListener
メソッドで複数のイベントリスナが同じノード上に登録されたとき、それらをどういう順番で呼び出すかは規定されていません。しかし Firefox 2、Opera 9、Safari 2 はいずれも、登録されたのと同じ順番でイベントリスナを呼び出します。また、DOM 3 Events 草案でも、addEventListener
メソッドで登録されたイベントリスナは登録順で呼び出されることになっています。
イベント発生中のイベントリスナの編集
DOM 2 Events では、あるノードがイベントを処理している間にそのノードに追加されたイベントリスナは、その処理中には呼び出されないことになっています。ただし、イベントフェーズが異なれば呼び出されるので、キャプチャリングフェーズ中に追加されたリスナなら、バブリングフェーズでは呼び出されます。また、あるノードがイベントを処理している間にそのノードから削除されたイベントリスナは、決してその処理中に呼び出されることはありません。
Firefox 2 および Safari 2 はこれを仕様どおり実装していますが、Opera 9 ではイベント処理中に現在のターゲットノードから削除されたイベントリスナも呼び出されてしまいます。
イベントリスナ関数内の this の値
JavaScript では関数オブジェクトをそのままイベントリスナとして使えますが、これが呼び出されたときの関数内の this
が指す値は DOM 2 Events では規定されていません。Web Applications 1.0 には EventListener
インターフェースに関して In the ECMAScript binding itself, however, the
とありますが、これは handleEvent()
method of the interface is not directly accessible on Function
objects. Such functions, when invoked, must be called in the global scope.this
の値が window
オブジェクトになるということでしょうか?
実際には、Firefox 2、Opera 9、Safari 2 いずれも this
の値は現在イベントを処理中のノード、すなわち引数として渡されたイベントオブジェクトの currentTarget
プロパティの値と等しくなります。Firefox 2 ではテキストノード上のイベントリスナに関して、this
の値がイベントリスナ関数自身になってしまうバグがありますが、ナイトリービルドではすでに修正されています。また、Safari 2 では window
オブジェクト上のイベントリスナに渡されるイベントオブジェクトの currentTarget
プロパティの値が、キャプチャリングフェーズでは null
、バブリングフェーズでは document
オブジェクトになります。
余談ですが、上の引用をする際、Web Applications 1.0 の謝辞に Taken さんが載っていることに気づきました。一人だけ漢字なのでやたら目立ちますね。
createEvent メソッドの引数
イベントオブジェクトを作成するためには文書ノードの createEvent
メソッドを呼び出します。その際第 1 引数 eventType
に渡す文字列は、DOM 2 Events では "HTMLEvents"
、"UIEvents"
などとなっていますが、DOM 3 Events 草案では "Event"
、"UIEvent"
などに改められています (互換性のために DOM 2 Events の形式も認められています)。
Firefox 2、Opera 9 は DOM 2 Events および DOM 3 Events 草案の両形式に対応していますが、Safari 2 は DOM 2 Events の形式にしか対応していません。つまり、Safari 2 で基本的な Event インターフェースのみを実装するイベントオブジェクトを作成するためには、document.createEvent("HTMLEvents")
とする必要があります。なお、Safari のナイトリービルドでは DOM 3 Events の形式にも対応しているようです。
イベントオブジェクトの timeStamp プロパティ
DOM 2 Events において、Event
インターフェースの timeStamp
プロパティは、基準時刻からの経過ミリ秒数を表し、ECMAScript Language Binding ではその型は Date
型であるとされています。ただし、基準時刻ははっきりとは決められておらず、例としてシステムの開始時刻や Unix epoch (協定世界時 1970 年 1 月 1 日 0 時 0 分 0 秒) が挙げられています。DOM 3 Events 草案では基準時刻は Unix epoch であるとはっきり定められ、ECMAScript Language Binding での型も Number
型に変更されました。また、DOM 2 Events および DOM 3 Events 草案双方で、時間情報が利用できない場合は 0 を返すと規定されています。
各ブラウザの実装はというと、Firefox 2、Opera 9、Safari 2 のすべてにおいて timeStamp
プロパティの型は Number
型になっています。ただし、その値はばらばらで、Windows 版 Firefox 2 ではイベントにより、OS 起動時からの経過ミリ秒数を返すもの、0 を返すもの、どこから取ってきたのかわからない数値を返すものがあるようです。Windows 版 Opera 9 では常に 0 を返すようです。Safari 2 がどんな数値を返しているのかはよくわかりませんが、Unix epoch からの経過ミリ秒数ではないようです。
検証
以上の点を確認するためのサンプルを作りました。実際に各ブラウザでの違いを確かめてみてください。
秒速 5 センチメートル ― 2007年03月31日 20時48分
風邪を押して行ったらずるずると長引いてしまい、感想を書くのも遅くなってしまいましたが、新海誠監督の『秒速 5 センチメートル』を見てきました。しかも舞台挨拶とサイン会つき。
新海作品を劇場で見るのは初めてで、全編を通してその輝きに満ちた美術に圧倒されっぱなしだったのですが、中でも第一話「桜花抄」には完全に心を持っていかれました。転校、文通、再会という流れは完全に私のつぼですし、ジャンプの表紙に『とっても! ラッキーマン』(?)、部屋にはスーパーファミコンという時代設定まで私と重なります。おまけに『耳をすませば』を思わせる図書館の貸し出しカードの描写もあったりして、まさしくこれでもかこれでもかと畳み掛けられる気分です。(関係ないけどおにぎりでかすぎ!)
それだけに、続 2 編での彼らのあり方には切ないとしか言いようがありません。現実世界ではむしろそのようなあり方のほうが普通であろうだけに、ここでもやるのかと胸を締め付けられます。ラスト、前を向いて歩き出した貴樹とは逆に、過去への悔いが胸に残った作品でした。
新海誠監督と西村貴世作画監督の舞台挨拶は風邪で頭がぼうっとしながら聞いていましたが、覚えているところだけいくと、コマ数は 2 万枚、人物の塗りも背景の塗りと同じく Photoshop で塗っていったので、背景にキャラが載っているという感じではなく、同じ世界を動いている感じにできたとのことでした。今しかできない、今やらないとだめな作品をと思って作ったとも語っていましたが、これは第 1 話の舞台が 1995 年、第 2 話が 1999 年、第 3 話が現在 (2006 ~ 2008 年?) というのを見ても納得です。また、貴樹にも明里にも花苗にもまだまだ広大な人生が待っているわけで、見た人の明日を生きる力になるような映画であってほしいともおっしゃっていました。
サイン会の開催は舞台挨拶中に明かされたもので、うれしい驚きとなりました。ネコのロゴ (一瞬クマ? と思いましたが、新海監督ですからネコですよね) も添えていただき、握手もいただくなど、大変気さくな雰囲気でした。このようなすばらしい作品を制作し、関連企画を実行してくださった新海監督、西村作画監督はじめ関係者の皆様には感謝するばかりです。
最近のコメント