# 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 {
254 | // The else above used to be else if (src.attachEvent) and then there was
255 | // another else statement that threw an exception warning the developer
256 | // they made a mistake. This resulted in an extra object allocation in IE6
257 | // due to a wrapper object that had to be implemented around the element
258 | // and so was removed.
259 | src.attachEvent(goog.events.getOnString_(type.toString()), proxy);
260 | }
261 |
262 | goog.events.listenerCountEstimate_++;
263 | return listenerObj;
264 | };
265 |
266 |
267 | /**
268 | * Helper function for returning a proxy function.
269 | * @return {!Function} A new or reused function object.
270 | */
271 | goog.events.getProxy = function() {
272 | var proxyCallbackFunction = goog.events.handleBrowserEvent_;
273 | // Use a local var f to prevent one allocation.
274 | var f = goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT ?
275 | function(eventObject) {
276 | return proxyCallbackFunction.call(f.src, f.listener, eventObject);
277 | } :
278 | function(eventObject) {
279 | var v = proxyCallbackFunction.call(f.src, f.listener, eventObject);
280 | // NOTE(chrishenry): In IE, we hack in a capture phase. However, if
281 | // there is inline event handler which tries to prevent default (for
282 | // example <a href="..." onclick="return false">...</a>) in a
283 | // descendant element, the prevent default will be overridden
284 | // by this listener if this listener were to return true. Hence, we
285 | // return undefined.
286 | if (!v) return v;
287 | };
288 | return f;
289 | };
290 |
291 |
292 | /**
293 | * Adds an event listener for a specific event on a native event
294 | * target (such as a DOM element) or an object that has implemented
295 | * {@link goog.events.Listenable}. After the event has fired the event
296 | * listener is removed from the target.
297 | *
298 | * If an existing listener already exists, listenOnce will do
299 | * nothing. In particular, if the listener was previously registered
300 | * via listen(), listenOnce() will not turn the listener into a
301 | * one-off listener. Similarly, if there is already an existing
302 | * one-off listener, listenOnce does not modify the listeners (it is
303 | * still a once listener).
304 | *
305 | * @param {EventTarget|goog.events.Listenable} src The node to listen
306 | * to events on.
307 | * @param {string|Array<string>|
308 | * !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
309 | * type Event type or array of event types.
310 | * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null}
311 | * listener Callback method.
312 | * @param {boolean=} opt_capt Fire in capture phase?.
313 | * @param {T=} opt_handler Element in whose scope to call the listener.
314 | * @return {goog.events.Key} Unique key for the listener.
315 | * @template T,EVENTOBJ
316 | */
317 | goog.events.listenOnce = function(src, type, listener, opt_capt, opt_handler) {
318 | if (goog.isArray(type)) {
319 | for (var i = 0; i < type.length; i++) {
320 | goog.events.listenOnce(src, type[i], listener, opt_capt, opt_handler);
321 | }
322 | return null;
323 | }
324 |
325 | listener = goog.events.wrapListener(listener);
326 | if (goog.events.Listenable.isImplementedBy(src)) {
327 | return src.listenOnce(
328 | /** @type {string|!goog.events.EventId} */ (type),
329 | listener, opt_capt, opt_handler);
330 | } else {
331 | return goog.events.listen_(
332 | /** @type {!EventTarget} */ (src),
333 | /** @type {string|!goog.events.EventId} */ (type),
334 | listener, /* callOnce */ true, opt_capt, opt_handler);
335 | }
336 | };
337 |
338 |
339 | /**
340 | * Adds an event listener with a specific event wrapper on a DOM Node or an
341 | * object that has implemented {@link goog.events.Listenable}. A listener can
342 | * only be added once to an object.
343 | *
344 | * @param {EventTarget|goog.events.Listenable} src The target to
345 | * listen to events on.
346 | * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
347 | * @param {function(this:T, ?):?|{handleEvent:function(?):?}|null} listener
348 | * Callback method, or an object with a handleEvent function.
349 | * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
350 | * false).
351 | * @param {T=} opt_handler Element in whose scope to call the listener.
352 | * @template T
353 | */
354 | goog.events.listenWithWrapper = function(src, wrapper, listener, opt_capt,
355 | opt_handler) {
356 | wrapper.listen(src, listener, opt_capt, opt_handler);
357 | };
358 |
359 |
360 | /**
361 | * Removes an event listener which was added with listen().
362 | *
363 | * @param {EventTarget|goog.events.Listenable} src The target to stop
364 | * listening to events on.
365 | * @param {string|Array<string>|
366 | * !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
367 | * type Event type or array of event types to unlisten to.
368 | * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
369 | * listener function to remove.
370 | * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
371 | * whether the listener is fired during the capture or bubble phase of the
372 | * event.
373 | * @param {Object=} opt_handler Element in whose scope to call the listener.
374 | * @return {?boolean} indicating whether the listener was there to remove.
375 | * @template EVENTOBJ
376 | */
377 | goog.events.unlisten = function(src, type, listener, opt_capt, opt_handler) {
378 | if (goog.isArray(type)) {
379 | for (var i = 0; i < type.length; i++) {
380 | goog.events.unlisten(src, type[i], listener, opt_capt, opt_handler);
381 | }
382 | return null;
383 | }
384 |
385 | listener = goog.events.wrapListener(listener);
386 | if (goog.events.Listenable.isImplementedBy(src)) {
387 | return src.unlisten(
388 | /** @type {string|!goog.events.EventId} */ (type),
389 | listener, opt_capt, opt_handler);
390 | }
391 |
392 | if (!src) {
393 | // TODO(chrishenry): We should tighten the API to only accept
394 | // non-null objects, or add an assertion here.
395 | return false;
396 | }
397 |
398 | var capture = !!opt_capt;
399 | var listenerMap = goog.events.getListenerMap_(
400 | /** @type {!EventTarget} */ (src));
401 | if (listenerMap) {
402 | var listenerObj = listenerMap.getListener(
403 | /** @type {string|!goog.events.EventId} */ (type),
404 | listener, capture, opt_handler);
405 | if (listenerObj) {
406 | return goog.events.unlistenByKey(listenerObj);
407 | }
408 | }
409 |
410 | return false;
411 | };
412 |
413 |
414 | /**
415 | * Removes an event listener which was added with listen() by the key
416 | * returned by listen().
417 | *
418 | * @param {goog.events.Key} key The key returned by listen() for this
419 | * event listener.
420 | * @return {boolean} indicating whether the listener was there to remove.
421 | */
422 | goog.events.unlistenByKey = function(key) {
423 | // TODO(chrishenry): Remove this check when tests that rely on this
424 | // are fixed.
425 | if (goog.isNumber(key)) {
426 | return false;
427 | }
428 |
429 | var listener = /** @type {goog.events.ListenableKey} */ (key);
430 | if (!listener || listener.removed) {
431 | return false;
432 | }
433 |
434 | var src = listener.src;
435 | if (goog.events.Listenable.isImplementedBy(src)) {
436 | return src.unlistenByKey(listener);
437 | }
438 |
439 | var type = listener.type;
440 | var proxy = listener.proxy;
441 | if (src.removeEventListener) {
442 | src.removeEventListener(type, proxy, listener.capture);
443 | } else if (src.detachEvent) {
444 | src.detachEvent(goog.events.getOnString_(type), proxy);
445 | }
446 | goog.events.listenerCountEstimate_--;
447 |
448 | var listenerMap = goog.events.getListenerMap_(
449 | /** @type {!EventTarget} */ (src));
450 | // TODO(chrishenry): Try to remove this conditional and execute the
451 | // first branch always. This should be safe.
452 | if (listenerMap) {
453 | listenerMap.removeByKey(listener);
454 | if (listenerMap.getTypeCount() == 0) {
455 | // Null the src, just because this is simple to do (and useful
456 | // for IE <= 7).
457 | listenerMap.src = null;
458 | // We don't use delete here because IE does not allow delete
459 | // on a window object.
460 | src[goog.events.LISTENER_MAP_PROP_] = null;
461 | }
462 | } else {
463 | listener.markAsRemoved();
464 | }
465 |
466 | return true;
467 | };
468 |
469 |
470 | /**
471 | * Removes an event listener which was added with listenWithWrapper().
472 | *
473 | * @param {EventTarget|goog.events.Listenable} src The target to stop
474 | * listening to events on.
475 | * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
476 | * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
477 | * listener function to remove.
478 | * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
479 | * whether the listener is fired during the capture or bubble phase of the
480 | * event.
481 | * @param {Object=} opt_handler Element in whose scope to call the listener.
482 | */
483 | goog.events.unlistenWithWrapper = function(src, wrapper, listener, opt_capt,
484 | opt_handler) {
485 | wrapper.unlisten(src, listener, opt_capt, opt_handler);
486 | };
487 |
488 |
489 | /**
490 | * Removes all listeners from an object. You can also optionally
491 | * remove listeners of a particular type.
492 | *
493 | * @param {Object|undefined} obj Object to remove listeners from. Must be an
494 | * EventTarget or a goog.events.Listenable.
495 | * @param {string|!goog.events.EventId=} opt_type Type of event to remove.
496 | * Default is all types.
497 | * @return {number} Number of listeners removed.
498 | */
499 | goog.events.removeAll = function(obj, opt_type) {
500 | // TODO(chrishenry): Change the type of obj to
501 | // (!EventTarget|!goog.events.Listenable).
502 |
503 | if (!obj) {
504 | return 0;
505 | }
506 |
507 | if (goog.events.Listenable.isImplementedBy(obj)) {
508 | return obj.removeAllListeners(opt_type);
509 | }
510 |
511 | var listenerMap = goog.events.getListenerMap_(
512 | /** @type {!EventTarget} */ (obj));
513 | if (!listenerMap) {
514 | return 0;
515 | }
516 |
517 | var count = 0;
518 | var typeStr = opt_type && opt_type.toString();
519 | for (var type in listenerMap.listeners) {
520 | if (!typeStr || type == typeStr) {
521 | // Clone so that we don't need to worry about unlistenByKey
522 | // changing the content of the ListenerMap.
523 | var listeners = listenerMap.listeners[type].concat();
524 | for (var i = 0; i < listeners.length; ++i) {
525 | if (goog.events.unlistenByKey(listeners[i])) {
526 | ++count;
527 | }
528 | }
529 | }
530 | }
531 | return count;
532 | };
533 |
534 |
535 | /**
536 | * Gets the listeners for a given object, type and capture phase.
537 | *
538 | * @param {Object} obj Object to get listeners for.
539 | * @param {string|!goog.events.EventId} type Event type.
540 | * @param {boolean} capture Capture phase?.
541 | * @return {Array<goog.events.Listener>} Array of listener objects.
542 | */
543 | goog.events.getListeners = function(obj, type, capture) {
544 | if (goog.events.Listenable.isImplementedBy(obj)) {
545 | return obj.getListeners(type, capture);
546 | } else {
547 | if (!obj) {
548 | // TODO(chrishenry): We should tighten the API to accept
549 | // !EventTarget|goog.events.Listenable, and add an assertion here.
550 | return [];
551 | }
552 |
553 | var listenerMap = goog.events.getListenerMap_(
554 | /** @type {!EventTarget} */ (obj));
555 | return listenerMap ? listenerMap.getListeners(type, capture) : [];
556 | }
557 | };
558 |
559 |
560 | /**
561 | * Gets the goog.events.Listener for the event or null if no such listener is
562 | * in use.
563 | *
564 | * @param {EventTarget|goog.events.Listenable} src The target from
565 | * which to get listeners.
566 | * @param {?string|!goog.events.EventId<EVENTOBJ>} type The type of the event.
567 | * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null} listener The
568 | * listener function to get.
569 | * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
570 | * whether the listener is fired during the
571 | * capture or bubble phase of the event.
572 | * @param {Object=} opt_handler Element in whose scope to call the listener.
573 | * @return {goog.events.ListenableKey} the found listener or null if not found.
574 | * @template EVENTOBJ
575 | */
576 | goog.events.getListener = function(src, type, listener, opt_capt, opt_handler) {
577 | // TODO(chrishenry): Change type from ?string to string, or add assertion.
578 | type = /** @type {string} */ (type);
579 | listener = goog.events.wrapListener(listener);
580 | var capture = !!opt_capt;
581 | if (goog.events.Listenable.isImplementedBy(src)) {
582 | return src.getListener(type, listener, capture, opt_handler);
583 | }
584 |
585 | if (!src) {
586 | // TODO(chrishenry): We should tighten the API to only accept
587 | // non-null objects, or add an assertion here.
588 | return null;
589 | }
590 |
591 | var listenerMap = goog.events.getListenerMap_(
592 | /** @type {!EventTarget} */ (src));
593 | if (listenerMap) {
594 | return listenerMap.getListener(type, listener, capture, opt_handler);
595 | }
596 | return null;
597 | };
598 |
599 |
600 | /**
601 | * Returns whether an event target has any active listeners matching the
602 | * specified signature. If either the type or capture parameters are
603 | * unspecified, the function will match on the remaining criteria.
604 | *
605 | * @param {EventTarget|goog.events.Listenable} obj Target to get
606 | * listeners for.
607 | * @param {string|!goog.events.EventId=} opt_type Event type.
608 | * @param {boolean=} opt_capture Whether to check for capture or bubble-phase
609 | * listeners.
610 | * @return {boolean} Whether an event target has one or more listeners matching
611 | * the requested type and/or capture phase.
612 | */
613 | goog.events.hasListener = function(obj, opt_type, opt_capture) {
614 | if (goog.events.Listenable.isImplementedBy(obj)) {
615 | return obj.hasListener(opt_type, opt_capture);
616 | }
617 |
618 | var listenerMap = goog.events.getListenerMap_(
619 | /** @type {!EventTarget} */ (obj));
620 | return !!listenerMap && listenerMap.hasListener(opt_type, opt_capture);
621 | };
622 |
623 |
624 | /**
625 | * Provides a nice string showing the normalized event objects public members
626 | * @param {Object} e Event Object.
627 | * @return {string} String of the public members of the normalized event object.
628 | */
629 | goog.events.expose = function(e) {
630 | var str = [];
631 | for (var key in e) {
632 | if (e[key] && e[key].id) {
633 | str.push(key + ' = ' + e[key] + ' (' + e[key].id + ')');
634 | } else {
635 | str.push(key + ' = ' + e[key]);
636 | }
637 | }
638 | return str.join('\n');
639 | };
640 |
641 |
642 | /**
643 | * Returns a string with on prepended to the specified type. This is used for IE
644 | * which expects "on" to be prepended. This function caches the string in order
645 | * to avoid extra allocations in steady state.
646 | * @param {string} type Event type.
647 | * @return {string} The type string with 'on' prepended.
648 | * @private
649 | */
650 | goog.events.getOnString_ = function(type) {
651 | if (type in goog.events.onStringMap_) {
652 | return goog.events.onStringMap_[type];
653 | }
654 | return goog.events.onStringMap_[type] = goog.events.onString_ + type;
655 | };
656 |
657 |
658 | /**
659 | * Fires an object's listeners of a particular type and phase
660 | *
661 | * @param {Object} obj Object whose listeners to call.
662 | * @param {string|!goog.events.EventId} type Event type.
663 | * @param {boolean} capture Which event phase.
664 | * @param {Object} eventObject Event object to be passed to listener.
665 | * @return {boolean} True if all listeners returned true else false.
666 | */
667 | goog.events.fireListeners = function(obj, type, capture, eventObject) {
668 | if (goog.events.Listenable.isImplementedBy(obj)) {
669 | return obj.fireListeners(type, capture, eventObject);
670 | }
671 |
672 | return goog.events.fireListeners_(obj, type, capture, eventObject);
673 | };
674 |
675 |
676 | /**
677 | * Fires an object's listeners of a particular type and phase.
678 | * @param {Object} obj Object whose listeners to call.
679 | * @param {string|!goog.events.EventId} type Event type.
680 | * @param {boolean} capture Which event phase.
681 | * @param {Object} eventObject Event object to be passed to listener.
682 | * @return {boolean} True if all listeners returned true else false.
683 | * @private
684 | */
685 | goog.events.fireListeners_ = function(obj, type, capture, eventObject) {
686 | var retval = 1;
687 |
688 | var listenerMap = goog.events.getListenerMap_(
689 | /** @type {EventTarget} */ (obj));
690 | if (listenerMap) {
691 | // TODO(chrishenry): Original code avoids array creation when there
692 | // is no listener, so we do the same. If this optimization turns
693 | // out to be not required, we can replace this with
694 | // listenerMap.getListeners(type, capture) instead, which is simpler.
695 | var listenerArray = listenerMap.listeners[type.toString()];
696 | if (listenerArray) {
697 | listenerArray = listenerArray.concat();
698 | for (var i = 0; i < listenerArray.length; i++) {
699 | var listener = listenerArray[i];
700 | // We might not have a listener if the listener was removed.
701 | if (listener && listener.capture == capture && !listener.removed) {
702 | retval &=
703 | goog.events.fireListener(listener, eventObject) !== false;
704 | }
705 | }
706 | }
707 | }
708 | return Boolean(retval);
709 | };
710 |
711 |
712 | /**
713 | * Fires a listener with a set of arguments
714 | *
715 | * @param {goog.events.Listener} listener The listener object to call.
716 | * @param {Object} eventObject The event object to pass to the listener.
717 | * @return {boolean} Result of listener.
718 | */
719 | goog.events.fireListener = function(listener, eventObject) {
720 | var listenerFn = listener.listener;
721 | var listenerHandler = listener.handler || listener.src;
722 |
723 | if (listener.callOnce) {
724 | goog.events.unlistenByKey(listener);
725 | }
726 | return listenerFn.call(listenerHandler, eventObject);
727 | };
728 |
729 |
730 | /**
731 | * Gets the total number of listeners currently in the system.
732 | * @return {number} Number of listeners.
733 | * @deprecated This returns estimated count, now that Closure no longer
734 | * stores a central listener registry. We still return an estimation
735 | * to keep existing listener-related tests passing. In the near future,
736 | * this function will be removed.
737 | */
738 | goog.events.getTotalListenerCount = function() {
739 | return goog.events.listenerCountEstimate_;
740 | };
741 |
742 |
743 | /**
744 | * Dispatches an event (or event like object) and calls all listeners
745 | * listening for events of this type. The type of the event is decided by the
746 | * type property on the event object.
747 | *
748 | * If any of the listeners returns false OR calls preventDefault then this
749 | * function will return false. If one of the capture listeners calls
750 | * stopPropagation, then the bubble listeners won't fire.
751 | *
752 | * @param {goog.events.Listenable} src The event target.
753 | * @param {goog.events.EventLike} e Event object.
754 | * @return {boolean} If anyone called preventDefault on the event object (or
755 | * if any of the handlers returns false) this will also return false.
756 | * If there are no handlers, or if all handlers return true, this returns
757 | * true.
758 | */
759 | goog.events.dispatchEvent = function(src, e) {
760 | goog.asserts.assert(
761 | goog.events.Listenable.isImplementedBy(src),
762 | 'Can not use goog.events.dispatchEvent with ' +
763 | 'non-goog.events.Listenable instance.');
764 | return src.dispatchEvent(e);
765 | };
766 |
767 |
768 | /**
769 | * Installs exception protection for the browser event entry point using the
770 | * given error handler.
771 | *
772 | * @param {goog.debug.ErrorHandler} errorHandler Error handler with which to
773 | * protect the entry point.
774 | */
775 | goog.events.protectBrowserEventEntryPoint = function(errorHandler) {
776 | goog.events.handleBrowserEvent_ = errorHandler.protectEntryPoint(
777 | goog.events.handleBrowserEvent_);
778 | };
779 |
780 |
781 | /**
782 | * Handles an event and dispatches it to the correct listeners. This
783 | * function is a proxy for the real listener the user specified.
784 | *
785 | * @param {goog.events.Listener} listener The listener object.
786 | * @param {Event=} opt_evt Optional event object that gets passed in via the
787 | * native event handlers.
788 | * @return {boolean} Result of the event handler.
789 | * @this {EventTarget} The object or Element that fired the event.
790 | * @private
791 | */
792 | goog.events.handleBrowserEvent_ = function(listener, opt_evt) {
793 | if (listener.removed) {
794 | return true;
795 | }
796 |
797 | // Synthesize event propagation if the browser does not support W3C
798 | // event model.
799 | if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
800 | var ieEvent = opt_evt ||
801 | /** @type {Event} */ (goog.getObjectByName('window.event'));
802 | var evt = new goog.events.BrowserEvent(ieEvent, this);
803 | var retval = true;
804 |
805 | if (goog.events.CAPTURE_SIMULATION_MODE ==
806 | goog.events.CaptureSimulationMode.ON) {
807 | // If we have not marked this event yet, we should perform capture
808 | // simulation.
809 | if (!goog.events.isMarkedIeEvent_(ieEvent)) {
810 | goog.events.markIeEvent_(ieEvent);
811 |
812 | var ancestors = [];
813 | for (var parent = evt.currentTarget; parent;
814 | parent = parent.parentNode) {
815 | ancestors.push(parent);
816 | }
817 |
818 | // Fire capture listeners.
819 | var type = listener.type;
820 | for (var i = ancestors.length - 1; !evt.propagationStopped_ && i >= 0;
821 | i--) {
822 | evt.currentTarget = ancestors[i];
823 | retval &= goog.events.fireListeners_(ancestors[i], type, true, evt);
824 | }
825 |
826 | // Fire bubble listeners.
827 | //
828 | // We can technically rely on IE to perform bubble event
829 | // propagation. However, it turns out that IE fires events in
830 | // opposite order of attachEvent registration, which broke
831 | // some code and tests that rely on the order. (While W3C DOM
832 | // Level 2 Events TR leaves the event ordering unspecified,
833 | // modern browsers and W3C DOM Level 3 Events Working Draft
834 | // actually specify the order as the registration order.)
835 | for (var i = 0; !evt.propagationStopped_ && i < ancestors.length; i++) {
836 | evt.currentTarget = ancestors[i];
837 | retval &= goog.events.fireListeners_(ancestors[i], type, false, evt);
838 | }
839 | }
840 | } else {
841 | retval = goog.events.fireListener(listener, evt);
842 | }
843 | return retval;
844 | }
845 |
846 | // Otherwise, simply fire the listener.
847 | return goog.events.fireListener(
848 | listener, new goog.events.BrowserEvent(opt_evt, this));
849 | };
850 |
851 |
852 | /**
853 | * This is used to mark the IE event object so we do not do the Closure pass
854 | * twice for a bubbling event.
855 | * @param {Event} e The IE browser event.
856 | * @private
857 | */
858 | goog.events.markIeEvent_ = function(e) {
859 | // Only the keyCode and the returnValue can be changed. We use keyCode for
860 | // non keyboard events.
861 | // event.returnValue is a bit more tricky. It is undefined by default. A
862 | // boolean false prevents the default action. In a window.onbeforeunload and
863 | // the returnValue is non undefined it will be alerted. However, we will only
864 | // modify the returnValue for keyboard events. We can get a problem if non
865 | // closure events sets the keyCode or the returnValue
866 |
867 | var useReturnValue = false;
868 |
869 | if (e.keyCode == 0) {
870 | // We cannot change the keyCode in case that srcElement is input[type=file].
871 | // We could test that that is the case but that would allocate 3 objects.
872 | // If we use try/catch we will only allocate extra objects in the case of a
873 | // failure.
874 | /** @preserveTry */
875 | try {
876 | e.keyCode = -1;
877 | return;
878 | } catch (ex) {
879 | useReturnValue = true;
880 | }
881 | }
882 |
883 | if (useReturnValue ||
884 | /** @type {boolean|undefined} */ (e.returnValue) == undefined) {
885 | e.returnValue = true;
886 | }
887 | };
888 |
889 |
890 | /**
891 | * This is used to check if an IE event has already been handled by the Closure
892 | * system so we do not do the Closure pass twice for a bubbling event.
893 | * @param {Event} e The IE browser event.
894 | * @return {boolean} True if the event object has been marked.
895 | * @private
896 | */
897 | goog.events.isMarkedIeEvent_ = function(e) {
898 | return e.keyCode < 0 || e.returnValue != undefined;
899 | };
900 |
901 |
902 | /**
903 | * Counter to create unique event ids.
904 | * @private {number}
905 | */
906 | goog.events.uniqueIdCounter_ = 0;
907 |
908 |
909 | /**
910 | * Creates a unique event id.
911 | *
912 | * @param {string} identifier The identifier.
913 | * @return {string} A unique identifier.
914 | * @idGenerator
915 | */
916 | goog.events.getUniqueId = function(identifier) {
917 | return identifier + '_' + goog.events.uniqueIdCounter_++;
918 | };
919 |
920 |
921 | /**
922 | * @param {EventTarget} src The source object.
923 | * @return {goog.events.ListenerMap} A listener map for the given
924 | * source object, or null if none exists.
925 | * @private
926 | */
927 | goog.events.getListenerMap_ = function(src) {
928 | var listenerMap = src[goog.events.LISTENER_MAP_PROP_];
929 | // IE serializes the property as well (e.g. when serializing outer
930 | // HTML). So we must check that the value is of the correct type.
931 | return listenerMap instanceof goog.events.ListenerMap ? listenerMap : null;
932 | };
933 |
934 |
935 | /**
936 | * Expando property for listener function wrapper for Object with
937 | * handleEvent.
938 | * @private @const {string}
939 | */
940 | goog.events.LISTENER_WRAPPER_PROP_ = '__closure_events_fn_' +
941 | ((Math.random() * 1e9) >>> 0);
942 |
943 |
944 | /**
945 | * @param {Object|Function} listener The listener function or an
946 | * object that contains handleEvent method.
947 | * @return {!Function} Either the original function or a function that
948 | * calls obj.handleEvent. If the same listener is passed to this
949 | * function more than once, the same function is guaranteed to be
950 | * returned.
951 | */
952 | goog.events.wrapListener = function(listener) {
953 | goog.asserts.assert(listener, 'Listener can not be null.');
954 |
955 | if (goog.isFunction(listener)) {
956 | return listener;
957 | }
958 |
959 | goog.asserts.assert(
960 | listener.handleEvent, 'An object listener must have handleEvent method.');
961 | if (!listener[goog.events.LISTENER_WRAPPER_PROP_]) {
962 | listener[goog.events.LISTENER_WRAPPER_PROP_] =
963 | function(e) { return listener.handleEvent(e); };
964 | }
965 | return listener[goog.events.LISTENER_WRAPPER_PROP_];
966 | };
967 |
968 |
969 | // Register the browser event handler as an entry point, so that
970 | // it can be monitored for exception handling, etc.
971 | goog.debug.entryPointRegistry.register(
972 | /**
973 | * @param {function(!Function): !Function} transformer The transforming
974 | * function.
975 | */
976 | function(transformer) {
977 | goog.events.handleBrowserEvent_ = transformer(
978 | goog.events.handleBrowserEvent_);
979 | });