TEST
Simple Implementation of DOM Events in JavaScript
最終更新:
eriax
-
view
制限
- DOM Events のシンプル(?)な実装試案。名前空間ありの旧仕様。
ソースコード
if ('undefined' === typeof Array.prototype.indexOf) {
Array.prototype.indexOf = function (ceil, floor) {
return function (searchElement) {
var fromIndex = arguments[1];
var count = this.length;
var i = Number(fromIndex) || 0;
i = (i < 0) ? ceil(i) : floor(i);
if (i < 0) {
i += count;
}
for (; i < count; i++) {
if (i in this) {
if (this[i] === searchElement) {
return i;
}
}
}
return -1;
};
}(Math.ceil, Math.floor);
}
////////////////////////////////////////////////////////////////////////
function DOMObject(arg) {
if (arguments.length > 0) {
var name;
for (name in arg) {
if (arg.hasOwnProperty(name)) {
this[name] = arg[name];
}
}
}
}
(function () {
this.constructor = DOMObject;
}).call(DOMObject.prototype);
////////////////////////////////////////////////////////////////////////
(function () {
// EventPhase
this.CAPTURING_PHASE = 1;
this.AT_TARGET = 2;
this.BUBBLING_PHASE = 3;
}).call(DOMEvent);
function DOMEvent(arg) {
if (arguments.length > 0) {
if (arg) {
DOMObject.apply(this, arguments);
}
}
}
DOMEvent.prototype = new DOMObject;
(function () {
this.constructor = DOMEvent;
// Level 2
this.type = null;
this.target = null;
this.currentTarget = null;
this.eventPhase = null;
this.bubbles = null;
this.cancelable = null;
this.timeStamp = null;
// Level 3
this.namespaceURI = null;
this.defaultPrevented = false;
this.trusted = false;
// extension
this.ownerDocument = null;
this.propagationStopped = false;
this.immediatePropagationStopped = false;
}).call(DOMEvent.prototype);
(function () {
this.stopPropagation = function () {
this.propagationStopped = true;
};
this.preventDefault = function () {
if (this.cancelable) {
this.defaultPrevented = true;
}
};
this.initEvent = function (eventTypeArg, canBubbleArg, cancelableArg) {
this.initEventNS(null, eventTypeArg, canBubbleArg, cancelableArg);
};
this.stopImmediatePropagation = function () {
this.immediatePropagationStopped = true;
};
this.toString = function () {
return '[object Event]';
};
this.initEventNS = function (namespaceURI, eventTypeArg, canBubbleArg, cancelableArg) {
this.namespaceURI = namespaceURI;
this.type = eventTypeArg;
this.bubbles = canBubbleArg;
this.cancelable = cancelableArg;
};
}).call(DOMEvent.prototype);
////////////////////////////////////////////////////////////////////////
function DOMEventTarget(arg) {
if (arguments.length > 0) {
if (arg) {
DOMObject.apply(this, arguments);
}
var name = '[EventRegistry]';
if (!this.hasOwnProperty(name)) {
var memo = this[name] = [];
memo[DOMEvent.CAPTURING_PHASE] = {
'': []
};
memo[DOMEvent.BUBBLING_PHASE] = {
'': []
};
}
}
}
DOMEventTarget.prototype = new DOMObject;
(function () {
this.constructor = DOMEventTarget;
this['[EventRegistry]'] = null;
}).call(DOMEventTarget.prototype);
(function () {
this['[IsValidEventListener]'] = function (listener) {
return ('function' === typeof listener) || ('object' === typeof listener && 'function' === typeof listener.handleEvent);
};
this.addEventListener = function (type, listener, useCapture) {
return this.addEventListenerNS(null, type, listener, useCapture);
};
this.removeEventListener = function (type, listener, useCapture) {
return this.removeEventListenerNS(null, type, listener, useCapture);
};
this.dispatchEvent = function (evt) {
evt.target = this;
evt.timeStamp = new Date;
PROPAGATION: {
var targets = this['[TraceRoute]']();
var targetCount = targets.length;
var i;
// capture
evt.eventPhase = DOMEvent.CAPTURING_PHASE;
for (i = targetCount - 1; 0 <= i; i -= 1) {
if (!targets[i]['[CallEventListener]'](evt)) {
break PROPAGATION; // stop (Immediate)Propagation () called
}
}
// target
evt.eventPhase = DOMEvent.AT_TARGET;
if (!this['[CallEventListener]'](evt)) {
break PROPAGATION; // stop (Immediate)Propagation () called
}
// bubble
if (evt.bubbles) {
evt.eventPhase = DOMEvent.BUBBLING_PHASE;
for (i = 0; i < targetCount; i++) {
if (!targets[i]['[CallEventListener]'](evt)) {
break PROPAGATION; // stop (Immediate)Propagation () called
}
}
}
}
return evt.defaultPrevented; // preventDefault () called?
};
this['[TraceRoute]'] = function () {
var result = [];
var node = this;
if (node.nodeType > 0) {
while ((node = node.parentNode)) {
result[result.length] = node;
}
}
return result;
};
this['[GetEventListener]'] = function (evt) {
var type = evt.type;
var eventPhase = evt.eventPhase;
var namespaceURI = evt.namespaceURI;
if (namespaceURI == null) {
namespaceURI = '';
}
switch (eventPhase) {
case DOMEvent.CAPTURING_PHASE:
case DOMEvent.BUBBLING_PHASE:
var registry = this['[EventRegistry]'][eventPhase];
var listeners = registry[namespaceURI];
if (listeners instanceof Object) {
var handlers = listeners[type];
if (handlers instanceof Array) {
return handlers;
}
return listeners[type] = [];
}
listeners = registry[namespaceURI] = {};
return listeners[type] = [];
case DOMEvent.AT_TARGET:
var listeners1 = this['[GetEventListener]']({
namespaceURI: namespaceURI,
type: type,
eventPhase: DOMEvent.CAPTURING_PHASE
});
var listeners2 = this['[GetEventListener]']({
namespaceURI: namespaceURI,
type: type,
eventPhase: DOMEvent.BUBBLING_PHASE
});
return listeners1.concat(listeners2);
default:
return [];
}
};
this['[CallEventListener]'] = function (evt) {
var listeners = this['[GetEventListener]'](evt);
var listenerCount = listeners.length;
var listener;
var i;
for (i = 0; i < listenerCount; i++) {
listener = listeners[i];
try {
evt.currentTarget = this;
if ('function' === typeof listener) {
listener.call(this, evt);
}
else if ('function' === typeof listener.handleEvent) {
listener.handleEvent(evt);
}
}
catch (err) {
;
}
finally {
if (evt.immediatePropagationStopped) { // stopImmediatePropagation () called
return false;
}
}
}
return !evt.propagationStopped; // stopPropagation () called
};
this.toString = function () {
return '[object EventTarget]';
};
this.addEventListenerNS = function (namespaceURI, type, listener, useCapture) {
if (this['[IsValidEventListener]'](listener)) {
var listeners = this['[GetEventListener]']({
namespaceURI: namespaceURI,
type: type,
eventPhase: useCapture ? DOMEvent.CAPTURING_PHASE : DOMEvent.BUBBLING_PHASE
});
listeners.push(listener);
}
};
this.removeEventListenerNS = function (namespaceURI, type, listener, useCapture) {
if (this['[IsValidEventListener]'](listener)) {
var listeners = this['[GetEventListener]']({
namespaceURI: namespaceURI,
type: type,
eventPhase: useCapture ? DOMEvent.CAPTURING_PHASE : DOMEvent.BUBBLING_PHASE
});
if (listeners.length > 0) {
var index = listeners.indexOf(listener);
if (index >= 0) {
listeners.splice(index, 1);
}
}
}
};
}).call(DOMEventTarget.prototype);
テスト。
<!DOCTYPE HTML>
<title>TEST</title>
<script type="text/javascript"></script>
<script type="text/javascript">
function TestController(controls) {
var Log1 = [];
// T1 を要素ノードとして作る
var T1 = new DOMEventTarget({
nodeType: 1,
parentNode: null,
childNodes: []
});
// T1 をイベントリスナで監視(キャプチャ)
T1.addEventListener('click', function (e) {
var log = [];
log.push('T1: capturing_listener[0]');
// デフォルトアクションを取り消す
if (controls['preventDefault'].checked) {
e.preventDefault();
log.push('preventDefault() was called');
}
// イベント伝播を抑止する
if (controls['stopPropagation'].checked) {
e.stopPropagation();
log.push('stopPropagation() was called');
}
// イベント伝播を直ちに抑止する
if (controls['stopImmediatePropagation'].checked) {
e.stopImmediatePropagation();
log.push('stopImmediatePropagation() was called');
}
Log1.push(log.join(' | '));
}, true);
T1.addEventListener('click', function (e) {
Log1.push('T1: capturing_listener[1]');
}, true);
T1.addEventListener('click', function (e) {
Log1.push('T1: capturing_listener[2]');
}, true);
// T1 をイベントリスナで監視(バブル)
T1.addEventListener('click', function (e) {
Log1.push('T1: bubbling_listener[0]');
}, false);
T1.addEventListener('click', function (e) {
Log1.push('T1: bubbling_listener[1]');
}, false);
T1.addEventListener('click', function (e) {
Log1.push('T1: bubbling_listener[2]');
}, false);
////////////////////////////////////////////////////////////////////////
// T2 を要素ノード(T1 の子)として作る
var T2 = new DOMEventTarget({
nodeType: 1,
parentNode: T1,
childNodes: []
});
T1.childNodes.push(T2);
// T2 をイベントリスナで監視(キャプチャ)
T2.addEventListener('click', function (e) {
Log1.push('T2: capturing_listener[0]');
}, true);
T2.addEventListener('click', function (e) {
Log1.push('T2: capturing_listener[1]');
}, true);
T2.addEventListener('click', function (e) {
Log1.push('T2: capturing_listener[2]');
}, true);
// T2 をイベントリスナで監視(バブル)
var T2_listener0 = {
handleEvent: function (e) {
Log1.push('T2: bubbling_listener[0], this=' + this);
},
toString: function () {
return '[object DOMEventListener]';
}
};
T2.addEventListener('click', T2_listener0, false);
T2.addEventListener('click', function (e) {
Log1.push('T2: bubbling_listener[1], this=' + this);
}, false);
T2.addEventListener('click', function (e) {
Log1.push('T2: bubbling_listener[2]');
}, false);
// T2 を監視している T2_listener0 を除去する(バブル)
// T2.removeEventListener ('click', T2_listener0, false);
////////////////////////////////////////////////////////////////////////
// T3 を要素ノード(T2 の子)として作る
var T3 = new DOMEventTarget({
nodeType: 1,
parentNode: T2,
childNodes: []
});
T2.childNodes.push(T3);
// T3 をイベントリスナで監視(キャプチャ)
T3.addEventListener('click', function (e) {
Log1.push('T3: capturing_listener[0]');
}, true);
T3.addEventListener('click', function (e) {
Log1.push('T3: capturing_listener[1]');
}, true);
T3.addEventListener('click', function (e) {
Log1.push('T3: capturing_listener[2]');
}, true);
// T3 をイベントリスナで監視(バブル)
T3.addEventListener('click', function (e) {
Log1.push('T3: bubbling_listener[0]');
}, false);
T3.addEventListener('click', function (e) {
Log1.push('T3: bubbling_listener[1]');
}, false);
T3.addEventListener('click', function (e) {
Log1.push('T3: bubbling_listener[2]');
}, false);
////////////////////////////////////////////////////////////////////////
// イベント生成
var e = new DOMEvent({
ownerDocument: document
});
// イベント初期化
e.initEvent('click', controls['bubbles'].checked, controls['cancelable'].checked);
Log1.push('bubbles: ' + e.bubbles);
Log1.push('cancelable: ' + e.cancelable);
// イベント通知
var defaultPrevented = T3.dispatchEvent(e);
// デフォルトアクションが取り消されたか
Log1.push('defaultPrevented: ' + defaultPrevented);
////////////////////////////////////////////////////////////////////////
controls['OUTPUT-1'].value = Log1.join('.\n');
}
</script>
<!-- ここから -->
<p id="D1">T1 と、その子 T2 と、その子 T3 を作り、それぞれにイベントリスナを取り付けた後、T3 にイベントを送る。</p>
<pre role="img" aria-describedby="D1">
+ T1
|
+ T2
|
+ T3 <--- click!
</pre>
<form action="#" id="FORM-1">
<p>
<input type="button" value="試す" aria-describedby="D1" onclick="TestController(this.form.elements)">
<label><input type="checkbox" name="bubbles" checked="checked">bubbles</label>
<label><input type="checkbox" name="cancelable" checked="checked">cancelable</label>
<label><input type="checkbox" name="preventDefault">preventDefault</label>
<label><input type="checkbox" name="stopPropagation">stopPropagation</label>
<label><input type="checkbox" name="stopImmediatePropagation">stopImmediatePropagation</label>
</p>
<p><textarea rows="20" cols="100" name="OUTPUT-1"></textarea></p>
</form>
- 初出 2011-08-23/25