1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD
2 * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the
3 * full text of the license. */
7 * @requires OpenLayers/Util.js
11 * Namespace: OpenLayers.Event
12 * Utility functions for event handling.
18 * {Object} A hashtable cache of the event observers. Keyed by
19 * element._eventCacheID
24 * Constant: KEY_BACKSPACE
36 * Constant: KEY_RETURN
72 * Constant: KEY_DELETE
80 * Cross browser event element detection.
86 * {DOMElement} The element that caused the event
88 element: function(event) {
89 return event.target || event.srcElement;
94 * Determine whether event was caused by a left click.
102 isLeftClick: function(event) {
103 return (((event.which) && (event.which == 1)) ||
104 ((event.button) && (event.button == 1)));
108 * Method: isRightClick
109 * Determine whether event was caused by a right mouse click.
117 isRightClick: function(event) {
118 return (((event.which) && (event.which == 3)) ||
119 ((event.button) && (event.button == 2)));
124 * Stops an event from propagating.
128 * allowDefault - {Boolean} If true, we stop the event chain but
129 * still allow the default browser
130 * behaviour (text selection, radio-button
134 stop: function(event, allowDefault) {
137 if (event.preventDefault) {
138 event.preventDefault();
140 event.returnValue = false;
144 if (event.stopPropagation) {
145 event.stopPropagation();
147 event.cancelBubble = true;
152 * Method: findElement
159 * {DOMElement} The first node with the given tagName, starting from the
160 * node the event was triggered on and traversing the DOM upwards
162 findElement: function(event, tagName) {
163 var element = OpenLayers.Event.element(event);
164 while (element.parentNode && (!element.tagName ||
165 (element.tagName.toUpperCase() != tagName.toUpperCase()))){
166 element = element.parentNode;
175 * elementParam - {DOMElement || String}
177 * observer - {function}
178 * useCapture - {Boolean}
180 observe: function(elementParam, name, observer, useCapture) {
181 var element = OpenLayers.Util.getElement(elementParam);
182 useCapture = useCapture || false;
184 if (name == 'keypress' &&
185 (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
186 || element.attachEvent)) {
190 //if observers cache has not yet been created, create it
191 if (!this.observers) {
195 //if not already assigned, make a new unique cache ID
196 if (!element._eventCacheID) {
197 var idPrefix = "eventCacheID_";
199 idPrefix = element.id + "_" + idPrefix;
201 element._eventCacheID = OpenLayers.Util.createUniqueID(idPrefix);
204 var cacheID = element._eventCacheID;
206 //if there is not yet a hash entry for this element, add one
207 if (!this.observers[cacheID]) {
208 this.observers[cacheID] = [];
211 //add a new observer to this element's list
212 this.observers[cacheID].push({
215 'observer': observer,
216 'useCapture': useCapture
219 //add the actual browser event listener
220 if (element.addEventListener) {
221 element.addEventListener(name, observer, useCapture);
222 } else if (element.attachEvent) {
223 element.attachEvent('on' + name, observer);
228 * Method: stopObservingElement
229 * Given the id of an element to stop observing, cycle through the
230 * element's cached observers, calling stopObserving on each one,
231 * skipping those entries which can no longer be removed.
234 * elementParam - {DOMElement || String}
236 stopObservingElement: function(elementParam) {
237 var element = OpenLayers.Util.getElement(elementParam);
238 var cacheID = element._eventCacheID;
240 this._removeElementObservers(OpenLayers.Event.observers[cacheID]);
244 * Method: _removeElementObservers
247 * elementObservers - {Array(Object)} Array of (element, name,
248 * observer, usecapture) objects,
249 * taken directly from hashtable
251 _removeElementObservers: function(elementObservers) {
252 if (elementObservers) {
253 for(var i = elementObservers.length-1; i >= 0; i--) {
254 var entry = elementObservers[i];
255 var args = new Array(entry.element,
259 var removed = OpenLayers.Event.stopObserving.apply(this, args);
265 * Method: stopObserving
268 * elementParam - {DOMElement || String}
270 * observer - {function}
271 * useCapture - {Boolean}
274 * {Boolean} Whether or not the event observer was removed
276 stopObserving: function(elementParam, name, observer, useCapture) {
277 useCapture = useCapture || false;
279 var element = OpenLayers.Util.getElement(elementParam);
280 var cacheID = element._eventCacheID;
282 if (name == 'keypress') {
283 if ( navigator.appVersion.match(/Konqueror|Safari|KHTML/) ||
284 element.detachEvent) {
289 // find element's entry in this.observers cache and remove it
290 var foundEntry = false;
291 var elementObservers = OpenLayers.Event.observers[cacheID];
292 if (elementObservers) {
294 // find the specific event type in the element's list
296 while(!foundEntry && i < elementObservers.length) {
297 var cacheEntry = elementObservers[i];
299 if ((cacheEntry.name == name) &&
300 (cacheEntry.observer == observer) &&
301 (cacheEntry.useCapture == useCapture)) {
303 elementObservers.splice(i, 1);
304 if (elementObservers.length == 0) {
305 delete OpenLayers.Event.observers[cacheID];
314 //actually remove the event listener from browser
316 if (element.removeEventListener) {
317 element.removeEventListener(name, observer, useCapture);
318 } else if (element && element.detachEvent) {
319 element.detachEvent('on' + name, observer);
326 * Method: unloadCache
327 * Cycle through all the element entries in the events cache and call
328 * stopObservingElement on each.
330 unloadCache: function() {
331 // check for OpenLayers.Event before checking for observers, because
332 // OpenLayers.Event may be undefined in IE if no map instance was
334 if (OpenLayers.Event && OpenLayers.Event.observers) {
335 for (var cacheID in OpenLayers.Event.observers) {
336 var elementObservers = OpenLayers.Event.observers[cacheID];
337 OpenLayers.Event._removeElementObservers.apply(this,
340 OpenLayers.Event.observers = false;
344 CLASS_NAME: "OpenLayers.Event"
347 /* prevent memory leaks in IE */
348 OpenLayers.Event.observe(window, 'unload', OpenLayers.Event.unloadCache, false);
350 // FIXME: Remove this in 3.0. In 3.0, Event.stop will no longer be provided
353 OpenLayers.Util.applyDefaults(window.Event, OpenLayers.Event);
355 var Event = OpenLayers.Event;
359 * Class: OpenLayers.Events
361 OpenLayers.Events = OpenLayers.Class({
364 * Constant: BROWSER_EVENTS
365 * {Array(String)} supported events
368 "mouseover", "mouseout",
369 "mousedown", "mouseup", "mousemove",
370 "click", "dblclick", "rightclick", "dblrightclick",
371 "resize", "focus", "blur"
375 * Property: listeners
376 * {Object} Hashtable of Array(Function): events listener functions
382 * {Object} the code object issuing application events
388 * {DOMElement} the DOM element receiving browser events
393 * Property: eventTypes
394 * {Array(String)} list of support application events
399 * Property: eventHandler
400 * {Function} bound event handler attached to elements
405 * APIProperty: fallThrough
411 * APIProperty: includeXY
412 * {Boolean} Should the .xy property automatically be created for browser
413 * mouse events? In general, this should be false. If it is true, then
414 * mouse events will automatically generate a '.xy' property on the
415 * event object that is passed. (Prior to OpenLayers 2.7, this was true
416 * by default.) Otherwise, you can call the getMousePosition on the
417 * relevant events handler on the object available via the 'evt.object'
418 * property of the evt object. So, for most events, you can call:
419 * function named(evt) {
420 * this.xy = this.object.events.getMousePosition(evt)
423 * This option typically defaults to false for performance reasons:
424 * when creating an events object whose primary purpose is to manage
425 * relatively positioned mouse events within a div, it may make
426 * sense to set it to true.
428 * This option is also used to control whether the events object caches
429 * offsets. If this is false, it will not: the reason for this is that
430 * it is only expected to be called many times if the includeXY property
431 * is set to true. If you set this to true, you are expected to clear
432 * the offset cache manually (using this.clearMouseCache()) if:
433 * the border of the element changes
434 * the location of the element in the page changes
439 * Method: clearMouseListener
440 * A version of <clearMouseCache> that is bound to this instance so that
441 * it can be used with <OpenLayers.Event.observe> and
442 * <OpenLayers.Event.stopObserving>.
444 clearMouseListener: null,
447 * Constructor: OpenLayers.Events
448 * Construct an OpenLayers.Events object.
451 * object - {Object} The js object to which this Events object is being
452 * added element - {DOMElement} A dom element to respond to browser events
453 * eventTypes - {Array(String)} Array of custom application events
454 * fallThrough - {Boolean} Allow events to fall through after these have
456 * options - {Object} Options for the events object.
458 initialize: function (object, element, eventTypes, fallThrough, options) {
459 OpenLayers.Util.extend(this, options);
460 this.object = object;
461 this.fallThrough = fallThrough;
464 // keep a bound copy of handleBrowserEvent() so that we can
465 // pass the same function to both Event.observe() and .stopObserving()
466 this.eventHandler = OpenLayers.Function.bindAsEventListener(
467 this.handleBrowserEvent, this
470 // to be used with observe and stopObserving
471 this.clearMouseListener = OpenLayers.Function.bind(
472 this.clearMouseCache, this
475 // if eventTypes is specified, create a listeners list for each
476 // custom application event.
477 this.eventTypes = [];
478 if (eventTypes != null) {
479 for (var i=0, len=eventTypes.length; i<len; i++) {
480 this.addEventType(eventTypes[i]);
484 // if a dom element is specified, add a listeners list
485 // for browser events on the element and register them
486 if (element != null) {
487 this.attachToElement(element);
494 destroy: function () {
496 OpenLayers.Event.stopObservingElement(this.element);
497 if(this.element.hasScrollEvent) {
498 OpenLayers.Event.stopObserving(
499 window, "scroll", this.clearMouseListener
505 this.listeners = null;
507 this.eventTypes = null;
508 this.fallThrough = null;
509 this.eventHandler = null;
513 * APIMethod: addEventType
514 * Add a new event type to this events object.
515 * If the event type has already been added, do nothing.
518 * eventName - {String}
520 addEventType: function(eventName) {
521 if (!this.listeners[eventName]) {
522 this.eventTypes.push(eventName);
523 this.listeners[eventName] = [];
528 * Method: attachToElement
531 * element - {HTMLDOMElement} a DOM element to attach browser events to
533 attachToElement: function (element) {
535 OpenLayers.Event.stopObservingElement(this.element);
537 this.element = element;
538 for (var i=0, len=this.BROWSER_EVENTS.length; i<len; i++) {
539 var eventType = this.BROWSER_EVENTS[i];
541 // every browser event has a corresponding application event
542 // (whether it's listened for or not).
543 this.addEventType(eventType);
545 // use Prototype to register the event cross-browser
546 OpenLayers.Event.observe(element, eventType, this.eventHandler);
548 // disable dragstart in IE so that mousedown/move/up works normally
549 OpenLayers.Event.observe(element, "dragstart", OpenLayers.Event.stop);
554 * Convenience method for registering listeners with a common scope.
559 * "loadstart": loadStartListener,
560 * "loadend": loadEndListener,
565 on: function(object) {
566 for(var type in object) {
567 if(type != "scope") {
568 this.register(type, object.scope, object[type]);
574 * APIMethod: register
575 * Register an event on the events object.
577 * When the event is triggered, the 'func' function will be called, in the
578 * context of 'obj'. Imagine we were to register an event, specifying an
579 * OpenLayers.Bounds Object as 'obj'. When the event is triggered, the
580 * context in the callback function will be our Bounds object. This means
581 * that within our callback function, we can access the properties and
582 * methods of the Bounds object through the "this" variable. So our
583 * callback could execute something like:
584 * : leftStr = "Left: " + this.left;
588 * : centerStr = "Center: " + this.getCenterLonLat();
591 * type - {String} Name of the event to register
592 * obj - {Object} The object to bind the context to for the callback#.
593 * If no object is specified, default is the Events's
595 * func - {Function} The callback function. If no callback is
596 * specified, this function does nothing.
600 register: function (type, obj, func) {
602 if ( (func != null) &&
603 (OpenLayers.Util.indexOf(this.eventTypes, type) != -1) ) {
608 var listeners = this.listeners[type];
609 listeners.push( {obj: obj, func: func} );
614 * APIMethod: registerPriority
615 * Same as register() but adds the new listener to the *front* of the
616 * events queue instead of to the end.
618 * TODO: get rid of this in 3.0 - Decide whether listeners should be
619 * called in the order they were registered or in reverse order.
623 * type - {String} Name of the event to register
624 * obj - {Object} The object to bind the context to for the callback#.
625 * If no object is specified, default is the Events's
627 * func - {Function} The callback function. If no callback is
628 * specified, this function does nothing.
630 registerPriority: function (type, obj, func) {
636 var listeners = this.listeners[type];
637 if (listeners != null) {
638 listeners.unshift( {obj: obj, func: func} );
645 * Convenience method for unregistering listeners with a common scope.
650 * "loadstart": loadStartListener,
651 * "loadend": loadEndListener,
656 un: function(object) {
657 for(var type in object) {
658 if(type != "scope") {
659 this.unregister(type, object.scope, object[type]);
665 * APIMethod: unregister
669 * obj - {Object} If none specified, defaults to this.object
672 unregister: function (type, obj, func) {
676 var listeners = this.listeners[type];
677 if (listeners != null) {
678 for (var i=0, len=listeners.length; i<len; i++) {
679 if (listeners[i].obj == obj && listeners[i].func == func) {
680 listeners.splice(i, 1);
689 * Remove all listeners for a given event type. If type is not registered,
695 remove: function(type) {
696 if (this.listeners[type] != null) {
697 this.listeners[type] = [];
702 * APIMethod: triggerEvent
703 * Trigger a specified registered event.
710 * {Boolean} The last listener return. If a listener returns false, the
711 * chain of listeners will stop getting called.
713 triggerEvent: function (type, evt) {
714 var listeners = this.listeners[type];
717 if(!listeners || listeners.length == 0) {
721 // prep evt object with object & div references
725 evt.object = this.object;
726 evt.element = this.element;
731 // execute all callbacks registered for specified type
732 // get a clone of the listeners array to
733 // allow for splicing during callbacks
734 var listeners = listeners.slice(), continueChain;
735 for (var i=0, len=listeners.length; i<len; i++) {
736 var callback = listeners[i];
737 // bind the context to callback.obj
738 continueChain = callback.func.apply(callback.obj, [evt]);
740 if ((continueChain != undefined) && (continueChain == false)) {
741 // if callback returns false, execute no more callbacks.
745 // don't fall through to other DOM elements
746 if (!this.fallThrough) {
747 OpenLayers.Event.stop(evt, true);
749 return continueChain;
753 * Method: handleBrowserEvent
754 * Basically just a wrapper to the triggerEvent() function, but takes
755 * care to set a property 'xy' on the event with the current mouse
761 handleBrowserEvent: function (evt) {
762 if (this.includeXY) {
763 evt.xy = this.getMousePosition(evt);
765 this.triggerEvent(evt.type, evt);
769 * APIMethod: clearMouseCache
770 * Clear cached data about the mouse position. This should be called any
771 * time the element that events are registered on changes position
774 clearMouseCache: function() {
775 this.element.scrolls = null;
776 this.element.lefttop = null;
777 this.element.offsets = null;
781 * Method: getMousePosition
787 * {<OpenLayers.Pixel>} The current xy coordinate of the mouse, adjusted
790 getMousePosition: function (evt) {
791 if (!this.includeXY) {
792 this.clearMouseCache();
793 } else if (!this.element.hasScrollEvent) {
794 OpenLayers.Event.observe(window, "scroll", this.clearMouseListener);
795 this.element.hasScrollEvent = true;
798 if (!this.element.scrolls) {
799 this.element.scrolls = [
800 (document.documentElement.scrollLeft
801 || document.body.scrollLeft),
802 (document.documentElement.scrollTop
803 || document.body.scrollTop)
807 if (!this.element.lefttop) {
808 this.element.lefttop = [
809 (document.documentElement.clientLeft || 0),
810 (document.documentElement.clientTop || 0)
814 if (!this.element.offsets) {
815 this.element.offsets = OpenLayers.Util.pagePosition(this.element);
816 this.element.offsets[0] += this.element.scrolls[0];
817 this.element.offsets[1] += this.element.scrolls[1];
819 return new OpenLayers.Pixel(
820 (evt.clientX + this.element.scrolls[0]) - this.element.offsets[0]
821 - this.element.lefttop[0],
822 (evt.clientY + this.element.scrolls[1]) - this.element.offsets[1]
823 - this.element.lefttop[1]
827 CLASS_NAME: "OpenLayers.Events"