# lib/goog/events/eventtarget.js

    1// Copyright 2005 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 disposable implementation of a custom17 * listenable/event target. See also: documentation for18 * {@code goog.events.Listenable}.19 *20 * @author arv@google.com (Erik Arvidsson) [Original implementation]21 * @author pupius@google.com (Daniel Pupius) [Port to use goog.events]22 * @see ../demos/eventtarget.html23 * @see goog.events.Listenable24 */2526goog.provide('goog.events.EventTarget');2728goog.require('goog.Disposable');29goog.require('goog.asserts');30goog.require('goog.events');31goog.require('goog.events.Event');32goog.require('goog.events.Listenable');33goog.require('goog.events.ListenerMap');34goog.require('goog.object');35363738/**39 * An implementation of {@code goog.events.Listenable} with full W3C40 * EventTarget-like support (capture/bubble mechanism, stopping event41 * propagation, preventing default actions).42 *43 * You may subclass this class to turn your class into a Listenable.44 *45 * Unless propagation is stopped, an event dispatched by an46 * EventTarget will bubble to the parent returned by47 * {@code getParentEventTarget}. To set the parent, call48 * {@code setParentEventTarget}. Subclasses that don't support49 * changing the parent can override the setter to throw an error.50 *51 * Example usage:52 * <pre>53 *   var source = new goog.events.EventTarget();54 *   function handleEvent(e) {55 *     alert('Type: ' + e.type + '; Target: ' + e.target);56 *   }57 *   source.listen('foo', handleEvent);58 *   // Or: goog.events.listen(source, 'foo', handleEvent);59 *   ...60 *   source.dispatchEvent('foo');  // will call handleEvent61 *   ...62 *   source.unlisten('foo', handleEvent);63 *   // Or: goog.events.unlisten(source, 'foo', handleEvent);64 * </pre>65 *66 * @constructor67 * @extends {goog.Disposable}68 * @implements {goog.events.Listenable}69 */70goog.events.EventTarget = function() {71  goog.Disposable.call(this);7273  /**74   * Maps of event type to an array of listeners.75   * @private {!goog.events.ListenerMap}76   */77  this.eventTargetListeners_ = new goog.events.ListenerMap(this);7879  /**80   * The object to use for event.target. Useful when mixing in an81   * EventTarget to another object.82   * @private {!Object}83   */84  this.actualEventTarget_ = this;8586  /**87   * Parent event target, used during event bubbling.88   *89   * TODO(user): Change this to goog.events.Listenable. This90   * currently breaks people who expect getParentEventTarget to return91   * goog.events.EventTarget.92   *93   * @private {goog.events.EventTarget}94   */95  this.parentEventTarget_ = null;96};97goog.inherits(goog.events.EventTarget, goog.Disposable);98goog.events.Listenable.addImplementation(goog.events.EventTarget);99100101/**102 * An artificial cap on the number of ancestors you can have. This is mainly103 * for loop detection.104 * @const {number}105 * @private106 */107goog.events.EventTarget.MAX_ANCESTORS_ = 1000;108109110/**111 * Returns the parent of this event target to use for bubbling.112 *113 * @return {goog.events.EventTarget} The parent EventTarget or null if114 *     there is no parent.115 * @override116 */117goog.events.EventTarget.prototype.getParentEventTarget = function() {118  return this.parentEventTarget_;119};120121122/**123 * Sets the parent of this event target to use for capture/bubble124 * mechanism.125 * @param {goog.events.EventTarget} parent Parent listenable (null if none).126 */127goog.events.EventTarget.prototype.setParentEventTarget = function(parent) {128  this.parentEventTarget_ = parent;129};130131132/**133 * Adds an event listener to the event target. The same handler can only be134 * added once per the type. Even if you add the same handler multiple times135 * using the same type then it will only be called once when the event is136 * dispatched.137 *138 * @param {string} type The type of the event to listen for.139 * @param {function(?):?|{handleEvent:function(?):?}|null} handler The function140 *     to handle the event. The handler can also be an object that implements141 *     the handleEvent method which takes the event object as argument.142 * @param {boolean=} opt_capture In DOM-compliant browsers, this determines143 *     whether the listener is fired during the capture or bubble phase144 *     of the event.145 * @param {Object=} opt_handlerScope Object in whose scope to call146 *     the listener.147 * @deprecated Use {@code #listen} instead, when possible. Otherwise, use148 *     {@code goog.events.listen} if you are passing Object149 *     (instead of Function) as handler.150 */151goog.events.EventTarget.prototype.addEventListener = function(152    type, handler, opt_capture, opt_handlerScope) {153  goog.events.listen(this, type, handler, opt_capture, opt_handlerScope);154};155156157/**158 * Removes an event listener from the event target. The handler must be the159 * same object as the one added. If the handler has not been added then160 * nothing is done.161 *162 * @param {string} type The type of the event to listen for.163 * @param {function(?):?|{handleEvent:function(?):?}|null} handler The function164 *     to handle the event. The handler can also be an object that implements165 *     the handleEvent method which takes the event object as argument.166 * @param {boolean=} opt_capture In DOM-compliant browsers, this determines167 *     whether the listener is fired during the capture or bubble phase168 *     of the event.169 * @param {Object=} opt_handlerScope Object in whose scope to call170 *     the listener.171 * @deprecated Use {@code #unlisten} instead, when possible. Otherwise, use172 *     {@code goog.events.unlisten} if you are passing Object173 *     (instead of Function) as handler.174 */175goog.events.EventTarget.prototype.removeEventListener = function(176    type, handler, opt_capture, opt_handlerScope) {177  goog.events.unlisten(this, type, handler, opt_capture, opt_handlerScope);178};179180181/** @override */182goog.events.EventTarget.prototype.dispatchEvent = function(e) {183  this.assertInitialized_();184185  var ancestorsTree, ancestor = this.getParentEventTarget();186  if (ancestor) {187    ancestorsTree = [];188    var ancestorCount = 1;189    for (; ancestor; ancestor = ancestor.getParentEventTarget()) {190      ancestorsTree.push(ancestor);191      goog.asserts.assert(192          (++ancestorCount < goog.events.EventTarget.MAX_ANCESTORS_),193          'infinite loop');194    }195  }196197  return goog.events.EventTarget.dispatchEventInternal_(198      this.actualEventTarget_, e, ancestorsTree);199};200201202/**203 * Removes listeners from this object.  Classes that extend EventTarget may204 * need to override this method in order to remove references to DOM Elements205 * and additional listeners.206 * @override207 */208goog.events.EventTarget.prototype.disposeInternal = function() {209  goog.events.EventTarget.superClass_.disposeInternal.call(this);210211  this.removeAllListeners();212  this.parentEventTarget_ = null;213};214215216/** @override */217goog.events.EventTarget.prototype.listen = function(218    type, listener, opt_useCapture, opt_listenerScope) {219  this.assertInitialized_();220  return this.eventTargetListeners_.add(221      String(type), listener, false /* callOnce */, opt_useCapture,222      opt_listenerScope);223};224225226/** @override */227goog.events.EventTarget.prototype.listenOnce = function(228    type, listener, opt_useCapture, opt_listenerScope) {229  return this.eventTargetListeners_.add(230      String(type), listener, true /* callOnce */, opt_useCapture,231      opt_listenerScope);232};233234235/** @override */236goog.events.EventTarget.prototype.unlisten = function(237    type, listener, opt_useCapture, opt_listenerScope) {238  return this.eventTargetListeners_.remove(239      String(type), listener, opt_useCapture, opt_listenerScope);240};241242243/** @override */244goog.events.EventTarget.prototype.unlistenByKey = function(key) {245  return this.eventTargetListeners_.removeByKey(key);246};247248249/** @override */250goog.events.EventTarget.prototype.removeAllListeners = function(opt_type) {251  // TODO(user): Previously, removeAllListeners can be called on252  // uninitialized EventTarget, so we preserve that behavior. We253  // should remove this when usages that rely on that fact are purged.254  if (!this.eventTargetListeners_) {255    return 0;256  }257  return this.eventTargetListeners_.removeAll(opt_type);258};259260261/** @override */262goog.events.EventTarget.prototype.fireListeners = function(263    type, capture, eventObject) {264  // TODO(user): Original code avoids array creation when there265  // is no listener, so we do the same. If this optimization turns266  // out to be not required, we can replace this with267  // getListeners(type, capture) instead, which is simpler.268  var listenerArray = this.eventTargetListeners_.listeners[String(type)];269  if (!listenerArray) {270    return true;271  }272  listenerArray = listenerArray.concat();273274  var rv = true;275  for (var i = 0; i < listenerArray.length; ++i) {276    var listener = listenerArray[i];277    // We might not have a listener if the listener was removed.278    if (listener && !listener.removed && listener.capture == capture) {279      var listenerFn = listener.listener;280      var listenerHandler = listener.handler || listener.src;281282      if (listener.callOnce) {283        this.unlistenByKey(listener);284      }285      rv = listenerFn.call(listenerHandler, eventObject) !== false && rv;286    }287  }288289  return rv && eventObject.returnValue_ != false;290};291292293/** @override */294goog.events.EventTarget.prototype.getListeners = function(type, capture) {295  return this.eventTargetListeners_.getListeners(String(type), capture);296};297298299/** @override */300goog.events.EventTarget.prototype.getListener = function(301    type, listener, capture, opt_listenerScope) {302  return this.eventTargetListeners_.getListener(303      String(type), listener, capture, opt_listenerScope);304};305306307/** @override */308goog.events.EventTarget.prototype.hasListener = function(309    opt_type, opt_capture) {310  var id = goog.isDef(opt_type) ? String(opt_type) : undefined;311  return this.eventTargetListeners_.hasListener(id, opt_capture);312};313314315/**316 * Sets the target to be used for {@code event.target} when firing317 * event. Mainly used for testing. For example, see318 * {@code goog.testing.events.mixinListenable}.319 * @param {!Object} target The target.320 */321goog.events.EventTarget.prototype.setTargetForTesting = function(target) {322  this.actualEventTarget_ = target;323};324325326/**327 * Asserts that the event target instance is initialized properly.328 * @private329 */330goog.events.EventTarget.prototype.assertInitialized_ = function() {331  goog.asserts.assert(332      this.eventTargetListeners_,333      'Event target is not initialized. Did you call the superclass ' +334      '(goog.events.EventTarget) constructor?');335};336337338/**339 * Dispatches the given event on the ancestorsTree.340 *341 * @param {!Object} target The target to dispatch on.342 * @param {goog.events.Event|Object|string} e The event object.343 * @param {Array.<goog.events.Listenable>=} opt_ancestorsTree The ancestors344 *     tree of the target, in reverse order from the closest ancestor345 *     to the root event target. May be null if the target has no ancestor.346 * @return {boolean} If anyone called preventDefault on the event object (or347 *     if any of the listeners returns false) this will also return false.348 * @private349 */350goog.events.EventTarget.dispatchEventInternal_ = function(351    target, e, opt_ancestorsTree) {352  var type = e.type || /** @type {string} */ (e);353354  // If accepting a string or object, create a custom event object so that355  // preventDefault and stopPropagation work with the event.356  if (goog.isString(e)) {357    e = new goog.events.Event(e, target);358  } else if (!(e instanceof goog.events.Event)) {359    var oldEvent = e;360    e = new goog.events.Event(type, target);361    goog.object.extend(e, oldEvent);362  } else {363    e.target = e.target || target;364  }365366  var rv = true, currentTarget;367368  // Executes all capture listeners on the ancestors, if any.369  if (opt_ancestorsTree) {370    for (var i = opt_ancestorsTree.length - 1; !e.propagationStopped_ && i >= 0;371         i--) {372      currentTarget = e.currentTarget = opt_ancestorsTree[i];373      rv = currentTarget.fireListeners(type, true, e) && rv;374    }375  }376377  // Executes capture and bubble listeners on the target.378  if (!e.propagationStopped_) {379    currentTarget = e.currentTarget = target;380    rv = currentTarget.fireListeners(type, true, e) && rv;381    if (!e.propagationStopped_) {382      rv = currentTarget.fireListeners(type, false, e) && rv;383    }384  }385386  // Executes all bubble listeners on the ancestors, if any.387  if (opt_ancestorsTree) {388    for (i = 0; !e.propagationStopped_ && i < opt_ancestorsTree.length; i++) {389      currentTarget = e.currentTarget = opt_ancestorsTree[i];390      rv = currentTarget.fireListeners(type, false, e) && rv;391    }392  }393394  return rv;395};