299 lines
10 KiB
JavaScript
299 lines
10 KiB
JavaScript
|
"use strict";
|
||
|
var Event = require('./Event');
|
||
|
var MouseEvent = require('./MouseEvent');
|
||
|
var utils = require('./utils');
|
||
|
|
||
|
module.exports = EventTarget;
|
||
|
|
||
|
function EventTarget() {}
|
||
|
|
||
|
EventTarget.prototype = {
|
||
|
// XXX
|
||
|
// See WebIDL §4.8 for details on object event handlers
|
||
|
// and how they should behave. We actually have to accept
|
||
|
// any object to addEventListener... Can't type check it.
|
||
|
// on registration.
|
||
|
|
||
|
// XXX:
|
||
|
// Capturing event listeners are sort of rare. I think I can optimize
|
||
|
// them so that dispatchEvent can skip the capturing phase (or much of
|
||
|
// it). Each time a capturing listener is added, increment a flag on
|
||
|
// the target node and each of its ancestors. Decrement when removed.
|
||
|
// And update the counter when nodes are added and removed from the
|
||
|
// tree as well. Then, in dispatch event, the capturing phase can
|
||
|
// abort if it sees any node with a zero count.
|
||
|
addEventListener: function addEventListener(type, listener, capture) {
|
||
|
if (!listener) return;
|
||
|
if (capture === undefined) capture = false;
|
||
|
if (!this._listeners) this._listeners = Object.create(null);
|
||
|
if (!this._listeners[type]) this._listeners[type] = [];
|
||
|
var list = this._listeners[type];
|
||
|
|
||
|
// If this listener has already been registered, just return
|
||
|
for(var i = 0, n = list.length; i < n; i++) {
|
||
|
var l = list[i];
|
||
|
if (l.listener === listener && l.capture === capture)
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Add an object to the list of listeners
|
||
|
var obj = { listener: listener, capture: capture };
|
||
|
if (typeof listener === 'function') obj.f = listener;
|
||
|
list.push(obj);
|
||
|
},
|
||
|
|
||
|
removeEventListener: function removeEventListener(type,
|
||
|
listener,
|
||
|
capture) {
|
||
|
if (capture === undefined) capture = false;
|
||
|
if (this._listeners) {
|
||
|
var list = this._listeners[type];
|
||
|
if (list) {
|
||
|
// Find the listener in the list and remove it
|
||
|
for(var i = 0, n = list.length; i < n; i++) {
|
||
|
var l = list[i];
|
||
|
if (l.listener === listener && l.capture === capture) {
|
||
|
if (list.length === 1) {
|
||
|
this._listeners[type] = undefined;
|
||
|
}
|
||
|
else {
|
||
|
list.splice(i, 1);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// This is the public API for dispatching untrusted public events.
|
||
|
// See _dispatchEvent for the implementation
|
||
|
dispatchEvent: function dispatchEvent(event) {
|
||
|
// Dispatch an untrusted event
|
||
|
return this._dispatchEvent(event, false);
|
||
|
},
|
||
|
|
||
|
//
|
||
|
// See DOMCore §4.4
|
||
|
// XXX: I'll probably need another version of this method for
|
||
|
// internal use, one that does not set isTrusted to false.
|
||
|
// XXX: see Document._dispatchEvent: perhaps that and this could
|
||
|
// call a common internal function with different settings of
|
||
|
// a trusted boolean argument
|
||
|
//
|
||
|
// XXX:
|
||
|
// The spec has changed in how to deal with handlers registered
|
||
|
// on idl or content attributes rather than with addEventListener.
|
||
|
// Used to say that they always ran first. That's how webkit does it
|
||
|
// Spec now says that they run in a position determined by
|
||
|
// when they were first set. FF does it that way. See:
|
||
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#event-handlers
|
||
|
//
|
||
|
_dispatchEvent: function _dispatchEvent(event, trusted) {
|
||
|
if (typeof trusted !== 'boolean') trusted = false;
|
||
|
function invoke(target, event) {
|
||
|
var type = event.type, phase = event.eventPhase;
|
||
|
event.currentTarget = target;
|
||
|
|
||
|
// If there was an individual handler defined, invoke it first
|
||
|
// XXX: see comment above: this shouldn't always be first.
|
||
|
if (phase !== Event.CAPTURING_PHASE &&
|
||
|
target._handlers && target._handlers[type])
|
||
|
{
|
||
|
var handler = target._handlers[type];
|
||
|
var rv;
|
||
|
if (typeof handler === 'function') {
|
||
|
rv=handler.call(event.currentTarget, event);
|
||
|
}
|
||
|
else {
|
||
|
var f = handler.handleEvent;
|
||
|
if (typeof f !== 'function')
|
||
|
throw new TypeError('handleEvent property of ' +
|
||
|
'event handler object is' +
|
||
|
'not a function.');
|
||
|
rv=f.call(handler, event);
|
||
|
}
|
||
|
|
||
|
switch(event.type) {
|
||
|
case 'mouseover':
|
||
|
if (rv === true) // Historical baggage
|
||
|
event.preventDefault();
|
||
|
break;
|
||
|
case 'beforeunload':
|
||
|
// XXX: eventually we need a special case here
|
||
|
/* falls through */
|
||
|
default:
|
||
|
if (rv === false)
|
||
|
event.preventDefault();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Now invoke list list of listeners for this target and type
|
||
|
var list = target._listeners && target._listeners[type];
|
||
|
if (!list) return;
|
||
|
list = list.slice();
|
||
|
for(var i = 0, n = list.length; i < n; i++) {
|
||
|
if (event._immediatePropagationStopped) return;
|
||
|
var l = list[i];
|
||
|
if ((phase === Event.CAPTURING_PHASE && !l.capture) ||
|
||
|
(phase === Event.BUBBLING_PHASE && l.capture))
|
||
|
continue;
|
||
|
if (l.f) {
|
||
|
l.f.call(event.currentTarget, event);
|
||
|
}
|
||
|
else {
|
||
|
var fn = l.listener.handleEvent;
|
||
|
if (typeof fn !== 'function')
|
||
|
throw new TypeError('handleEvent property of event listener object is not a function.');
|
||
|
fn.call(l.listener, event);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!event._initialized || event._dispatching) utils.InvalidStateError();
|
||
|
event.isTrusted = trusted;
|
||
|
|
||
|
// Begin dispatching the event now
|
||
|
event._dispatching = true;
|
||
|
event.target = this;
|
||
|
|
||
|
// Build the list of targets for the capturing and bubbling phases
|
||
|
// XXX: we'll eventually have to add Window to this list.
|
||
|
var ancestors = [];
|
||
|
for(var n = this.parentNode; n; n = n.parentNode)
|
||
|
ancestors.push(n);
|
||
|
|
||
|
// Capturing phase
|
||
|
event.eventPhase = Event.CAPTURING_PHASE;
|
||
|
for(var i = ancestors.length-1; i >= 0; i--) {
|
||
|
invoke(ancestors[i], event);
|
||
|
if (event._propagationStopped) break;
|
||
|
}
|
||
|
|
||
|
// At target phase
|
||
|
if (!event._propagationStopped) {
|
||
|
event.eventPhase = Event.AT_TARGET;
|
||
|
invoke(this, event);
|
||
|
}
|
||
|
|
||
|
// Bubbling phase
|
||
|
if (event.bubbles && !event._propagationStopped) {
|
||
|
event.eventPhase = Event.BUBBLING_PHASE;
|
||
|
for(var ii = 0, nn = ancestors.length; ii < nn; ii++) {
|
||
|
invoke(ancestors[ii], event);
|
||
|
if (event._propagationStopped) break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
event._dispatching = false;
|
||
|
event.eventPhase = Event.AT_TARGET;
|
||
|
event.currentTarget = null;
|
||
|
|
||
|
// Deal with mouse events and figure out when
|
||
|
// a click has happened
|
||
|
if (trusted && !event.defaultPrevented && event instanceof MouseEvent) {
|
||
|
switch(event.type) {
|
||
|
case 'mousedown':
|
||
|
this._armed = {
|
||
|
x: event.clientX,
|
||
|
y: event.clientY,
|
||
|
t: event.timeStamp
|
||
|
};
|
||
|
break;
|
||
|
case 'mouseout':
|
||
|
case 'mouseover':
|
||
|
this._armed = null;
|
||
|
break;
|
||
|
case 'mouseup':
|
||
|
if (this._isClick(event)) this._doClick(event);
|
||
|
this._armed = null;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
return !event.defaultPrevented;
|
||
|
},
|
||
|
|
||
|
// Determine whether a click occurred
|
||
|
// XXX We don't support double clicks for now
|
||
|
_isClick: function(event) {
|
||
|
return (this._armed !== null &&
|
||
|
event.type === 'mouseup' &&
|
||
|
event.isTrusted &&
|
||
|
event.button === 0 &&
|
||
|
event.timeStamp - this._armed.t < 1000 &&
|
||
|
Math.abs(event.clientX - this._armed.x) < 10 &&
|
||
|
Math.abs(event.clientY - this._armed.Y) < 10);
|
||
|
},
|
||
|
|
||
|
// Clicks are handled like this:
|
||
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#interactive-content-0
|
||
|
//
|
||
|
// Note that this method is similar to the HTMLElement.click() method
|
||
|
// The event argument must be the trusted mouseup event
|
||
|
_doClick: function(event) {
|
||
|
if (this._click_in_progress) return;
|
||
|
this._click_in_progress = true;
|
||
|
|
||
|
// Find the nearest enclosing element that is activatable
|
||
|
// An element is activatable if it has a
|
||
|
// _post_click_activation_steps hook
|
||
|
var activated = this;
|
||
|
while(activated && !activated._post_click_activation_steps)
|
||
|
activated = activated.parentNode;
|
||
|
|
||
|
if (activated && activated._pre_click_activation_steps) {
|
||
|
activated._pre_click_activation_steps();
|
||
|
}
|
||
|
|
||
|
var click = this.ownerDocument.createEvent('MouseEvent');
|
||
|
click.initMouseEvent('click', true, true,
|
||
|
this.ownerDocument.defaultView, 1,
|
||
|
event.screenX, event.screenY,
|
||
|
event.clientX, event.clientY,
|
||
|
event.ctrlKey, event.altKey,
|
||
|
event.shiftKey, event.metaKey,
|
||
|
event.button, null);
|
||
|
|
||
|
var result = this._dispatchEvent(click, true);
|
||
|
|
||
|
if (activated) {
|
||
|
if (result) {
|
||
|
// This is where hyperlinks get followed, for example.
|
||
|
if (activated._post_click_activation_steps)
|
||
|
activated._post_click_activation_steps(click);
|
||
|
}
|
||
|
else {
|
||
|
if (activated._cancelled_activation_steps)
|
||
|
activated._cancelled_activation_steps();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
//
|
||
|
// An event handler is like an event listener, but it registered
|
||
|
// by setting an IDL or content attribute like onload or onclick.
|
||
|
// There can only be one of these at a time for any event type.
|
||
|
// This is an internal method for the attribute accessors and
|
||
|
// content attribute handlers that need to register events handlers.
|
||
|
// The type argument is the same as in addEventListener().
|
||
|
// The handler argument is the same as listeners in addEventListener:
|
||
|
// it can be a function or an object. Pass null to remove any existing
|
||
|
// handler. Handlers are always invoked before any listeners of
|
||
|
// the same type. They are not invoked during the capturing phase
|
||
|
// of event dispatch.
|
||
|
//
|
||
|
_setEventHandler: function _setEventHandler(type, handler) {
|
||
|
if (!this._handlers) this._handlers = Object.create(null);
|
||
|
this._handlers[type] = handler;
|
||
|
},
|
||
|
|
||
|
_getEventHandler: function _getEventHandler(type) {
|
||
|
return (this._handlers && this._handlers[type]) || null;
|
||
|
}
|
||
|
|
||
|
};
|