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 | }; |