// Copyright 2013 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview A map of listeners that provides utility functions to * deal with listeners on an event target. Used by * {@code goog.events.EventTarget}. * * WARNING: Do not use this class from outside goog.events package. * * @visibility {//closure/goog/bin/sizetests:__pkg__} * @visibility {//closure/goog/events:__pkg__} * @visibility {//closure/goog/labs/events:__pkg__} */ goog.provide('goog.events.ListenerMap'); goog.require('goog.array'); goog.require('goog.events.Listener'); goog.require('goog.object'); /** * Creates a new listener map. * @param {EventTarget|goog.events.Listenable} src The src object. * @constructor * @final */ goog.events.ListenerMap = function(src) { /** @type {EventTarget|goog.events.Listenable} */ this.src = src; /** * Maps of event type to an array of listeners. * @type {!Object>} */ this.listeners = {}; /** * The count of types in this map that have registered listeners. * @private {number} */ this.typeCount_ = 0; }; /** * @return {number} The count of event types in this map that actually * have registered listeners. */ goog.events.ListenerMap.prototype.getTypeCount = function() { return this.typeCount_; }; /** * @return {number} Total number of registered listeners. */ goog.events.ListenerMap.prototype.getListenerCount = function() { var count = 0; for (var type in this.listeners) { count += this.listeners[type].length; } return count; }; /** * Adds an event listener. A listener can only be added once to an * object and if it is added again the key for the listener is * returned. * * Note that a one-off listener will not change an existing listener, * if any. On the other hand a normal listener will change existing * one-off listener to become a normal listener. * * @param {string|!goog.events.EventId} type The listener event type. * @param {!Function} listener This listener callback method. * @param {boolean} callOnce Whether the listener is a one-off * listener. * @param {boolean=} opt_useCapture The capture mode of the listener. * @param {Object=} opt_listenerScope Object in whose scope to call the * listener. * @return {!goog.events.ListenableKey} Unique key for the listener. */ goog.events.ListenerMap.prototype.add = function( type, listener, callOnce, opt_useCapture, opt_listenerScope) { var typeStr = type.toString(); var listenerArray = this.listeners[typeStr]; if (!listenerArray) { listenerArray = this.listeners[typeStr] = []; this.typeCount_++; } var listenerObj; var index = goog.events.ListenerMap.findListenerIndex_( listenerArray, listener, opt_useCapture, opt_listenerScope); if (index > -1) { listenerObj = listenerArray[index]; if (!callOnce) { // Ensure that, if there is an existing callOnce listener, it is no // longer a callOnce listener. listenerObj.callOnce = false; } } else { listenerObj = new goog.events.Listener( listener, null, this.src, typeStr, !!opt_useCapture, opt_listenerScope); listenerObj.callOnce = callOnce; listenerArray.push(listenerObj); } return listenerObj; }; /** * Removes a matching listener. * @param {string|!goog.events.EventId} type The listener event type. * @param {!Function} listener This listener callback method. * @param {boolean=} opt_useCapture The capture mode of the listener. * @param {Object=} opt_listenerScope Object in whose scope to call the * listener. * @return {boolean} Whether any listener was removed. */ goog.events.ListenerMap.prototype.remove = function( type, listener, opt_useCapture, opt_listenerScope) { var typeStr = type.toString(); if (!(typeStr in this.listeners)) { return false; } var listenerArray = this.listeners[typeStr]; var index = goog.events.ListenerMap.findListenerIndex_( listenerArray, listener, opt_useCapture, opt_listenerScope); if (index > -1) { var listenerObj = listenerArray[index]; listenerObj.markAsRemoved(); goog.array.removeAt(listenerArray, index); if (listenerArray.length == 0) { delete this.listeners[typeStr]; this.typeCount_--; } return true; } return false; }; /** * Removes the given listener object. * @param {!goog.events.ListenableKey} listener The listener to remove. * @return {boolean} Whether the listener is removed. */ goog.events.ListenerMap.prototype.removeByKey = function(listener) { var type = listener.type; if (!(type in this.listeners)) { return false; } var removed = goog.array.remove(this.listeners[type], listener); if (removed) { listener.markAsRemoved(); if (this.listeners[type].length == 0) { delete this.listeners[type]; this.typeCount_--; } } return removed; }; /** * Removes all listeners from this map. If opt_type is provided, only * listeners that match the given type are removed. * @param {string|!goog.events.EventId=} opt_type Type of event to remove. * @return {number} Number of listeners removed. */ goog.events.ListenerMap.prototype.removeAll = function(opt_type) { var typeStr = opt_type && opt_type.toString(); var count = 0; for (var type in this.listeners) { if (!typeStr || type == typeStr) { var listenerArray = this.listeners[type]; for (var i = 0; i < listenerArray.length; i++) { ++count; listenerArray[i].markAsRemoved(); } delete this.listeners[type]; this.typeCount_--; } } return count; }; /** * Gets all listeners that match the given type and capture mode. The * returned array is a copy (but the listener objects are not). * @param {string|!goog.events.EventId} type The type of the listeners * to retrieve. * @param {boolean} capture The capture mode of the listeners to retrieve. * @return {!Array} An array of matching * listeners. */ goog.events.ListenerMap.prototype.getListeners = function(type, capture) { var listenerArray = this.listeners[type.toString()]; var rv = []; if (listenerArray) { for (var i = 0; i < listenerArray.length; ++i) { var listenerObj = listenerArray[i]; if (listenerObj.capture == capture) { rv.push(listenerObj); } } } return rv; }; /** * Gets the goog.events.ListenableKey for the event or null if no such * listener is in use. * * @param {string|!goog.events.EventId} type The type of the listener * to retrieve. * @param {!Function} listener The listener function to get. * @param {boolean} capture Whether the listener is a capturing listener. * @param {Object=} opt_listenerScope Object in whose scope to call the * listener. * @return {goog.events.ListenableKey} the found listener or null if not found. */ goog.events.ListenerMap.prototype.getListener = function( type, listener, capture, opt_listenerScope) { var listenerArray = this.listeners[type.toString()]; var i = -1; if (listenerArray) { i = goog.events.ListenerMap.findListenerIndex_( listenerArray, listener, capture, opt_listenerScope); } return i > -1 ? listenerArray[i] : null; }; /** * Whether there is a matching listener. If either the type or capture * parameters are unspecified, the function will match on the * remaining criteria. * * @param {string|!goog.events.EventId=} opt_type The type of the listener. * @param {boolean=} opt_capture The capture mode of the listener. * @return {boolean} Whether there is an active listener matching * the requested type and/or capture phase. */ goog.events.ListenerMap.prototype.hasListener = function( opt_type, opt_capture) { var hasType = goog.isDef(opt_type); var typeStr = hasType ? opt_type.toString() : ''; var hasCapture = goog.isDef(opt_capture); return goog.object.some(this.listeners, function(listenerArray, type) { for (var i = 0; i < listenerArray.length; ++i) { if ((!hasType || listenerArray[i].type == typeStr) && (!hasCapture || listenerArray[i].capture == opt_capture)) { return true; } } return false; }); }; /** * Finds the index of a matching goog.events.Listener in the given * listenerArray. * @param {!Array} listenerArray Array of listener. * @param {!Function} listener The listener function. * @param {boolean=} opt_useCapture The capture flag for the listener. * @param {Object=} opt_listenerScope The listener scope. * @return {number} The index of the matching listener within the * listenerArray. * @private */ goog.events.ListenerMap.findListenerIndex_ = function( listenerArray, listener, opt_useCapture, opt_listenerScope) { for (var i = 0; i < listenerArray.length; ++i) { var listenerObj = listenerArray[i]; if (!listenerObj.removed && listenerObj.listener == listener && listenerObj.capture == !!opt_useCapture && listenerObj.handler == opt_listenerScope) { return i; } } return -1; };