# lib/goog/events/listenermap.js

    1// Copyright 2013 The Closure Library Authors. All Rights Reserved.2//3// Licensed under the Apache License, Version 2.0 (the "License");4// you may not use this file except in compliance with the License.5// You may obtain a copy of the License at6//7//      http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// distributed under the License is distributed on an "AS-IS" BASIS,11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12// See the License for the specific language governing permissions and13// limitations under the License.1415/**16 * @fileoverview A map of listeners that provides utility functions to17 * deal with listeners on an event target. Used by18 * {@code goog.events.EventTarget}.19 *20 * WARNING: Do not use this class from outside goog.events package.21 *22 * @visibility {//closure/goog/bin/sizetests:__pkg__}23 * @visibility {//closure/goog/events:__pkg__}24 * @visibility {//closure/goog/labs/events:__pkg__}25 */2627goog.provide('goog.events.ListenerMap');2829goog.require('goog.array');30goog.require('goog.events.Listener');31goog.require('goog.object');32333435/**36 * Creates a new listener map.37 * @param {EventTarget|goog.events.Listenable} src The src object.38 * @constructor39 * @final40 */41goog.events.ListenerMap = function(src) {42  /** @type {EventTarget|goog.events.Listenable} */43  this.src = src;4445  /**46   * Maps of event type to an array of listeners.47   * @type {Object.<string, !Array.<!goog.events.Listener>>}48   */49  this.listeners = {};5051  /**52   * The count of types in this map that have registered listeners.53   * @private {number}54   */55  this.typeCount_ = 0;56};575859/**60 * @return {number} The count of event types in this map that actually61 *     have registered listeners.62 */63goog.events.ListenerMap.prototype.getTypeCount = function() {64  return this.typeCount_;65};666768/**69 * @return {number} Total number of registered listeners.70 */71goog.events.ListenerMap.prototype.getListenerCount = function() {72  var count = 0;73  for (var type in this.listeners) {74    count += this.listeners[type].length;75  }76  return count;77};787980/**81 * Adds an event listener. A listener can only be added once to an82 * object and if it is added again the key for the listener is83 * returned.84 *85 * Note that a one-off listener will not change an existing listener,86 * if any. On the other hand a normal listener will change existing87 * one-off listener to become a normal listener.88 *89 * @param {string|!goog.events.EventId} type The listener event type.90 * @param {!Function} listener This listener callback method.91 * @param {boolean} callOnce Whether the listener is a one-off92 *     listener.93 * @param {boolean=} opt_useCapture The capture mode of the listener.94 * @param {Object=} opt_listenerScope Object in whose scope to call the95 *     listener.96 * @return {goog.events.ListenableKey} Unique key for the listener.97 */98goog.events.ListenerMap.prototype.add = function(99    type, listener, callOnce, opt_useCapture, opt_listenerScope) {100  var typeStr = type.toString();101  var listenerArray = this.listeners[typeStr];102  if (!listenerArray) {103    listenerArray = this.listeners[typeStr] = [];104    this.typeCount_++;105  }106107  var listenerObj;108  var index = goog.events.ListenerMap.findListenerIndex_(109      listenerArray, listener, opt_useCapture, opt_listenerScope);110  if (index > -1) {111    listenerObj = listenerArray[index];112    if (!callOnce) {113      // Ensure that, if there is an existing callOnce listener, it is no114      // longer a callOnce listener.115      listenerObj.callOnce = false;116    }117  } else {118    listenerObj = new goog.events.Listener(119        listener, null, this.src, typeStr, !!opt_useCapture, opt_listenerScope);120    listenerObj.callOnce = callOnce;121    listenerArray.push(listenerObj);122  }123  return listenerObj;124};125126127/**128 * Removes a matching listener.129 * @param {string|!goog.events.EventId} type The listener event type.130 * @param {!Function} listener This listener callback method.131 * @param {boolean=} opt_useCapture The capture mode of the listener.132 * @param {Object=} opt_listenerScope Object in whose scope to call the133 *     listener.134 * @return {boolean} Whether any listener was removed.135 */136goog.events.ListenerMap.prototype.remove = function(137    type, listener, opt_useCapture, opt_listenerScope) {138  var typeStr = type.toString();139  if (!(typeStr in this.listeners)) {140    return false;141  }142143  var listenerArray = this.listeners[typeStr];144  var index = goog.events.ListenerMap.findListenerIndex_(145      listenerArray, listener, opt_useCapture, opt_listenerScope);146  if (index > -1) {147    var listenerObj = listenerArray[index];148    listenerObj.markAsRemoved();149    goog.array.removeAt(listenerArray, index);150    if (listenerArray.length == 0) {151      delete this.listeners[typeStr];152      this.typeCount_--;153    }154    return true;155  }156  return false;157};158159160/**161 * Removes the given listener object.162 * @param {goog.events.ListenableKey} listener The listener to remove.163 * @return {boolean} Whether the listener is removed.164 */165goog.events.ListenerMap.prototype.removeByKey = function(listener) {166  var type = listener.type;167  if (!(type in this.listeners)) {168    return false;169  }170171  var removed = goog.array.remove(this.listeners[type], listener);172  if (removed) {173    listener.markAsRemoved();174    if (this.listeners[type].length == 0) {175      delete this.listeners[type];176      this.typeCount_--;177    }178  }179  return removed;180};181182183/**184 * Removes all listeners from this map. If opt_type is provided, only185 * listeners that match the given type are removed.186 * @param {string|!goog.events.EventId=} opt_type Type of event to remove.187 * @return {number} Number of listeners removed.188 */189goog.events.ListenerMap.prototype.removeAll = function(opt_type) {190  var typeStr = opt_type && opt_type.toString();191  var count = 0;192  for (var type in this.listeners) {193    if (!typeStr || type == typeStr) {194      var listenerArray = this.listeners[type];195      for (var i = 0; i < listenerArray.length; i++) {196        ++count;197        listenerArray[i].markAsRemoved();198      }199      delete this.listeners[type];200      this.typeCount_--;201    }202  }203  return count;204};205206207/**208 * Gets all listeners that match the given type and capture mode. The209 * returned array is a copy (but the listener objects are not).210 * @param {string|!goog.events.EventId} type The type of the listeners211 *     to retrieve.212 * @param {boolean} capture The capture mode of the listeners to retrieve.213 * @return {!Array.<goog.events.ListenableKey>} An array of matching214 *     listeners.215 */216goog.events.ListenerMap.prototype.getListeners = function(type, capture) {217  var listenerArray = this.listeners[type.toString()];218  var rv = [];219  if (listenerArray) {220    for (var i = 0; i < listenerArray.length; ++i) {221      var listenerObj = listenerArray[i];222      if (listenerObj.capture == capture) {223        rv.push(listenerObj);224      }225    }226  }227  return rv;228};229230231/**232 * Gets the goog.events.ListenableKey for the event or null if no such233 * listener is in use.234 *235 * @param {string|!goog.events.EventId} type The type of the listener236 *     to retrieve.237 * @param {!Function} listener The listener function to get.238 * @param {boolean} capture Whether the listener is a capturing listener.239 * @param {Object=} opt_listenerScope Object in whose scope to call the240 *     listener.241 * @return {goog.events.ListenableKey} the found listener or null if not found.242 */243goog.events.ListenerMap.prototype.getListener = function(244    type, listener, capture, opt_listenerScope) {245  var listenerArray = this.listeners[type.toString()];246  var i = -1;247  if (listenerArray) {248    i = goog.events.ListenerMap.findListenerIndex_(249        listenerArray, listener, capture, opt_listenerScope);250  }251  return i > -1 ? listenerArray[i] : null;252};253254255/**256 * Whether there is a matching listener. If either the type or capture257 * parameters are unspecified, the function will match on the258 * remaining criteria.259 *260 * @param {string|!goog.events.EventId=} opt_type The type of the listener.261 * @param {boolean=} opt_capture The capture mode of the listener.262 * @return {boolean} Whether there is an active listener matching263 *     the requested type and/or capture phase.264 */265goog.events.ListenerMap.prototype.hasListener = function(266    opt_type, opt_capture) {267  var hasType = goog.isDef(opt_type);268  var typeStr = hasType ? opt_type.toString() : '';269  var hasCapture = goog.isDef(opt_capture);270271  return goog.object.some(272      this.listeners, function(listenerArray, type) {273        for (var i = 0; i < listenerArray.length; ++i) {274          if ((!hasType || listenerArray[i].type == typeStr) &&275              (!hasCapture || listenerArray[i].capture == opt_capture)) {276            return true;277          }278        }279280        return false;281      });282};283284285/**286 * Finds the index of a matching goog.events.Listener in the given287 * listenerArray.288 * @param {!Array.<!goog.events.Listener>} listenerArray Array of listener.289 * @param {!Function} listener The listener function.290 * @param {boolean=} opt_useCapture The capture flag for the listener.291 * @param {Object=} opt_listenerScope The listener scope.292 * @return {number} The index of the matching listener within the293 *     listenerArray.294 * @private295 */296goog.events.ListenerMap.findListenerIndex_ = function(297    listenerArray, listener, opt_useCapture, opt_listenerScope) {298  for (var i = 0; i < listenerArray.length; ++i) {299    var listenerObj = listenerArray[i];300    if (!listenerObj.removed &&301        listenerObj.listener == listener &&302        listenerObj.capture == !!opt_useCapture &&303        listenerObj.handler == opt_listenerScope) {304      return i;305    }306  }307  return -1;308};