# 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 |
27 | goog.provide('goog.events.ListenerMap');
28 |
29 | goog.require('goog.array');
30 | goog.require('goog.events.Listener');
31 | goog.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 | */
41 | goog.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 | */
63 | goog.events.ListenerMap.prototype.getTypeCount = function() {
64 | return this.typeCount_;
65 | };
66 |
67 |
68 | /**
69 | * @return {number} Total number of registered listeners.
70 | */
71 | goog.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 | */
98 | goog.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 | */
136 | goog.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 | */
165 | goog.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 | */
189 | goog.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 | */
216 | goog.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 | */
243 | goog.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 | */
265 | goog.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 | */
296 | goog.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 | };