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 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 An event manager for both native browser event |
17 | * targets and custom JavaScript event targets |
18 | * ({@code goog.events.Listenable}). This provides an abstraction |
19 | * over browsers' event systems. |
20 | * |
21 | * It also provides a simulation of W3C event model's capture phase in |
22 | * Internet Explorer (IE 8 and below). Caveat: the simulation does not |
23 | * interact well with listeners registered directly on the elements |
24 | * (bypassing goog.events) or even with listeners registered via |
25 | * goog.events in a separate JS binary. In these cases, we provide |
26 | * no ordering guarantees. |
27 | * |
28 | * The listeners will receive a "patched" event object. Such event object |
29 | * contains normalized values for certain event properties that differs in |
30 | * different browsers. |
31 | * |
32 | * Example usage: |
33 | * <pre> |
34 | * goog.events.listen(myNode, 'click', function(e) { alert('woo') }); |
35 | * goog.events.listen(myNode, 'mouseover', mouseHandler, true); |
36 | * goog.events.unlisten(myNode, 'mouseover', mouseHandler, true); |
37 | * goog.events.removeAll(myNode); |
38 | * </pre> |
39 | * |
40 | * in IE and event object patching] |
41 | * |
42 | * @see ../demos/events.html |
43 | * @see ../demos/event-propagation.html |
44 | * @see ../demos/stopevent.html |
45 | */ |
46 | |
47 | // IMPLEMENTATION NOTES: |
48 | // goog.events stores an auxiliary data structure on each EventTarget |
49 | // source being listened on. This allows us to take advantage of GC, |
50 | // having the data structure GC'd when the EventTarget is GC'd. This |
51 | // GC behavior is equivalent to using W3C DOM Events directly. |
52 | |
53 | goog.provide('goog.events'); |
54 | goog.provide('goog.events.CaptureSimulationMode'); |
55 | goog.provide('goog.events.Key'); |
56 | goog.provide('goog.events.ListenableType'); |
57 | |
58 | goog.require('goog.asserts'); |
59 | goog.require('goog.debug.entryPointRegistry'); |
60 | goog.require('goog.events.BrowserEvent'); |
61 | goog.require('goog.events.BrowserFeature'); |
62 | goog.require('goog.events.Listenable'); |
63 | goog.require('goog.events.ListenerMap'); |
64 | |
65 | goog.forwardDeclare('goog.debug.ErrorHandler'); |
66 | goog.forwardDeclare('goog.events.EventWrapper'); |
67 | |
68 | |
69 | /** |
70 | * @typedef {number|goog.events.ListenableKey} |
71 | */ |
72 | goog.events.Key; |
73 | |
74 | |
75 | /** |
76 | * @typedef {EventTarget|goog.events.Listenable} |
77 | */ |
78 | goog.events.ListenableType; |
79 | |
80 | |
81 | /** |
82 | * Property name on a native event target for the listener map |
83 | * associated with the event target. |
84 | * @private @const {string} |
85 | */ |
86 | goog.events.LISTENER_MAP_PROP_ = 'closure_lm_' + ((Math.random() * 1e6) | 0); |
87 | |
88 | |
89 | /** |
90 | * String used to prepend to IE event types. |
91 | * @const |
92 | * @private |
93 | */ |
94 | goog.events.onString_ = 'on'; |
95 | |
96 | |
97 | /** |
98 | * Map of computed "on<eventname>" strings for IE event types. Caching |
99 | * this removes an extra object allocation in goog.events.listen which |
100 | * improves IE6 performance. |
101 | * @const |
102 | * @dict |
103 | * @private |
104 | */ |
105 | goog.events.onStringMap_ = {}; |
106 | |
107 | |
108 | /** |
109 | * @enum {number} Different capture simulation mode for IE8-. |
110 | */ |
111 | goog.events.CaptureSimulationMode = { |
112 | /** |
113 | * Does not perform capture simulation. Will asserts in IE8- when you |
114 | * add capture listeners. |
115 | */ |
116 | OFF_AND_FAIL: 0, |
117 | |
118 | /** |
119 | * Does not perform capture simulation, silently ignore capture |
120 | * listeners. |
121 | */ |
122 | OFF_AND_SILENT: 1, |
123 | |
124 | /** |
125 | * Performs capture simulation. |
126 | */ |
127 | ON: 2 |
128 | }; |
129 | |
130 | |
131 | /** |
132 | * @define {number} The capture simulation mode for IE8-. By default, |
133 | * this is ON. |
134 | */ |
135 | goog.define('goog.events.CAPTURE_SIMULATION_MODE', 2); |
136 | |
137 | |
138 | /** |
139 | * Estimated count of total native listeners. |
140 | * @private {number} |
141 | */ |
142 | goog.events.listenerCountEstimate_ = 0; |
143 | |
144 | |
145 | /** |
146 | * Adds an event listener for a specific event on a native event |
147 | * target (such as a DOM element) or an object that has implemented |
148 | * {@link goog.events.Listenable}. A listener can only be added once |
149 | * to an object and if it is added again the key for the listener is |
150 | * returned. Note that if the existing listener is a one-off listener |
151 | * (registered via listenOnce), it will no longer be a one-off |
152 | * listener after a call to listen(). |
153 | * |
154 | * @param {EventTarget|goog.events.Listenable} src The node to listen |
155 | * to events on. |
156 | * @param {string|Array.<string>| |
157 | * !goog.events.EventId.<EVENTOBJ>|!Array.<!goog.events.EventId.<EVENTOBJ>>} |
158 | * type Event type or array of event types. |
159 | * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null} |
160 | * listener Callback method, or an object with a handleEvent function. |
161 | * WARNING: passing an Object is now softly deprecated. |
162 | * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to |
163 | * false). |
164 | * @param {T=} opt_handler Element in whose scope to call the listener. |
165 | * @return {goog.events.Key} Unique key for the listener. |
166 | * @template T,EVENTOBJ |
167 | */ |
168 | goog.events.listen = function(src, type, listener, opt_capt, opt_handler) { |
169 | if (goog.isArray(type)) { |
170 | for (var i = 0; i < type.length; i++) { |
171 | goog.events.listen(src, type[i], listener, opt_capt, opt_handler); |
172 | } |
173 | return null; |
174 | } |
175 | |
176 | listener = goog.events.wrapListener(listener); |
177 | if (goog.events.Listenable.isImplementedBy(src)) { |
178 | return src.listen( |
179 | /** @type {string|!goog.events.EventId} */ (type), |
180 | listener, opt_capt, opt_handler); |
181 | } else { |
182 | return goog.events.listen_( |
183 | /** @type {EventTarget} */ (src), |
184 | /** @type {string|!goog.events.EventId} */ (type), |
185 | listener, /* callOnce */ false, opt_capt, opt_handler); |
186 | } |
187 | }; |
188 | |
189 | |
190 | /** |
191 | * Adds an event listener for a specific event on a native event |
192 | * target. A listener can only be added once to an object and if it |
193 | * is added again the key for the listener is returned. |
194 | * |
195 | * Note that a one-off listener will not change an existing listener, |
196 | * if any. On the other hand a normal listener will change existing |
197 | * one-off listener to become a normal listener. |
198 | * |
199 | * @param {EventTarget} src The node to listen to events on. |
200 | * @param {string|!goog.events.EventId} type Event type. |
201 | * @param {!Function} listener Callback function. |
202 | * @param {boolean} callOnce Whether the listener is a one-off |
203 | * listener or otherwise. |
204 | * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to |
205 | * false). |
206 | * @param {Object=} opt_handler Element in whose scope to call the listener. |
207 | * @return {goog.events.ListenableKey} Unique key for the listener. |
208 | * @private |
209 | */ |
210 | goog.events.listen_ = function( |
211 | src, type, listener, callOnce, opt_capt, opt_handler) { |
212 | if (!type) { |
213 | throw Error('Invalid event type'); |
214 | } |
215 | |
216 | var capture = !!opt_capt; |
217 | if (capture && !goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) { |
218 | if (goog.events.CAPTURE_SIMULATION_MODE == |
219 | goog.events.CaptureSimulationMode.OFF_AND_FAIL) { |
220 | goog.asserts.fail('Can not register capture listener in IE8-.'); |
221 | return null; |
222 | } else if (goog.events.CAPTURE_SIMULATION_MODE == |
223 | goog.events.CaptureSimulationMode.OFF_AND_SILENT) { |
224 | return null; |
225 | } |
226 | } |
227 | |
228 | var listenerMap = goog.events.getListenerMap_(src); |
229 | if (!listenerMap) { |
230 | src[goog.events.LISTENER_MAP_PROP_] = listenerMap = |
231 | new goog.events.ListenerMap(src); |
232 | } |
233 | |
234 | var listenerObj = listenerMap.add( |
235 | type, listener, callOnce, opt_capt, opt_handler); |
236 | |
237 | // If the listenerObj already has a proxy, it has been set up |
238 | // previously. We simply return. |
239 | if (listenerObj.proxy) { |
240 | return listenerObj; |
241 | } |
242 | |
243 | var proxy = goog.events.getProxy(); |
244 | listenerObj.proxy = proxy; |
245 | |
246 | proxy.src = src; |
247 | proxy.listener = listenerObj; |
248 | |
249 | // Attach the proxy through the browser's API |
250 | if (src.addEventListener) { |
251 | src.addEventListener(type.toString(), proxy, capture); |
252 | } else { |
253 | // The else above used to be else if (src.attachEvent) and then there was |
254 | // another else statement that threw an exception warning the developer |
255 | // they made a mistake. This resulted in an extra object allocation in IE6 |
256 | // due to a wrapper object that had to be implemented around the element |
257 | // and so was removed. |
258 | src.attachEvent(goog.events.getOnString_(type.toString()), proxy); |
259 | } |
260 | |
261 | goog.events.listenerCountEstimate_++; |
262 | return listenerObj; |
263 | }; |
264 | |
265 | |
266 | /** |
267 | * Helper function for returning a proxy function. |
268 | * @return {!Function} A new or reused function object. |
269 | */ |
270 | goog.events.getProxy = function() { |
271 | var proxyCallbackFunction = goog.events.handleBrowserEvent_; |
272 | // Use a local var f to prevent one allocation. |
273 | var f = goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT ? |
274 | function(eventObject) { |
275 | return proxyCallbackFunction.call(f.src, f.listener, eventObject); |
276 | } : |
277 | function(eventObject) { |
278 | var v = proxyCallbackFunction.call(f.src, f.listener, eventObject); |
279 | // NOTE(user): In IE, we hack in a capture phase. However, if |
280 | // there is inline event handler which tries to prevent default (for |
281 | // example <a href="..." onclick="return false">...</a>) in a |
282 | // descendant element, the prevent default will be overridden |
283 | // by this listener if this listener were to return true. Hence, we |
284 | // return undefined. |
285 | if (!v) return v; |
286 | }; |
287 | return f; |
288 | }; |
289 | |
290 | |
291 | /** |
292 | * Adds an event listener for a specific event on a native event |
293 | * target (such as a DOM element) or an object that has implemented |
294 | * {@link goog.events.Listenable}. After the event has fired the event |
295 | * listener is removed from the target. |
296 | * |
297 | * If an existing listener already exists, listenOnce will do |
298 | * nothing. In particular, if the listener was previously registered |
299 | * via listen(), listenOnce() will not turn the listener into a |
300 | * one-off listener. Similarly, if there is already an existing |
301 | * one-off listener, listenOnce does not modify the listeners (it is |
302 | * still a once listener). |
303 | * |
304 | * @param {EventTarget|goog.events.Listenable} src The node to listen |
305 | * to events on. |
306 | * @param {string|Array.<string>| |
307 | * !goog.events.EventId.<EVENTOBJ>|!Array.<!goog.events.EventId.<EVENTOBJ>>} |
308 | * type Event type or array of event types. |
309 | * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null} |
310 | * listener Callback method. |
311 | * @param {boolean=} opt_capt Fire in capture phase?. |
312 | * @param {T=} opt_handler Element in whose scope to call the listener. |
313 | * @return {goog.events.Key} Unique key for the listener. |
314 | * @template T,EVENTOBJ |
315 | */ |
316 | goog.events.listenOnce = function(src, type, listener, opt_capt, opt_handler) { |
317 | if (goog.isArray(type)) { |
318 | for (var i = 0; i < type.length; i++) { |
319 | goog.events.listenOnce(src, type[i], listener, opt_capt, opt_handler); |
320 | } |
321 | return null; |
322 | } |
323 | |
324 | listener = goog.events.wrapListener(listener); |
325 | if (goog.events.Listenable.isImplementedBy(src)) { |
326 | return src.listenOnce( |
327 | /** @type {string|!goog.events.EventId} */ (type), |
328 | listener, opt_capt, opt_handler); |
329 | } else { |
330 | return goog.events.listen_( |
331 | /** @type {EventTarget} */ (src), |
332 | /** @type {string|!goog.events.EventId} */ (type), |
333 | listener, /* callOnce */ true, opt_capt, opt_handler); |
334 | } |
335 | }; |
336 | |
337 | |
338 | /** |
339 | * Adds an event listener with a specific event wrapper on a DOM Node or an |
340 | * object that has implemented {@link goog.events.Listenable}. A listener can |
341 | * only be added once to an object. |
342 | * |
343 | * @param {EventTarget|goog.events.Listenable} src The target to |
344 | * listen to events on. |
345 | * @param {goog.events.EventWrapper} wrapper Event wrapper to use. |
346 | * @param {function(this:T, ?):?|{handleEvent:function(?):?}|null} listener |
347 | * Callback method, or an object with a handleEvent function. |
348 | * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to |
349 | * false). |
350 | * @param {T=} opt_handler Element in whose scope to call the listener. |
351 | * @template T |
352 | */ |
353 | goog.events.listenWithWrapper = function(src, wrapper, listener, opt_capt, |
354 | opt_handler) { |
355 | wrapper.listen(src, listener, opt_capt, opt_handler); |
356 | }; |
357 | |
358 | |
359 | /** |
360 | * Removes an event listener which was added with listen(). |
361 | * |
362 | * @param {EventTarget|goog.events.Listenable} src The target to stop |
363 | * listening to events on. |
364 | * @param {string|Array.<string>| |
365 | * !goog.events.EventId.<EVENTOBJ>|!Array.<!goog.events.EventId.<EVENTOBJ>>} |
366 | * type Event type or array of event types to unlisten to. |
367 | * @param {function(?):?|{handleEvent:function(?):?}|null} listener The |
368 | * listener function to remove. |
369 | * @param {boolean=} opt_capt In DOM-compliant browsers, this determines |
370 | * whether the listener is fired during the capture or bubble phase of the |
371 | * event. |
372 | * @param {Object=} opt_handler Element in whose scope to call the listener. |
373 | * @return {?boolean} indicating whether the listener was there to remove. |
374 | * @template EVENTOBJ |
375 | */ |
376 | goog.events.unlisten = function(src, type, listener, opt_capt, opt_handler) { |
377 | if (goog.isArray(type)) { |
378 | for (var i = 0; i < type.length; i++) { |
379 | goog.events.unlisten(src, type[i], listener, opt_capt, opt_handler); |
380 | } |
381 | return null; |
382 | } |
383 | |
384 | listener = goog.events.wrapListener(listener); |
385 | if (goog.events.Listenable.isImplementedBy(src)) { |
386 | return src.unlisten( |
387 | /** @type {string|!goog.events.EventId} */ (type), |
388 | listener, opt_capt, opt_handler); |
389 | } |
390 | |
391 | if (!src) { |
392 | // TODO(user): We should tighten the API to only accept |
393 | // non-null objects, or add an assertion here. |
394 | return false; |
395 | } |
396 | |
397 | var capture = !!opt_capt; |
398 | var listenerMap = goog.events.getListenerMap_( |
399 | /** @type {EventTarget} */ (src)); |
400 | if (listenerMap) { |
401 | var listenerObj = listenerMap.getListener( |
402 | /** @type {string|!goog.events.EventId} */ (type), |
403 | listener, capture, opt_handler); |
404 | if (listenerObj) { |
405 | return goog.events.unlistenByKey(listenerObj); |
406 | } |
407 | } |
408 | |
409 | return false; |
410 | }; |
411 | |
412 | |
413 | /** |
414 | * Removes an event listener which was added with listen() by the key |
415 | * returned by listen(). |
416 | * |
417 | * @param {goog.events.Key} key The key returned by listen() for this |
418 | * event listener. |
419 | * @return {boolean} indicating whether the listener was there to remove. |
420 | */ |
421 | goog.events.unlistenByKey = function(key) { |
422 | // TODO(user): Remove this check when tests that rely on this |
423 | // are fixed. |
424 | if (goog.isNumber(key)) { |
425 | return false; |
426 | } |
427 | |
428 | var listener = /** @type {goog.events.ListenableKey} */ (key); |
429 | if (!listener || listener.removed) { |
430 | return false; |
431 | } |
432 | |
433 | var src = listener.src; |
434 | if (goog.events.Listenable.isImplementedBy(src)) { |
435 | return src.unlistenByKey(listener); |
436 | } |
437 | |
438 | var type = listener.type; |
439 | var proxy = listener.proxy; |
440 | if (src.removeEventListener) { |
441 | src.removeEventListener(type, proxy, listener.capture); |
442 | } else if (src.detachEvent) { |
443 | src.detachEvent(goog.events.getOnString_(type), proxy); |
444 | } |
445 | goog.events.listenerCountEstimate_--; |
446 | |
447 | var listenerMap = goog.events.getListenerMap_( |
448 | /** @type {EventTarget} */ (src)); |
449 | // TODO(user): Try to remove this conditional and execute the |
450 | // first branch always. This should be safe. |
451 | if (listenerMap) { |
452 | listenerMap.removeByKey(listener); |
453 | if (listenerMap.getTypeCount() == 0) { |
454 | // Null the src, just because this is simple to do (and useful |
455 | // for IE <= 7). |
456 | listenerMap.src = null; |
457 | // We don't use delete here because IE does not allow delete |
458 | // on a window object. |
459 | src[goog.events.LISTENER_MAP_PROP_] = null; |
460 | } |
461 | } else { |
462 | listener.markAsRemoved(); |
463 | } |
464 | |
465 | return true; |
466 | }; |
467 | |
468 | |
469 | /** |
470 | * Removes an event listener which was added with listenWithWrapper(). |
471 | * |
472 | * @param {EventTarget|goog.events.Listenable} src The target to stop |
473 | * listening to events on. |
474 | * @param {goog.events.EventWrapper} wrapper Event wrapper to use. |
475 | * @param {function(?):?|{handleEvent:function(?):?}|null} listener The |
476 | * listener function to remove. |
477 | * @param {boolean=} opt_capt In DOM-compliant browsers, this determines |
478 | * whether the listener is fired during the capture or bubble phase of the |
479 | * event. |
480 | * @param {Object=} opt_handler Element in whose scope to call the listener. |
481 | */ |
482 | goog.events.unlistenWithWrapper = function(src, wrapper, listener, opt_capt, |
483 | opt_handler) { |
484 | wrapper.unlisten(src, listener, opt_capt, opt_handler); |
485 | }; |
486 | |
487 | |
488 | /** |
489 | * Removes all listeners from an object. You can also optionally |
490 | * remove listeners of a particular type. |
491 | * |
492 | * @param {Object|undefined} obj Object to remove listeners from. Must be an |
493 | * EventTarget or a goog.events.Listenable. |
494 | * @param {string|!goog.events.EventId=} opt_type Type of event to remove. |
495 | * Default is all types. |
496 | * @return {number} Number of listeners removed. |
497 | */ |
498 | goog.events.removeAll = function(obj, opt_type) { |
499 | // TODO(user): Change the type of obj to |
500 | // (!EventTarget|!goog.events.Listenable). |
501 | |
502 | if (!obj) { |
503 | return 0; |
504 | } |
505 | |
506 | if (goog.events.Listenable.isImplementedBy(obj)) { |
507 | return obj.removeAllListeners(opt_type); |
508 | } |
509 | |
510 | var listenerMap = goog.events.getListenerMap_( |
511 | /** @type {EventTarget} */ (obj)); |
512 | if (!listenerMap) { |
513 | return 0; |
514 | } |
515 | |
516 | var count = 0; |
517 | var typeStr = opt_type && opt_type.toString(); |
518 | for (var type in listenerMap.listeners) { |
519 | if (!typeStr || type == typeStr) { |
520 | // Clone so that we don't need to worry about unlistenByKey |
521 | // changing the content of the ListenerMap. |
522 | var listeners = listenerMap.listeners[type].concat(); |
523 | for (var i = 0; i < listeners.length; ++i) { |
524 | if (goog.events.unlistenByKey(listeners[i])) { |
525 | ++count; |
526 | } |
527 | } |
528 | } |
529 | } |
530 | return count; |
531 | }; |
532 | |
533 | |
534 | /** |
535 | * Removes all native listeners registered via goog.events. Native |
536 | * listeners are listeners on native browser objects (such as DOM |
537 | * elements). In particular, goog.events.Listenable and |
538 | * goog.events.EventTarget listeners will NOT be removed. |
539 | * @return {number} Number of listeners removed. |
540 | * @deprecated This doesn't do anything, now that Closure no longer |
541 | * stores a central listener registry. |
542 | */ |
543 | goog.events.removeAllNativeListeners = function() { |
544 | goog.events.listenerCountEstimate_ = 0; |
545 | return 0; |
546 | }; |
547 | |
548 | |
549 | /** |
550 | * Gets the listeners for a given object, type and capture phase. |
551 | * |
552 | * @param {Object} obj Object to get listeners for. |
553 | * @param {string|!goog.events.EventId} type Event type. |
554 | * @param {boolean} capture Capture phase?. |
555 | * @return {Array.<goog.events.Listener>} Array of listener objects. |
556 | */ |
557 | goog.events.getListeners = function(obj, type, capture) { |
558 | if (goog.events.Listenable.isImplementedBy(obj)) { |
559 | return obj.getListeners(type, capture); |
560 | } else { |
561 | if (!obj) { |
562 | // TODO(user): We should tighten the API to accept |
563 | // !EventTarget|goog.events.Listenable, and add an assertion here. |
564 | return []; |
565 | } |
566 | |
567 | var listenerMap = goog.events.getListenerMap_( |
568 | /** @type {EventTarget} */ (obj)); |
569 | return listenerMap ? listenerMap.getListeners(type, capture) : []; |
570 | } |
571 | }; |
572 | |
573 | |
574 | /** |
575 | * Gets the goog.events.Listener for the event or null if no such listener is |
576 | * in use. |
577 | * |
578 | * @param {EventTarget|goog.events.Listenable} src The target from |
579 | * which to get listeners. |
580 | * @param {?string|!goog.events.EventId.<EVENTOBJ>} type The type of the event. |
581 | * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null} listener The |
582 | * listener function to get. |
583 | * @param {boolean=} opt_capt In DOM-compliant browsers, this determines |
584 | * whether the listener is fired during the |
585 | * capture or bubble phase of the event. |
586 | * @param {Object=} opt_handler Element in whose scope to call the listener. |
587 | * @return {goog.events.ListenableKey} the found listener or null if not found. |
588 | * @template EVENTOBJ |
589 | */ |
590 | goog.events.getListener = function(src, type, listener, opt_capt, opt_handler) { |
591 | // TODO(user): Change type from ?string to string, or add assertion. |
592 | type = /** @type {string} */ (type); |
593 | listener = goog.events.wrapListener(listener); |
594 | var capture = !!opt_capt; |
595 | if (goog.events.Listenable.isImplementedBy(src)) { |
596 | return src.getListener(type, listener, capture, opt_handler); |
597 | } |
598 | |
599 | if (!src) { |
600 | // TODO(user): We should tighten the API to only accept |
601 | // non-null objects, or add an assertion here. |
602 | return null; |
603 | } |
604 | |
605 | var listenerMap = goog.events.getListenerMap_( |
606 | /** @type {EventTarget} */ (src)); |
607 | if (listenerMap) { |
608 | return listenerMap.getListener(type, listener, capture, opt_handler); |
609 | } |
610 | return null; |
611 | }; |
612 | |
613 | |
614 | /** |
615 | * Returns whether an event target has any active listeners matching the |
616 | * specified signature. If either the type or capture parameters are |
617 | * unspecified, the function will match on the remaining criteria. |
618 | * |
619 | * @param {EventTarget|goog.events.Listenable} obj Target to get |
620 | * listeners for. |
621 | * @param {string|!goog.events.EventId=} opt_type Event type. |
622 | * @param {boolean=} opt_capture Whether to check for capture or bubble-phase |
623 | * listeners. |
624 | * @return {boolean} Whether an event target has one or more listeners matching |
625 | * the requested type and/or capture phase. |
626 | */ |
627 | goog.events.hasListener = function(obj, opt_type, opt_capture) { |
628 | if (goog.events.Listenable.isImplementedBy(obj)) { |
629 | return obj.hasListener(opt_type, opt_capture); |
630 | } |
631 | |
632 | var listenerMap = goog.events.getListenerMap_( |
633 | /** @type {EventTarget} */ (obj)); |
634 | return !!listenerMap && listenerMap.hasListener(opt_type, opt_capture); |
635 | }; |
636 | |
637 | |
638 | /** |
639 | * Provides a nice string showing the normalized event objects public members |
640 | * @param {Object} e Event Object. |
641 | * @return {string} String of the public members of the normalized event object. |
642 | */ |
643 | goog.events.expose = function(e) { |
644 | var str = []; |
645 | for (var key in e) { |
646 | if (e[key] && e[key].id) { |
647 | str.push(key + ' = ' + e[key] + ' (' + e[key].id + ')'); |
648 | } else { |
649 | str.push(key + ' = ' + e[key]); |
650 | } |
651 | } |
652 | return str.join('\n'); |
653 | }; |
654 | |
655 | |
656 | /** |
657 | * Returns a string with on prepended to the specified type. This is used for IE |
658 | * which expects "on" to be prepended. This function caches the string in order |
659 | * to avoid extra allocations in steady state. |
660 | * @param {string} type Event type. |
661 | * @return {string} The type string with 'on' prepended. |
662 | * @private |
663 | */ |
664 | goog.events.getOnString_ = function(type) { |
665 | if (type in goog.events.onStringMap_) { |
666 | return goog.events.onStringMap_[type]; |
667 | } |
668 | return goog.events.onStringMap_[type] = goog.events.onString_ + type; |
669 | }; |
670 | |
671 | |
672 | /** |
673 | * Fires an object's listeners of a particular type and phase |
674 | * |
675 | * @param {Object} obj Object whose listeners to call. |
676 | * @param {string|!goog.events.EventId} type Event type. |
677 | * @param {boolean} capture Which event phase. |
678 | * @param {Object} eventObject Event object to be passed to listener. |
679 | * @return {boolean} True if all listeners returned true else false. |
680 | */ |
681 | goog.events.fireListeners = function(obj, type, capture, eventObject) { |
682 | if (goog.events.Listenable.isImplementedBy(obj)) { |
683 | return obj.fireListeners(type, capture, eventObject); |
684 | } |
685 | |
686 | return goog.events.fireListeners_(obj, type, capture, eventObject); |
687 | }; |
688 | |
689 | |
690 | /** |
691 | * Fires an object's listeners of a particular type and phase. |
692 | * @param {Object} obj Object whose listeners to call. |
693 | * @param {string|!goog.events.EventId} type Event type. |
694 | * @param {boolean} capture Which event phase. |
695 | * @param {Object} eventObject Event object to be passed to listener. |
696 | * @return {boolean} True if all listeners returned true else false. |
697 | * @private |
698 | */ |
699 | goog.events.fireListeners_ = function(obj, type, capture, eventObject) { |
700 | var retval = 1; |
701 | |
702 | var listenerMap = goog.events.getListenerMap_( |
703 | /** @type {EventTarget} */ (obj)); |
704 | if (listenerMap) { |
705 | // TODO(user): Original code avoids array creation when there |
706 | // is no listener, so we do the same. If this optimization turns |
707 | // out to be not required, we can replace this with |
708 | // listenerMap.getListeners(type, capture) instead, which is simpler. |
709 | var listenerArray = listenerMap.listeners[type.toString()]; |
710 | if (listenerArray) { |
711 | listenerArray = listenerArray.concat(); |
712 | for (var i = 0; i < listenerArray.length; i++) { |
713 | var listener = listenerArray[i]; |
714 | // We might not have a listener if the listener was removed. |
715 | if (listener && listener.capture == capture && !listener.removed) { |
716 | retval &= |
717 | goog.events.fireListener(listener, eventObject) !== false; |
718 | } |
719 | } |
720 | } |
721 | } |
722 | return Boolean(retval); |
723 | }; |
724 | |
725 | |
726 | /** |
727 | * Fires a listener with a set of arguments |
728 | * |
729 | * @param {goog.events.Listener} listener The listener object to call. |
730 | * @param {Object} eventObject The event object to pass to the listener. |
731 | * @return {boolean} Result of listener. |
732 | */ |
733 | goog.events.fireListener = function(listener, eventObject) { |
734 | var listenerFn = listener.listener; |
735 | var listenerHandler = listener.handler || listener.src; |
736 | |
737 | if (listener.callOnce) { |
738 | goog.events.unlistenByKey(listener); |
739 | } |
740 | return listenerFn.call(listenerHandler, eventObject); |
741 | }; |
742 | |
743 | |
744 | /** |
745 | * Gets the total number of listeners currently in the system. |
746 | * @return {number} Number of listeners. |
747 | * @deprecated This returns estimated count, now that Closure no longer |
748 | * stores a central listener registry. We still return an estimation |
749 | * to keep existing listener-related tests passing. In the near future, |
750 | * this function will be removed. |
751 | */ |
752 | goog.events.getTotalListenerCount = function() { |
753 | return goog.events.listenerCountEstimate_; |
754 | }; |
755 | |
756 | |
757 | /** |
758 | * Dispatches an event (or event like object) and calls all listeners |
759 | * listening for events of this type. The type of the event is decided by the |
760 | * type property on the event object. |
761 | * |
762 | * If any of the listeners returns false OR calls preventDefault then this |
763 | * function will return false. If one of the capture listeners calls |
764 | * stopPropagation, then the bubble listeners won't fire. |
765 | * |
766 | * @param {goog.events.Listenable} src The event target. |
767 | * @param {goog.events.EventLike} e Event object. |
768 | * @return {boolean} If anyone called preventDefault on the event object (or |
769 | * if any of the handlers returns false) this will also return false. |
770 | * If there are no handlers, or if all handlers return true, this returns |
771 | * true. |
772 | */ |
773 | goog.events.dispatchEvent = function(src, e) { |
774 | goog.asserts.assert( |
775 | goog.events.Listenable.isImplementedBy(src), |
776 | 'Can not use goog.events.dispatchEvent with ' + |
777 | 'non-goog.events.Listenable instance.'); |
778 | return src.dispatchEvent(e); |
779 | }; |
780 | |
781 | |
782 | /** |
783 | * Installs exception protection for the browser event entry point using the |
784 | * given error handler. |
785 | * |
786 | * @param {goog.debug.ErrorHandler} errorHandler Error handler with which to |
787 | * protect the entry point. |
788 | */ |
789 | goog.events.protectBrowserEventEntryPoint = function(errorHandler) { |
790 | goog.events.handleBrowserEvent_ = errorHandler.protectEntryPoint( |
791 | goog.events.handleBrowserEvent_); |
792 | }; |
793 | |
794 | |
795 | /** |
796 | * Handles an event and dispatches it to the correct listeners. This |
797 | * function is a proxy for the real listener the user specified. |
798 | * |
799 | * @param {goog.events.Listener} listener The listener object. |
800 | * @param {Event=} opt_evt Optional event object that gets passed in via the |
801 | * native event handlers. |
802 | * @return {boolean} Result of the event handler. |
803 | * @this {EventTarget} The object or Element that fired the event. |
804 | * @private |
805 | */ |
806 | goog.events.handleBrowserEvent_ = function(listener, opt_evt) { |
807 | if (listener.removed) { |
808 | return true; |
809 | } |
810 | |
811 | // Synthesize event propagation if the browser does not support W3C |
812 | // event model. |
813 | if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) { |
814 | var ieEvent = opt_evt || |
815 | /** @type {Event} */ (goog.getObjectByName('window.event')); |
816 | var evt = new goog.events.BrowserEvent(ieEvent, this); |
817 | var retval = true; |
818 | |
819 | if (goog.events.CAPTURE_SIMULATION_MODE == |
820 | goog.events.CaptureSimulationMode.ON) { |
821 | // If we have not marked this event yet, we should perform capture |
822 | // simulation. |
823 | if (!goog.events.isMarkedIeEvent_(ieEvent)) { |
824 | goog.events.markIeEvent_(ieEvent); |
825 | |
826 | var ancestors = []; |
827 | for (var parent = evt.currentTarget; parent; |
828 | parent = parent.parentNode) { |
829 | ancestors.push(parent); |
830 | } |
831 | |
832 | // Fire capture listeners. |
833 | var type = listener.type; |
834 | for (var i = ancestors.length - 1; !evt.propagationStopped_ && i >= 0; |
835 | i--) { |
836 | evt.currentTarget = ancestors[i]; |
837 | retval &= goog.events.fireListeners_(ancestors[i], type, true, evt); |
838 | } |
839 | |
840 | // Fire bubble listeners. |
841 | // |
842 | // We can technically rely on IE to perform bubble event |
843 | // propagation. However, it turns out that IE fires events in |
844 | // opposite order of attachEvent registration, which broke |
845 | // some code and tests that rely on the order. (While W3C DOM |
846 | // Level 2 Events TR leaves the event ordering unspecified, |
847 | // modern browsers and W3C DOM Level 3 Events Working Draft |
848 | // actually specify the order as the registration order.) |
849 | for (var i = 0; !evt.propagationStopped_ && i < ancestors.length; i++) { |
850 | evt.currentTarget = ancestors[i]; |
851 | retval &= goog.events.fireListeners_(ancestors[i], type, false, evt); |
852 | } |
853 | } |
854 | } else { |
855 | retval = goog.events.fireListener(listener, evt); |
856 | } |
857 | return retval; |
858 | } |
859 | |
860 | // Otherwise, simply fire the listener. |
861 | return goog.events.fireListener( |
862 | listener, new goog.events.BrowserEvent(opt_evt, this)); |
863 | }; |
864 | |
865 | |
866 | /** |
867 | * This is used to mark the IE event object so we do not do the Closure pass |
868 | * twice for a bubbling event. |
869 | * @param {Event} e The IE browser event. |
870 | * @private |
871 | */ |
872 | goog.events.markIeEvent_ = function(e) { |
873 | // Only the keyCode and the returnValue can be changed. We use keyCode for |
874 | // non keyboard events. |
875 | // event.returnValue is a bit more tricky. It is undefined by default. A |
876 | // boolean false prevents the default action. In a window.onbeforeunload and |
877 | // the returnValue is non undefined it will be alerted. However, we will only |
878 | // modify the returnValue for keyboard events. We can get a problem if non |
879 | // closure events sets the keyCode or the returnValue |
880 | |
881 | var useReturnValue = false; |
882 | |
883 | if (e.keyCode == 0) { |
884 | // We cannot change the keyCode in case that srcElement is input[type=file]. |
885 | // We could test that that is the case but that would allocate 3 objects. |
886 | // If we use try/catch we will only allocate extra objects in the case of a |
887 | // failure. |
888 | /** @preserveTry */ |
889 | try { |
890 | e.keyCode = -1; |
891 | return; |
892 | } catch (ex) { |
893 | useReturnValue = true; |
894 | } |
895 | } |
896 | |
897 | if (useReturnValue || |
898 | /** @type {boolean|undefined} */ (e.returnValue) == undefined) { |
899 | e.returnValue = true; |
900 | } |
901 | }; |
902 | |
903 | |
904 | /** |
905 | * This is used to check if an IE event has already been handled by the Closure |
906 | * system so we do not do the Closure pass twice for a bubbling event. |
907 | * @param {Event} e The IE browser event. |
908 | * @return {boolean} True if the event object has been marked. |
909 | * @private |
910 | */ |
911 | goog.events.isMarkedIeEvent_ = function(e) { |
912 | return e.keyCode < 0 || e.returnValue != undefined; |
913 | }; |
914 | |
915 | |
916 | /** |
917 | * Counter to create unique event ids. |
918 | * @private {number} |
919 | */ |
920 | goog.events.uniqueIdCounter_ = 0; |
921 | |
922 | |
923 | /** |
924 | * Creates a unique event id. |
925 | * |
926 | * @param {string} identifier The identifier. |
927 | * @return {string} A unique identifier. |
928 | * @idGenerator |
929 | */ |
930 | goog.events.getUniqueId = function(identifier) { |
931 | return identifier + '_' + goog.events.uniqueIdCounter_++; |
932 | }; |
933 | |
934 | |
935 | /** |
936 | * @param {EventTarget} src The source object. |
937 | * @return {goog.events.ListenerMap} A listener map for the given |
938 | * source object, or null if none exists. |
939 | * @private |
940 | */ |
941 | goog.events.getListenerMap_ = function(src) { |
942 | var listenerMap = src[goog.events.LISTENER_MAP_PROP_]; |
943 | // IE serializes the property as well (e.g. when serializing outer |
944 | // HTML). So we must check that the value is of the correct type. |
945 | return listenerMap instanceof goog.events.ListenerMap ? listenerMap : null; |
946 | }; |
947 | |
948 | |
949 | /** |
950 | * Expando property for listener function wrapper for Object with |
951 | * handleEvent. |
952 | * @private @const {string} |
953 | */ |
954 | goog.events.LISTENER_WRAPPER_PROP_ = '__closure_events_fn_' + |
955 | ((Math.random() * 1e9) >>> 0); |
956 | |
957 | |
958 | /** |
959 | * @param {Object|Function} listener The listener function or an |
960 | * object that contains handleEvent method. |
961 | * @return {!Function} Either the original function or a function that |
962 | * calls obj.handleEvent. If the same listener is passed to this |
963 | * function more than once, the same function is guaranteed to be |
964 | * returned. |
965 | */ |
966 | goog.events.wrapListener = function(listener) { |
967 | goog.asserts.assert(listener, 'Listener can not be null.'); |
968 | |
969 | if (goog.isFunction(listener)) { |
970 | return listener; |
971 | } |
972 | |
973 | goog.asserts.assert( |
974 | listener.handleEvent, 'An object listener must have handleEvent method.'); |
975 | if (!listener[goog.events.LISTENER_WRAPPER_PROP_]) { |
976 | listener[goog.events.LISTENER_WRAPPER_PROP_] = |
977 | function(e) { return listener.handleEvent(e); }; |
978 | } |
979 | return listener[goog.events.LISTENER_WRAPPER_PROP_]; |
980 | }; |
981 | |
982 | |
983 | // Register the browser event handler as an entry point, so that |
984 | // it can be monitored for exception handling, etc. |
985 | goog.debug.entryPointRegistry.register( |
986 | /** |
987 | * @param {function(!Function): !Function} transformer The transforming |
988 | * function. |
989 | */ |
990 | function(transformer) { |
991 | goog.events.handleBrowserEvent_ = transformer( |
992 | goog.events.handleBrowserEvent_); |
993 | }); |