// 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<string, !Array<!goog.events.Listener>>}
*/
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<goog.events.ListenableKey>} 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<!goog.events.Listener>} 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;
};