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 at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// 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 and
13// limitations under the License.
14
15/**
16 * @fileoverview A map of listeners that provides utility functions to
17 * deal with listeners on an event target. Used by
18 * {@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 */
26
27goog.provide('goog.events.ListenerMap');
28
29goog.require('goog.array');
30goog.require('goog.events.Listener');
31goog.require('goog.object');
32
33
34
35/**
36 * Creates a new listener map.
37 * @param {EventTarget|goog.events.Listenable} src The src object.
38 * @constructor
39 * @final
40 */
41goog.events.ListenerMap = function(src) {
42 /** @type {EventTarget|goog.events.Listenable} */
43 this.src = src;
44
45 /**
46 * Maps of event type to an array of listeners.
47 * @type {Object.<string, !Array.<!goog.events.Listener>>}
48 */
49 this.listeners = {};
50
51 /**
52 * The count of types in this map that have registered listeners.
53 * @private {number}
54 */
55 this.typeCount_ = 0;
56};
57
58
59/**
60 * @return {number} The count of event types in this map that actually
61 * have registered listeners.
62 */
63goog.events.ListenerMap.prototype.getTypeCount = function() {
64 return this.typeCount_;
65};
66
67
68/**
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};
78
79
80/**
81 * Adds an event listener. A listener can only be added once to an
82 * object and if it is added again the key for the listener is
83 * 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 existing
87 * 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-off
92 * listener.
93 * @param {boolean=} opt_useCapture The capture mode of the listener.
94 * @param {Object=} opt_listenerScope Object in whose scope to call the
95 * 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 }
106
107 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 no
114 // 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};
125
126
127/**
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 the
133 * 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 }
142
143 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};
158
159
160/**
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 }
170
171 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};
181
182
183/**
184 * Removes all listeners from this map. If opt_type is provided, only
185 * 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};
205
206
207/**
208 * Gets all listeners that match the given type and capture mode. The
209 * returned array is a copy (but the listener objects are not).
210 * @param {string|!goog.events.EventId} type The type of the listeners
211 * to retrieve.
212 * @param {boolean} capture The capture mode of the listeners to retrieve.
213 * @return {!Array.<goog.events.ListenableKey>} An array of matching
214 * 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};
229
230
231/**
232 * Gets the goog.events.ListenableKey for the event or null if no such
233 * listener is in use.
234 *
235 * @param {string|!goog.events.EventId} type The type of the listener
236 * 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 the
240 * 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};
253
254
255/**
256 * Whether there is a matching listener. If either the type or capture
257 * parameters are unspecified, the function will match on the
258 * 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 matching
263 * 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);
270
271 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 }
279
280 return false;
281 });
282};
283
284
285/**
286 * Finds the index of a matching goog.events.Listener in the given
287 * 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 the
293 * listenerArray.
294 * @private
295 */
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};