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