// Copyright 2005 Google Inc. All Rights Reserved
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in
// the documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
/**
* @fileoverview Event Manager.
*
* Provides an abstracted interface to the browsers' event
* systems. Based on Aaron's listen(), this uses an indirect lookup of listener
* functions to avoid circular references between DOM (in IE) or XPCOM
* (in Mozilla) objects which leak memory. This makes it easier to write OO
* Javascript/DOM code.
*
* It simulates capture & bubble in Internet Explorer.
*
* The listeners will also automagically have their event objects patched, so
* your handlers don't need to worry about the browser.
*
* Example usage:
*
* goog.events.listen(myNode, 'click', function(e) { alert('woo') });
* goog.events.listen(myNode, 'mouseover', mouseHandler, true);
* goog.events.unlisten(myNode, 'mouseover', mouseHandler, true);
* goog.events.removeAll(myNode);
* goog.events.removeAll();
*
*
* @supported IE6, IE7, FF1.5+, Safari, Opera 9 (key codes are problematic in
* Safari and Opera, preventDefault is also not properly patched in Safari).
*/
// This uses 3 lookup tables/trees.
// listenerTree_ is a tree of type -> capture -> src hash code -> [Listener]
// listeners_ is a map of key -> [Listener]
//
// The key is a field of the Listener. The Listener class also has the type,
// capture and the src so one can always trace back in the tree
//
// sources_: src hc -> [Listener]
goog.provide('goog.events');
goog.provide('goog.events.EventType');
goog.require('goog.array');
goog.require('goog.debug.errorHandlerWeakDep');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.Event');
goog.require('goog.events.Listener');
goog.require('goog.object');
goog.require('goog.structs.SimplePool');
goog.require('goog.userAgent');
/**
* Container for storing event listeners and their proxies
* @private
* @type {Object}
*/
goog.events.listeners_ = {};
/**
* The root of the listener tree
* @private
* @type {Object}
*/
goog.events.listenerTree_ = {};
/**
* Lookup for mapping source hash codes to listeners
* @private
* @type {Object}
*/
goog.events.sources_ = {};
/**
* Initial count for the objectPool_
* @type {number}
*/
goog.events.OBJECT_POOL_INITIAL_COUNT = 0;
/**
* Max count for the objectPool_
* @type {number}
*/
goog.events.OBJECT_POOL_MAX_COUNT = 600;
/**
* SimplePool to cache the lookup objects. This was implemented to make IE6
* performance better and removed an object allocation in goog.events.listen
* when in steady state.
* @type {goog.structs.SimplePool}
* @private
*/
goog.events.objectPool_ = new goog.structs.SimplePool(
goog.events.OBJECT_POOL_INITIAL_COUNT,
goog.events.OBJECT_POOL_MAX_COUNT);
// Override to add the count_ fields
goog.events.objectPool_.setCreateObjectFn(function() {
return {count_: 0, remaining_: 0};
});
// Override dispose method to prevent for in loop.
goog.events.objectPool_.setDisposeObjectFn(function(obj) {
obj.count_ = 0;
// No need to reset remaining_ since it is always reset before it is used.
});
/**
* Initial count for the arrayPool_
* @type {number}
*/
goog.events.ARRAY_POOL_INITIAL_COUNT = 0;
/**
* Max count for the arrayPool_
* @type {number}
*/
goog.events.ARRAY_POOL_MAX_COUNT = 600;
/**
* SimplePool to cache the type arrays. This was implemented to make IE6
* performance better and removed an object allocation in goog.events.listen
* when in steady state.
* @type {goog.structs.SimplePool}
* @private
*/
goog.events.arrayPool_ = new goog.structs.SimplePool(
goog.events.ARRAY_POOL_INITIAL_COUNT,
goog.events.ARRAY_POOL_MAX_COUNT);
// Override create function to return an array.
goog.events.arrayPool_.setCreateObjectFn(function() {
return [];
});
// Override dispose method to prevent for in loop.
goog.events.arrayPool_.setDisposeObjectFn(function(obj) {
obj.length = 0;
delete obj.locked_;
delete obj.needsCleanup_;
});
/**
* Initial count for the handleEventProxyPool_
* @type {number}
*/
goog.events.HANDLE_EVENT_PROXY_POOL_INITIAL_COUNT = 0;
/**
* Max count for the handleEventProxyPool_
* @type {number}
*/
goog.events.HANDLE_EVENT_PROXY_POOL_MAX_COUNT = 600;
/**
* SimplePool to cache the handle event proxy. This was implemented to make IE6
* performance better and removed an object allocation in goog.events.listen
* when in steady state.
* @type {goog.structs.SimplePool}
* @private
*/
goog.events.handleEventProxyPool_ = new goog.structs.SimplePool(
goog.events.HANDLE_EVENT_PROXY_POOL_INITIAL_COUNT,
goog.events.HANDLE_EVENT_PROXY_POOL_MAX_COUNT);
goog.events.handleEventProxyPool_.setCreateObjectFn(function() {
// Use a local var f to prevent one allocation.
var f = function(eventObject) {
return goog.events.handleBrowserEvent_.call(f.src, f.key, eventObject);
};
return f;
});
/**
* Initial count for the listenerPool_
* @type {number}
*/
goog.events.LISTENER_POOL_INITIAL_COUNT = 0;
/**
* Max count for the listenerPool_
* @type {number}
*/
goog.events.LISTENER_POOL_MAX_COUNT = 600;
/**
* Function for creating a listener for goog.events.listenerPool_. This could
* be an anonymous function below but the JSCompiler seems to have a bug where
* it thinks goog.events.Listener is not referenced if it's only referenced from
* the anonymous function.
* @return {goog.events.Listener} A new listener.
* @private
*/
goog.events.createListenerFunction_ = function() {
return new goog.events.Listener();
};
/**
* SimplePool to cache the listener objects. This was implemented to make IE6
* performance better and removed an object allocation in goog.events.listen
* when in steady state.
* @type {goog.structs.SimplePool}
* @private
*/
goog.events.listenerPool_ = new goog.structs.SimplePool(
goog.events.LISTENER_POOL_INITIAL_COUNT,
goog.events.LISTENER_POOL_MAX_COUNT);
goog.events.listenerPool_.setCreateObjectFn(
goog.events.createListenerFunction_);
/**
* Initial count for the eventPool_
* @type {number}
*/
goog.events.EVENT_POOL_INITIAL_COUNT = 0;
/**
* Max count for the eventPool_
* @type {number}
*/
goog.events.EVENT_POOL_MAX_COUNT = 600;
/**
* Function for creating an event object for goog.events.eventPool_.
* @return {goog.events.BrowserEvent} Event object.
* @private
*/
goog.events.createEventFunction_ = function() {
return new goog.events.BrowserEvent();
};
/**
* Created the BrowserEvent object pool.
* @return {goog.structs.SimplePool?} The event pool for IE browsers,
* null for other browsers.
* @private
*/
goog.events.createEventPool_ = function() {
var eventPool = null;
if (goog.userAgent.IE) {
eventPool = new goog.structs.SimplePool(
goog.events.EVENT_POOL_INITIAL_COUNT,
goog.events.EVENT_POOL_MAX_COUNT);
eventPool.setCreateObjectFn(goog.events.createEventFunction_);
}
return eventPool;
};
/**
* SimplePool to cache the event objects. This was implemented to make IE6
* performance better and removed an object allocation in
* goog.events.handleBrowserEvent_ when in steady state.
* This pool is only used for IE events.
* @type {goog.structs.SimplePool?}
* @private
*/
goog.events.eventPool_ = goog.events.createEventPool_();
/**
* String used to prepend to IE event types. Not a constant so that it is not
* inlined.
* @type {string}
* @private
*/
goog.events.onString_ = 'on';
/**
* Map of computed on strings for IE event types. Caching this removes an extra
* object allocation in goog.events.listen which improves IE6 performance.
* @type {Object}
* @private
*/
goog.events.onStringMap_ = {};
/**
* Separator used to split up the various parts of an event key, to help avoid
* the possibilities of collisions.
* @type {string}
* @private
*/
goog.events.keySeparator_ = '_';
/**
* Adds an event listener for a specific event on a DOM Node or an object that
* has implemented {@link goog.events.EventTarget}. A listener can only be
* added once to an object and if it is added again the key for the listener
* is returned.
*
* @param {EventTarget|goog.events.EventTarget} src The node to listen to
* events on.
* @param {string|Array.} type Event type or array of event types.
* @param {Function|Object} listener Callback method, or an object with a
* handleEvent function.
* @param {boolean} opt_capt Fire in capture phase?.
* @param {Object} opt_handler Element in who's scope to call the listener.
* @return {number?} Unique key for the listener.
*/
goog.events.listen = function(src, type, listener, opt_capt, opt_handler) {
if (!type) {
throw Error('Invalid event type');
} else if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
goog.events.listen(src, type[i], listener, opt_capt, opt_handler);
}
return null;
} else {
var capture = !!opt_capt;
var map = goog.events.listenerTree_;
if (!(type in map)) {
map[type] = goog.events.objectPool_.getObject();
}
map = map[type];
if (!(capture in map)) {
map[capture] = goog.events.objectPool_.getObject();
map.count_++;
}
map = map[capture];
var srcHashCode = goog.getHashCode(src);
var listenerArray, listenerObj;
// The remaining_ property is used to be able to short circuit the iteration
// of the event listeners.
//
// Increment the remaining event listeners to call even if this event might
// already have been fired. At this point we do not know if the event has
// been fired and it is too expensive to find out. By incrementing it we are
// guaranteed that we will not skip any event listeners.
map.remaining_++;
// Do not use srcHashCode in map here since that will cast the number to a
// string which will allocate one string object.
if (!map[srcHashCode]) {
listenerArray = map[srcHashCode] = goog.events.arrayPool_.getObject();
map.count_++;
} else {
listenerArray = map[srcHashCode];
// Ensure that the listeners do not already contain the current listener
for (var i = 0; i < listenerArray.length; i++) {
listenerObj = listenerArray[i];
if (listenerObj.listener == listener &&
listenerObj.handler == opt_handler) {
// If this listener has been removed we should not return its key. It
// is OK that we create new listenerObj below since the removed one
// will be cleaned up later.
if (listenerObj.removed) {
break;
}
// We already have this listener. Return its key.
return listenerArray[i].key;
}
}
}
var proxy = goog.events.handleEventProxyPool_.getObject();
proxy.src = src;
listenerObj = goog.events.listenerPool_.getObject();
listenerObj.init(listener, proxy, src, type, capture, opt_handler);
var key = listenerObj.key;
proxy.key = key;
listenerArray.push(listenerObj);
goog.events.listeners_[key] = listenerObj;
if (!goog.events.sources_[srcHashCode]) {
goog.events.sources_[srcHashCode] = goog.events.arrayPool_.getObject();
}
goog.events.sources_[srcHashCode].push(listenerObj);
// Attach the proxy through the browser's API
if (src.addEventListener) {
if (src == goog.global || !src.customEvent_) {
src.addEventListener(type, proxy, capture);
}
} else {
// The else above used to be else if (src.attachEvent) and then there was
// another else statement that threw an exception warning the developer
// they made a mistake. This resulted in an extra object allocation in IE6
// due to a wrapper object that had to be implemented around the element
// and so was removed.
src.attachEvent(goog.events.getOnString_(type), proxy);
}
return key;
}
};
/**
* Adds an event listener for a specific event on a DomNode or an object that
* has implemented {@link goog.events.EventTarget}. After the event has fired
* the event listener is removed from the target.
*
* @param {EventTarget|goog.events.EventTarget} src The node to listen to
* events on.
* @param {string|Array.} type Event type or array of event types.
* @param {Function} listener Callback method.
* @param {boolean} opt_capt Fire in capture phase?.
* @param {Object} opt_handler Element in who's scope to call the listener.
* @return {number?} Unique key for the listener.
*/
goog.events.listenOnce = function(src, type, listener, opt_capt, opt_handler) {
if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
goog.events.listenOnce(src, type[i], listener, opt_capt, opt_handler);
}
return null;
}
var key = goog.events.listen(src, type, listener, opt_capt, opt_handler);
var listenerObj = goog.events.listeners_[key];
listenerObj.callOnce = true;
return key;
};
/**
* Removes an event listener which was added with listen().
*
* @param {EventTarget|goog.events.EventTarget} src The target to stop
* listening to events on.
* @param {string|Array.} type The name of the event without the 'on'
* prefix.
* @param {Function} listener The listener function to remove.
* @param {boolean} opt_capt In DOM-compliant browsers, this determines
* whether the listener is fired during the capture or bubble phase of the
* event.
* @param {Object} opt_handler Element in who's scope to call the listener.
* @return {boolean?} indicating whether the listener was there to remove.
*/
goog.events.unlisten = function(src, type, listener, opt_capt, opt_handler) {
if (goog.isArray(type)) {
for (var i = 0; i < type.length; i++) {
goog.events.unlisten(src, type[i], listener, opt_capt, opt_handler);
}
return null;
}
var capture = !!opt_capt;
var listenerArray = goog.events.getListeners_(src, type, capture);
if (!listenerArray) {
return false;
}
for (var i = 0; i < listenerArray.length; i++) {
if (listenerArray[i].listener == listener &&
listenerArray[i].capture == capture &&
listenerArray[i].handler == opt_handler) {
return goog.events.unlistenByKey(listenerArray[i].key);
}
}
return false;
};
/**
* Removes an event listener which was added with listen() by the key
* returned by listen().
*
* @param {number} key The key returned by listen() for this event listener.
* @return {boolean} indicating whether the listener was there to remove.
*/
goog.events.unlistenByKey = function(key) {
// Do not use key in listeners here since that will cast the number to a
// string which will allocate one string object.
if (!goog.events.listeners_[key]) {
return false;
}
var listener = goog.events.listeners_[key];
if (listener.removed) {
return false;
}
var src = listener.src;
var type = listener.type;
var proxy = listener.proxy;
var capture = listener.capture;
if (src.removeEventListener) {
// EventTarget calls unlisten so we need to ensure that the source is not
// an event target to prevent re-entry.
// TODO(arv): What is this goog.global for? Why would anyone listen to
// events on the [[Global]] object? Is it supposed to be window? Why would
// we not want to allow removing event listeners on the window?
if (src == goog.global || !src.customEvent_) {
src.removeEventListener(type, proxy, capture);
}
} else if (src.detachEvent) {
src.detachEvent(goog.events.getOnString_(type), proxy);
}
var srcHashCode = goog.getHashCode(src);
var listenerArray = goog.events.listenerTree_[type][capture][srcHashCode];
// In a perfect implementation we would decrement the remaining_ field here
// but then we would need to know if the listener has already been fired or
// not. We therefore skip doing this and in this uncommon case the entire
// ancestor chain will need to be traversed as before.
// Remove from sources_
if (goog.events.sources_[srcHashCode]) {
var sourcesArray = goog.events.sources_[srcHashCode];
goog.array.remove(sourcesArray, listener);
if (sourcesArray.length == 0) {
delete goog.events.sources_[srcHashCode];
}
}
listener.removed = true;
listenerArray.needsCleanup_ = true;
goog.events.cleanUp_(type, capture, srcHashCode, listenerArray);
delete goog.events.listeners_[key];
return true;
};
/**
* Cleans up the listener array as well as the listener tree
* @param {string} type The type of the event.
* @param {boolean} capture Whether to clean up capture phase listeners instead
* bubble phase listeners.
* @param {number} srcHashCode The hash code of the source.
* @param {Array.} listenerArray The array being cleaned.
* @private
*/
goog.events.cleanUp_ = function(type, capture, srcHashCode, listenerArray) {
// The listener array gets locked during the dispatch phase so that removals
// of listeners during this phase does not screw up the indeces. This method
// is called after we have removed a listener as well as after the dispatch
// phase in case any listeners were removed.
if (!listenerArray.locked_) { // catches both 0 and not set
if (listenerArray.needsCleanup_) {
// Loop over the listener array and remove listeners that have removed set
// to true. This could have been done with filter or something similar but
// we want to change the array in place and we want to minimize
// allocations. Adding a listener during this phase adds to the end of the
// array so that works fine as long as the length is rechecked every in
// iteration.
for (var oldIndex = 0, newIndex = 0;
oldIndex < listenerArray.length;
oldIndex++) {
if (listenerArray[oldIndex].removed) {
goog.events.listenerPool_.releaseObject(listenerArray[oldIndex]);
continue;
}
if (oldIndex != newIndex) {
listenerArray[newIndex] = listenerArray[oldIndex];
}
newIndex++;
}
listenerArray.length = newIndex;
listenerArray.needsCleanup_ = false;
// In case the length is now zero we release the object.
if (newIndex == 0) {
goog.events.arrayPool_.releaseObject(listenerArray);
delete goog.events.listenerTree_[type][capture][srcHashCode];
goog.events.listenerTree_[type][capture].count_--;
if (goog.events.listenerTree_[type][capture].count_ == 0) {
goog.events.objectPool_.releaseObject(
goog.events.listenerTree_[type][capture]);
delete goog.events.listenerTree_[type][capture];
goog.events.listenerTree_[type].count_--;
}
if (goog.events.listenerTree_[type].count_ == 0) {
goog.events.objectPool_.releaseObject(
goog.events.listenerTree_[type]);
delete goog.events.listenerTree_[type];
}
}
}
}
};
/**
* Removes all listeners from an object, if no object is specified it will
* remove all listeners that have been registered. You can also optionally
* remove listeners of a particular type or capture phase.
*
* @param {Object} opt_obj Object to remove listeners from.
* @param {string} opt_type Type of event to, default is all types.
* @param {boolean} opt_capt Whether to remove the listeners from the capture or
* bubble phase. If unspecified, will remove both.
* @return {number} Number of listeners removed.
*/
goog.events.removeAll = function(opt_obj, opt_type, opt_capt) {
var count = 0;
var noObj = opt_obj == null;
var noType = opt_type == null;
var noCapt = opt_capt == null;
opt_capt = !!opt_capt;
if (!noObj) {
var srcHashCode = goog.getHashCode(/** @type {Object} */ (opt_obj));
if (goog.events.sources_[srcHashCode]) {
var sourcesArray = goog.events.sources_[srcHashCode];
for (var i = sourcesArray.length - 1; i >= 0; i--) {
var listener = sourcesArray[i];
if ((noType || opt_type == listener.type) &&
(noCapt || opt_capt == listener.capture)) {
goog.events.unlistenByKey(listener.key);
count++;
}
}
}
} else {
// Loop over the sources_ map instead of over the listeners_ since it is
// smaller which results in fewer allocations.
goog.object.forEach(goog.events.sources_, function(listeners) {
for (var i = listeners.length - 1; i >= 0; i--) {
var listener = listeners[i];
if ((noType || opt_type == listener.type) &&
(noCapt || opt_capt == listener.capture)) {
goog.events.unlistenByKey(listener.key);
count++;
}
}
});
}
return count;
};
/**
* Gets the listeners for a given object, type and capture phase.
*
* @param {Object} obj Object to get listeners for.
* @param {string} type Event type.
* @param {boolean} capture Capture phase?.
* @return {Array.} Array of listener objects.
*/
goog.events.getListeners = function(obj, type, capture) {
return goog.events.getListeners_(obj, type, capture) || [];
};
/**
* Gets the listeners for a given object, type and capture phase.
*
* @param {Object} obj Object to get listeners for.
* @param {string} type Event type.
* @param {boolean} capture Capture phase?.
* @return {Array.?} Array of listener objects.
* Returns null if object has no lsiteners of that type.
* @private
*/
goog.events.getListeners_ = function(obj, type, capture) {
var map = goog.events.listenerTree_;
if (type in map) {
map = map[type];
if (capture in map) {
map = map[capture];
var objHashCode = goog.getHashCode(obj);
if (map[objHashCode]) {
return map[objHashCode];
}
}
}
return null;
};
/**
* Gets the goog.events.Listener for the event or null if no such listener is
* in use.
*
* @param {EventTarget|goog.events.EventTarget} src The node to stop
* listening to events on.
* @param {string} type The name of the event without the 'on' prefix.
* @param {Function} listener The listener function to remove.
* @param {boolean} opt_capt In DOM-compliant browsers, this determines
* whether the listener is fired during the
* capture or bubble phase of the event.
* @param {Object} opt_handler Element in who's scope to call the listener.
* @return {goog.events.Listener?} the found listener or null if not found.
*/
goog.events.getListener = function(src, type, listener, opt_capt, opt_handler) {
var capture = !!opt_capt;
var listenerArray = goog.events.getListeners_(src, type, capture);
if (listenerArray) {
for (var i = 0; i < listenerArray.length; i++) {
if (listenerArray[i].listener == listener &&
listenerArray[i].capture == capture &&
listenerArray[i].handler == opt_handler) {
// We already have this listener. Return its key.
return listenerArray[i];
}
}
}
return null;
};
/**
* Returns whether an event target has any active listeners matching the
* specified signature. If either the type or capture parameters are
* unspecified, the function will match on the remaining criteria.
*
* @param {EventTarget|goog.events.EventTarget} obj Target to get listeners for.
* @param {string} opt_type Event type.
* @param {boolean} opt_capture Whether to check for capture or bubble-phase
* listeners.
* @return {boolean} Whether an event target has one or more listeners matching
* the requested type and/or capture phase.
*/
goog.events.hasListener = function(obj, opt_type, opt_capture) {
var objHashCode = goog.getHashCode(obj)
var listeners = goog.events.sources_[objHashCode];
if (listeners) {
var hasType = goog.isDef(opt_type);
var hasCapture = goog.isDef(opt_capture);
if (hasType && hasCapture) {
// Lookup in the listener tree whether the specified listener exists.
var map = goog.events.listenerTree_[opt_type]
return !!map && !!map[opt_capture] && objHashCode in map[opt_capture];
} else if (!(hasType || hasCapture)) {
// Simple check for whether the event target has any listeners at all.
return true;
} else {
// Iterate through the listeners for the event target to find a match.
return goog.array.some(listeners, function(listener) {
return (hasType && listener.type == opt_type) ||
(hasCapture && listener.capture == opt_capture);
});
}
}
return false;
};
/**
* Provides a nice string showing the normalized event objects public members
* @param {Object} e Event Object.
* @return {string} String of the public members of the normalized event object.
*/
goog.events.expose = function(e) {
var str = [];
for (var key in e) {
if (e[key] && e[key].id) {
str.push(key + ' = ' + e[key] + ' (' + e[key].id + ')');
} else {
str.push(key + ' = ' + e[key]);
}
}
return str.join('\n');
};
/**
* Constants for event names.
* @enum {string}
*/
goog.events.EventType = {
// Mouse events
CLICK: 'click',
DBLCLICK: 'dblclick',
MOUSEDOWN: 'mousedown',
MOUSEUP: 'mouseup',
MOUSEOVER: 'mouseover',
MOUSEOUT: 'mouseout',
MOUSEMOVE: 'mousemove',
SELECTSTART: 'selectstart', // IE, Safari, Chrome
// Key events
KEYPRESS: 'keypress',
KEYDOWN: 'keydown',
KEYUP: 'keyup',
// Focus
BLUR: 'blur',
FOCUS: 'focus',
DEACTIVATE: 'deactivate', // IE only
// TODO(pupius): Test these. I experienced problems with DOMFocusIn, the event
// just wasn't firing.
FOCUSIN: goog.userAgent.IE ? 'focusin' : 'DOMFocusIn',
FOCUSOUT: goog.userAgent.IE ? 'focusout' : 'DOMFocusOut',
// Forms
CHANGE: 'change',
SELECT: 'select',
SUBMIT: 'submit',
// Misc
LOAD: 'load',
UNLOAD: 'unload',
ERROR: 'error',
HELP: 'help',
RESIZE: 'resize',
SCROLL: 'scroll',
READYSTATECHANGE: 'readystatechange',
CONTEXTMENU: 'contextmenu'
};
/**
* Returns a string wth on prepended to the specified type. This is used for IE
* which expects "on" to be prepended. This function caches the string in order
* to avoid extra allocations in steady state.
* @param {string} type Event type strng.
* @return {string} The type string with 'on' prepended.
* @private
*/
goog.events.getOnString_ = function(type) {
if (type in goog.events.onStringMap_) {
return goog.events.onStringMap_[type];
}
return goog.events.onStringMap_[type] = goog.events.onString_ + type;
};
/**
* Fires an object's listeners of a particular type and phase
*
* @param {Object} obj Object who's listeners to call.
* @param {string} type Event type.
* @param {boolean} capture Which event phase.
* @param {Object} eventObject Event object to be passed to listener.
* @return {boolean} True if all listeners returned true else false.
*/
goog.events.fireListeners = function(obj, type, capture, eventObject) {
var map = goog.events.listenerTree_;
if (type in map) {
map = map[type];
if (capture in map) {
return goog.events.fireListeners_(map[capture], obj, type,
capture, eventObject);
}
}
return true;
};
/**
* Fires an object's listeners of a particular type and phase.
*
* @param {Object} map Object with listeners in it.
* @param {Object} obj Object who's listeners to call.
* @param {string} type Event type.
* @param {boolean} capture Which event phase.
* @param {Object} eventObject Event object to be passed to listener.
* @return {boolean} True if all listeners returned true else false.
* @private
*/
goog.events.fireListeners_ = function(map, obj, type, capture, eventObject) {
var retval = 1;
var objHashCode = goog.getHashCode(obj);
if (map[objHashCode]) {
map.remaining_--;
var listenerArray = map[objHashCode];
// If locked_ is not set (and if already 0) initialize it to 1.
if (!listenerArray.locked_) {
listenerArray.locked_ = 1;
} else {
listenerArray.locked_++;
}
try {
// Events added in the dispatch phase should not be dispatched in
// the current dispatch phase. They will be included in the next
// dispatch phase though.
var length = listenerArray.length;
for (var i = 0; i < length; i++) {
var listener = listenerArray[i];
// We might not have a listener if the listener was removed.
if (listener && !listener.removed) {
retval &=
goog.events.fireListener(listener, eventObject) !== false;
}
}
} finally {
listenerArray.locked_--;
goog.events.cleanUp_(type, capture, objHashCode, listenerArray);
}
}
return Boolean(retval);
};
/**
* Fires a listener with a set of arguments
*
* @param {goog.events.Listener} listener The listener object to call.
* @param {Object} eventObject The event object to pass to the listener.
* @return {boolean} Result of listener.
*/
goog.events.fireListener = function(listener, eventObject) {
var rv = listener.handleEvent(eventObject);
if (listener.callOnce) {
goog.events.unlistenByKey(listener.key);
}
return rv;
};
/**
* Gets the total number of listeners currently in the system.
* @return {number} Number of listeners.
*/
goog.events.getTotalListenerCount = function() {
return goog.object.getCount(goog.events.listeners_);
};
/**
* Dispatches an event (or event like object) and calls all listeners
* listening for events of this type. The type of the event is decided by the
* type property on the event object.
*
* If any of the listeners returns false OR calls preventDefault then this
* function will return false. If one of the capture listeners calls
* stopPropagation, then the bubble listeners won't fire.
*
* @param {goog.events.EventTarget} src The event target.
* @param {string|Object|goog.events.Event} e Event object.
* @return {boolean} If anyone called preventDefault on the event object (or
* if any of the handlers returns false) this will also return false.
* If there are no handlers, or if all handlers return true, this returns
* true.
*/
goog.events.dispatchEvent = function(src, e) {
// If accepting a string or object, create a custom event object so that
// preventDefault and stopPropagation work with the event.
if (goog.isString(e)) {
e = new goog.events.Event(e, src);
} else if (!(e instanceof goog.events.Event)) {
var oldEvent = e;
e = new goog.events.Event(e.type, src);
goog.object.extend(e, oldEvent);
} else {
e.target = e.target || src;
}
var rv = 1, ancestors;
var type = e.type;
var map = goog.events.listenerTree_;
if (!(type in map)) {
return true;
}
map = map[type];
var hasCapture = true in map;
var hasBubble = false in map;
var targetsMap;
if (hasCapture) {
// Build ancestors now
ancestors = [];
for (var parent = src; parent; parent = parent.getParentEventTarget()) {
ancestors.push(parent);
}
targetsMap = map[true];
targetsMap.remaining_ = targetsMap.count_;
// Call capture listeners
for (var i = ancestors.length - 1;
!e.propagationStopped_ && i >= 0 && targetsMap.remaining_;
i--) {
e.currentTarget = ancestors[i];
rv &= goog.events.fireListeners_(targetsMap, ancestors[i], e.type,
true, e) &&
e.returnValue_ != false;
}
}
if (hasBubble) {
targetsMap = map[false];
targetsMap.remaining_ = targetsMap.count_;
if (hasCapture) { // We have the ancestors.
// Call bubble listeners
for (var i = 0; !e.propagationStopped_ && i < ancestors.length &&
targetsMap.remaining_;
i++) {
e.currentTarget = ancestors[i];
rv &= goog.events.fireListeners_(targetsMap, ancestors[i], e.type,
false, e) &&
e.returnValue_ != false;
}
} else {
// In case we don't have capture we don't have to build up the
// ancestors array.
for (var current = src;
!e.propagationStopped_ && current && targetsMap.remaining_;
current = current.getParentEventTarget()) {
e.currentTarget = current;
rv &= goog.events.fireListeners_(targetsMap, current, e.type,
false, e) &&
e.returnValue_ != false;
}
}
}
return Boolean(rv);
};
/**
* Installs exception protection for the browser event entry point using the
* given error handler.
*
* @param {goog.debug.ErrorHandler} errorHandler Error handler with which to
* protect the entry point.
* @param {boolean} opt_tracers Whether to install tracers around the browser
* event entry point.
*/
goog.events.protectBrowserEventEntryPoint = function(
errorHandler, opt_tracers) {
goog.events.handleBrowserEvent_ = errorHandler.protectEntryPoint(
goog.events.handleBrowserEvent_, opt_tracers);
};
/**
* Handles an event and dispatches it to the correct listeners. This
* function is a proxy for the real listener the user specified.
*
* @param {string} key Unique key for the listener.
* @param {Object} opt_evt Optional event object that gets passed in via the
* native event handlers.
* @return {boolean} Result of the event handler.
* @this {goog.events.EventTarget|Object|Element} The object or Element that
* fired the event.
* @private
*/
goog.events.handleBrowserEvent_ = function(key, opt_evt) {
// If the listener isn't there it was probably removed when processing
// another listener on the same event (e.g. the later listener is
// not managed by closure so that they are both fired under IE)
if (!goog.events.listeners_[key]) {
return true;
}
var listener = goog.events.listeners_[key];
var type = listener.type;
var map = goog.events.listenerTree_;
if (!(type in map)) {
return true;
}
map = map[type];
var retval, targetsMap;
if (goog.userAgent.IE) {
var ieEvent = opt_evt || goog.getObjectByName('window.event');
// Check if we have any capturing event listeners for this type.
var hasCapture = true in map;
var hasBubble = false in map;
if (hasCapture) {
if (goog.events.isMarkedIeEvent_(ieEvent)) {
return true;
}
goog.events.markIeEvent_(ieEvent);
}
var evt = goog.events.eventPool_.getObject();
evt.init(ieEvent, this);
retval = true;
try {
if (hasCapture) {
// Use a pool so we don't allocate a new array
var ancestors = goog.events.arrayPool_.getObject();
for (var parent = evt.currentTarget;
parent;
parent = parent.parentNode) {
ancestors.push(parent);
}
targetsMap = map[true];
targetsMap.remaining_ = targetsMap.count_;
// Call capture listeners
for (var i = ancestors.length - 1;
!evt.propagationStopped_ && i >= 0 && targetsMap.remaining_;
i--) {
evt.currentTarget = ancestors[i];
retval &= goog.events.fireListeners_(targetsMap, ancestors[i], type,
true, evt);
}
if (hasBubble) {
targetsMap = map[false];
targetsMap.remaining_ = targetsMap.count_;
// Call bubble listeners
for (var i = 0;
!evt.propagationStopped_ && i < ancestors.length &&
targetsMap.remaining_;
i++) {
evt.currentTarget = ancestors[i];
retval &= goog.events.fireListeners_(targetsMap, ancestors[i], type,
false, evt);
}
}
} else {
// Bubbling, let IE handle the propagation.
retval = goog.events.fireListener(listener, evt);
}
} finally {
if (ancestors) {
ancestors.length = 0;
goog.events.arrayPool_.releaseObject(ancestors);
}
evt.dispose();
goog.events.eventPool_.releaseObject(evt);
}
return retval;
} // IE
// Caught a non-IE DOM event. 1 additional argument which is the event object
var be = new goog.events.BrowserEvent(/** @type {Event} */ (opt_evt), this);
try {
retval = goog.events.fireListener(listener, be);
} finally {
be.dispose();
}
return retval;
};
/**
* This is used to mark the IE event object so we do not do the Closure pass
* twice for a bubbling event.
* @param {Object} e The IE browser event.
* @private
*/
goog.events.markIeEvent_ = function(e) {
// Only the keyCode and the returnValue can be changed. We use keyCode for
// non keyboard events.
// event.returnValue is a bit more tricky. It is undefined by default. A
// boolean false prevents the default action. In a window.onbeforeunload and
// the returnValue is non undefined it will be alerted. However, we will only
// modify the returnValue for keyboard events. We can get a problem if non
// closure events sets the keyCode or the returnValue
var useReturnValue = false;
if (e.keyCode == 0) {
// We cannot change the keyCode in case that srcElement is input[type=file].
// We could test that that is the case but that would allocate 3 objects.
// If we use try/catch we will only allocate extra objects in the case of a
// failure.
/** @preserveTry */
try {
e.keyCode = -1;
return;
} catch (ex) {
useReturnValue = true;
}
}
if (useReturnValue ||
/** @type {boolean|undefined} */ e.returnValue == undefined) {
e.returnValue = true;
}
};
/**
* This is used to check if an IE event has already been handled by the Closure
* system so we do not do the Closure pass twice for a bubbling event.
* @param {Event} e The IE browser event.
* @return {boolean} True if the event object has been marked.
* @private
* @notypecheck TODO(nicksantos): Fix this.
*/
goog.events.isMarkedIeEvent_ = function(e) {
return e.keyCode < 0 || e.returnValue != undefined;
};