UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

990 lines (814 loc) 29.4 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2007-2008 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Fabian Jakobs (fjakobs) * Sebastian Werner (wpbasti) ************************************************************************ */ /** * Wrapper for browser DOM event handling for each browser window/frame. * * @require(qx.bom.Event) */ qx.Class.define("qx.event.Manager", { extend : Object, implement: [ qx.core.IDisposable ], /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * Creates a new instance of the event handler. * * @param win {Window} The DOM window this manager handles the events for * @param registration {qx.event.Registration} The event registration to use */ construct : function(win, registration) { // Assign window object this.__window = win; this.__windowId = qx.core.ObjectRegistry.toHashCode(win); this.__registration = registration; // Register to the page unload event. // Only for iframes and other secondary documents. if (win.qx !== qx) { var self = this; var method = function () { qx.bom.Event.removeNativeListener(win, "unload", method); self.dispose(); }; if (qx.core.Environment.get("qx.globalErrorHandling")) { qx.bom.Event.addNativeListener(win, "unload", qx.event.GlobalError.observeMethod(method)); } else { qx.bom.Event.addNativeListener(win, "unload", method); } } // Registry for event listeners this.__listeners = {}; // The handler and dispatcher instances this.__handlers = {}; this.__dispatchers = {}; this.__handlerCache = {}; this.__clearBlackList = new qx.util.DeferredCall(function() { this.__blacklist = null; }, this); this.__clearBlackList.$$blackListCleaner = true; }, /* ***************************************************************************** STATICS ***************************************************************************** */ statics : { /** @type {Integer} Last used ID for an event */ __lastUnique : 0, /** * Returns an unique ID which may be used in combination with a target and * a type to identify an event entry. * * @return {String} The next free identifier (auto-incremented) */ getNextUniqueId : function() { return (this.__lastUnique++) + ""; }, /** @type {Function} global event monitor, called with parameters of target and event */ __globalEventMonitor: null, /** * Returns the global event monitor * * @return {Function?} the global monitor function */ getGlobalEventMonitor: function() { return this.__globalEventMonitor; }, /** * Sets the global event monitor * * @param cb {Function?} the global monitor function */ setGlobalEventMonitor: function(cb) { this.__globalEventMonitor = cb; } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { __registration : null, __listeners : null, __dispatchers : null, __disposeWrapper : null, __handlers : null, __handlerCache : null, __window : null, __windowId : null, __blacklist : null, __clearBlackList : null, /* --------------------------------------------------------------------------- HELPERS --------------------------------------------------------------------------- */ /** * Get the window instance the event manager is responsible for * * @return {Window} DOM window instance */ getWindow : function() { return this.__window; }, /** * Get the hashcode of the manager's window * * @return {String} The window's hashcode */ getWindowId : function() { return this.__windowId; }, /** * Returns an instance of the given handler class for this manager(window). * * @param clazz {Class} Any class which implements {@link qx.event.IEventHandler} * @return {Object} The instance used by this manager */ getHandler : function(clazz) { var handler = this.__handlers[clazz.classname]; if (handler) { return handler; } return this.__handlers[clazz.classname] = new clazz(this); }, /** * Returns an instance of the given dispatcher class for this manager(window). * * @param clazz {Class} Any class which implements {@link qx.event.IEventHandler} * @return {Object} The instance used by this manager */ getDispatcher : function(clazz) { var dispatcher = this.__dispatchers[clazz.classname]; if (dispatcher) { return dispatcher; } return this.__dispatchers[clazz.classname] = new clazz(this, this.__registration); }, /* --------------------------------------------------------------------------- EVENT LISTENER MANAGEMENT --------------------------------------------------------------------------- */ /** * Get a copy of all event listeners for the given combination * of target, event type and phase. * * This method is especially useful and for event handlers to * to query the listeners registered in the manager. * * @param target {Object} Any valid event target * @param type {String} Event type * @param capture {Boolean ? false} Whether the listener is for the * capturing phase of the bubbling phase. * @return {Array | null} Array of registered event handlers. May return * null when no listener were found. */ getListeners : function(target, type, capture) { var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; if (!targetMap) { return null; } var entryKey = type + (capture ? "|capture" : "|bubble"); var entryList = targetMap[entryKey]; return entryList ? entryList.concat() : null; }, /** * Returns all registered listeners. * * @internal * * @return {Map} All registered listeners. The key is the hash code form an object. */ getAllListeners : function() { return this.__listeners; }, /** * Returns a serialized array of all events attached on the given target. * * @param target {Object} Any valid event target * @return {Map[]} Array of maps where everyone contains the keys: * <code>handler</code>, <code>self</code>, <code>type</code> and <code>capture</code>. */ serializeListeners : function(target) { var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; var result = []; if (targetMap) { var indexOf, type, capture, entryList, entry; for (var entryKey in targetMap) { indexOf = entryKey.indexOf("|"); type = entryKey.substring(0, indexOf); capture = entryKey.charAt(indexOf+1) == "c"; entryList = targetMap[entryKey]; for (var i=0, l=entryList.length; i<l; i++) { entry = entryList[i]; result.push( { self: entry.context, handler: entry.handler, type: type, capture: capture }); } } } return result; }, /** * This method might be used to temporally remove all events * directly attached to the given target. This do not work * have any effect on bubbling events normally. * * This is mainly thought for detaching events in IE, before * cloning them. It also removes all leak scenarios * when unloading a document and may be used here as well. * * @internal * @param target {Object} Any valid event target * @param enable {Boolean} Whether to enable or disable the events */ toggleAttachedEvents : function(target, enable) { var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; if (targetMap) { var indexOf, type, capture, entryList; for (var entryKey in targetMap) { indexOf = entryKey.indexOf("|"); type = entryKey.substring(0, indexOf); capture = entryKey.charCodeAt(indexOf+1) === 99; // checking for character "c". entryList = targetMap[entryKey]; if (enable) { this.__registerAtHandler(target, type, capture); } else { this.__unregisterAtHandler(target, type, capture); } } } }, /** * Check whether there are one or more listeners for an event type * registered at the target. * * @param target {Object} Any valid event target * @param type {String} The event type * @param capture {Boolean ? false} Whether to check for listeners of * the bubbling or of the capturing phase. * @return {Boolean} Whether the target has event listeners of the given type. */ hasListener : function(target, type, capture) { if (qx.core.Environment.get("qx.debug")) { if (target == null) { qx.log.Logger.trace(this); throw new Error("Invalid object: " + target); } } var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; if (!targetMap) { return false; } var entryKey = type + (capture ? "|capture" : "|bubble"); var entryList = targetMap[entryKey]; return !!(entryList && entryList.length > 0); }, /** * Imports a list of event listeners at once. This only * works for newly created elements as it replaces * all existing data structures. * * Works with a map of data. Each entry in this map should be a * map again with the keys <code>type</code>, <code>listener</code>, * <code>self</code>, <code>capture</code> and an optional <code>unique</code>. * * The values are identical to the parameters of {@link #addListener}. * For details please have a look there. * * @param target {Object} Any valid event target * @param list {Map} A map where every listener has an unique key. */ importListeners : function(target, list) { if (qx.core.Environment.get("qx.debug")) { if (target == null) { qx.log.Logger.trace(this); throw new Error("Invalid object: " + target); } } var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey] = {}; var clazz = qx.event.Manager; for (var listKey in list) { var item = list[listKey]; var entryKey = item.type + (item.capture ? "|capture" : "|bubble"); var entryList = targetMap[entryKey]; if (!entryList) { entryList = targetMap[entryKey] = []; // This is the first event listener for this type and target // Inform the event handler about the new event // they perform the event registration at DOM level if needed this.__registerAtHandler(target, item.type, item.capture); } // Append listener to list entryList.push( { handler : item.listener, context : item.self, unique : item.unique || (clazz.__lastUnique++) + "" }); } }, /** * Add an event listener to any valid target. The event listener is passed an * instance of {@link qx.event.type.Event} containing all relevant information * about the event as parameter. * * @param target {Object} Any valid event target * @param type {String} Name of the event e.g. "click", "keydown", ... * @param listener {Function} Event listener function * @param self {Object ? null} Reference to the 'this' variable inside * the event listener. When not given, the corresponding dispatcher * usually falls back to a default, which is the target * by convention. Note this is not a strict requirement, i.e. * custom dispatchers can follow a different strategy. * @param capture {Boolean ? false} Whether to attach the event to the * capturing phase or the bubbling phase of the event. The default is * to attach the event handler to the bubbling phase. * @return {String} An opaque ID, which can be used to remove the event listener * using the {@link #removeListenerById} method. * @throws {Error} if the parameters are wrong */ addListener : function(target, type, listener, self, capture) { if (qx.core.Environment.get("qx.debug")) { var msg = "Failed to add event listener for type '"+ type +"'" + " to the target '" + target.classname + "': "; qx.core.Assert.assertObject(target, msg + "Invalid Target."); qx.core.Assert.assertString(type, msg + "Invalid event type."); qx.core.Assert.assertFunctionOrAsyncFunction(listener, msg + "Invalid callback function"); if (capture !== undefined) { qx.core.Assert.assertBoolean(capture, "Invalid capture flag."); } } var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; if (!targetMap) { targetMap = this.__listeners[targetKey] = {}; } var entryKey = type + (capture ? "|capture" : "|bubble"); var entryList = targetMap[entryKey]; if (!entryList) { entryList = targetMap[entryKey] = []; } // This is the first event listener for this type and target // Inform the event handler about the new event // they perform the event registration at DOM level if needed if (entryList.length === 0) { this.__registerAtHandler(target, type, capture); } // Append listener to list var unique = (qx.event.Manager.__lastUnique++) + ""; var entry = { handler : listener, context : self, unique : unique }; entryList.push(entry); return entryKey + "|" + unique; }, /** * Get the event handler class matching the given event target and type * * @param target {var} The event target * @param type {String} The event type * @return {qx.event.IEventHandler|null} The best matching event handler or * <code>null</code>. */ findHandler : function(target, type) { var isDomNode=false, isWindow=false, isObject=false, isDocument = false; var key; if (target.nodeType === 1) { isDomNode = true; key = "DOM_" + target.tagName.toLowerCase() + "_" + type; } else if (target.nodeType === 9) { isDocument = true; key = "DOCUMENT_" + type; } // Please note: // Identical operator does not work in IE (as of version 7) because // document.parentWindow is not identical to window. Crazy stuff. else if (target == this.__window) { isWindow = true; key = "WIN_" + type; } else if (target.classname) { isObject = true; key = "QX_" + target.classname + "_" + type; } else { key = "UNKNOWN_" + target + "_" + type; } var cache = this.__handlerCache; if (cache[key]) { return cache[key]; } var classes = this.__registration.getHandlers(); var IEventHandler = qx.event.IEventHandler; var clazz, instance, supportedTypes, targetCheck; for (var i=0, l=classes.length; i<l; i++) { clazz = classes[i]; // shortcut type check supportedTypes = clazz.SUPPORTED_TYPES; if (supportedTypes && !supportedTypes[type]) { continue; } // shortcut target check targetCheck = clazz.TARGET_CHECK; if (targetCheck) { // use bitwise & to compare for the bitmask! var found = false; if (isDomNode && ((targetCheck & IEventHandler.TARGET_DOMNODE) != 0)) { found = true; } else if (isWindow && ((targetCheck & IEventHandler.TARGET_WINDOW) != 0)) { found = true; } else if (isObject && ((targetCheck & IEventHandler.TARGET_OBJECT) != 0)) { found = true; } else if (isDocument && ((targetCheck & IEventHandler.TARGET_DOCUMENT) != 0)) { found = true; } if (!found) { continue; } } instance = this.getHandler(classes[i]); if (clazz.IGNORE_CAN_HANDLE || instance.canHandleEvent(target, type)) { cache[key] = instance; return instance; } } return null; }, /** * This method is called each time an event listener for one of the * supported events is added using {qx.event.Manager#addListener}. * * @param target {Object} Any valid event target * @param type {String} event type * @param capture {Boolean} Whether to attach the event to the * capturing phase or the bubbling phase of the event. * @throws {Error} if there is no handler for the event */ __registerAtHandler : function(target, type, capture) { var handler = this.findHandler(target, type); if (handler) { handler.registerEvent(target, type, capture); return; } if (qx.core.Environment.get("qx.debug")) { qx.log.Logger.warn( this, "There is no event handler for the event '" + type + "' on target '" + target + "'!" ); } }, /** * Remove an event listener from an event target. * * @param target {Object} Any valid event target * @param type {String} Name of the event * @param listener {Function} The pointer to the event listener * @param self {Object ? null} Reference to the 'this' variable inside * the event listener. * @param capture {Boolean ? false} Whether to remove the event listener of * the bubbling or of the capturing phase. * @return {Boolean} Whether the event was removed successfully (was existant) * @throws {Error} if the parameters are wrong */ removeListener : function(target, type, listener, self, capture) { if (qx.core.Environment.get("qx.debug")) { var msg = "Failed to remove event listener for type '" + type + "'" + " from the target '" + target.classname + "': "; qx.core.Assert.assertObject(target, msg + "Invalid Target."); qx.core.Assert.assertString(type, msg + "Invalid event type."); qx.core.Assert.assertFunction(listener, msg + "Invalid callback function"); if (self !== undefined) { qx.core.Assert.assertObject(self, "Invalid context for callback."); } if (capture !== undefined) { qx.core.Assert.assertBoolean(capture, "Invalid capture flag."); } } var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; if (!targetMap) { return false; } var entryKey = type + (capture ? "|capture" : "|bubble"); var entryList = targetMap[entryKey]; if (!entryList) { return false; } var entry; for (var i=0, l=entryList.length; i<l; i++) { entry = entryList[i]; if (entry.handler === listener && entry.context === self) { qx.lang.Array.removeAt(entryList, i); this.__addToBlacklist(entry.unique); if (entryList.length == 0) { this.__unregisterAtHandler(target, type, capture); } return true; } } return false; }, /** * Removes an event listener from an event target by an ID returned by * {@link #addListener}. * * @param target {Object} The event target * @param id {String} The ID returned by {@link #addListener} * @return {Boolean} <code>true</code> if the handler was removed */ removeListenerById : function(target, id) { if (qx.core.Environment.get("qx.debug")) { var msg = "Failed to remove event listener for id '" + id + "'" + " from the target '" + target.classname + "': "; qx.core.Assert.assertObject(target, msg + "Invalid Target."); qx.core.Assert.assertString(id, msg + "Invalid id type."); } var split = id.split("|"); var type = split[0]; var capture = split[1].charCodeAt(0) == 99; // detect leading "c" var unique = split[2]; var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; if (!targetMap) { return false; } var entryKey = type + (capture ? "|capture" : "|bubble"); var entryList = targetMap[entryKey]; if (!entryList) { return false; } var entry; for (var i=0, l=entryList.length; i<l; i++) { entry = entryList[i]; if (entry.unique === unique) { qx.lang.Array.removeAt(entryList, i); this.__addToBlacklist(entry.unique); if (entryList.length == 0) { this.__unregisterAtHandler(target, type, capture); } return true; } } return false; }, /** * Remove all event listeners, which are attached to the given event target. * * @param target {Object} The event target to remove all event listeners from. * @return {Boolean} Whether the events were existant and were removed successfully. */ removeAllListeners : function(target) { var targetKey = target.$$hash || qx.core.ObjectRegistry.toHashCode(target); var targetMap = this.__listeners[targetKey]; if (!targetMap) { return false; } // Deregister from event handlers var split, type, capture; for (var entryKey in targetMap) { if (targetMap[entryKey].length > 0) { // This is quite expensive, see bug #1283 split = entryKey.split("|"); targetMap[entryKey].forEach(function(entry) { this.__addToBlacklist(entry.unique); }, this); type = split[0]; capture = split[1] === "capture"; this.__unregisterAtHandler(target, type, capture); } } delete this.__listeners[targetKey]; return true; }, /** * Internal helper for deleting the internal listener data structure for * the given targetKey. * * @param targetKey {String} Hash code for the object to delete its * listeners. * * @internal */ deleteAllListeners : function(targetKey) { delete this.__listeners[targetKey]; }, /** * This method is called each time the an event listener for one of the * supported events is removed by using {qx.event.Manager#removeListener} * and no other event listener is listening on this type. * * @param target {Object} Any valid event target * @param type {String} event type * @param capture {Boolean} Whether to attach the event to the * capturing phase or the bubbling phase of the event. * @throws {Error} if there is no handler for the event */ __unregisterAtHandler : function(target, type, capture) { var handler = this.findHandler(target, type); if (handler) { handler.unregisterEvent(target, type, capture); return; } if (qx.core.Environment.get("qx.debug")) { qx.log.Logger.warn( this, "There is no event handler for the event '" + type + "' on target '" + target + "'!" ); } }, /* --------------------------------------------------------------------------- EVENT DISPATCH --------------------------------------------------------------------------- */ /** * Dispatches an event object using the qooxdoo event handler system. The * event will only be visible in event listeners attached using * {@link #addListener}. After dispatching the event object will be pooled * for later reuse or disposed. * * @param target {Object} Any valid event target * @param event {qx.event.type.Event} The event object to dispatch. The event * object must be obtained using {@link qx.event.Registration#createEvent} * and initialized using {@link qx.event.type.Event#init}. * @return {Boolean|qx.Promise} whether the event default was prevented or not. * Returns true, when the event was NOT prevented. * @throws {Error} if there is no dispatcher for the event */ dispatchEvent : function(target, event) { if (qx.core.Environment.get("qx.debug")) { var msg = "Could not dispatch event '" + event + "' on target '" + target.classname +"': "; qx.core.Assert.assertNotUndefined(target, msg + "Invalid event target."); qx.core.Assert.assertNotNull(target, msg + "Invalid event target."); qx.core.Assert.assertInstance(event, qx.event.type.Event, msg + "Invalid event object."); } if (qx.event.Manager.__globalEventMonitor) { try { var preventDefault = event.getDefaultPrevented(); qx.event.Manager.__globalEventMonitor(target, event); if (preventDefault != event.getDefaultPrevented()) { qx.log.Logger.error("Unexpected change by GlobalEventMonitor, modifications to events: "); } }catch (ex) { qx.log.Logger.error("Error in GlobalEventMonitor: " + ex); } } // Preparations var type = event.getType(); if (!event.getBubbles() && !this.hasListener(target, type)) { qx.event.Pool.getInstance().poolObject(event); return true; } if (!event.getTarget()) { event.setTarget(target); } // Interacion data var classes = this.__registration.getDispatchers(); var instance; // Loop through the dispatchers var dispatched = false; var tracker = {}; for (var i=0, l=classes.length; i<l; i++) { instance = this.getDispatcher(classes[i]); // Ask if the dispatcher can handle this event if (instance.canDispatchEvent(target, event, type)) { qx.event.Utils.track(tracker, instance.dispatchEvent(target, event, type)); dispatched = true; break; } } if (!dispatched) { if (qx.core.Environment.get("qx.debug")) { qx.log.Logger.error(this, "No dispatcher can handle event of type " + type + " on " + target); } return true; } return qx.event.Utils.then(tracker, function() { // check whether "preventDefault" has been called var preventDefault = event.getDefaultPrevented(); // Release the event instance to the event pool qx.event.Pool.getInstance().poolObject(event); return !preventDefault; }); }, /** * Dispose the event manager */ dispose : function() { // Remove from manager list this.__registration.removeManager(this); qx.util.DisposeUtil.disposeMap(this, "__handlers"); qx.util.DisposeUtil.disposeMap(this, "__dispatchers"); // Dispose data fields this.__listeners = this.__window = this.__disposeWrapper = null; this.__registration = this.__handlerCache = null; }, /** * Add event to blacklist. * * @param uid {number} unique event id */ __addToBlacklist : function(uid) { if (this.__blacklist === null) { this.__blacklist = {}; this.__clearBlackList.schedule(); } this.__blacklist[uid] = true; }, /** * Check if the event with the given id has been removed and is therefore blacklisted for event handling * * @param uid {number} unique event id * @return {boolean} */ isBlacklisted : function(uid) { return (this.__blacklist !== null && this.__blacklist[uid] === true); } } });